amd-gaia 0.14.3__py3-none-any.whl → 0.15.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/METADATA +223 -223
  2. amd_gaia-0.15.1.dist-info/RECORD +178 -0
  3. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/entry_points.txt +1 -0
  4. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/licenses/LICENSE.md +20 -20
  5. gaia/__init__.py +29 -29
  6. gaia/agents/__init__.py +19 -19
  7. gaia/agents/base/__init__.py +9 -9
  8. gaia/agents/base/agent.py +2177 -2177
  9. gaia/agents/base/api_agent.py +120 -120
  10. gaia/agents/base/console.py +1841 -1841
  11. gaia/agents/base/errors.py +237 -237
  12. gaia/agents/base/mcp_agent.py +86 -86
  13. gaia/agents/base/tools.py +83 -83
  14. gaia/agents/blender/agent.py +556 -556
  15. gaia/agents/blender/agent_simple.py +133 -135
  16. gaia/agents/blender/app.py +211 -211
  17. gaia/agents/blender/app_simple.py +41 -41
  18. gaia/agents/blender/core/__init__.py +16 -16
  19. gaia/agents/blender/core/materials.py +506 -506
  20. gaia/agents/blender/core/objects.py +316 -316
  21. gaia/agents/blender/core/rendering.py +225 -225
  22. gaia/agents/blender/core/scene.py +220 -220
  23. gaia/agents/blender/core/view.py +146 -146
  24. gaia/agents/chat/__init__.py +9 -9
  25. gaia/agents/chat/agent.py +835 -835
  26. gaia/agents/chat/app.py +1058 -1058
  27. gaia/agents/chat/session.py +508 -508
  28. gaia/agents/chat/tools/__init__.py +15 -15
  29. gaia/agents/chat/tools/file_tools.py +96 -96
  30. gaia/agents/chat/tools/rag_tools.py +1729 -1729
  31. gaia/agents/chat/tools/shell_tools.py +436 -436
  32. gaia/agents/code/__init__.py +7 -7
  33. gaia/agents/code/agent.py +549 -549
  34. gaia/agents/code/cli.py +377 -0
  35. gaia/agents/code/models.py +135 -135
  36. gaia/agents/code/orchestration/__init__.py +24 -24
  37. gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
  38. gaia/agents/code/orchestration/checklist_generator.py +713 -713
  39. gaia/agents/code/orchestration/factories/__init__.py +9 -9
  40. gaia/agents/code/orchestration/factories/base.py +63 -63
  41. gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
  42. gaia/agents/code/orchestration/factories/python_factory.py +106 -106
  43. gaia/agents/code/orchestration/orchestrator.py +841 -841
  44. gaia/agents/code/orchestration/project_analyzer.py +391 -391
  45. gaia/agents/code/orchestration/steps/__init__.py +67 -67
  46. gaia/agents/code/orchestration/steps/base.py +188 -188
  47. gaia/agents/code/orchestration/steps/error_handler.py +314 -314
  48. gaia/agents/code/orchestration/steps/nextjs.py +828 -828
  49. gaia/agents/code/orchestration/steps/python.py +307 -307
  50. gaia/agents/code/orchestration/template_catalog.py +469 -469
  51. gaia/agents/code/orchestration/workflows/__init__.py +14 -14
  52. gaia/agents/code/orchestration/workflows/base.py +80 -80
  53. gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
  54. gaia/agents/code/orchestration/workflows/python.py +94 -94
  55. gaia/agents/code/prompts/__init__.py +11 -11
  56. gaia/agents/code/prompts/base_prompt.py +77 -77
  57. gaia/agents/code/prompts/code_patterns.py +2036 -2036
  58. gaia/agents/code/prompts/nextjs_prompt.py +40 -40
  59. gaia/agents/code/prompts/python_prompt.py +109 -109
  60. gaia/agents/code/schema_inference.py +365 -365
  61. gaia/agents/code/system_prompt.py +41 -41
  62. gaia/agents/code/tools/__init__.py +42 -42
  63. gaia/agents/code/tools/cli_tools.py +1138 -1138
  64. gaia/agents/code/tools/code_formatting.py +319 -319
  65. gaia/agents/code/tools/code_tools.py +769 -769
  66. gaia/agents/code/tools/error_fixing.py +1347 -1347
  67. gaia/agents/code/tools/external_tools.py +180 -180
  68. gaia/agents/code/tools/file_io.py +845 -845
  69. gaia/agents/code/tools/prisma_tools.py +190 -190
  70. gaia/agents/code/tools/project_management.py +1016 -1016
  71. gaia/agents/code/tools/testing.py +321 -321
  72. gaia/agents/code/tools/typescript_tools.py +122 -122
  73. gaia/agents/code/tools/validation_parsing.py +461 -461
  74. gaia/agents/code/tools/validation_tools.py +806 -806
  75. gaia/agents/code/tools/web_dev_tools.py +1758 -1758
  76. gaia/agents/code/validators/__init__.py +16 -16
  77. gaia/agents/code/validators/antipattern_checker.py +241 -241
  78. gaia/agents/code/validators/ast_analyzer.py +197 -197
  79. gaia/agents/code/validators/requirements_validator.py +145 -145
  80. gaia/agents/code/validators/syntax_validator.py +171 -171
  81. gaia/agents/docker/__init__.py +7 -7
  82. gaia/agents/docker/agent.py +642 -642
  83. gaia/agents/emr/__init__.py +8 -8
  84. gaia/agents/emr/agent.py +1506 -1506
  85. gaia/agents/emr/cli.py +1322 -1322
  86. gaia/agents/emr/constants.py +475 -475
  87. gaia/agents/emr/dashboard/__init__.py +4 -4
  88. gaia/agents/emr/dashboard/server.py +1974 -1974
  89. gaia/agents/jira/__init__.py +11 -11
  90. gaia/agents/jira/agent.py +894 -894
  91. gaia/agents/jira/jql_templates.py +299 -299
  92. gaia/agents/routing/__init__.py +7 -7
  93. gaia/agents/routing/agent.py +567 -570
  94. gaia/agents/routing/system_prompt.py +75 -75
  95. gaia/agents/summarize/__init__.py +11 -0
  96. gaia/agents/summarize/agent.py +885 -0
  97. gaia/agents/summarize/prompts.py +129 -0
  98. gaia/api/__init__.py +23 -23
  99. gaia/api/agent_registry.py +238 -238
  100. gaia/api/app.py +305 -305
  101. gaia/api/openai_server.py +575 -575
  102. gaia/api/schemas.py +186 -186
  103. gaia/api/sse_handler.py +373 -373
  104. gaia/apps/__init__.py +4 -4
  105. gaia/apps/llm/__init__.py +6 -6
  106. gaia/apps/llm/app.py +173 -169
  107. gaia/apps/summarize/app.py +116 -633
  108. gaia/apps/summarize/html_viewer.py +133 -133
  109. gaia/apps/summarize/pdf_formatter.py +284 -284
  110. gaia/audio/__init__.py +2 -2
  111. gaia/audio/audio_client.py +439 -439
  112. gaia/audio/audio_recorder.py +269 -269
  113. gaia/audio/kokoro_tts.py +599 -599
  114. gaia/audio/whisper_asr.py +432 -432
  115. gaia/chat/__init__.py +16 -16
  116. gaia/chat/app.py +430 -430
  117. gaia/chat/prompts.py +522 -522
  118. gaia/chat/sdk.py +1228 -1225
  119. gaia/cli.py +5481 -5621
  120. gaia/database/__init__.py +10 -10
  121. gaia/database/agent.py +176 -176
  122. gaia/database/mixin.py +290 -290
  123. gaia/database/testing.py +64 -64
  124. gaia/eval/batch_experiment.py +2332 -2332
  125. gaia/eval/claude.py +542 -542
  126. gaia/eval/config.py +37 -37
  127. gaia/eval/email_generator.py +512 -512
  128. gaia/eval/eval.py +3179 -3179
  129. gaia/eval/groundtruth.py +1130 -1130
  130. gaia/eval/transcript_generator.py +582 -582
  131. gaia/eval/webapp/README.md +167 -167
  132. gaia/eval/webapp/package-lock.json +875 -875
  133. gaia/eval/webapp/package.json +20 -20
  134. gaia/eval/webapp/public/app.js +3402 -3402
  135. gaia/eval/webapp/public/index.html +87 -87
  136. gaia/eval/webapp/public/styles.css +3661 -3661
  137. gaia/eval/webapp/server.js +415 -415
  138. gaia/eval/webapp/test-setup.js +72 -72
  139. gaia/llm/__init__.py +9 -2
  140. gaia/llm/base_client.py +60 -0
  141. gaia/llm/exceptions.py +12 -0
  142. gaia/llm/factory.py +70 -0
  143. gaia/llm/lemonade_client.py +3236 -3221
  144. gaia/llm/lemonade_manager.py +294 -294
  145. gaia/llm/providers/__init__.py +9 -0
  146. gaia/llm/providers/claude.py +108 -0
  147. gaia/llm/providers/lemonade.py +120 -0
  148. gaia/llm/providers/openai_provider.py +79 -0
  149. gaia/llm/vlm_client.py +382 -382
  150. gaia/logger.py +189 -189
  151. gaia/mcp/agent_mcp_server.py +245 -245
  152. gaia/mcp/blender_mcp_client.py +138 -138
  153. gaia/mcp/blender_mcp_server.py +648 -648
  154. gaia/mcp/context7_cache.py +332 -332
  155. gaia/mcp/external_services.py +518 -518
  156. gaia/mcp/mcp_bridge.py +811 -550
  157. gaia/mcp/servers/__init__.py +6 -6
  158. gaia/mcp/servers/docker_mcp.py +83 -83
  159. gaia/perf_analysis.py +361 -0
  160. gaia/rag/__init__.py +10 -10
  161. gaia/rag/app.py +293 -293
  162. gaia/rag/demo.py +304 -304
  163. gaia/rag/pdf_utils.py +235 -235
  164. gaia/rag/sdk.py +2194 -2194
  165. gaia/security.py +163 -163
  166. gaia/talk/app.py +289 -289
  167. gaia/talk/sdk.py +538 -538
  168. gaia/testing/__init__.py +87 -87
  169. gaia/testing/assertions.py +330 -330
  170. gaia/testing/fixtures.py +333 -333
  171. gaia/testing/mocks.py +493 -493
  172. gaia/util.py +46 -46
  173. gaia/utils/__init__.py +33 -33
  174. gaia/utils/file_watcher.py +675 -675
  175. gaia/utils/parsing.py +223 -223
  176. gaia/version.py +100 -100
  177. amd_gaia-0.14.3.dist-info/RECORD +0 -168
  178. gaia/agents/code/app.py +0 -266
  179. gaia/llm/llm_client.py +0 -729
  180. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/WHEEL +0 -0
  181. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/top_level.txt +0 -0
@@ -1,1763 +1,1763 @@
1
- # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
- # SPDX-License-Identifier: MIT
3
- """Checklist Executor for LLM-Driven Code Generation.
4
-
5
- This module executes checklist items generated by ChecklistGenerator.
6
- For code-generating templates, the LLM is invoked per item to produce
7
- contextual, high-quality code. CLI commands remain deterministic.
8
-
9
- The executor:
10
- 1. Receives a GeneratedChecklist from ChecklistGenerator
11
- 2. For each item, routes to LLM generation or deterministic execution
12
- 3. Tracks results and handles errors with recovery
13
- 4. Returns a complete ExecutionResult
14
-
15
- Error handling follows the three-tier strategy:
16
- 1. RETRY: Simple retries for transient errors
17
- 2. FIX_AND_RETRY: Auto-fix and retry for known issues
18
- 3. ABORT: Stop execution for unrecoverable errors
19
- """
20
-
21
- import inspect
22
- import json
23
- import logging
24
- import os
25
- import sys
26
- from dataclasses import dataclass, field
27
- from typing import Any, Callable, Dict, List, Optional, Protocol
28
-
29
- from gaia.agents.base.console import AgentConsole
30
- from gaia.agents.base.tools import _TOOL_REGISTRY
31
-
32
- from .checklist_generator import ChecklistItem, GeneratedChecklist
33
- from .steps.base import StepResult, ToolExecutor, UserContext
34
- from .steps.error_handler import ErrorHandler, RecoveryAction
35
- from .template_catalog import get_template
36
-
37
- logger = logging.getLogger(__name__)
38
-
39
-
40
- class ChatSDK(Protocol):
41
- """Protocol for chat SDK interface used by LLM code generation."""
42
-
43
- def send(self, message: str, timeout: int = 600, no_history: bool = False) -> Any:
44
- """Send a message and get response."""
45
- ...
46
-
47
- def send_stream(self, message: str, **kwargs) -> Any:
48
- """Send a message and get streaming response."""
49
- ...
50
-
51
-
52
- # ============================================================================
53
- # Template Classification
54
- # ============================================================================
55
-
56
- # Templates that execute CLI commands - remain deterministic (no LLM)
57
- DETERMINISTIC_TEMPLATES = {
58
- "create_next_app",
59
- "setup_prisma",
60
- "prisma_db_sync",
61
- "setup_testing",
62
- "run_typescript_check",
63
- "validate_styles",
64
- "generate_style_tests",
65
- "run_tests",
66
- "fix_code",
67
- }
68
-
69
- # Templates that generate code files - use LLM for contextual generation
70
- # NOTE: generate_prisma_model is NOT here because it must APPEND to schema.prisma,
71
- # not overwrite it. The manage_data_model tool handles this correctly.
72
- LLM_GENERATED_TEMPLATES = {
73
- "generate_react_component",
74
- "generate_api_route",
75
- "setup_app_styling",
76
- "update_landing_page",
77
- }
78
-
79
- # Template metadata for validation of LLM-generated code
80
- TEMPLATE_METADATA: Dict[str, Dict[str, Any]] = {
81
- "generate_react_component": {
82
- "list": {
83
- "requires_client": False,
84
- "expected_classes": ["page-title", "btn-primary"],
85
- "file_pattern": "src/app/{resource}s/page.tsx",
86
- },
87
- "form": {
88
- "requires_client": True,
89
- "expected_classes": [
90
- "input-field",
91
- "btn-primary",
92
- "btn-secondary",
93
- ],
94
- "file_pattern": "src/components/{Resource}Form.tsx",
95
- },
96
- "new": {
97
- "requires_client": True,
98
- "expected_classes": ["page-title", "link-back"],
99
- "file_pattern": "src/app/{resource}s/new/page.tsx",
100
- },
101
- "detail": {
102
- "requires_client": True,
103
- "expected_classes": [
104
- "page-title",
105
- "btn-primary",
106
- "btn-danger",
107
- ],
108
- "file_pattern": "src/app/{resource}s/[id]/page.tsx",
109
- },
110
- "artifact-timer": {
111
- "requires_client": True,
112
- "expected_classes": [
113
- "glass-card",
114
- ],
115
- },
116
- },
117
- "generate_api_route": {
118
- "collection": {
119
- "requires_client": False,
120
- "expected_classes": [],
121
- "file_pattern": "src/app/api/{resource}s/route.ts",
122
- },
123
- "item": {
124
- "requires_client": False,
125
- "expected_classes": [],
126
- "file_pattern": "src/app/api/{resource}s/[id]/route.ts",
127
- },
128
- },
129
- # NOTE: generate_prisma_model removed - uses tool-based execution, not LLM
130
- "setup_app_styling": {
131
- "default": {
132
- "requires_client": False,
133
- "expected_classes": ["page-title", "btn-primary"],
134
- "file_pattern": "src/app/globals.css",
135
- },
136
- },
137
- "update_landing_page": {
138
- "default": {
139
- "requires_client": False,
140
- "expected_classes": ["page-title"],
141
- "file_pattern": "src/app/page.tsx",
142
- },
143
- },
144
- }
145
-
146
- # Templates whose results should be logged for downstream validation/QA prompts
147
- VALIDATION_TEMPLATES = {
148
- "run_typescript_check",
149
- "validate_styles",
150
- "run_tests",
151
- }
152
-
153
-
154
- @dataclass
155
- class ItemExecutionResult:
156
- """Result of executing a single checklist item."""
157
-
158
- template: str
159
- params: Dict[str, Any]
160
- description: str
161
- success: bool
162
- files: List[str] = field(default_factory=list)
163
- warnings: List[str] = field(default_factory=list)
164
- error: Optional[str] = None
165
- error_recoverable: bool = True
166
- output: Dict[str, Any] = field(default_factory=dict)
167
-
168
- def to_dict(self) -> Dict[str, Any]:
169
- """Convert to dictionary representation."""
170
- return {
171
- "template": self.template,
172
- "params": self.params,
173
- "description": self.description,
174
- "success": self.success,
175
- "files": self.files,
176
- "warnings": self.warnings,
177
- "error": self.error,
178
- }
179
-
180
-
181
- @dataclass
182
- class ValidationLogEntry:
183
- """Structured log entry for validation/test steps."""
184
-
185
- template: str
186
- description: str
187
- success: bool
188
- error: Optional[str]
189
- output: Dict[str, Any] = field(default_factory=dict)
190
- files: List[str] = field(default_factory=list)
191
-
192
- def to_dict(self) -> Dict[str, Any]:
193
- """Convert to plain dictionary for serialization."""
194
- return {
195
- "template": self.template,
196
- "description": self.description,
197
- "success": self.success,
198
- "error": self.error,
199
- "files": self.files,
200
- "output": self.output,
201
- }
202
-
203
-
204
- @dataclass
205
- class ChecklistExecutionResult:
206
- """Result of executing a complete checklist."""
207
-
208
- checklist: GeneratedChecklist
209
- item_results: List[ItemExecutionResult] = field(default_factory=list)
210
- success: bool = True
211
- total_files: List[str] = field(default_factory=list)
212
- errors: List[str] = field(default_factory=list)
213
- warnings: List[str] = field(default_factory=list)
214
- validation_logs: List[ValidationLogEntry] = field(default_factory=list)
215
-
216
- @property
217
- def items_succeeded(self) -> int:
218
- """Count of successfully executed items."""
219
- return sum(1 for r in self.item_results if r.success)
220
-
221
- @property
222
- def items_failed(self) -> int:
223
- """Count of failed items."""
224
- return sum(1 for r in self.item_results if not r.success)
225
-
226
- @property
227
- def summary(self) -> str:
228
- """Human-readable summary of execution."""
229
- status = "SUCCESS" if self.success else "FAILED"
230
- return (
231
- f"{status}: {self.items_succeeded}/{len(self.item_results)} items completed, "
232
- f"{len(self.total_files)} files created"
233
- )
234
-
235
- def to_dict(self) -> Dict[str, Any]:
236
- """Convert to dictionary representation."""
237
- return {
238
- "success": self.success,
239
- "summary": self.summary,
240
- "reasoning": self.checklist.reasoning,
241
- "items": [r.to_dict() for r in self.item_results],
242
- "files": self.total_files,
243
- "errors": self.errors,
244
- "warnings": self.warnings,
245
- "validation_logs": [log.to_dict() for log in self.validation_logs],
246
- }
247
-
248
-
249
- # Template to tool mapping
250
- TEMPLATE_TO_TOOL: Dict[str, str] = {
251
- "create_next_app": "run_cli_command", # Uses npx create-next-app
252
- "setup_app_styling": "setup_app_styling",
253
- "setup_prisma": "run_cli_command", # Uses npx prisma init + creates singleton
254
- "prisma_db_sync": "run_cli_command", # Uses npx prisma generate && npx prisma db push
255
- "setup_testing": "setup_nextjs_testing",
256
- "generate_prisma_model": "manage_data_model",
257
- "generate_api_route": "manage_api_endpoint",
258
- "generate_react_component": "manage_react_component",
259
- "update_landing_page": "update_landing_page",
260
- "run_typescript_check": "validate_typescript",
261
- "validate_styles": "validate_styles", # CSS/design system validation (Issue #1002)
262
- "generate_style_tests": "generate_style_tests", # Generate CSS tests
263
- "run_tests": "run_cli_command", # Uses npm test
264
- "fix_code": "fix_code",
265
- }
266
-
267
- # Next.js version for create-next-app
268
- NEXTJS_VERSION = "14.2.33"
269
-
270
-
271
- class ChecklistExecutor:
272
- """Execute checklist items with LLM-driven code generation.
273
-
274
- For code-generating templates, the LLM is invoked per item to produce
275
- contextual, high-quality code. CLI commands remain deterministic.
276
-
277
- Template routing:
278
- - DETERMINISTIC_TEMPLATES: Execute CLI commands directly (no LLM)
279
- - LLM_GENERATED_TEMPLATES: Use LLM to generate code with template as guidance
280
- - Fallback: Use tool executor for unknown templates
281
-
282
- Error recovery uses the three-tier strategy via ErrorHandler:
283
- 1. RETRY: Simple retry for transient errors
284
- 2. FIX_AND_RETRY: LLM fixes code then retry
285
- 3. ESCALATE: LLM rewrites from scratch
286
- 4. ABORT: Give up after max attempts
287
- """
288
-
289
- def __init__(
290
- self,
291
- tool_executor: ToolExecutor,
292
- llm_client: Optional[ChatSDK] = None,
293
- error_handler: Optional[ErrorHandler] = None,
294
- progress_callback: Optional[Callable[[str, int, int], None]] = None,
295
- console: Optional[AgentConsole] = None,
296
- ):
297
- """Initialize the checklist executor.
298
-
299
- Args:
300
- tool_executor: Function to execute tools (name, args) -> result
301
- llm_client: Optional LLM client for code generation (enables per-item LLM)
302
- error_handler: Optional error handler for recovery (enables retries)
303
- progress_callback: Optional callback(item_desc, current, total)
304
- console: Optional console for displaying output
305
- """
306
- self.tool_executor = tool_executor
307
- self.llm_client = llm_client
308
- self.error_handler = error_handler
309
- self.progress_callback = progress_callback
310
- self.console = console or AgentConsole()
311
- self._tool_signature_cache: Dict[str, inspect.Signature] = {}
312
-
313
- def _tool_accepts_parameter(self, tool_name: str, parameter: str) -> bool:
314
- """Return True if the tool accepts the provided parameter."""
315
- tool_entry = _TOOL_REGISTRY.get(tool_name)
316
- if not tool_entry:
317
- return False
318
-
319
- if tool_name in self._tool_signature_cache:
320
- signature = self._tool_signature_cache[tool_name]
321
- else:
322
- tool_func = tool_entry.get("function")
323
- if not tool_func:
324
- return False
325
- try:
326
- signature = inspect.signature(tool_func)
327
- except (TypeError, ValueError):
328
- return False
329
- self._tool_signature_cache[tool_name] = signature
330
-
331
- if parameter in signature.parameters:
332
- return True
333
-
334
- return any(
335
- param.kind == inspect.Parameter.VAR_KEYWORD
336
- for param in signature.parameters.values()
337
- )
338
-
339
- def execute(
340
- self,
341
- checklist: GeneratedChecklist,
342
- context: UserContext,
343
- stop_on_error: bool = True,
344
- step_through: bool = False,
345
- ) -> ChecklistExecutionResult:
346
- """Execute all checklist items in order.
347
-
348
- Args:
349
- checklist: Checklist to execute
350
- context: User context with project info
351
- stop_on_error: Whether to stop on first critical error
352
- step_through: Enable step-through debugging
353
-
354
- Returns:
355
- ChecklistExecutionResult with all item results
356
- """
357
- logger.debug(
358
- f"Executing checklist with {len(checklist.items)} items: "
359
- f"{checklist.reasoning}"
360
- )
361
-
362
- self.console.print_checklist_reasoning(checklist.reasoning)
363
-
364
- result = ChecklistExecutionResult(checklist=checklist)
365
-
366
- # Check for validation errors first
367
- if not checklist.is_valid:
368
- logger.error(
369
- f"Checklist has validation errors: {checklist.validation_errors}"
370
- )
371
- result.success = False
372
- result.errors.extend(checklist.validation_errors)
373
- return result
374
-
375
- # Use items in the order generated by LLM
376
- ordered_items = checklist.items
377
- total = len(ordered_items)
378
-
379
- # Execute each item with error recovery
380
- for idx, item in enumerate(ordered_items, 1):
381
- self.console.print_checklist(ordered_items, idx - 1)
382
- self._report_progress(item.description, idx, total)
383
-
384
- # Use recovery wrapper if error_handler is available
385
- item_result = self._execute_item_with_recovery(item, context)
386
- result.item_results.append(item_result)
387
-
388
- # Capture validation/test output for downstream prompts
389
- if item.template in VALIDATION_TEMPLATES:
390
- result.validation_logs.append(
391
- ValidationLogEntry(
392
- template=item.template,
393
- description=item.description,
394
- success=item_result.success,
395
- error=item_result.error,
396
- output=item_result.output,
397
- files=item_result.files,
398
- )
399
- )
400
-
401
- # Collect files
402
- result.total_files.extend(item_result.files)
403
-
404
- # Handle errors
405
- if not item_result.success:
406
- result.errors.append(item_result.error or "Unknown error")
407
-
408
- if stop_on_error and not item_result.error_recoverable:
409
- logger.error(
410
- f"Stopping execution due to critical error in "
411
- f"{item.template}: {item_result.error}"
412
- )
413
- result.success = False
414
- break
415
-
416
- # Collect warnings
417
- result.warnings.extend(item_result.warnings)
418
-
419
- # Handle step-through
420
- if step_through:
421
- if not self._handle_step_through(item.description):
422
- logger.info("Execution stopped by user during step-through")
423
- break
424
-
425
- # Update overall success
426
- if result.items_failed > 0:
427
- result.success = False
428
-
429
- logger.info(result.summary)
430
- return result
431
-
432
- def _execute_item(
433
- self,
434
- item: ChecklistItem,
435
- context: UserContext,
436
- ) -> ItemExecutionResult:
437
- """Execute a single checklist item with routing.
438
-
439
- Routes execution based on template type:
440
- - DETERMINISTIC_TEMPLATES: Execute CLI commands directly (no LLM)
441
- - LLM_GENERATED_TEMPLATES: Use LLM to generate code (if llm_client available)
442
- - Fallback: Use tool executor for unknown templates
443
-
444
- Args:
445
- item: Checklist item to execute
446
- context: User context
447
-
448
- Returns:
449
- ItemExecutionResult
450
- """
451
- logger.debug(f"Executing: {item.template} - {item.description}")
452
-
453
- try:
454
- # Route 1: Deterministic templates (CLI commands) - no LLM needed
455
- if item.template in DETERMINISTIC_TEMPLATES:
456
- logger.debug(f"Deterministic execution for {item.template}")
457
- return self._execute_deterministic(item, context)
458
-
459
- # Route 2: LLM-generated templates - use LLM for code generation
460
- if item.template in LLM_GENERATED_TEMPLATES and self.llm_client:
461
- logger.info(f"LLM code generation for {item.template}")
462
- return self._execute_with_llm(item, context)
463
-
464
- # Route 3: Fallback to tool execution (no LLM or unknown template)
465
- logger.debug(f"Fallback tool execution for {item.template}")
466
- return self._execute_via_tool(item, context)
467
-
468
- except Exception as e:
469
- logger.exception(f"Exception executing {item.template}")
470
- return ItemExecutionResult(
471
- template=item.template,
472
- params=item.params,
473
- description=item.description,
474
- success=False,
475
- error=str(e),
476
- error_recoverable=False,
477
- )
478
-
479
- def _execute_deterministic(
480
- self,
481
- item: ChecklistItem,
482
- context: UserContext,
483
- ) -> ItemExecutionResult:
484
- """Execute a deterministic template (CLI command).
485
-
486
- These templates don't need LLM - they run predefined commands.
487
-
488
- Args:
489
- item: Checklist item to execute
490
- context: User context
491
-
492
- Returns:
493
- ItemExecutionResult
494
- """
495
- # Map template to tool name
496
- tool_name = TEMPLATE_TO_TOOL.get(item.template, item.template)
497
-
498
- # Build params for CLI execution
499
- params = self._build_params(item, context)
500
-
501
- logger.debug(f"Calling tool '{tool_name}' with params: {params}")
502
-
503
- # Execute the tool
504
- raw_result = self.tool_executor(tool_name, params)
505
-
506
- # Parse result
507
- result = self._parse_tool_result(item, raw_result)
508
-
509
- # Post-command file operations (cross-platform, avoids shell file writing)
510
- if result.success and item.template == "setup_prisma":
511
- self._write_prisma_singleton(context.project_dir)
512
-
513
- return result
514
-
515
- def _execute_via_tool(
516
- self,
517
- item: ChecklistItem,
518
- context: UserContext,
519
- ) -> ItemExecutionResult:
520
- """Execute via tool executor (fallback when no LLM).
521
-
522
- Args:
523
- item: Checklist item to execute
524
- context: User context
525
-
526
- Returns:
527
- ItemExecutionResult
528
- """
529
- # Map template to tool name
530
- tool_name = TEMPLATE_TO_TOOL.get(item.template, item.template)
531
-
532
- # Build params with project_dir
533
- params = self._build_params(item, context)
534
-
535
- logger.debug(f"Calling tool '{tool_name}' with params: {params}")
536
-
537
- # Execute the tool
538
- raw_result = self.tool_executor(tool_name, params)
539
-
540
- # Parse result
541
- return self._parse_tool_result(item, raw_result)
542
-
543
- def _execute_with_llm(
544
- self,
545
- item: ChecklistItem,
546
- context: UserContext,
547
- ) -> ItemExecutionResult:
548
- """Execute a checklist item using LLM code generation.
549
-
550
- This is the core of Phase 9 - LLM generates contextual code using
551
- templates as structural guidance.
552
-
553
- Args:
554
- item: Checklist item to execute
555
- context: User context
556
-
557
- Returns:
558
- ItemExecutionResult
559
- """
560
- logger.info(f"Generating code with LLM for {item.template}")
561
-
562
- try:
563
- # 1. Get template as guidance
564
- template_guidance = self._get_template_guidance(item)
565
- if not template_guidance:
566
- logger.warning(
567
- f"No template guidance for {item.template}, falling back to tool"
568
- )
569
- return self._execute_via_tool(item, context)
570
-
571
- # 1b. Resolve field definitions for prompt + post-processing
572
- resolved_fields = self._resolve_fields(item, context)
573
-
574
- # 2. Build prompt
575
- prompt = self._build_code_generation_prompt(
576
- item, context, template_guidance, resolved_fields
577
- )
578
-
579
- # 3. Call LLM
580
- logger.debug(f"Sending prompt to LLM ({len(prompt)} chars)")
581
-
582
- file_path = self._determine_file_path(item, context)
583
-
584
- # Start file preview
585
- self.console.start_file_preview(file_path, max_lines=15)
586
-
587
- # Stream the response
588
- full_response = ""
589
- try:
590
- # Try streaming first if available
591
- if hasattr(self.llm_client, "send_stream"):
592
- for chunk in self.llm_client.send_stream(prompt, timeout=1200):
593
- if hasattr(chunk, "text"):
594
- text = chunk.text
595
- self.console.update_file_preview(text)
596
- full_response += text
597
-
598
- if full_response.strip():
599
- generated_code = full_response
600
- else:
601
- raise ValueError("Empty streaming response")
602
- else:
603
- # Fallback to non-streaming
604
- response = self.llm_client.send(prompt, timeout=1200)
605
- if hasattr(response, "text"):
606
- generated_code = response.text
607
- elif hasattr(response, "content"):
608
- generated_code = response.content
609
- else:
610
- generated_code = str(response)
611
-
612
- self.console.update_file_preview(generated_code)
613
-
614
- except Exception as e:
615
- # Fallback if streaming fails
616
- logger.warning(f"Streaming failed, falling back to standard send: {e}")
617
- response = self.llm_client.send(prompt, timeout=1200)
618
- if hasattr(response, "text"):
619
- generated_code = response.text
620
- elif hasattr(response, "content"):
621
- generated_code = response.content
622
- else:
623
- generated_code = str(response)
624
-
625
- self.console.update_file_preview(generated_code)
626
-
627
- # Stop file preview
628
- self.console.stop_file_preview()
629
-
630
- # 4. Clean response (strip markdown if present)
631
- clean_code = self._clean_llm_response(generated_code)
632
-
633
- # 5. Validate generated code
634
- is_valid, issues, is_blocking = self._validate_generated_code(
635
- clean_code, item
636
- )
637
- if not is_valid:
638
- logger.warning(f"Validation issues for {item.template}: {issues}")
639
-
640
- # CRITICAL: Block file write for blocking errors (Issue #1002)
641
- # This prevents TypeScript code from being written to CSS files
642
- if is_blocking:
643
- logger.error(
644
- f"BLOCKING validation error for {item.template}: {issues}"
645
- )
646
- return ItemExecutionResult(
647
- template=item.template,
648
- params=item.params,
649
- description=item.description,
650
- success=False,
651
- error=f"Content validation failed: {'; '.join(issues)}",
652
- error_recoverable=True, # Allow LLM retry with recovery
653
- )
654
- # Non-blocking issues: log warning and continue (best effort)
655
-
656
- # 6. Write to file
657
- full_path = os.path.join(context.project_dir, file_path)
658
-
659
- # Create directory if needed
660
- os.makedirs(os.path.dirname(full_path), exist_ok=True)
661
-
662
- # Write the generated code
663
- with open(full_path, "w", encoding="utf-8") as f:
664
- f.write(clean_code)
665
-
666
- logger.info(f"Wrote LLM-generated code to {file_path}")
667
-
668
- generated_files = [file_path]
669
-
670
- return ItemExecutionResult(
671
- template=item.template,
672
- params=item.params,
673
- description=item.description,
674
- success=True,
675
- files=generated_files,
676
- warnings=issues if not is_valid else [],
677
- )
678
-
679
- except Exception as e:
680
- logger.exception(f"LLM generation failed for {item.template}")
681
- return ItemExecutionResult(
682
- template=item.template,
683
- params=item.params,
684
- description=item.description,
685
- success=False,
686
- error=str(e),
687
- error_recoverable=True,
688
- )
689
-
690
- def _build_code_generation_prompt(
691
- self,
692
- item: ChecklistItem,
693
- context: UserContext,
694
- template_guidance: str,
695
- fields_override: Optional[Dict[str, str]] = None,
696
- ) -> str:
697
- """Build prompt for LLM code generation.
698
-
699
- Args:
700
- item: Checklist item describing what to generate
701
- context: User context with project info
702
- template_guidance: Template structure as guidance
703
-
704
- Returns:
705
- Prompt string for LLM
706
- """
707
- # CSS templates need a different prompt - they should NOT generate TypeScript
708
- css_templates = {"setup_app_styling"}
709
- if item.template in css_templates:
710
- return self._build_css_generation_prompt(item, template_guidance)
711
-
712
- resource = item.params.get("resource", "item")
713
- variant = item.params.get("variant", "default")
714
- fields = (
715
- fields_override
716
- if fields_override is not None
717
- else item.params.get("fields", context.schema_fields or {})
718
- )
719
-
720
- # Determine file type and output format
721
- is_css_file = item.template == "setup_app_styling"
722
- file_type = "CSS" if is_css_file else "TypeScript/TSX"
723
- code_language = "css" if is_css_file else "typescript"
724
- start_instruction = (
725
- "Start immediately with @tailwind directives or CSS rules"
726
- if is_css_file
727
- else 'Start immediately with imports or "use client"'
728
- )
729
-
730
- # Get required classes for this variant
731
- required_classes = self._get_required_classes(item)
732
- required_classes_str = (
733
- ", ".join(f"`{cls}`" for cls in required_classes)
734
- if required_classes
735
- else "None specified"
736
- )
737
-
738
- # Build architecture rules - skip TypeScript-specific rules for CSS
739
- architecture_rules = ""
740
- if not is_css_file:
741
- architecture_rules = """## Architecture Rules
742
- - Use Server Components by default for data fetching
743
- - Add "use client" directive ONLY when using hooks, event handlers, or browser APIs
744
- - Define explicit TypeScript types for all props and return values
745
- - Use Prisma-generated types where applicable
746
- - Never use `any` type
747
-
748
- ---"""
749
-
750
- # Add CSS-specific warning for CSS files
751
- css_warning = ""
752
- if is_css_file:
753
- css_warning = """## CRITICAL: CSS File Requirements
754
-
755
- This is a CSS file (globals.css). You MUST generate ONLY CSS code:
756
- - NO TypeScript/JavaScript code
757
- - NO import statements
758
- - NO export statements
759
- - NO const/let/function declarations
760
- - NO JSX/React components
761
- - NO TypeScript interfaces or types
762
- - ONLY CSS rules, @tailwind directives, @layer directives, and CSS selectors
763
-
764
- ---
765
-
766
- """
767
-
768
- return f"""You are an expert Next.js 14+ developer specializing in full-stack TypeScript applications.
769
-
770
- {css_warning}## CRITICAL: Required CSS Classes
771
-
772
- Your generated code MUST include these CSS classes:
773
- {required_classes_str}
774
-
775
- These classes are MANDATORY and will be validated. Code without them will fail validation.
776
-
777
- ---
778
-
779
- ## Task
780
- Generate a {item.template} ({variant}) for the {resource} resource.
781
-
782
- ### Purpose
783
- {item.description}
784
-
785
- ### User Request Context
786
- {context.user_request}
787
-
788
- ### Parameters
789
- {json.dumps(item.params, indent=2)}
790
-
791
- ### Data Model Fields
792
- {json.dumps(fields, indent=2) if fields else "Not specified - use reasonable defaults based on resource name"}
793
-
794
- ---
795
-
796
- {architecture_rules}## Design System Classes (Dark Theme)
797
-
798
- **Containers**: `glass-card` (ALWAYS use for main content container - glassmorphism effect)
799
- **Typography**: `page-title` (ALWAYS use for h1 headers - gradient text)
800
- **Buttons**: `btn-primary` (blue gradient), `btn-secondary` (outline), `btn-danger` (red)
801
- **Forms**: `input-field`, `select-field`, `textarea-field`, `label-text`, `form-group`
802
- **Navigation**: `link-back` (for ← back links)
803
- **Checkboxes**: `checkbox-modern` (for boolean fields)
804
-
805
- Theme: Dark backgrounds (slate-900), white text, blue-500 accents, white/10 borders.
806
-
807
- ---
808
-
809
- ## Reference Pattern
810
-
811
- Adapt this structural pattern. Replace placeholders with actual values for {resource}:
812
-
813
- ```{code_language}
814
- {template_guidance}
815
- ```
816
-
817
- ---
818
-
819
- ## Output Format
820
-
821
- Return ONLY raw {file_type} code:
822
- - NO markdown code blocks (no ```)
823
- - NO explanatory text before or after
824
- - {start_instruction}
825
- - MUST include all required CSS classes listed above"""
826
-
827
- def _build_css_generation_prompt(
828
- self,
829
- item: ChecklistItem,
830
- template_guidance: str,
831
- ) -> str:
832
- """Build prompt for CSS code generation.
833
-
834
- This is a specialized prompt for CSS files that ensures the LLM
835
- generates Tailwind CSS instead of TypeScript/JSX.
836
-
837
- Args:
838
- item: Checklist item describing what to generate
839
- template_guidance: CSS template as guidance
840
-
841
- Returns:
842
- Prompt string for LLM
843
- """
844
- return f"""You are an expert CSS developer specializing in Tailwind CSS.
845
-
846
- ## Task
847
- Generate a Tailwind CSS stylesheet for: {item.description}
848
-
849
- ## Rules
850
- - Return ONLY CSS code (Tailwind CSS with @apply directives is valid CSS)
851
- - NO TypeScript, JavaScript, imports, or exports
852
- - NO React components or JSX
853
- - NO markdown code blocks
854
- - Start with @tailwind directives
855
-
856
- ## Required Structure
857
- The CSS must include:
858
- 1. @tailwind base, components, utilities directives
859
- 2. :root CSS variables for theming
860
- 3. @layer components with these classes using @apply:
861
- - .glass-card (glassmorphism container)
862
- - .page-title (gradient text heading)
863
- - .btn-primary, .btn-secondary, .btn-danger (buttons)
864
- - .input-field (form inputs)
865
- - .checkbox-modern (styled checkboxes)
866
- - .link-back (navigation links)
867
-
868
- ## Reference Pattern
869
-
870
- Follow this Tailwind CSS template exactly:
871
-
872
- {template_guidance}
873
-
874
- ## Output
875
- Return raw CSS starting with @tailwind base;"""
876
-
877
- def _get_template_guidance(self, item: ChecklistItem) -> Optional[str]:
878
- """Get template content as guidance for LLM.
879
-
880
- The templates from code_patterns.py serve as structural guidance,
881
- not verbatim content to copy.
882
-
883
- Args:
884
- item: Checklist item with template and params
885
-
886
- Returns:
887
- Template string if found, None otherwise
888
- """
889
- # Import templates lazily to avoid circular imports
890
- try:
891
- from ..prompts.code_patterns import (
892
- API_ROUTE_DYNAMIC_DELETE,
893
- API_ROUTE_DYNAMIC_GET,
894
- API_ROUTE_DYNAMIC_PATCH,
895
- API_ROUTE_GET,
896
- API_ROUTE_POST,
897
- APP_GLOBALS_CSS,
898
- CLIENT_COMPONENT_FORM,
899
- CLIENT_COMPONENT_NEW_PAGE,
900
- CLIENT_COMPONENT_TIMER,
901
- SERVER_COMPONENT_DETAIL,
902
- SERVER_COMPONENT_LIST,
903
- )
904
- except ImportError:
905
- logger.warning("Could not import code_patterns templates")
906
- return None
907
-
908
- # Compose API route templates
909
- api_route_collection = f"""import {{ NextResponse }} from "next/server";
910
- import {{ prisma }} from "@/lib/prisma";
911
- import {{ z }} from "zod";
912
-
913
- // Schema for validation
914
- // IMPORTANT: Use z.coerce.date() for any date/datetime/timestamp fields
915
- const {{Resource}}Schema = z.object({{
916
- // Example: publishedOn: z.coerce.date(),
917
- // Define fields based on your data model
918
- }});
919
-
920
- {API_ROUTE_GET}
921
-
922
- {API_ROUTE_POST}
923
- """
924
-
925
- api_route_item = f"""import {{ NextResponse }} from "next/server";
926
- import {{ prisma }} from "@/lib/prisma";
927
- import {{ z }} from "zod";
928
-
929
- // Schema for update validation
930
- // IMPORTANT: Use z.coerce.date() for any date/datetime/timestamp fields
931
- const {{Resource}}UpdateSchema = z.object({{
932
- // Example: publishedOn: z.coerce.date().optional(),
933
- // Define update fields (all optional for PATCH)
934
- }}).partial();
935
-
936
- {API_ROUTE_DYNAMIC_GET}
937
-
938
- {API_ROUTE_DYNAMIC_PATCH}
939
-
940
- {API_ROUTE_DYNAMIC_DELETE}
941
- """
942
-
943
- # NOTE: Prisma model guidance removed - uses manage_data_model tool, not LLM
944
-
945
- # Landing page template guidance
946
- landing_page_guidance = """import Link from "next/link";
947
-
948
- export default function Home() {
949
- return (
950
- <main className="min-h-screen">
951
- <div className="container mx-auto px-4 py-12 max-w-4xl">
952
- <h1 className="page-title mb-8">Welcome</h1>
953
-
954
- <div className="grid gap-6">
955
- <Link
956
- href="/{resource}s"
957
- className="glass-card p-6 block hover:border-indigo-500/50 transition-all duration-300 group"
958
- >
959
- <div className="flex items-center justify-between">
960
- <div>
961
- <h2 className="text-2xl font-semibold text-slate-100 mb-2 group-hover:text-indigo-400 transition-colors">
962
- {Resource}s
963
- </h2>
964
- <p className="text-slate-400">Manage your {resource}s</p>
965
- </div>
966
- <svg className="w-6 h-6 text-slate-500 group-hover:text-indigo-400 group-hover:translate-x-1 transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
967
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
968
- </svg>
969
- </div>
970
- </Link>
971
- </div>
972
- </div>
973
- </main>
974
- );
975
- }
976
- """
977
-
978
- # Mapping of template names to their guidance
979
- TEMPLATE_GUIDANCE = {
980
- "generate_react_component": {
981
- "list": SERVER_COMPONENT_LIST,
982
- "form": CLIENT_COMPONENT_FORM,
983
- "new": CLIENT_COMPONENT_NEW_PAGE,
984
- "detail": SERVER_COMPONENT_DETAIL,
985
- "artifact-timer": CLIENT_COMPONENT_TIMER,
986
- },
987
- "generate_api_route": {
988
- "collection": api_route_collection,
989
- "item": api_route_item,
990
- },
991
- # NOTE: generate_prisma_model removed - uses manage_data_model tool
992
- "setup_app_styling": {
993
- "default": APP_GLOBALS_CSS,
994
- },
995
- "update_landing_page": {
996
- "default": landing_page_guidance,
997
- },
998
- }
999
-
1000
- template_name = item.template
1001
- guidance_map = TEMPLATE_GUIDANCE.get(template_name)
1002
-
1003
- if guidance_map is None:
1004
- return None
1005
-
1006
- if isinstance(guidance_map, str):
1007
- return guidance_map
1008
-
1009
- # Get variant-specific guidance
1010
- variant = item.params.get("variant", "default")
1011
- route_type = item.params.get("type", "default") # For API routes
1012
-
1013
- # Try variant first, then route_type, then default
1014
- return (
1015
- guidance_map.get(variant)
1016
- or guidance_map.get(route_type)
1017
- or guidance_map.get("default")
1018
- )
1019
-
1020
- def _determine_file_path(
1021
- self,
1022
- item: ChecklistItem,
1023
- context: UserContext, # pylint: disable=unused-argument
1024
- ) -> str:
1025
- """Determine the output file path for generated code.
1026
-
1027
- Args:
1028
- item: Checklist item with template and params
1029
- context: User context (unused but kept for consistency)
1030
-
1031
- Returns:
1032
- Relative file path from project root
1033
- """
1034
- template = item.template
1035
- params = item.params
1036
- resource = params.get("resource", "item")
1037
- resource_cap = resource.capitalize()
1038
-
1039
- if template == "generate_react_component":
1040
- variant = params.get("variant", "list")
1041
- component_name = params.get("component_name")
1042
- if component_name:
1043
- safe_name = "".join(
1044
- ch for ch in component_name if ch.isalnum() or ch == "_"
1045
- )
1046
- component_name = safe_name or component_name
1047
-
1048
- if variant == "list":
1049
- return f"src/app/{resource}s/page.tsx"
1050
- elif variant == "form":
1051
- return f"src/components/{resource_cap}Form.tsx"
1052
- elif variant == "new":
1053
- return f"src/app/{resource}s/new/page.tsx"
1054
- elif variant == "detail":
1055
- return f"src/app/{resource}s/[id]/page.tsx"
1056
- elif variant == "artifact-timer":
1057
- file_component = component_name or f"{resource_cap}Timer"
1058
- return f"src/components/{file_component}.tsx"
1059
- else:
1060
- if component_name:
1061
- return f"src/components/{component_name}.tsx"
1062
- return f"src/components/{resource_cap}{variant.capitalize()}.tsx"
1063
-
1064
- elif template == "generate_api_route":
1065
- route_type = params.get("type", "collection")
1066
- if route_type == "item":
1067
- return f"src/app/api/{resource}s/[id]/route.ts"
1068
- else:
1069
- return f"src/app/api/{resource}s/route.ts"
1070
-
1071
- # NOTE: generate_prisma_model removed - uses manage_data_model tool, not LLM
1072
-
1073
- elif template == "setup_app_styling":
1074
- return "src/app/globals.css"
1075
-
1076
- elif template == "update_landing_page":
1077
- return "src/app/page.tsx"
1078
-
1079
- # Default fallback
1080
- return f"src/generated/{template}.tsx"
1081
-
1082
- def _resolve_fields(
1083
- self, item: ChecklistItem, context: UserContext
1084
- ) -> Dict[str, str]:
1085
- """Resolve resource fields for code generation prompts."""
1086
- params = item.params or {}
1087
-
1088
- def _normalize(fields: Dict[str, str]) -> Dict[str, str]:
1089
- type_map = {
1090
- "string": "string",
1091
- "text": "string",
1092
- "int": "number",
1093
- "float": "float",
1094
- "double": "float",
1095
- "number": "number",
1096
- "boolean": "boolean",
1097
- "datetime": "datetime",
1098
- "date": "date",
1099
- "timestamp": "datetime",
1100
- "email": "email",
1101
- "url": "url",
1102
- }
1103
- normalized = {}
1104
- for name, field_type in (fields or {}).items():
1105
- mapped = type_map.get(field_type.lower(), "string")
1106
- normalized[name] = mapped
1107
- return normalized
1108
-
1109
- if "fields" in params and isinstance(params["fields"], dict):
1110
- return _normalize(params["fields"])
1111
-
1112
- if context.schema_fields:
1113
- return _normalize(context.schema_fields)
1114
-
1115
- resource = params.get("resource")
1116
- if not resource:
1117
- return {}
1118
-
1119
- try:
1120
- from ..tools.web_dev_tools import read_prisma_model
1121
- except ImportError:
1122
- logger.debug("read_prisma_model unavailable for field resolution")
1123
- return {}
1124
-
1125
- try:
1126
- model_info = read_prisma_model(context.project_dir, resource.capitalize())
1127
- except Exception as exc: # noqa: BLE001
1128
- logger.warning(f"Failed to read Prisma model for {resource}: {exc}")
1129
- return {}
1130
-
1131
- if not model_info.get("success"):
1132
- logger.debug(
1133
- "Could not resolve Prisma fields for %s: %s",
1134
- resource,
1135
- model_info.get("error"),
1136
- )
1137
- return {}
1138
-
1139
- prisma_fields = model_info.get("fields", {})
1140
- return _normalize(prisma_fields)
1141
-
1142
- def _clean_llm_response(self, response: str) -> str:
1143
- """Clean LLM response by removing markdown artifacts.
1144
-
1145
- Args:
1146
- response: Raw LLM response
1147
-
1148
- Returns:
1149
- Cleaned code string
1150
- """
1151
- code = response.strip()
1152
-
1153
- # Remove markdown code blocks
1154
- if code.startswith("```"):
1155
- lines = code.split("\n")
1156
- # Remove first line (```typescript or ```)
1157
- lines = lines[1:]
1158
- # Remove last line if it's closing ```
1159
- if lines and lines[-1].strip() == "```":
1160
- lines = lines[:-1]
1161
- code = "\n".join(lines)
1162
-
1163
- # Also handle case where there might be text before code block
1164
- if "```typescript" in code or "```tsx" in code:
1165
- # Find start of code block
1166
- for marker in ["```typescript", "```tsx", "```"]:
1167
- if marker in code:
1168
- start = code.find(marker)
1169
- end = code.find("```", start + len(marker))
1170
- if end > start:
1171
- code = code[start + len(marker) : end]
1172
- break
1173
-
1174
- return code.strip()
1175
-
1176
- def _get_required_classes(self, item: ChecklistItem) -> List[str]:
1177
- """Get list of required CSS classes for a checklist item.
1178
-
1179
- Args:
1180
- item: Checklist item with template and variant info
1181
-
1182
- Returns:
1183
- List of required CSS class names
1184
- """
1185
- metadata = TEMPLATE_METADATA.get(item.template, {})
1186
- variant = item.params.get("variant", "default")
1187
- route_type = item.params.get("type", "default")
1188
-
1189
- # Try variant, then route_type, then default
1190
- variant_meta = (
1191
- metadata.get(variant)
1192
- or metadata.get(route_type)
1193
- or metadata.get("default")
1194
- or {}
1195
- )
1196
-
1197
- return variant_meta.get("expected_classes", [])
1198
-
1199
- def _validate_generated_code(
1200
- self,
1201
- code: str,
1202
- item: ChecklistItem,
1203
- ) -> tuple[bool, List[str], bool]:
1204
- """Validate generated code meets requirements.
1205
-
1206
- Args:
1207
- code: Generated code to validate
1208
- item: Checklist item with template info
1209
-
1210
- Returns:
1211
- Tuple of (is_valid, list_of_issues, is_blocking)
1212
- - is_valid: True if no issues found
1213
- - list_of_issues: List of validation issue messages
1214
- - is_blocking: True if issues should prevent file write (CRITICAL errors)
1215
- """
1216
- issues = []
1217
- is_blocking = False
1218
-
1219
- # Check for markdown artifacts that slipped through
1220
- if code.strip().startswith("```"):
1221
- issues.append("Code still contains markdown block markers")
1222
-
1223
- # CRITICAL: For CSS files (setup_app_styling), check for TypeScript content
1224
- # This catches Issue #1002 where CSS files contain TypeScript code
1225
- if item.template == "setup_app_styling":
1226
- css_validation = self._validate_css_content_inline(code)
1227
- if css_validation["errors"]:
1228
- issues.extend(css_validation["errors"])
1229
- is_blocking = True # TypeScript in CSS is a BLOCKING error
1230
-
1231
- # Get metadata for this template
1232
- metadata = TEMPLATE_METADATA.get(item.template, {})
1233
- variant = item.params.get("variant", "default")
1234
- route_type = item.params.get("type", "default")
1235
-
1236
- # Try variant, then route_type, then default
1237
- variant_meta = (
1238
- metadata.get(variant)
1239
- or metadata.get(route_type)
1240
- or metadata.get("default")
1241
- or {}
1242
- )
1243
-
1244
- # Check for expected CSS classes (for UI components)
1245
- expected_classes = variant_meta.get("expected_classes", [])
1246
- for cls in expected_classes:
1247
- if cls not in code:
1248
- issues.append(f"Missing expected class: {cls}")
1249
-
1250
- # Check for 'use client' when needed
1251
- if variant_meta.get("requires_client"):
1252
- if '"use client"' not in code and "'use client'" not in code:
1253
- issues.append("Missing 'use client' directive for client component")
1254
-
1255
- # Check for basic TypeScript syntax
1256
- if item.template == "generate_react_component":
1257
- if "export default" not in code and "export function" not in code:
1258
- issues.append("Missing export statement")
1259
-
1260
- return len(issues) == 0, issues, is_blocking
1261
-
1262
- def _validate_css_content_inline(self, content: str) -> Dict[str, Any]:
1263
- """Validate CSS content for TypeScript/JavaScript code (Issue #1002).
1264
-
1265
- This is an inline version of the CSS validation for use during LLM
1266
- code generation. It detects when the LLM accidentally generates
1267
- TypeScript/JSX code instead of CSS.
1268
-
1269
- Args:
1270
- content: File content to validate
1271
-
1272
- Returns:
1273
- Dictionary with errors (blocking) and warnings
1274
- """
1275
- import re
1276
-
1277
- errors = []
1278
- warnings = []
1279
-
1280
- # CRITICAL: Detect TypeScript/JavaScript code in CSS files
1281
- # These patterns indicate wrong file content - always invalid
1282
- typescript_indicators = [
1283
- (r"^\s*import\s+.*from", "import statement"),
1284
- (r"^\s*export\s+(default|const|function|class|async)", "export statement"),
1285
- (r'"use client"|\'use client\'', "React client directive"),
1286
- (r"^\s*interface\s+\w+", "TypeScript interface"),
1287
- (r"^\s*type\s+\w+\s*=", "TypeScript type alias"),
1288
- (r"^\s*const\s+\w+\s*[=:]", "const declaration"),
1289
- (r"^\s*let\s+\w+\s*[=:]", "let declaration"),
1290
- (r"^\s*function\s+\w+", "function declaration"),
1291
- (r"^\s*async\s+function", "async function"),
1292
- (r"<[A-Z][a-zA-Z]*[\s/>]", "JSX component tag"),
1293
- (r"useState|useEffect|useRouter|usePathname", "React hook"),
1294
- ]
1295
-
1296
- for pattern, description in typescript_indicators:
1297
- if re.search(pattern, content, re.MULTILINE):
1298
- errors.append(
1299
- f"CRITICAL - CSS file contains {description}. "
1300
- f"This is TypeScript/JSX code, not CSS."
1301
- )
1302
-
1303
- # Check for balanced braces
1304
- if content.count("{") != content.count("}"):
1305
- errors.append("Mismatched braces in CSS")
1306
-
1307
- # Check for Tailwind directives
1308
- has_tailwind = "@tailwind" in content or '@import "tailwindcss' in content
1309
- if not has_tailwind and len(content.strip()) > 50:
1310
- warnings.append(
1311
- "Missing Tailwind directives (@tailwind base/components/utilities)"
1312
- )
1313
-
1314
- return {
1315
- "errors": errors,
1316
- "warnings": warnings,
1317
- "is_valid": len(errors) == 0,
1318
- }
1319
-
1320
- def _execute_item_with_recovery(
1321
- self,
1322
- item: ChecklistItem,
1323
- context: UserContext,
1324
- max_attempts: int = 3,
1325
- ) -> ItemExecutionResult:
1326
- """Execute a checklist item with error recovery.
1327
-
1328
- Uses the three-tier recovery strategy via ErrorHandler:
1329
- 1. RETRY: Simple retry (transient errors)
1330
- 2. FIX_AND_RETRY: LLM fixes code then retry
1331
- 3. ESCALATE: LLM rewrites from scratch
1332
- 4. ABORT: Give up after max attempts
1333
-
1334
- Args:
1335
- item: Checklist item to execute
1336
- context: User context
1337
- max_attempts: Maximum recovery attempts (default 3)
1338
-
1339
- Returns:
1340
- ItemExecutionResult from execution (or recovery attempts)
1341
- """
1342
- last_result = None
1343
-
1344
- for attempt in range(max_attempts):
1345
- try:
1346
- # Execute the item
1347
- result = self._execute_item(item, context)
1348
- last_result = result
1349
-
1350
- if result.success:
1351
- # Reset retry count on success
1352
- if self.error_handler:
1353
- self.error_handler.reset_retry_count(item.template)
1354
- return result
1355
-
1356
- # No error handler - return failure immediately
1357
- if not self.error_handler:
1358
- logger.warning(
1359
- f"No error handler available for {item.template}, "
1360
- f"cannot retry: {result.error}"
1361
- )
1362
- return result
1363
-
1364
- # Last attempt - return failure
1365
- if attempt >= max_attempts - 1:
1366
- logger.error(
1367
- f"Max attempts ({max_attempts}) exceeded for {item.template}"
1368
- )
1369
- return result
1370
-
1371
- # Handle failure with error handler
1372
- logger.info(
1373
- f"Attempting recovery for {item.template} "
1374
- f"(attempt {attempt + 1}/{max_attempts}): {result.error}"
1375
- )
1376
-
1377
- action, fix_info = self.error_handler.handle_error(
1378
- item.template,
1379
- result.error or "Unknown error",
1380
- {
1381
- "code": "", # Tool output doesn't include code
1382
- "project_dir": context.project_dir,
1383
- },
1384
- )
1385
-
1386
- if action == RecoveryAction.ABORT:
1387
- logger.error(f"Recovery aborted for {item.template}")
1388
- return result
1389
-
1390
- if action == RecoveryAction.RETRY:
1391
- logger.info(
1392
- f"Retrying {item.template} "
1393
- f"(attempt {attempt + 2}/{max_attempts})"
1394
- )
1395
- continue
1396
-
1397
- if action in (RecoveryAction.FIX_AND_RETRY, RecoveryAction.ESCALATE):
1398
- if fix_info:
1399
- logger.info(
1400
- f"Fix applied for {item.template}: {fix_info[:100]}..."
1401
- )
1402
- logger.info(
1403
- f"Retrying {item.template} after fix "
1404
- f"(attempt {attempt + 2}/{max_attempts})"
1405
- )
1406
- continue
1407
-
1408
- except Exception as e:
1409
- logger.exception(
1410
- f"Exception in {item.template} (attempt {attempt + 1})"
1411
- )
1412
- last_result = ItemExecutionResult(
1413
- template=item.template,
1414
- params=item.params,
1415
- description=item.description,
1416
- success=False,
1417
- error=str(e),
1418
- error_recoverable=True,
1419
- )
1420
-
1421
- # Last attempt - return exception result
1422
- if attempt >= max_attempts - 1:
1423
- last_result.error_recoverable = False
1424
- return last_result
1425
-
1426
- # Should not reach here, but return last result just in case
1427
- if last_result:
1428
- return last_result
1429
-
1430
- return ItemExecutionResult(
1431
- template=item.template,
1432
- params=item.params,
1433
- description=item.description,
1434
- success=False,
1435
- error=f"Max attempts ({max_attempts}) exceeded",
1436
- error_recoverable=False,
1437
- )
1438
-
1439
- def _build_params(
1440
- self,
1441
- item: ChecklistItem,
1442
- context: UserContext,
1443
- ) -> Dict[str, Any]:
1444
- """Build tool parameters from checklist item and context.
1445
-
1446
- Args:
1447
- item: Checklist item
1448
- context: User context
1449
-
1450
- Returns:
1451
- Dictionary of tool parameters
1452
- """
1453
- params = dict(item.params)
1454
- tool_name = TEMPLATE_TO_TOOL.get(item.template, item.template)
1455
-
1456
- # Handle CLI command templates specially
1457
- if item.template == "create_next_app":
1458
- # Convert to run_cli_command format
1459
- return {
1460
- "command": (
1461
- f"npx -y create-next-app@{NEXTJS_VERSION} . "
1462
- "--typescript --tailwind --eslint --app --src-dir --import-alias '@/*' --yes"
1463
- ),
1464
- "working_dir": context.project_dir,
1465
- "timeout": 1200,
1466
- }
1467
-
1468
- if item.template == "run_tests":
1469
- # Convert to run_cli_command format
1470
- return {
1471
- "command": "npm test",
1472
- "working_dir": context.project_dir,
1473
- "timeout": 1200,
1474
- }
1475
-
1476
- if item.template == "prisma_db_sync":
1477
- # Generate Prisma client and push schema to database
1478
- # This MUST run after generate_prisma_model and before API routes
1479
- return {
1480
- "command": "npx -y prisma generate && npx -y prisma db push",
1481
- "working_dir": context.project_dir,
1482
- "timeout": 1200,
1483
- }
1484
-
1485
- # Handle setup_prisma specially - needs to initialize Prisma first
1486
- if item.template == "setup_prisma":
1487
- return self._build_setup_prisma_params(item, context)
1488
-
1489
- # Handle generate_react_component specially - needs component_name derivation
1490
- if item.template == "generate_react_component":
1491
- return self._build_react_component_params(item, context)
1492
-
1493
- # Add project_dir only if tool expects it
1494
- if "project_dir" not in params and self._tool_accepts_parameter(
1495
- tool_name, "project_dir"
1496
- ):
1497
- params["project_dir"] = context.project_dir
1498
-
1499
- # Map checklist param names to tool param names
1500
- param_mapping = {
1501
- "resource": "resource_name", # generate_api_route -> manage_api_endpoint
1502
- "model_name": "model_name", # stays the same
1503
- "variant": "variant", # stays the same
1504
- }
1505
-
1506
- # Apply mappings
1507
- for checklist_name, tool_name in param_mapping.items():
1508
- if checklist_name in params and checklist_name != tool_name:
1509
- params[tool_name] = params.pop(checklist_name)
1510
-
1511
- # Handle specific template parameters
1512
- template_def = get_template(item.template)
1513
- if template_def:
1514
- # Add entity name for data models
1515
- if item.template == "generate_prisma_model" and "model_name" in params:
1516
- context.entity_name = params["model_name"]
1517
-
1518
- # Handle API route type
1519
- if item.template == "generate_api_route":
1520
- route_type = params.pop("type", "collection")
1521
- if route_type == "item":
1522
- # For item routes, set operations appropriately
1523
- if "operations" not in params:
1524
- params["operations"] = ["GET", "PATCH", "DELETE"]
1525
-
1526
- return params
1527
-
1528
- def _build_setup_prisma_params(
1529
- self,
1530
- item: ChecklistItem, # pylint: disable=unused-argument
1531
- context: UserContext,
1532
- ) -> Dict[str, Any]:
1533
- """Build parameters for Prisma initialization.
1534
-
1535
- The setup_prisma template needs to:
1536
- 1. Initialize Prisma with SQLite (npx prisma init)
1537
- 2. Create the singleton file (src/lib/prisma.ts)
1538
-
1539
- The CLI commands run via shell, but the singleton file is written
1540
- via Python's pathlib in _execute_deterministic() for cross-platform
1541
- compatibility (Windows doesn't support Unix shell file operations).
1542
-
1543
- Args:
1544
- item: Checklist item (unused, kept for consistency)
1545
- context: User context
1546
-
1547
- Returns:
1548
- Dictionary of tool parameters for run_cli_command
1549
- """
1550
- # Only run CLI commands - file writing is handled separately in
1551
- # _execute_deterministic() via _write_prisma_singleton() for
1552
- # cross-platform compatibility (mkdir -p and echo don't work on Windows)
1553
- command = (
1554
- "npm install prisma@5 @prisma/client@5 zod && "
1555
- "npx -y prisma init --datasource-provider sqlite"
1556
- )
1557
-
1558
- return {
1559
- "command": command,
1560
- "working_dir": context.project_dir,
1561
- "timeout": 1200,
1562
- }
1563
-
1564
- def _write_prisma_singleton(self, project_dir: str) -> None:
1565
- """Write Prisma singleton file using cross-platform Python.
1566
-
1567
- This method is called after setup_prisma CLI commands succeed.
1568
- We use Python's pathlib instead of shell commands (mkdir -p, echo)
1569
- because those Unix commands don't work on Windows.
1570
-
1571
- Args:
1572
- project_dir: Project root directory
1573
- """
1574
- from pathlib import Path
1575
-
1576
- singleton_content = """import { PrismaClient } from "@prisma/client";
1577
-
1578
- const globalForPrisma = globalThis as unknown as {
1579
- prisma: PrismaClient | undefined;
1580
- };
1581
-
1582
- export const prisma = globalForPrisma.prisma ?? new PrismaClient();
1583
-
1584
- if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
1585
- """
1586
- lib_dir = Path(project_dir) / "src" / "lib"
1587
- lib_dir.mkdir(parents=True, exist_ok=True)
1588
- singleton_file = lib_dir / "prisma.ts"
1589
- singleton_file.write_text(singleton_content)
1590
- logger.debug(f"Created Prisma singleton at {singleton_file}")
1591
-
1592
- def _build_react_component_params(
1593
- self,
1594
- item: ChecklistItem,
1595
- context: UserContext,
1596
- ) -> Dict[str, Any]:
1597
- """Build parameters for manage_react_component tool.
1598
-
1599
- The template catalog defines:
1600
- - resource: Resource name (lowercase, singular)
1601
- - variant: Component variant (list|form|new|detail|actions)
1602
- - with_checkboxes: Boolean (optional, not supported by tool)
1603
-
1604
- The tool expects:
1605
- - project_dir: Path to project
1606
- - component_name: Component name (e.g., "TodoList", "UserForm")
1607
- - component_type: "server" or "client"
1608
- - resource_name: Associated resource
1609
- - fields: Resource fields (optional)
1610
- - variant: Component variant
1611
-
1612
- Args:
1613
- item: Checklist item with template params
1614
- context: User context
1615
-
1616
- Returns:
1617
- Dictionary of tool parameters
1618
- """
1619
- template_params = dict(item.params)
1620
-
1621
- # Extract resource and variant
1622
- resource = template_params.get("resource", "")
1623
- variant = template_params.get("variant", "list")
1624
-
1625
- # Generate component_name from resource + variant
1626
- # Allow caller to provide an explicit component_name (used for timers)
1627
- resource_capitalized = resource.capitalize() if resource else "Item"
1628
- variant_capitalized = variant.capitalize() if variant else "List"
1629
- explicit_component = template_params.get("component_name")
1630
-
1631
- # Build component name based on variant
1632
- if explicit_component:
1633
- component_name = explicit_component
1634
- elif variant == "list":
1635
- component_name = f"{resource_capitalized}List"
1636
- elif variant == "form":
1637
- component_name = f"{resource_capitalized}Form"
1638
- elif variant == "new":
1639
- component_name = f"New{resource_capitalized}"
1640
- elif variant == "detail":
1641
- component_name = f"{resource_capitalized}Detail"
1642
- elif variant == "actions":
1643
- component_name = f"{resource_capitalized}Actions"
1644
- elif variant == "artifact-timer":
1645
- component_name = f"{resource_capitalized}Timer"
1646
- else:
1647
- component_name = f"{resource_capitalized}{variant_capitalized}"
1648
-
1649
- # Determine component_type based on variant
1650
- # list pages are server components, forms and interactive pages are client
1651
- if variant in ("list",):
1652
- component_type = "server"
1653
- else:
1654
- component_type = "client"
1655
-
1656
- # Build the actual tool params
1657
- tool_params = {
1658
- "project_dir": context.project_dir,
1659
- "component_name": component_name,
1660
- "component_type": component_type,
1661
- "resource_name": resource,
1662
- "variant": variant,
1663
- }
1664
-
1665
- # Add fields from context if available
1666
- if context.schema_fields:
1667
- tool_params["fields"] = context.schema_fields
1668
-
1669
- # Note: with_checkboxes is NOT passed - tool doesn't support it
1670
- # The variant and resource determine the component behavior
1671
-
1672
- return tool_params
1673
-
1674
- def _parse_tool_result(
1675
- self,
1676
- item: ChecklistItem,
1677
- raw_result: Any,
1678
- ) -> ItemExecutionResult:
1679
- """Parse raw tool result into ItemExecutionResult.
1680
-
1681
- Args:
1682
- item: Original checklist item
1683
- raw_result: Raw result from tool execution
1684
-
1685
- Returns:
1686
- ItemExecutionResult
1687
- """
1688
- # Handle different result types
1689
- if isinstance(raw_result, StepResult):
1690
- return ItemExecutionResult(
1691
- template=item.template,
1692
- params=item.params,
1693
- description=item.description,
1694
- success=raw_result.success,
1695
- files=raw_result.output.get("files", []),
1696
- warnings=raw_result.output.get("warnings", []),
1697
- error=raw_result.error_message,
1698
- error_recoverable=raw_result.retryable,
1699
- output=raw_result.output,
1700
- )
1701
-
1702
- if isinstance(raw_result, dict):
1703
- success = raw_result.get("success", True)
1704
- return ItemExecutionResult(
1705
- template=item.template,
1706
- params=item.params,
1707
- description=item.description,
1708
- success=success,
1709
- files=raw_result.get("files", []),
1710
- warnings=raw_result.get("warnings", []),
1711
- error=raw_result.get("error"),
1712
- error_recoverable=raw_result.get("retryable", True),
1713
- output=raw_result,
1714
- )
1715
-
1716
- # Unknown result type - treat as success if truthy
1717
- return ItemExecutionResult(
1718
- template=item.template,
1719
- params=item.params,
1720
- description=item.description,
1721
- success=bool(raw_result),
1722
- output={"raw": raw_result},
1723
- )
1724
-
1725
- def _report_progress(self, description: str, current: int, total: int) -> None:
1726
- """Report progress via callback if available.
1727
-
1728
- Args:
1729
- description: Current item description
1730
- current: Current item number
1731
- total: Total items
1732
- """
1733
- # Log at debug level to avoid duplicate console output (checklist state is already printed)
1734
- logger.debug(f"[{current}/{total}] {description}")
1735
- if self.progress_callback:
1736
- self.progress_callback(description, current, total)
1737
-
1738
- def _handle_step_through(self, description: str) -> bool:
1739
- """Handle step-through pause.
1740
-
1741
- Args:
1742
- description: Description of the completed step
1743
-
1744
- Returns:
1745
- True to continue, False to stop
1746
- """
1747
- # Check for TTY to avoid hanging in non-interactive modes
1748
- if not sys.stdin or not sys.stdin.isatty():
1749
- # In non-interactive mode, log and continue
1750
- logger.debug(
1751
- f"Step-through enabled but no TTY. Continuing after: {description}"
1752
- )
1753
- return True
1754
-
1755
- self.console.print_step_paused(description)
1756
-
1757
- try:
1758
- response = input("> ").strip().lower()
1759
- if response in ["n", "no", "q", "quit", "exit"]:
1760
- return False
1761
- return True
1762
- except (EOFError, KeyboardInterrupt):
1763
- return False
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+ """Checklist Executor for LLM-Driven Code Generation.
4
+
5
+ This module executes checklist items generated by ChecklistGenerator.
6
+ For code-generating templates, the LLM is invoked per item to produce
7
+ contextual, high-quality code. CLI commands remain deterministic.
8
+
9
+ The executor:
10
+ 1. Receives a GeneratedChecklist from ChecklistGenerator
11
+ 2. For each item, routes to LLM generation or deterministic execution
12
+ 3. Tracks results and handles errors with recovery
13
+ 4. Returns a complete ExecutionResult
14
+
15
+ Error handling follows the three-tier strategy:
16
+ 1. RETRY: Simple retries for transient errors
17
+ 2. FIX_AND_RETRY: Auto-fix and retry for known issues
18
+ 3. ABORT: Stop execution for unrecoverable errors
19
+ """
20
+
21
+ import inspect
22
+ import json
23
+ import logging
24
+ import os
25
+ import sys
26
+ from dataclasses import dataclass, field
27
+ from typing import Any, Callable, Dict, List, Optional, Protocol
28
+
29
+ from gaia.agents.base.console import AgentConsole
30
+ from gaia.agents.base.tools import _TOOL_REGISTRY
31
+
32
+ from .checklist_generator import ChecklistItem, GeneratedChecklist
33
+ from .steps.base import StepResult, ToolExecutor, UserContext
34
+ from .steps.error_handler import ErrorHandler, RecoveryAction
35
+ from .template_catalog import get_template
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class ChatSDK(Protocol):
41
+ """Protocol for chat SDK interface used by LLM code generation."""
42
+
43
+ def send(self, message: str, timeout: int = 600, no_history: bool = False) -> Any:
44
+ """Send a message and get response."""
45
+ ...
46
+
47
+ def send_stream(self, message: str, **kwargs) -> Any:
48
+ """Send a message and get streaming response."""
49
+ ...
50
+
51
+
52
+ # ============================================================================
53
+ # Template Classification
54
+ # ============================================================================
55
+
56
+ # Templates that execute CLI commands - remain deterministic (no LLM)
57
+ DETERMINISTIC_TEMPLATES = {
58
+ "create_next_app",
59
+ "setup_prisma",
60
+ "prisma_db_sync",
61
+ "setup_testing",
62
+ "run_typescript_check",
63
+ "validate_styles",
64
+ "generate_style_tests",
65
+ "run_tests",
66
+ "fix_code",
67
+ }
68
+
69
+ # Templates that generate code files - use LLM for contextual generation
70
+ # NOTE: generate_prisma_model is NOT here because it must APPEND to schema.prisma,
71
+ # not overwrite it. The manage_data_model tool handles this correctly.
72
+ LLM_GENERATED_TEMPLATES = {
73
+ "generate_react_component",
74
+ "generate_api_route",
75
+ "setup_app_styling",
76
+ "update_landing_page",
77
+ }
78
+
79
+ # Template metadata for validation of LLM-generated code
80
+ TEMPLATE_METADATA: Dict[str, Dict[str, Any]] = {
81
+ "generate_react_component": {
82
+ "list": {
83
+ "requires_client": False,
84
+ "expected_classes": ["page-title", "btn-primary"],
85
+ "file_pattern": "src/app/{resource}s/page.tsx",
86
+ },
87
+ "form": {
88
+ "requires_client": True,
89
+ "expected_classes": [
90
+ "input-field",
91
+ "btn-primary",
92
+ "btn-secondary",
93
+ ],
94
+ "file_pattern": "src/components/{Resource}Form.tsx",
95
+ },
96
+ "new": {
97
+ "requires_client": True,
98
+ "expected_classes": ["page-title", "link-back"],
99
+ "file_pattern": "src/app/{resource}s/new/page.tsx",
100
+ },
101
+ "detail": {
102
+ "requires_client": True,
103
+ "expected_classes": [
104
+ "page-title",
105
+ "btn-primary",
106
+ "btn-danger",
107
+ ],
108
+ "file_pattern": "src/app/{resource}s/[id]/page.tsx",
109
+ },
110
+ "artifact-timer": {
111
+ "requires_client": True,
112
+ "expected_classes": [
113
+ "glass-card",
114
+ ],
115
+ },
116
+ },
117
+ "generate_api_route": {
118
+ "collection": {
119
+ "requires_client": False,
120
+ "expected_classes": [],
121
+ "file_pattern": "src/app/api/{resource}s/route.ts",
122
+ },
123
+ "item": {
124
+ "requires_client": False,
125
+ "expected_classes": [],
126
+ "file_pattern": "src/app/api/{resource}s/[id]/route.ts",
127
+ },
128
+ },
129
+ # NOTE: generate_prisma_model removed - uses tool-based execution, not LLM
130
+ "setup_app_styling": {
131
+ "default": {
132
+ "requires_client": False,
133
+ "expected_classes": ["page-title", "btn-primary"],
134
+ "file_pattern": "src/app/globals.css",
135
+ },
136
+ },
137
+ "update_landing_page": {
138
+ "default": {
139
+ "requires_client": False,
140
+ "expected_classes": ["page-title"],
141
+ "file_pattern": "src/app/page.tsx",
142
+ },
143
+ },
144
+ }
145
+
146
+ # Templates whose results should be logged for downstream validation/QA prompts
147
+ VALIDATION_TEMPLATES = {
148
+ "run_typescript_check",
149
+ "validate_styles",
150
+ "run_tests",
151
+ }
152
+
153
+
154
+ @dataclass
155
+ class ItemExecutionResult:
156
+ """Result of executing a single checklist item."""
157
+
158
+ template: str
159
+ params: Dict[str, Any]
160
+ description: str
161
+ success: bool
162
+ files: List[str] = field(default_factory=list)
163
+ warnings: List[str] = field(default_factory=list)
164
+ error: Optional[str] = None
165
+ error_recoverable: bool = True
166
+ output: Dict[str, Any] = field(default_factory=dict)
167
+
168
+ def to_dict(self) -> Dict[str, Any]:
169
+ """Convert to dictionary representation."""
170
+ return {
171
+ "template": self.template,
172
+ "params": self.params,
173
+ "description": self.description,
174
+ "success": self.success,
175
+ "files": self.files,
176
+ "warnings": self.warnings,
177
+ "error": self.error,
178
+ }
179
+
180
+
181
+ @dataclass
182
+ class ValidationLogEntry:
183
+ """Structured log entry for validation/test steps."""
184
+
185
+ template: str
186
+ description: str
187
+ success: bool
188
+ error: Optional[str]
189
+ output: Dict[str, Any] = field(default_factory=dict)
190
+ files: List[str] = field(default_factory=list)
191
+
192
+ def to_dict(self) -> Dict[str, Any]:
193
+ """Convert to plain dictionary for serialization."""
194
+ return {
195
+ "template": self.template,
196
+ "description": self.description,
197
+ "success": self.success,
198
+ "error": self.error,
199
+ "files": self.files,
200
+ "output": self.output,
201
+ }
202
+
203
+
204
+ @dataclass
205
+ class ChecklistExecutionResult:
206
+ """Result of executing a complete checklist."""
207
+
208
+ checklist: GeneratedChecklist
209
+ item_results: List[ItemExecutionResult] = field(default_factory=list)
210
+ success: bool = True
211
+ total_files: List[str] = field(default_factory=list)
212
+ errors: List[str] = field(default_factory=list)
213
+ warnings: List[str] = field(default_factory=list)
214
+ validation_logs: List[ValidationLogEntry] = field(default_factory=list)
215
+
216
+ @property
217
+ def items_succeeded(self) -> int:
218
+ """Count of successfully executed items."""
219
+ return sum(1 for r in self.item_results if r.success)
220
+
221
+ @property
222
+ def items_failed(self) -> int:
223
+ """Count of failed items."""
224
+ return sum(1 for r in self.item_results if not r.success)
225
+
226
+ @property
227
+ def summary(self) -> str:
228
+ """Human-readable summary of execution."""
229
+ status = "SUCCESS" if self.success else "FAILED"
230
+ return (
231
+ f"{status}: {self.items_succeeded}/{len(self.item_results)} items completed, "
232
+ f"{len(self.total_files)} files created"
233
+ )
234
+
235
+ def to_dict(self) -> Dict[str, Any]:
236
+ """Convert to dictionary representation."""
237
+ return {
238
+ "success": self.success,
239
+ "summary": self.summary,
240
+ "reasoning": self.checklist.reasoning,
241
+ "items": [r.to_dict() for r in self.item_results],
242
+ "files": self.total_files,
243
+ "errors": self.errors,
244
+ "warnings": self.warnings,
245
+ "validation_logs": [log.to_dict() for log in self.validation_logs],
246
+ }
247
+
248
+
249
+ # Template to tool mapping
250
+ TEMPLATE_TO_TOOL: Dict[str, str] = {
251
+ "create_next_app": "run_cli_command", # Uses npx create-next-app
252
+ "setup_app_styling": "setup_app_styling",
253
+ "setup_prisma": "run_cli_command", # Uses npx prisma init + creates singleton
254
+ "prisma_db_sync": "run_cli_command", # Uses npx prisma generate && npx prisma db push
255
+ "setup_testing": "setup_nextjs_testing",
256
+ "generate_prisma_model": "manage_data_model",
257
+ "generate_api_route": "manage_api_endpoint",
258
+ "generate_react_component": "manage_react_component",
259
+ "update_landing_page": "update_landing_page",
260
+ "run_typescript_check": "validate_typescript",
261
+ "validate_styles": "validate_styles", # CSS/design system validation (Issue #1002)
262
+ "generate_style_tests": "generate_style_tests", # Generate CSS tests
263
+ "run_tests": "run_cli_command", # Uses npm test
264
+ "fix_code": "fix_code",
265
+ }
266
+
267
+ # Next.js version for create-next-app
268
+ NEXTJS_VERSION = "14.2.33"
269
+
270
+
271
+ class ChecklistExecutor:
272
+ """Execute checklist items with LLM-driven code generation.
273
+
274
+ For code-generating templates, the LLM is invoked per item to produce
275
+ contextual, high-quality code. CLI commands remain deterministic.
276
+
277
+ Template routing:
278
+ - DETERMINISTIC_TEMPLATES: Execute CLI commands directly (no LLM)
279
+ - LLM_GENERATED_TEMPLATES: Use LLM to generate code with template as guidance
280
+ - Fallback: Use tool executor for unknown templates
281
+
282
+ Error recovery uses the three-tier strategy via ErrorHandler:
283
+ 1. RETRY: Simple retry for transient errors
284
+ 2. FIX_AND_RETRY: LLM fixes code then retry
285
+ 3. ESCALATE: LLM rewrites from scratch
286
+ 4. ABORT: Give up after max attempts
287
+ """
288
+
289
+ def __init__(
290
+ self,
291
+ tool_executor: ToolExecutor,
292
+ llm_client: Optional[ChatSDK] = None,
293
+ error_handler: Optional[ErrorHandler] = None,
294
+ progress_callback: Optional[Callable[[str, int, int], None]] = None,
295
+ console: Optional[AgentConsole] = None,
296
+ ):
297
+ """Initialize the checklist executor.
298
+
299
+ Args:
300
+ tool_executor: Function to execute tools (name, args) -> result
301
+ llm_client: Optional LLM client for code generation (enables per-item LLM)
302
+ error_handler: Optional error handler for recovery (enables retries)
303
+ progress_callback: Optional callback(item_desc, current, total)
304
+ console: Optional console for displaying output
305
+ """
306
+ self.tool_executor = tool_executor
307
+ self.llm_client = llm_client
308
+ self.error_handler = error_handler
309
+ self.progress_callback = progress_callback
310
+ self.console = console or AgentConsole()
311
+ self._tool_signature_cache: Dict[str, inspect.Signature] = {}
312
+
313
+ def _tool_accepts_parameter(self, tool_name: str, parameter: str) -> bool:
314
+ """Return True if the tool accepts the provided parameter."""
315
+ tool_entry = _TOOL_REGISTRY.get(tool_name)
316
+ if not tool_entry:
317
+ return False
318
+
319
+ if tool_name in self._tool_signature_cache:
320
+ signature = self._tool_signature_cache[tool_name]
321
+ else:
322
+ tool_func = tool_entry.get("function")
323
+ if not tool_func:
324
+ return False
325
+ try:
326
+ signature = inspect.signature(tool_func)
327
+ except (TypeError, ValueError):
328
+ return False
329
+ self._tool_signature_cache[tool_name] = signature
330
+
331
+ if parameter in signature.parameters:
332
+ return True
333
+
334
+ return any(
335
+ param.kind == inspect.Parameter.VAR_KEYWORD
336
+ for param in signature.parameters.values()
337
+ )
338
+
339
+ def execute(
340
+ self,
341
+ checklist: GeneratedChecklist,
342
+ context: UserContext,
343
+ stop_on_error: bool = True,
344
+ step_through: bool = False,
345
+ ) -> ChecklistExecutionResult:
346
+ """Execute all checklist items in order.
347
+
348
+ Args:
349
+ checklist: Checklist to execute
350
+ context: User context with project info
351
+ stop_on_error: Whether to stop on first critical error
352
+ step_through: Enable step-through debugging
353
+
354
+ Returns:
355
+ ChecklistExecutionResult with all item results
356
+ """
357
+ logger.debug(
358
+ f"Executing checklist with {len(checklist.items)} items: "
359
+ f"{checklist.reasoning}"
360
+ )
361
+
362
+ self.console.print_checklist_reasoning(checklist.reasoning)
363
+
364
+ result = ChecklistExecutionResult(checklist=checklist)
365
+
366
+ # Check for validation errors first
367
+ if not checklist.is_valid:
368
+ logger.error(
369
+ f"Checklist has validation errors: {checklist.validation_errors}"
370
+ )
371
+ result.success = False
372
+ result.errors.extend(checklist.validation_errors)
373
+ return result
374
+
375
+ # Use items in the order generated by LLM
376
+ ordered_items = checklist.items
377
+ total = len(ordered_items)
378
+
379
+ # Execute each item with error recovery
380
+ for idx, item in enumerate(ordered_items, 1):
381
+ self.console.print_checklist(ordered_items, idx - 1)
382
+ self._report_progress(item.description, idx, total)
383
+
384
+ # Use recovery wrapper if error_handler is available
385
+ item_result = self._execute_item_with_recovery(item, context)
386
+ result.item_results.append(item_result)
387
+
388
+ # Capture validation/test output for downstream prompts
389
+ if item.template in VALIDATION_TEMPLATES:
390
+ result.validation_logs.append(
391
+ ValidationLogEntry(
392
+ template=item.template,
393
+ description=item.description,
394
+ success=item_result.success,
395
+ error=item_result.error,
396
+ output=item_result.output,
397
+ files=item_result.files,
398
+ )
399
+ )
400
+
401
+ # Collect files
402
+ result.total_files.extend(item_result.files)
403
+
404
+ # Handle errors
405
+ if not item_result.success:
406
+ result.errors.append(item_result.error or "Unknown error")
407
+
408
+ if stop_on_error and not item_result.error_recoverable:
409
+ logger.error(
410
+ f"Stopping execution due to critical error in "
411
+ f"{item.template}: {item_result.error}"
412
+ )
413
+ result.success = False
414
+ break
415
+
416
+ # Collect warnings
417
+ result.warnings.extend(item_result.warnings)
418
+
419
+ # Handle step-through
420
+ if step_through:
421
+ if not self._handle_step_through(item.description):
422
+ logger.info("Execution stopped by user during step-through")
423
+ break
424
+
425
+ # Update overall success
426
+ if result.items_failed > 0:
427
+ result.success = False
428
+
429
+ logger.info(result.summary)
430
+ return result
431
+
432
+ def _execute_item(
433
+ self,
434
+ item: ChecklistItem,
435
+ context: UserContext,
436
+ ) -> ItemExecutionResult:
437
+ """Execute a single checklist item with routing.
438
+
439
+ Routes execution based on template type:
440
+ - DETERMINISTIC_TEMPLATES: Execute CLI commands directly (no LLM)
441
+ - LLM_GENERATED_TEMPLATES: Use LLM to generate code (if llm_client available)
442
+ - Fallback: Use tool executor for unknown templates
443
+
444
+ Args:
445
+ item: Checklist item to execute
446
+ context: User context
447
+
448
+ Returns:
449
+ ItemExecutionResult
450
+ """
451
+ logger.debug(f"Executing: {item.template} - {item.description}")
452
+
453
+ try:
454
+ # Route 1: Deterministic templates (CLI commands) - no LLM needed
455
+ if item.template in DETERMINISTIC_TEMPLATES:
456
+ logger.debug(f"Deterministic execution for {item.template}")
457
+ return self._execute_deterministic(item, context)
458
+
459
+ # Route 2: LLM-generated templates - use LLM for code generation
460
+ if item.template in LLM_GENERATED_TEMPLATES and self.llm_client:
461
+ logger.info(f"LLM code generation for {item.template}")
462
+ return self._execute_with_llm(item, context)
463
+
464
+ # Route 3: Fallback to tool execution (no LLM or unknown template)
465
+ logger.debug(f"Fallback tool execution for {item.template}")
466
+ return self._execute_via_tool(item, context)
467
+
468
+ except Exception as e:
469
+ logger.exception(f"Exception executing {item.template}")
470
+ return ItemExecutionResult(
471
+ template=item.template,
472
+ params=item.params,
473
+ description=item.description,
474
+ success=False,
475
+ error=str(e),
476
+ error_recoverable=False,
477
+ )
478
+
479
+ def _execute_deterministic(
480
+ self,
481
+ item: ChecklistItem,
482
+ context: UserContext,
483
+ ) -> ItemExecutionResult:
484
+ """Execute a deterministic template (CLI command).
485
+
486
+ These templates don't need LLM - they run predefined commands.
487
+
488
+ Args:
489
+ item: Checklist item to execute
490
+ context: User context
491
+
492
+ Returns:
493
+ ItemExecutionResult
494
+ """
495
+ # Map template to tool name
496
+ tool_name = TEMPLATE_TO_TOOL.get(item.template, item.template)
497
+
498
+ # Build params for CLI execution
499
+ params = self._build_params(item, context)
500
+
501
+ logger.debug(f"Calling tool '{tool_name}' with params: {params}")
502
+
503
+ # Execute the tool
504
+ raw_result = self.tool_executor(tool_name, params)
505
+
506
+ # Parse result
507
+ result = self._parse_tool_result(item, raw_result)
508
+
509
+ # Post-command file operations (cross-platform, avoids shell file writing)
510
+ if result.success and item.template == "setup_prisma":
511
+ self._write_prisma_singleton(context.project_dir)
512
+
513
+ return result
514
+
515
+ def _execute_via_tool(
516
+ self,
517
+ item: ChecklistItem,
518
+ context: UserContext,
519
+ ) -> ItemExecutionResult:
520
+ """Execute via tool executor (fallback when no LLM).
521
+
522
+ Args:
523
+ item: Checklist item to execute
524
+ context: User context
525
+
526
+ Returns:
527
+ ItemExecutionResult
528
+ """
529
+ # Map template to tool name
530
+ tool_name = TEMPLATE_TO_TOOL.get(item.template, item.template)
531
+
532
+ # Build params with project_dir
533
+ params = self._build_params(item, context)
534
+
535
+ logger.debug(f"Calling tool '{tool_name}' with params: {params}")
536
+
537
+ # Execute the tool
538
+ raw_result = self.tool_executor(tool_name, params)
539
+
540
+ # Parse result
541
+ return self._parse_tool_result(item, raw_result)
542
+
543
+ def _execute_with_llm(
544
+ self,
545
+ item: ChecklistItem,
546
+ context: UserContext,
547
+ ) -> ItemExecutionResult:
548
+ """Execute a checklist item using LLM code generation.
549
+
550
+ This is the core of Phase 9 - LLM generates contextual code using
551
+ templates as structural guidance.
552
+
553
+ Args:
554
+ item: Checklist item to execute
555
+ context: User context
556
+
557
+ Returns:
558
+ ItemExecutionResult
559
+ """
560
+ logger.info(f"Generating code with LLM for {item.template}")
561
+
562
+ try:
563
+ # 1. Get template as guidance
564
+ template_guidance = self._get_template_guidance(item)
565
+ if not template_guidance:
566
+ logger.warning(
567
+ f"No template guidance for {item.template}, falling back to tool"
568
+ )
569
+ return self._execute_via_tool(item, context)
570
+
571
+ # 1b. Resolve field definitions for prompt + post-processing
572
+ resolved_fields = self._resolve_fields(item, context)
573
+
574
+ # 2. Build prompt
575
+ prompt = self._build_code_generation_prompt(
576
+ item, context, template_guidance, resolved_fields
577
+ )
578
+
579
+ # 3. Call LLM
580
+ logger.debug(f"Sending prompt to LLM ({len(prompt)} chars)")
581
+
582
+ file_path = self._determine_file_path(item, context)
583
+
584
+ # Start file preview
585
+ self.console.start_file_preview(file_path, max_lines=15)
586
+
587
+ # Stream the response
588
+ full_response = ""
589
+ try:
590
+ # Try streaming first if available
591
+ if hasattr(self.llm_client, "send_stream"):
592
+ for chunk in self.llm_client.send_stream(prompt, timeout=1200):
593
+ if hasattr(chunk, "text"):
594
+ text = chunk.text
595
+ self.console.update_file_preview(text)
596
+ full_response += text
597
+
598
+ if full_response.strip():
599
+ generated_code = full_response
600
+ else:
601
+ raise ValueError("Empty streaming response")
602
+ else:
603
+ # Fallback to non-streaming
604
+ response = self.llm_client.send(prompt, timeout=1200)
605
+ if hasattr(response, "text"):
606
+ generated_code = response.text
607
+ elif hasattr(response, "content"):
608
+ generated_code = response.content
609
+ else:
610
+ generated_code = str(response)
611
+
612
+ self.console.update_file_preview(generated_code)
613
+
614
+ except Exception as e:
615
+ # Fallback if streaming fails
616
+ logger.warning(f"Streaming failed, falling back to standard send: {e}")
617
+ response = self.llm_client.send(prompt, timeout=1200)
618
+ if hasattr(response, "text"):
619
+ generated_code = response.text
620
+ elif hasattr(response, "content"):
621
+ generated_code = response.content
622
+ else:
623
+ generated_code = str(response)
624
+
625
+ self.console.update_file_preview(generated_code)
626
+
627
+ # Stop file preview
628
+ self.console.stop_file_preview()
629
+
630
+ # 4. Clean response (strip markdown if present)
631
+ clean_code = self._clean_llm_response(generated_code)
632
+
633
+ # 5. Validate generated code
634
+ is_valid, issues, is_blocking = self._validate_generated_code(
635
+ clean_code, item
636
+ )
637
+ if not is_valid:
638
+ logger.warning(f"Validation issues for {item.template}: {issues}")
639
+
640
+ # CRITICAL: Block file write for blocking errors (Issue #1002)
641
+ # This prevents TypeScript code from being written to CSS files
642
+ if is_blocking:
643
+ logger.error(
644
+ f"BLOCKING validation error for {item.template}: {issues}"
645
+ )
646
+ return ItemExecutionResult(
647
+ template=item.template,
648
+ params=item.params,
649
+ description=item.description,
650
+ success=False,
651
+ error=f"Content validation failed: {'; '.join(issues)}",
652
+ error_recoverable=True, # Allow LLM retry with recovery
653
+ )
654
+ # Non-blocking issues: log warning and continue (best effort)
655
+
656
+ # 6. Write to file
657
+ full_path = os.path.join(context.project_dir, file_path)
658
+
659
+ # Create directory if needed
660
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
661
+
662
+ # Write the generated code
663
+ with open(full_path, "w", encoding="utf-8") as f:
664
+ f.write(clean_code)
665
+
666
+ logger.info(f"Wrote LLM-generated code to {file_path}")
667
+
668
+ generated_files = [file_path]
669
+
670
+ return ItemExecutionResult(
671
+ template=item.template,
672
+ params=item.params,
673
+ description=item.description,
674
+ success=True,
675
+ files=generated_files,
676
+ warnings=issues if not is_valid else [],
677
+ )
678
+
679
+ except Exception as e:
680
+ logger.exception(f"LLM generation failed for {item.template}")
681
+ return ItemExecutionResult(
682
+ template=item.template,
683
+ params=item.params,
684
+ description=item.description,
685
+ success=False,
686
+ error=str(e),
687
+ error_recoverable=True,
688
+ )
689
+
690
+ def _build_code_generation_prompt(
691
+ self,
692
+ item: ChecklistItem,
693
+ context: UserContext,
694
+ template_guidance: str,
695
+ fields_override: Optional[Dict[str, str]] = None,
696
+ ) -> str:
697
+ """Build prompt for LLM code generation.
698
+
699
+ Args:
700
+ item: Checklist item describing what to generate
701
+ context: User context with project info
702
+ template_guidance: Template structure as guidance
703
+
704
+ Returns:
705
+ Prompt string for LLM
706
+ """
707
+ # CSS templates need a different prompt - they should NOT generate TypeScript
708
+ css_templates = {"setup_app_styling"}
709
+ if item.template in css_templates:
710
+ return self._build_css_generation_prompt(item, template_guidance)
711
+
712
+ resource = item.params.get("resource", "item")
713
+ variant = item.params.get("variant", "default")
714
+ fields = (
715
+ fields_override
716
+ if fields_override is not None
717
+ else item.params.get("fields", context.schema_fields or {})
718
+ )
719
+
720
+ # Determine file type and output format
721
+ is_css_file = item.template == "setup_app_styling"
722
+ file_type = "CSS" if is_css_file else "TypeScript/TSX"
723
+ code_language = "css" if is_css_file else "typescript"
724
+ start_instruction = (
725
+ "Start immediately with @tailwind directives or CSS rules"
726
+ if is_css_file
727
+ else 'Start immediately with imports or "use client"'
728
+ )
729
+
730
+ # Get required classes for this variant
731
+ required_classes = self._get_required_classes(item)
732
+ required_classes_str = (
733
+ ", ".join(f"`{cls}`" for cls in required_classes)
734
+ if required_classes
735
+ else "None specified"
736
+ )
737
+
738
+ # Build architecture rules - skip TypeScript-specific rules for CSS
739
+ architecture_rules = ""
740
+ if not is_css_file:
741
+ architecture_rules = """## Architecture Rules
742
+ - Use Server Components by default for data fetching
743
+ - Add "use client" directive ONLY when using hooks, event handlers, or browser APIs
744
+ - Define explicit TypeScript types for all props and return values
745
+ - Use Prisma-generated types where applicable
746
+ - Never use `any` type
747
+
748
+ ---"""
749
+
750
+ # Add CSS-specific warning for CSS files
751
+ css_warning = ""
752
+ if is_css_file:
753
+ css_warning = """## CRITICAL: CSS File Requirements
754
+
755
+ This is a CSS file (globals.css). You MUST generate ONLY CSS code:
756
+ - NO TypeScript/JavaScript code
757
+ - NO import statements
758
+ - NO export statements
759
+ - NO const/let/function declarations
760
+ - NO JSX/React components
761
+ - NO TypeScript interfaces or types
762
+ - ONLY CSS rules, @tailwind directives, @layer directives, and CSS selectors
763
+
764
+ ---
765
+
766
+ """
767
+
768
+ return f"""You are an expert Next.js 14+ developer specializing in full-stack TypeScript applications.
769
+
770
+ {css_warning}## CRITICAL: Required CSS Classes
771
+
772
+ Your generated code MUST include these CSS classes:
773
+ {required_classes_str}
774
+
775
+ These classes are MANDATORY and will be validated. Code without them will fail validation.
776
+
777
+ ---
778
+
779
+ ## Task
780
+ Generate a {item.template} ({variant}) for the {resource} resource.
781
+
782
+ ### Purpose
783
+ {item.description}
784
+
785
+ ### User Request Context
786
+ {context.user_request}
787
+
788
+ ### Parameters
789
+ {json.dumps(item.params, indent=2)}
790
+
791
+ ### Data Model Fields
792
+ {json.dumps(fields, indent=2) if fields else "Not specified - use reasonable defaults based on resource name"}
793
+
794
+ ---
795
+
796
+ {architecture_rules}## Design System Classes (Dark Theme)
797
+
798
+ **Containers**: `glass-card` (ALWAYS use for main content container - glassmorphism effect)
799
+ **Typography**: `page-title` (ALWAYS use for h1 headers - gradient text)
800
+ **Buttons**: `btn-primary` (blue gradient), `btn-secondary` (outline), `btn-danger` (red)
801
+ **Forms**: `input-field`, `select-field`, `textarea-field`, `label-text`, `form-group`
802
+ **Navigation**: `link-back` (for ← back links)
803
+ **Checkboxes**: `checkbox-modern` (for boolean fields)
804
+
805
+ Theme: Dark backgrounds (slate-900), white text, blue-500 accents, white/10 borders.
806
+
807
+ ---
808
+
809
+ ## Reference Pattern
810
+
811
+ Adapt this structural pattern. Replace placeholders with actual values for {resource}:
812
+
813
+ ```{code_language}
814
+ {template_guidance}
815
+ ```
816
+
817
+ ---
818
+
819
+ ## Output Format
820
+
821
+ Return ONLY raw {file_type} code:
822
+ - NO markdown code blocks (no ```)
823
+ - NO explanatory text before or after
824
+ - {start_instruction}
825
+ - MUST include all required CSS classes listed above"""
826
+
827
+ def _build_css_generation_prompt(
828
+ self,
829
+ item: ChecklistItem,
830
+ template_guidance: str,
831
+ ) -> str:
832
+ """Build prompt for CSS code generation.
833
+
834
+ This is a specialized prompt for CSS files that ensures the LLM
835
+ generates Tailwind CSS instead of TypeScript/JSX.
836
+
837
+ Args:
838
+ item: Checklist item describing what to generate
839
+ template_guidance: CSS template as guidance
840
+
841
+ Returns:
842
+ Prompt string for LLM
843
+ """
844
+ return f"""You are an expert CSS developer specializing in Tailwind CSS.
845
+
846
+ ## Task
847
+ Generate a Tailwind CSS stylesheet for: {item.description}
848
+
849
+ ## Rules
850
+ - Return ONLY CSS code (Tailwind CSS with @apply directives is valid CSS)
851
+ - NO TypeScript, JavaScript, imports, or exports
852
+ - NO React components or JSX
853
+ - NO markdown code blocks
854
+ - Start with @tailwind directives
855
+
856
+ ## Required Structure
857
+ The CSS must include:
858
+ 1. @tailwind base, components, utilities directives
859
+ 2. :root CSS variables for theming
860
+ 3. @layer components with these classes using @apply:
861
+ - .glass-card (glassmorphism container)
862
+ - .page-title (gradient text heading)
863
+ - .btn-primary, .btn-secondary, .btn-danger (buttons)
864
+ - .input-field (form inputs)
865
+ - .checkbox-modern (styled checkboxes)
866
+ - .link-back (navigation links)
867
+
868
+ ## Reference Pattern
869
+
870
+ Follow this Tailwind CSS template exactly:
871
+
872
+ {template_guidance}
873
+
874
+ ## Output
875
+ Return raw CSS starting with @tailwind base;"""
876
+
877
+ def _get_template_guidance(self, item: ChecklistItem) -> Optional[str]:
878
+ """Get template content as guidance for LLM.
879
+
880
+ The templates from code_patterns.py serve as structural guidance,
881
+ not verbatim content to copy.
882
+
883
+ Args:
884
+ item: Checklist item with template and params
885
+
886
+ Returns:
887
+ Template string if found, None otherwise
888
+ """
889
+ # Import templates lazily to avoid circular imports
890
+ try:
891
+ from ..prompts.code_patterns import (
892
+ API_ROUTE_DYNAMIC_DELETE,
893
+ API_ROUTE_DYNAMIC_GET,
894
+ API_ROUTE_DYNAMIC_PATCH,
895
+ API_ROUTE_GET,
896
+ API_ROUTE_POST,
897
+ APP_GLOBALS_CSS,
898
+ CLIENT_COMPONENT_FORM,
899
+ CLIENT_COMPONENT_NEW_PAGE,
900
+ CLIENT_COMPONENT_TIMER,
901
+ SERVER_COMPONENT_DETAIL,
902
+ SERVER_COMPONENT_LIST,
903
+ )
904
+ except ImportError:
905
+ logger.warning("Could not import code_patterns templates")
906
+ return None
907
+
908
+ # Compose API route templates
909
+ api_route_collection = f"""import {{ NextResponse }} from "next/server";
910
+ import {{ prisma }} from "@/lib/prisma";
911
+ import {{ z }} from "zod";
912
+
913
+ // Schema for validation
914
+ // IMPORTANT: Use z.coerce.date() for any date/datetime/timestamp fields
915
+ const {{Resource}}Schema = z.object({{
916
+ // Example: publishedOn: z.coerce.date(),
917
+ // Define fields based on your data model
918
+ }});
919
+
920
+ {API_ROUTE_GET}
921
+
922
+ {API_ROUTE_POST}
923
+ """
924
+
925
+ api_route_item = f"""import {{ NextResponse }} from "next/server";
926
+ import {{ prisma }} from "@/lib/prisma";
927
+ import {{ z }} from "zod";
928
+
929
+ // Schema for update validation
930
+ // IMPORTANT: Use z.coerce.date() for any date/datetime/timestamp fields
931
+ const {{Resource}}UpdateSchema = z.object({{
932
+ // Example: publishedOn: z.coerce.date().optional(),
933
+ // Define update fields (all optional for PATCH)
934
+ }}).partial();
935
+
936
+ {API_ROUTE_DYNAMIC_GET}
937
+
938
+ {API_ROUTE_DYNAMIC_PATCH}
939
+
940
+ {API_ROUTE_DYNAMIC_DELETE}
941
+ """
942
+
943
+ # NOTE: Prisma model guidance removed - uses manage_data_model tool, not LLM
944
+
945
+ # Landing page template guidance
946
+ landing_page_guidance = """import Link from "next/link";
947
+
948
+ export default function Home() {
949
+ return (
950
+ <main className="min-h-screen">
951
+ <div className="container mx-auto px-4 py-12 max-w-4xl">
952
+ <h1 className="page-title mb-8">Welcome</h1>
953
+
954
+ <div className="grid gap-6">
955
+ <Link
956
+ href="/{resource}s"
957
+ className="glass-card p-6 block hover:border-indigo-500/50 transition-all duration-300 group"
958
+ >
959
+ <div className="flex items-center justify-between">
960
+ <div>
961
+ <h2 className="text-2xl font-semibold text-slate-100 mb-2 group-hover:text-indigo-400 transition-colors">
962
+ {Resource}s
963
+ </h2>
964
+ <p className="text-slate-400">Manage your {resource}s</p>
965
+ </div>
966
+ <svg className="w-6 h-6 text-slate-500 group-hover:text-indigo-400 group-hover:translate-x-1 transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
967
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
968
+ </svg>
969
+ </div>
970
+ </Link>
971
+ </div>
972
+ </div>
973
+ </main>
974
+ );
975
+ }
976
+ """
977
+
978
+ # Mapping of template names to their guidance
979
+ TEMPLATE_GUIDANCE = {
980
+ "generate_react_component": {
981
+ "list": SERVER_COMPONENT_LIST,
982
+ "form": CLIENT_COMPONENT_FORM,
983
+ "new": CLIENT_COMPONENT_NEW_PAGE,
984
+ "detail": SERVER_COMPONENT_DETAIL,
985
+ "artifact-timer": CLIENT_COMPONENT_TIMER,
986
+ },
987
+ "generate_api_route": {
988
+ "collection": api_route_collection,
989
+ "item": api_route_item,
990
+ },
991
+ # NOTE: generate_prisma_model removed - uses manage_data_model tool
992
+ "setup_app_styling": {
993
+ "default": APP_GLOBALS_CSS,
994
+ },
995
+ "update_landing_page": {
996
+ "default": landing_page_guidance,
997
+ },
998
+ }
999
+
1000
+ template_name = item.template
1001
+ guidance_map = TEMPLATE_GUIDANCE.get(template_name)
1002
+
1003
+ if guidance_map is None:
1004
+ return None
1005
+
1006
+ if isinstance(guidance_map, str):
1007
+ return guidance_map
1008
+
1009
+ # Get variant-specific guidance
1010
+ variant = item.params.get("variant", "default")
1011
+ route_type = item.params.get("type", "default") # For API routes
1012
+
1013
+ # Try variant first, then route_type, then default
1014
+ return (
1015
+ guidance_map.get(variant)
1016
+ or guidance_map.get(route_type)
1017
+ or guidance_map.get("default")
1018
+ )
1019
+
1020
+ def _determine_file_path(
1021
+ self,
1022
+ item: ChecklistItem,
1023
+ context: UserContext, # pylint: disable=unused-argument
1024
+ ) -> str:
1025
+ """Determine the output file path for generated code.
1026
+
1027
+ Args:
1028
+ item: Checklist item with template and params
1029
+ context: User context (unused but kept for consistency)
1030
+
1031
+ Returns:
1032
+ Relative file path from project root
1033
+ """
1034
+ template = item.template
1035
+ params = item.params
1036
+ resource = params.get("resource", "item")
1037
+ resource_cap = resource.capitalize()
1038
+
1039
+ if template == "generate_react_component":
1040
+ variant = params.get("variant", "list")
1041
+ component_name = params.get("component_name")
1042
+ if component_name:
1043
+ safe_name = "".join(
1044
+ ch for ch in component_name if ch.isalnum() or ch == "_"
1045
+ )
1046
+ component_name = safe_name or component_name
1047
+
1048
+ if variant == "list":
1049
+ return f"src/app/{resource}s/page.tsx"
1050
+ elif variant == "form":
1051
+ return f"src/components/{resource_cap}Form.tsx"
1052
+ elif variant == "new":
1053
+ return f"src/app/{resource}s/new/page.tsx"
1054
+ elif variant == "detail":
1055
+ return f"src/app/{resource}s/[id]/page.tsx"
1056
+ elif variant == "artifact-timer":
1057
+ file_component = component_name or f"{resource_cap}Timer"
1058
+ return f"src/components/{file_component}.tsx"
1059
+ else:
1060
+ if component_name:
1061
+ return f"src/components/{component_name}.tsx"
1062
+ return f"src/components/{resource_cap}{variant.capitalize()}.tsx"
1063
+
1064
+ elif template == "generate_api_route":
1065
+ route_type = params.get("type", "collection")
1066
+ if route_type == "item":
1067
+ return f"src/app/api/{resource}s/[id]/route.ts"
1068
+ else:
1069
+ return f"src/app/api/{resource}s/route.ts"
1070
+
1071
+ # NOTE: generate_prisma_model removed - uses manage_data_model tool, not LLM
1072
+
1073
+ elif template == "setup_app_styling":
1074
+ return "src/app/globals.css"
1075
+
1076
+ elif template == "update_landing_page":
1077
+ return "src/app/page.tsx"
1078
+
1079
+ # Default fallback
1080
+ return f"src/generated/{template}.tsx"
1081
+
1082
+ def _resolve_fields(
1083
+ self, item: ChecklistItem, context: UserContext
1084
+ ) -> Dict[str, str]:
1085
+ """Resolve resource fields for code generation prompts."""
1086
+ params = item.params or {}
1087
+
1088
+ def _normalize(fields: Dict[str, str]) -> Dict[str, str]:
1089
+ type_map = {
1090
+ "string": "string",
1091
+ "text": "string",
1092
+ "int": "number",
1093
+ "float": "float",
1094
+ "double": "float",
1095
+ "number": "number",
1096
+ "boolean": "boolean",
1097
+ "datetime": "datetime",
1098
+ "date": "date",
1099
+ "timestamp": "datetime",
1100
+ "email": "email",
1101
+ "url": "url",
1102
+ }
1103
+ normalized = {}
1104
+ for name, field_type in (fields or {}).items():
1105
+ mapped = type_map.get(field_type.lower(), "string")
1106
+ normalized[name] = mapped
1107
+ return normalized
1108
+
1109
+ if "fields" in params and isinstance(params["fields"], dict):
1110
+ return _normalize(params["fields"])
1111
+
1112
+ if context.schema_fields:
1113
+ return _normalize(context.schema_fields)
1114
+
1115
+ resource = params.get("resource")
1116
+ if not resource:
1117
+ return {}
1118
+
1119
+ try:
1120
+ from ..tools.web_dev_tools import read_prisma_model
1121
+ except ImportError:
1122
+ logger.debug("read_prisma_model unavailable for field resolution")
1123
+ return {}
1124
+
1125
+ try:
1126
+ model_info = read_prisma_model(context.project_dir, resource.capitalize())
1127
+ except Exception as exc: # noqa: BLE001
1128
+ logger.warning(f"Failed to read Prisma model for {resource}: {exc}")
1129
+ return {}
1130
+
1131
+ if not model_info.get("success"):
1132
+ logger.debug(
1133
+ "Could not resolve Prisma fields for %s: %s",
1134
+ resource,
1135
+ model_info.get("error"),
1136
+ )
1137
+ return {}
1138
+
1139
+ prisma_fields = model_info.get("fields", {})
1140
+ return _normalize(prisma_fields)
1141
+
1142
+ def _clean_llm_response(self, response: str) -> str:
1143
+ """Clean LLM response by removing markdown artifacts.
1144
+
1145
+ Args:
1146
+ response: Raw LLM response
1147
+
1148
+ Returns:
1149
+ Cleaned code string
1150
+ """
1151
+ code = response.strip()
1152
+
1153
+ # Remove markdown code blocks
1154
+ if code.startswith("```"):
1155
+ lines = code.split("\n")
1156
+ # Remove first line (```typescript or ```)
1157
+ lines = lines[1:]
1158
+ # Remove last line if it's closing ```
1159
+ if lines and lines[-1].strip() == "```":
1160
+ lines = lines[:-1]
1161
+ code = "\n".join(lines)
1162
+
1163
+ # Also handle case where there might be text before code block
1164
+ if "```typescript" in code or "```tsx" in code:
1165
+ # Find start of code block
1166
+ for marker in ["```typescript", "```tsx", "```"]:
1167
+ if marker in code:
1168
+ start = code.find(marker)
1169
+ end = code.find("```", start + len(marker))
1170
+ if end > start:
1171
+ code = code[start + len(marker) : end]
1172
+ break
1173
+
1174
+ return code.strip()
1175
+
1176
+ def _get_required_classes(self, item: ChecklistItem) -> List[str]:
1177
+ """Get list of required CSS classes for a checklist item.
1178
+
1179
+ Args:
1180
+ item: Checklist item with template and variant info
1181
+
1182
+ Returns:
1183
+ List of required CSS class names
1184
+ """
1185
+ metadata = TEMPLATE_METADATA.get(item.template, {})
1186
+ variant = item.params.get("variant", "default")
1187
+ route_type = item.params.get("type", "default")
1188
+
1189
+ # Try variant, then route_type, then default
1190
+ variant_meta = (
1191
+ metadata.get(variant)
1192
+ or metadata.get(route_type)
1193
+ or metadata.get("default")
1194
+ or {}
1195
+ )
1196
+
1197
+ return variant_meta.get("expected_classes", [])
1198
+
1199
+ def _validate_generated_code(
1200
+ self,
1201
+ code: str,
1202
+ item: ChecklistItem,
1203
+ ) -> tuple[bool, List[str], bool]:
1204
+ """Validate generated code meets requirements.
1205
+
1206
+ Args:
1207
+ code: Generated code to validate
1208
+ item: Checklist item with template info
1209
+
1210
+ Returns:
1211
+ Tuple of (is_valid, list_of_issues, is_blocking)
1212
+ - is_valid: True if no issues found
1213
+ - list_of_issues: List of validation issue messages
1214
+ - is_blocking: True if issues should prevent file write (CRITICAL errors)
1215
+ """
1216
+ issues = []
1217
+ is_blocking = False
1218
+
1219
+ # Check for markdown artifacts that slipped through
1220
+ if code.strip().startswith("```"):
1221
+ issues.append("Code still contains markdown block markers")
1222
+
1223
+ # CRITICAL: For CSS files (setup_app_styling), check for TypeScript content
1224
+ # This catches Issue #1002 where CSS files contain TypeScript code
1225
+ if item.template == "setup_app_styling":
1226
+ css_validation = self._validate_css_content_inline(code)
1227
+ if css_validation["errors"]:
1228
+ issues.extend(css_validation["errors"])
1229
+ is_blocking = True # TypeScript in CSS is a BLOCKING error
1230
+
1231
+ # Get metadata for this template
1232
+ metadata = TEMPLATE_METADATA.get(item.template, {})
1233
+ variant = item.params.get("variant", "default")
1234
+ route_type = item.params.get("type", "default")
1235
+
1236
+ # Try variant, then route_type, then default
1237
+ variant_meta = (
1238
+ metadata.get(variant)
1239
+ or metadata.get(route_type)
1240
+ or metadata.get("default")
1241
+ or {}
1242
+ )
1243
+
1244
+ # Check for expected CSS classes (for UI components)
1245
+ expected_classes = variant_meta.get("expected_classes", [])
1246
+ for cls in expected_classes:
1247
+ if cls not in code:
1248
+ issues.append(f"Missing expected class: {cls}")
1249
+
1250
+ # Check for 'use client' when needed
1251
+ if variant_meta.get("requires_client"):
1252
+ if '"use client"' not in code and "'use client'" not in code:
1253
+ issues.append("Missing 'use client' directive for client component")
1254
+
1255
+ # Check for basic TypeScript syntax
1256
+ if item.template == "generate_react_component":
1257
+ if "export default" not in code and "export function" not in code:
1258
+ issues.append("Missing export statement")
1259
+
1260
+ return len(issues) == 0, issues, is_blocking
1261
+
1262
+ def _validate_css_content_inline(self, content: str) -> Dict[str, Any]:
1263
+ """Validate CSS content for TypeScript/JavaScript code (Issue #1002).
1264
+
1265
+ This is an inline version of the CSS validation for use during LLM
1266
+ code generation. It detects when the LLM accidentally generates
1267
+ TypeScript/JSX code instead of CSS.
1268
+
1269
+ Args:
1270
+ content: File content to validate
1271
+
1272
+ Returns:
1273
+ Dictionary with errors (blocking) and warnings
1274
+ """
1275
+ import re
1276
+
1277
+ errors = []
1278
+ warnings = []
1279
+
1280
+ # CRITICAL: Detect TypeScript/JavaScript code in CSS files
1281
+ # These patterns indicate wrong file content - always invalid
1282
+ typescript_indicators = [
1283
+ (r"^\s*import\s+.*from", "import statement"),
1284
+ (r"^\s*export\s+(default|const|function|class|async)", "export statement"),
1285
+ (r'"use client"|\'use client\'', "React client directive"),
1286
+ (r"^\s*interface\s+\w+", "TypeScript interface"),
1287
+ (r"^\s*type\s+\w+\s*=", "TypeScript type alias"),
1288
+ (r"^\s*const\s+\w+\s*[=:]", "const declaration"),
1289
+ (r"^\s*let\s+\w+\s*[=:]", "let declaration"),
1290
+ (r"^\s*function\s+\w+", "function declaration"),
1291
+ (r"^\s*async\s+function", "async function"),
1292
+ (r"<[A-Z][a-zA-Z]*[\s/>]", "JSX component tag"),
1293
+ (r"useState|useEffect|useRouter|usePathname", "React hook"),
1294
+ ]
1295
+
1296
+ for pattern, description in typescript_indicators:
1297
+ if re.search(pattern, content, re.MULTILINE):
1298
+ errors.append(
1299
+ f"CRITICAL - CSS file contains {description}. "
1300
+ f"This is TypeScript/JSX code, not CSS."
1301
+ )
1302
+
1303
+ # Check for balanced braces
1304
+ if content.count("{") != content.count("}"):
1305
+ errors.append("Mismatched braces in CSS")
1306
+
1307
+ # Check for Tailwind directives
1308
+ has_tailwind = "@tailwind" in content or '@import "tailwindcss' in content
1309
+ if not has_tailwind and len(content.strip()) > 50:
1310
+ warnings.append(
1311
+ "Missing Tailwind directives (@tailwind base/components/utilities)"
1312
+ )
1313
+
1314
+ return {
1315
+ "errors": errors,
1316
+ "warnings": warnings,
1317
+ "is_valid": len(errors) == 0,
1318
+ }
1319
+
1320
+ def _execute_item_with_recovery(
1321
+ self,
1322
+ item: ChecklistItem,
1323
+ context: UserContext,
1324
+ max_attempts: int = 3,
1325
+ ) -> ItemExecutionResult:
1326
+ """Execute a checklist item with error recovery.
1327
+
1328
+ Uses the three-tier recovery strategy via ErrorHandler:
1329
+ 1. RETRY: Simple retry (transient errors)
1330
+ 2. FIX_AND_RETRY: LLM fixes code then retry
1331
+ 3. ESCALATE: LLM rewrites from scratch
1332
+ 4. ABORT: Give up after max attempts
1333
+
1334
+ Args:
1335
+ item: Checklist item to execute
1336
+ context: User context
1337
+ max_attempts: Maximum recovery attempts (default 3)
1338
+
1339
+ Returns:
1340
+ ItemExecutionResult from execution (or recovery attempts)
1341
+ """
1342
+ last_result = None
1343
+
1344
+ for attempt in range(max_attempts):
1345
+ try:
1346
+ # Execute the item
1347
+ result = self._execute_item(item, context)
1348
+ last_result = result
1349
+
1350
+ if result.success:
1351
+ # Reset retry count on success
1352
+ if self.error_handler:
1353
+ self.error_handler.reset_retry_count(item.template)
1354
+ return result
1355
+
1356
+ # No error handler - return failure immediately
1357
+ if not self.error_handler:
1358
+ logger.warning(
1359
+ f"No error handler available for {item.template}, "
1360
+ f"cannot retry: {result.error}"
1361
+ )
1362
+ return result
1363
+
1364
+ # Last attempt - return failure
1365
+ if attempt >= max_attempts - 1:
1366
+ logger.error(
1367
+ f"Max attempts ({max_attempts}) exceeded for {item.template}"
1368
+ )
1369
+ return result
1370
+
1371
+ # Handle failure with error handler
1372
+ logger.info(
1373
+ f"Attempting recovery for {item.template} "
1374
+ f"(attempt {attempt + 1}/{max_attempts}): {result.error}"
1375
+ )
1376
+
1377
+ action, fix_info = self.error_handler.handle_error(
1378
+ item.template,
1379
+ result.error or "Unknown error",
1380
+ {
1381
+ "code": "", # Tool output doesn't include code
1382
+ "project_dir": context.project_dir,
1383
+ },
1384
+ )
1385
+
1386
+ if action == RecoveryAction.ABORT:
1387
+ logger.error(f"Recovery aborted for {item.template}")
1388
+ return result
1389
+
1390
+ if action == RecoveryAction.RETRY:
1391
+ logger.info(
1392
+ f"Retrying {item.template} "
1393
+ f"(attempt {attempt + 2}/{max_attempts})"
1394
+ )
1395
+ continue
1396
+
1397
+ if action in (RecoveryAction.FIX_AND_RETRY, RecoveryAction.ESCALATE):
1398
+ if fix_info:
1399
+ logger.info(
1400
+ f"Fix applied for {item.template}: {fix_info[:100]}..."
1401
+ )
1402
+ logger.info(
1403
+ f"Retrying {item.template} after fix "
1404
+ f"(attempt {attempt + 2}/{max_attempts})"
1405
+ )
1406
+ continue
1407
+
1408
+ except Exception as e:
1409
+ logger.exception(
1410
+ f"Exception in {item.template} (attempt {attempt + 1})"
1411
+ )
1412
+ last_result = ItemExecutionResult(
1413
+ template=item.template,
1414
+ params=item.params,
1415
+ description=item.description,
1416
+ success=False,
1417
+ error=str(e),
1418
+ error_recoverable=True,
1419
+ )
1420
+
1421
+ # Last attempt - return exception result
1422
+ if attempt >= max_attempts - 1:
1423
+ last_result.error_recoverable = False
1424
+ return last_result
1425
+
1426
+ # Should not reach here, but return last result just in case
1427
+ if last_result:
1428
+ return last_result
1429
+
1430
+ return ItemExecutionResult(
1431
+ template=item.template,
1432
+ params=item.params,
1433
+ description=item.description,
1434
+ success=False,
1435
+ error=f"Max attempts ({max_attempts}) exceeded",
1436
+ error_recoverable=False,
1437
+ )
1438
+
1439
+ def _build_params(
1440
+ self,
1441
+ item: ChecklistItem,
1442
+ context: UserContext,
1443
+ ) -> Dict[str, Any]:
1444
+ """Build tool parameters from checklist item and context.
1445
+
1446
+ Args:
1447
+ item: Checklist item
1448
+ context: User context
1449
+
1450
+ Returns:
1451
+ Dictionary of tool parameters
1452
+ """
1453
+ params = dict(item.params)
1454
+ tool_name = TEMPLATE_TO_TOOL.get(item.template, item.template)
1455
+
1456
+ # Handle CLI command templates specially
1457
+ if item.template == "create_next_app":
1458
+ # Convert to run_cli_command format
1459
+ return {
1460
+ "command": (
1461
+ f"npx -y create-next-app@{NEXTJS_VERSION} . "
1462
+ "--typescript --tailwind --eslint --app --src-dir --import-alias '@/*' --yes"
1463
+ ),
1464
+ "working_dir": context.project_dir,
1465
+ "timeout": 1200,
1466
+ }
1467
+
1468
+ if item.template == "run_tests":
1469
+ # Convert to run_cli_command format
1470
+ return {
1471
+ "command": "npm test",
1472
+ "working_dir": context.project_dir,
1473
+ "timeout": 1200,
1474
+ }
1475
+
1476
+ if item.template == "prisma_db_sync":
1477
+ # Generate Prisma client and push schema to database
1478
+ # This MUST run after generate_prisma_model and before API routes
1479
+ return {
1480
+ "command": "npx -y prisma generate && npx -y prisma db push",
1481
+ "working_dir": context.project_dir,
1482
+ "timeout": 1200,
1483
+ }
1484
+
1485
+ # Handle setup_prisma specially - needs to initialize Prisma first
1486
+ if item.template == "setup_prisma":
1487
+ return self._build_setup_prisma_params(item, context)
1488
+
1489
+ # Handle generate_react_component specially - needs component_name derivation
1490
+ if item.template == "generate_react_component":
1491
+ return self._build_react_component_params(item, context)
1492
+
1493
+ # Add project_dir only if tool expects it
1494
+ if "project_dir" not in params and self._tool_accepts_parameter(
1495
+ tool_name, "project_dir"
1496
+ ):
1497
+ params["project_dir"] = context.project_dir
1498
+
1499
+ # Map checklist param names to tool param names
1500
+ param_mapping = {
1501
+ "resource": "resource_name", # generate_api_route -> manage_api_endpoint
1502
+ "model_name": "model_name", # stays the same
1503
+ "variant": "variant", # stays the same
1504
+ }
1505
+
1506
+ # Apply mappings
1507
+ for checklist_name, tool_name in param_mapping.items():
1508
+ if checklist_name in params and checklist_name != tool_name:
1509
+ params[tool_name] = params.pop(checklist_name)
1510
+
1511
+ # Handle specific template parameters
1512
+ template_def = get_template(item.template)
1513
+ if template_def:
1514
+ # Add entity name for data models
1515
+ if item.template == "generate_prisma_model" and "model_name" in params:
1516
+ context.entity_name = params["model_name"]
1517
+
1518
+ # Handle API route type
1519
+ if item.template == "generate_api_route":
1520
+ route_type = params.pop("type", "collection")
1521
+ if route_type == "item":
1522
+ # For item routes, set operations appropriately
1523
+ if "operations" not in params:
1524
+ params["operations"] = ["GET", "PATCH", "DELETE"]
1525
+
1526
+ return params
1527
+
1528
+ def _build_setup_prisma_params(
1529
+ self,
1530
+ item: ChecklistItem, # pylint: disable=unused-argument
1531
+ context: UserContext,
1532
+ ) -> Dict[str, Any]:
1533
+ """Build parameters for Prisma initialization.
1534
+
1535
+ The setup_prisma template needs to:
1536
+ 1. Initialize Prisma with SQLite (npx prisma init)
1537
+ 2. Create the singleton file (src/lib/prisma.ts)
1538
+
1539
+ The CLI commands run via shell, but the singleton file is written
1540
+ via Python's pathlib in _execute_deterministic() for cross-platform
1541
+ compatibility (Windows doesn't support Unix shell file operations).
1542
+
1543
+ Args:
1544
+ item: Checklist item (unused, kept for consistency)
1545
+ context: User context
1546
+
1547
+ Returns:
1548
+ Dictionary of tool parameters for run_cli_command
1549
+ """
1550
+ # Only run CLI commands - file writing is handled separately in
1551
+ # _execute_deterministic() via _write_prisma_singleton() for
1552
+ # cross-platform compatibility (mkdir -p and echo don't work on Windows)
1553
+ command = (
1554
+ "npm install prisma@5 @prisma/client@5 zod && "
1555
+ "npx -y prisma init --datasource-provider sqlite"
1556
+ )
1557
+
1558
+ return {
1559
+ "command": command,
1560
+ "working_dir": context.project_dir,
1561
+ "timeout": 1200,
1562
+ }
1563
+
1564
+ def _write_prisma_singleton(self, project_dir: str) -> None:
1565
+ """Write Prisma singleton file using cross-platform Python.
1566
+
1567
+ This method is called after setup_prisma CLI commands succeed.
1568
+ We use Python's pathlib instead of shell commands (mkdir -p, echo)
1569
+ because those Unix commands don't work on Windows.
1570
+
1571
+ Args:
1572
+ project_dir: Project root directory
1573
+ """
1574
+ from pathlib import Path
1575
+
1576
+ singleton_content = """import { PrismaClient } from "@prisma/client";
1577
+
1578
+ const globalForPrisma = globalThis as unknown as {
1579
+ prisma: PrismaClient | undefined;
1580
+ };
1581
+
1582
+ export const prisma = globalForPrisma.prisma ?? new PrismaClient();
1583
+
1584
+ if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
1585
+ """
1586
+ lib_dir = Path(project_dir) / "src" / "lib"
1587
+ lib_dir.mkdir(parents=True, exist_ok=True)
1588
+ singleton_file = lib_dir / "prisma.ts"
1589
+ singleton_file.write_text(singleton_content)
1590
+ logger.debug(f"Created Prisma singleton at {singleton_file}")
1591
+
1592
+ def _build_react_component_params(
1593
+ self,
1594
+ item: ChecklistItem,
1595
+ context: UserContext,
1596
+ ) -> Dict[str, Any]:
1597
+ """Build parameters for manage_react_component tool.
1598
+
1599
+ The template catalog defines:
1600
+ - resource: Resource name (lowercase, singular)
1601
+ - variant: Component variant (list|form|new|detail|actions)
1602
+ - with_checkboxes: Boolean (optional, not supported by tool)
1603
+
1604
+ The tool expects:
1605
+ - project_dir: Path to project
1606
+ - component_name: Component name (e.g., "TodoList", "UserForm")
1607
+ - component_type: "server" or "client"
1608
+ - resource_name: Associated resource
1609
+ - fields: Resource fields (optional)
1610
+ - variant: Component variant
1611
+
1612
+ Args:
1613
+ item: Checklist item with template params
1614
+ context: User context
1615
+
1616
+ Returns:
1617
+ Dictionary of tool parameters
1618
+ """
1619
+ template_params = dict(item.params)
1620
+
1621
+ # Extract resource and variant
1622
+ resource = template_params.get("resource", "")
1623
+ variant = template_params.get("variant", "list")
1624
+
1625
+ # Generate component_name from resource + variant
1626
+ # Allow caller to provide an explicit component_name (used for timers)
1627
+ resource_capitalized = resource.capitalize() if resource else "Item"
1628
+ variant_capitalized = variant.capitalize() if variant else "List"
1629
+ explicit_component = template_params.get("component_name")
1630
+
1631
+ # Build component name based on variant
1632
+ if explicit_component:
1633
+ component_name = explicit_component
1634
+ elif variant == "list":
1635
+ component_name = f"{resource_capitalized}List"
1636
+ elif variant == "form":
1637
+ component_name = f"{resource_capitalized}Form"
1638
+ elif variant == "new":
1639
+ component_name = f"New{resource_capitalized}"
1640
+ elif variant == "detail":
1641
+ component_name = f"{resource_capitalized}Detail"
1642
+ elif variant == "actions":
1643
+ component_name = f"{resource_capitalized}Actions"
1644
+ elif variant == "artifact-timer":
1645
+ component_name = f"{resource_capitalized}Timer"
1646
+ else:
1647
+ component_name = f"{resource_capitalized}{variant_capitalized}"
1648
+
1649
+ # Determine component_type based on variant
1650
+ # list pages are server components, forms and interactive pages are client
1651
+ if variant in ("list",):
1652
+ component_type = "server"
1653
+ else:
1654
+ component_type = "client"
1655
+
1656
+ # Build the actual tool params
1657
+ tool_params = {
1658
+ "project_dir": context.project_dir,
1659
+ "component_name": component_name,
1660
+ "component_type": component_type,
1661
+ "resource_name": resource,
1662
+ "variant": variant,
1663
+ }
1664
+
1665
+ # Add fields from context if available
1666
+ if context.schema_fields:
1667
+ tool_params["fields"] = context.schema_fields
1668
+
1669
+ # Note: with_checkboxes is NOT passed - tool doesn't support it
1670
+ # The variant and resource determine the component behavior
1671
+
1672
+ return tool_params
1673
+
1674
+ def _parse_tool_result(
1675
+ self,
1676
+ item: ChecklistItem,
1677
+ raw_result: Any,
1678
+ ) -> ItemExecutionResult:
1679
+ """Parse raw tool result into ItemExecutionResult.
1680
+
1681
+ Args:
1682
+ item: Original checklist item
1683
+ raw_result: Raw result from tool execution
1684
+
1685
+ Returns:
1686
+ ItemExecutionResult
1687
+ """
1688
+ # Handle different result types
1689
+ if isinstance(raw_result, StepResult):
1690
+ return ItemExecutionResult(
1691
+ template=item.template,
1692
+ params=item.params,
1693
+ description=item.description,
1694
+ success=raw_result.success,
1695
+ files=raw_result.output.get("files", []),
1696
+ warnings=raw_result.output.get("warnings", []),
1697
+ error=raw_result.error_message,
1698
+ error_recoverable=raw_result.retryable,
1699
+ output=raw_result.output,
1700
+ )
1701
+
1702
+ if isinstance(raw_result, dict):
1703
+ success = raw_result.get("success", True)
1704
+ return ItemExecutionResult(
1705
+ template=item.template,
1706
+ params=item.params,
1707
+ description=item.description,
1708
+ success=success,
1709
+ files=raw_result.get("files", []),
1710
+ warnings=raw_result.get("warnings", []),
1711
+ error=raw_result.get("error"),
1712
+ error_recoverable=raw_result.get("retryable", True),
1713
+ output=raw_result,
1714
+ )
1715
+
1716
+ # Unknown result type - treat as success if truthy
1717
+ return ItemExecutionResult(
1718
+ template=item.template,
1719
+ params=item.params,
1720
+ description=item.description,
1721
+ success=bool(raw_result),
1722
+ output={"raw": raw_result},
1723
+ )
1724
+
1725
+ def _report_progress(self, description: str, current: int, total: int) -> None:
1726
+ """Report progress via callback if available.
1727
+
1728
+ Args:
1729
+ description: Current item description
1730
+ current: Current item number
1731
+ total: Total items
1732
+ """
1733
+ # Log at debug level to avoid duplicate console output (checklist state is already printed)
1734
+ logger.debug(f"[{current}/{total}] {description}")
1735
+ if self.progress_callback:
1736
+ self.progress_callback(description, current, total)
1737
+
1738
+ def _handle_step_through(self, description: str) -> bool:
1739
+ """Handle step-through pause.
1740
+
1741
+ Args:
1742
+ description: Description of the completed step
1743
+
1744
+ Returns:
1745
+ True to continue, False to stop
1746
+ """
1747
+ # Check for TTY to avoid hanging in non-interactive modes
1748
+ if not sys.stdin or not sys.stdin.isatty():
1749
+ # In non-interactive mode, log and continue
1750
+ logger.debug(
1751
+ f"Step-through enabled but no TTY. Continuing after: {description}"
1752
+ )
1753
+ return True
1754
+
1755
+ self.console.print_step_paused(description)
1756
+
1757
+ try:
1758
+ response = input("> ").strip().lower()
1759
+ if response in ["n", "no", "q", "quit", "exit"]:
1760
+ return False
1761
+ return True
1762
+ except (EOFError, KeyboardInterrupt):
1763
+ return False