foundry-mcp 0.3.3__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 (135) hide show
  1. foundry_mcp/__init__.py +7 -0
  2. foundry_mcp/cli/__init__.py +80 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +633 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +652 -0
  15. foundry_mcp/cli/commands/session.py +479 -0
  16. foundry_mcp/cli/commands/specs.py +856 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +259 -0
  22. foundry_mcp/cli/flags.py +266 -0
  23. foundry_mcp/cli/logging.py +212 -0
  24. foundry_mcp/cli/main.py +44 -0
  25. foundry_mcp/cli/output.py +122 -0
  26. foundry_mcp/cli/registry.py +110 -0
  27. foundry_mcp/cli/resilience.py +178 -0
  28. foundry_mcp/cli/transcript.py +217 -0
  29. foundry_mcp/config.py +850 -0
  30. foundry_mcp/core/__init__.py +144 -0
  31. foundry_mcp/core/ai_consultation.py +1636 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/feature_flags.py +592 -0
  40. foundry_mcp/core/health.py +749 -0
  41. foundry_mcp/core/journal.py +694 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1350 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +123 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +317 -0
  57. foundry_mcp/core/prometheus.py +577 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +546 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
  61. foundry_mcp/core/prompts/plan_review.py +623 -0
  62. foundry_mcp/core/providers/__init__.py +225 -0
  63. foundry_mcp/core/providers/base.py +476 -0
  64. foundry_mcp/core/providers/claude.py +460 -0
  65. foundry_mcp/core/providers/codex.py +619 -0
  66. foundry_mcp/core/providers/cursor_agent.py +642 -0
  67. foundry_mcp/core/providers/detectors.py +488 -0
  68. foundry_mcp/core/providers/gemini.py +405 -0
  69. foundry_mcp/core/providers/opencode.py +616 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +302 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +729 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/resilience.py +600 -0
  78. foundry_mcp/core/responses.py +934 -0
  79. foundry_mcp/core/review.py +366 -0
  80. foundry_mcp/core/security.py +438 -0
  81. foundry_mcp/core/spec.py +1650 -0
  82. foundry_mcp/core/task.py +1289 -0
  83. foundry_mcp/core/testing.py +450 -0
  84. foundry_mcp/core/validation.py +2081 -0
  85. foundry_mcp/dashboard/__init__.py +32 -0
  86. foundry_mcp/dashboard/app.py +119 -0
  87. foundry_mcp/dashboard/components/__init__.py +17 -0
  88. foundry_mcp/dashboard/components/cards.py +88 -0
  89. foundry_mcp/dashboard/components/charts.py +234 -0
  90. foundry_mcp/dashboard/components/filters.py +136 -0
  91. foundry_mcp/dashboard/components/tables.py +195 -0
  92. foundry_mcp/dashboard/data/__init__.py +11 -0
  93. foundry_mcp/dashboard/data/stores.py +433 -0
  94. foundry_mcp/dashboard/launcher.py +289 -0
  95. foundry_mcp/dashboard/views/__init__.py +12 -0
  96. foundry_mcp/dashboard/views/errors.py +217 -0
  97. foundry_mcp/dashboard/views/metrics.py +174 -0
  98. foundry_mcp/dashboard/views/overview.py +160 -0
  99. foundry_mcp/dashboard/views/providers.py +83 -0
  100. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  101. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  102. foundry_mcp/prompts/__init__.py +9 -0
  103. foundry_mcp/prompts/workflows.py +525 -0
  104. foundry_mcp/resources/__init__.py +9 -0
  105. foundry_mcp/resources/specs.py +591 -0
  106. foundry_mcp/schemas/__init__.py +38 -0
  107. foundry_mcp/schemas/sdd-spec-schema.json +386 -0
  108. foundry_mcp/server.py +164 -0
  109. foundry_mcp/tools/__init__.py +10 -0
  110. foundry_mcp/tools/unified/__init__.py +71 -0
  111. foundry_mcp/tools/unified/authoring.py +1487 -0
  112. foundry_mcp/tools/unified/context_helpers.py +98 -0
  113. foundry_mcp/tools/unified/documentation_helpers.py +198 -0
  114. foundry_mcp/tools/unified/environment.py +939 -0
  115. foundry_mcp/tools/unified/error.py +462 -0
  116. foundry_mcp/tools/unified/health.py +225 -0
  117. foundry_mcp/tools/unified/journal.py +841 -0
  118. foundry_mcp/tools/unified/lifecycle.py +632 -0
  119. foundry_mcp/tools/unified/metrics.py +777 -0
  120. foundry_mcp/tools/unified/plan.py +745 -0
  121. foundry_mcp/tools/unified/pr.py +294 -0
  122. foundry_mcp/tools/unified/provider.py +629 -0
  123. foundry_mcp/tools/unified/review.py +685 -0
  124. foundry_mcp/tools/unified/review_helpers.py +299 -0
  125. foundry_mcp/tools/unified/router.py +102 -0
  126. foundry_mcp/tools/unified/server.py +580 -0
  127. foundry_mcp/tools/unified/spec.py +808 -0
  128. foundry_mcp/tools/unified/task.py +2202 -0
  129. foundry_mcp/tools/unified/test.py +370 -0
  130. foundry_mcp/tools/unified/verification.py +520 -0
  131. foundry_mcp-0.3.3.dist-info/METADATA +337 -0
  132. foundry_mcp-0.3.3.dist-info/RECORD +135 -0
  133. foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
  134. foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
  135. foundry_mcp-0.3.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,652 @@
1
+ """Review commands for SDD CLI.
2
+
3
+ Provides commands for spec review including:
4
+ - Quick structural review (no LLM required)
5
+ - AI-powered full/security/feasibility reviews via ConsultationOrchestrator
6
+ - AI-powered fidelity reviews to compare implementation against spec
7
+
8
+ AI-enhanced reviews use:
9
+ - PLAN_REVIEW_FULL_V1: Comprehensive 6-dimension review
10
+ - PLAN_REVIEW_QUICK_V1: Critical blockers and questions focus
11
+ - PLAN_REVIEW_SECURITY_V1: Security-focused review
12
+ - PLAN_REVIEW_FEASIBILITY_V1: Technical complexity assessment
13
+ - SYNTHESIS_PROMPT_V1: Multi-model response synthesis
14
+ - FIDELITY_REVIEW_V1: Implementation vs specification comparison
15
+ """
16
+
17
+ import json
18
+ import time
19
+ from typing import Any, Dict, List, Optional
20
+
21
+ import click
22
+
23
+ from foundry_mcp.cli.logging import cli_command, get_cli_logger
24
+ from foundry_mcp.cli.output import emit_error, emit_success
25
+ from foundry_mcp.cli.registry import get_context
26
+ from foundry_mcp.cli.resilience import (
27
+ FAST_TIMEOUT,
28
+ SLOW_TIMEOUT,
29
+ handle_keyboard_interrupt,
30
+ with_sync_timeout,
31
+ )
32
+ from foundry_mcp.tools.unified.documentation_helpers import (
33
+ _build_implementation_artifacts,
34
+ _build_journal_entries,
35
+ _build_spec_requirements,
36
+ _build_test_results,
37
+ )
38
+ from foundry_mcp.tools.unified.review_helpers import (
39
+ DEFAULT_AI_TIMEOUT,
40
+ REVIEW_TYPES,
41
+ _get_llm_status,
42
+ _run_ai_review,
43
+ _run_quick_review,
44
+ )
45
+
46
+ logger = get_cli_logger()
47
+
48
+
49
+ def _emit_review_envelope(envelope: Dict[str, Any], *, duration_ms: float) -> None:
50
+ """Emit a response-v2 envelope returned by shared review helpers."""
51
+
52
+ if envelope.get("success") is True:
53
+ emit_success(
54
+ envelope.get("data", {}),
55
+ telemetry={"duration_ms": round(duration_ms, 2)},
56
+ )
57
+ return
58
+
59
+ payload = envelope.get("data") or {}
60
+
61
+ error_code = payload.get("error_code", "INTERNAL_ERROR")
62
+ if hasattr(error_code, "value"):
63
+ error_code = error_code.value
64
+
65
+ error_type = payload.get("error_type", "internal")
66
+ if hasattr(error_type, "value"):
67
+ error_type = error_type.value
68
+
69
+ emit_error(
70
+ envelope.get("error") or "Review failed",
71
+ code=str(error_code),
72
+ error_type=str(error_type),
73
+ remediation=payload.get("remediation"),
74
+ details=payload.get("details"),
75
+ )
76
+
77
+
78
+ REVIEW_TOOL_DEFINITIONS = [
79
+ {
80
+ "name": "quick-review",
81
+ "description": "Structural validation with schema & progress checks (native).",
82
+ "capabilities": ["structure", "progress", "quality"],
83
+ "requires_llm": False,
84
+ },
85
+ {
86
+ "name": "full-review",
87
+ "description": "LLM-powered deep review via sdd-toolkit.",
88
+ "capabilities": ["structure", "quality", "suggestions"],
89
+ "requires_llm": True,
90
+ "alternative": "sdd-toolkit:sdd-plan-review",
91
+ },
92
+ {
93
+ "name": "security-review",
94
+ "description": "Security-focused LLM analysis.",
95
+ "capabilities": ["security", "trust_boundaries"],
96
+ "requires_llm": True,
97
+ "alternative": "sdd-toolkit:sdd-plan-review",
98
+ },
99
+ {
100
+ "name": "feasibility-review",
101
+ "description": "Implementation feasibility assessment (LLM).",
102
+ "capabilities": ["complexity", "risk", "dependencies"],
103
+ "requires_llm": True,
104
+ "alternative": "sdd-toolkit:sdd-plan-review",
105
+ },
106
+ ]
107
+
108
+ # Fidelity review timeout (longer for AI consultation)
109
+ FIDELITY_TIMEOUT = 600
110
+
111
+
112
+ @click.group("review")
113
+ def review_group() -> None:
114
+ """Spec review and fidelity checking commands."""
115
+ pass
116
+
117
+
118
+ @review_group.command("spec")
119
+ @click.argument("spec_id")
120
+ @click.option(
121
+ "--type",
122
+ "review_type",
123
+ type=click.Choice(REVIEW_TYPES),
124
+ default="quick",
125
+ help="Type of review to perform.",
126
+ )
127
+ @click.option(
128
+ "--tools",
129
+ help="Comma-separated list of review tools to use (LLM types only).",
130
+ )
131
+ @click.option(
132
+ "--model",
133
+ help="LLM model to use for review (LLM types only).",
134
+ )
135
+ @click.option(
136
+ "--ai-provider",
137
+ help="Explicit AI provider selection (e.g., gemini, cursor-agent).",
138
+ )
139
+ @click.option(
140
+ "--ai-timeout",
141
+ type=float,
142
+ default=DEFAULT_AI_TIMEOUT,
143
+ help=f"AI consultation timeout in seconds (default: {DEFAULT_AI_TIMEOUT}).",
144
+ )
145
+ @click.option(
146
+ "--no-consultation-cache",
147
+ is_flag=True,
148
+ help="Bypass AI consultation cache (always query providers fresh).",
149
+ )
150
+ @click.option(
151
+ "--dry-run",
152
+ is_flag=True,
153
+ help="Show what would be reviewed without executing.",
154
+ )
155
+ @click.pass_context
156
+ @cli_command("spec")
157
+ @handle_keyboard_interrupt()
158
+ @with_sync_timeout(SLOW_TIMEOUT, "Review timed out")
159
+ def review_spec_cmd(
160
+ ctx: click.Context,
161
+ spec_id: str,
162
+ review_type: str,
163
+ tools: Optional[str],
164
+ model: Optional[str],
165
+ ai_provider: Optional[str],
166
+ ai_timeout: float,
167
+ no_consultation_cache: bool,
168
+ dry_run: bool,
169
+ ) -> None:
170
+ """Run a structural or AI-powered review on a specification."""
171
+ start_time = time.perf_counter()
172
+ cli_ctx = get_context(ctx)
173
+ specs_dir = cli_ctx.specs_dir
174
+
175
+ if specs_dir is None:
176
+ emit_error(
177
+ "No specs directory found",
178
+ code="VALIDATION_ERROR",
179
+ error_type="validation",
180
+ remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
181
+ details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
182
+ )
183
+
184
+ llm_status = _get_llm_status()
185
+
186
+ if review_type == "quick":
187
+ envelope = _run_quick_review(
188
+ spec_id=spec_id,
189
+ specs_dir=specs_dir,
190
+ dry_run=dry_run,
191
+ llm_status=llm_status,
192
+ start_time=start_time,
193
+ )
194
+ else:
195
+ envelope = _run_ai_review(
196
+ spec_id=spec_id,
197
+ specs_dir=specs_dir,
198
+ review_type=review_type,
199
+ ai_provider=ai_provider,
200
+ model=model,
201
+ ai_timeout=ai_timeout,
202
+ consultation_cache=not no_consultation_cache,
203
+ dry_run=dry_run,
204
+ llm_status=llm_status,
205
+ start_time=start_time,
206
+ )
207
+
208
+ duration_ms = (time.perf_counter() - start_time) * 1000
209
+ _emit_review_envelope(envelope, duration_ms=duration_ms)
210
+
211
+
212
+ @review_group.command("tools")
213
+ @click.pass_context
214
+ @cli_command("tools")
215
+ @handle_keyboard_interrupt()
216
+ @with_sync_timeout(FAST_TIMEOUT, "Review tools lookup timed out")
217
+ def review_tools_cmd(ctx: click.Context) -> None:
218
+ """List native and external review toolchains."""
219
+ start_time = time.perf_counter()
220
+
221
+ llm_status = _get_llm_status()
222
+
223
+ tools_info = []
224
+ for definition in REVIEW_TOOL_DEFINITIONS:
225
+ requires_llm = definition.get("requires_llm", False)
226
+ available = not requires_llm # LLM reviews are handled by external workflows
227
+ tool_info = {
228
+ "name": definition["name"],
229
+ "description": definition["description"],
230
+ "capabilities": definition.get("capabilities", []),
231
+ "requires_llm": requires_llm,
232
+ "available": available,
233
+ "status": "native" if available else "external",
234
+ }
235
+ if not available:
236
+ tool_info["alternative"] = definition.get("alternative")
237
+ tool_info["message"] = "Use the sdd-toolkit workflow for this review type"
238
+ tools_info.append(tool_info)
239
+
240
+ duration_ms = (time.perf_counter() - start_time) * 1000
241
+
242
+ emit_success(
243
+ {
244
+ "tools": tools_info,
245
+ "llm_status": llm_status,
246
+ "review_types": REVIEW_TYPES,
247
+ },
248
+ telemetry={"duration_ms": round(duration_ms, 2)},
249
+ )
250
+
251
+
252
+ @review_group.command("plan-tools")
253
+ @click.pass_context
254
+ @cli_command("plan-tools")
255
+ @handle_keyboard_interrupt()
256
+ @with_sync_timeout(FAST_TIMEOUT, "Plan tools lookup timed out")
257
+ def review_plan_tools_cmd(ctx: click.Context) -> None:
258
+ """List available plan review toolchains."""
259
+ start_time = time.perf_counter()
260
+
261
+ llm_status = _get_llm_status()
262
+
263
+ # Define plan review toolchains
264
+ plan_tools = [
265
+ {
266
+ "name": "quick-review",
267
+ "description": "Fast structural review for basic validation",
268
+ "capabilities": ["structure", "syntax", "basic_quality"],
269
+ "llm_required": False,
270
+ "estimated_time": "< 10 seconds",
271
+ },
272
+ {
273
+ "name": "full-review",
274
+ "description": "Comprehensive review with LLM analysis",
275
+ "capabilities": ["structure", "quality", "feasibility", "suggestions"],
276
+ "llm_required": True,
277
+ "estimated_time": "30-60 seconds",
278
+ },
279
+ {
280
+ "name": "security-review",
281
+ "description": "Security-focused analysis of plan",
282
+ "capabilities": ["security", "trust_boundaries", "data_flow"],
283
+ "llm_required": True,
284
+ "estimated_time": "30-60 seconds",
285
+ },
286
+ {
287
+ "name": "feasibility-review",
288
+ "description": "Implementation feasibility assessment",
289
+ "capabilities": ["complexity", "dependencies", "risk"],
290
+ "llm_required": True,
291
+ "estimated_time": "30-60 seconds",
292
+ },
293
+ ]
294
+
295
+ # Add availability status (only quick review is native today)
296
+ available_tools = []
297
+ for tool in plan_tools:
298
+ tool_info = tool.copy()
299
+ if tool["llm_required"]:
300
+ tool_info["status"] = "external"
301
+ tool_info["available"] = False
302
+ tool_info["reason"] = "Use the sdd-toolkit:sdd-plan-review workflow"
303
+ tool_info["alternative"] = "sdd-toolkit:sdd-plan-review"
304
+ else:
305
+ tool_info["status"] = "native"
306
+ tool_info["available"] = True
307
+ available_tools.append(tool_info)
308
+
309
+ recommendations = [
310
+ "Use 'quick-review' for structural validation inside foundry-mcp",
311
+ "Invoke sdd-toolkit:sdd-plan-review for AI-assisted plan analysis",
312
+ "Configure LLM credentials when ready to adopt the toolkit workflow",
313
+ ]
314
+
315
+ duration_ms = (time.perf_counter() - start_time) * 1000
316
+
317
+ emit_success(
318
+ {
319
+ "plan_tools": available_tools,
320
+ "llm_status": llm_status,
321
+ "recommendations": recommendations,
322
+ },
323
+ telemetry={"duration_ms": round(duration_ms, 2)},
324
+ )
325
+
326
+
327
+ @review_group.command("fidelity")
328
+ @click.argument("spec_id")
329
+ @click.option(
330
+ "--task",
331
+ "task_id",
332
+ help="Review specific task implementation.",
333
+ )
334
+ @click.option(
335
+ "--phase",
336
+ "phase_id",
337
+ help="Review entire phase implementation.",
338
+ )
339
+ @click.option(
340
+ "--files",
341
+ multiple=True,
342
+ help="Review specific file(s) only.",
343
+ )
344
+ @click.option(
345
+ "--incremental",
346
+ is_flag=True,
347
+ help="Only review changed files since last run.",
348
+ )
349
+ @click.option(
350
+ "--base-branch",
351
+ default="main",
352
+ help="Base branch for git diff.",
353
+ )
354
+ @click.option(
355
+ "--ai-provider",
356
+ help="Explicit AI provider selection (e.g., gemini, cursor-agent).",
357
+ )
358
+ @click.option(
359
+ "--ai-timeout",
360
+ type=float,
361
+ default=DEFAULT_AI_TIMEOUT,
362
+ help=f"AI consultation timeout in seconds (default: {DEFAULT_AI_TIMEOUT}).",
363
+ )
364
+ @click.option(
365
+ "--no-consultation-cache",
366
+ is_flag=True,
367
+ help="Bypass AI consultation cache (always query providers fresh).",
368
+ )
369
+ @click.pass_context
370
+ @cli_command("fidelity")
371
+ @handle_keyboard_interrupt()
372
+ @with_sync_timeout(FIDELITY_TIMEOUT, "Fidelity review timed out")
373
+ def review_fidelity_cmd(
374
+ ctx: click.Context,
375
+ spec_id: str,
376
+ task_id: Optional[str],
377
+ phase_id: Optional[str],
378
+ files: tuple,
379
+ incremental: bool,
380
+ base_branch: str,
381
+ ai_provider: Optional[str],
382
+ ai_timeout: float,
383
+ no_consultation_cache: bool,
384
+ ) -> None:
385
+ """Compare implementation against specification.
386
+
387
+ SPEC_ID is the specification identifier.
388
+
389
+ Performs a fidelity review to verify that code implementation
390
+ matches the specification requirements using the AI consultation layer.
391
+ """
392
+ start_time = time.perf_counter()
393
+ cli_ctx = get_context(ctx)
394
+ specs_dir = cli_ctx.specs_dir
395
+ consultation_cache = not no_consultation_cache
396
+
397
+ if specs_dir is None:
398
+ emit_error(
399
+ "No specs directory found",
400
+ code="VALIDATION_ERROR",
401
+ error_type="validation",
402
+ remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
403
+ details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
404
+ )
405
+
406
+ # Validate mutually exclusive options
407
+ if task_id and phase_id:
408
+ emit_error(
409
+ "Cannot specify both --task and --phase",
410
+ code="INVALID_OPTIONS",
411
+ error_type="validation",
412
+ remediation="Use either --task or --phase, not both",
413
+ details={"hint": "Use either --task or --phase, not both"},
414
+ )
415
+
416
+ llm_status = _get_llm_status()
417
+
418
+ # Determine scope
419
+ if task_id:
420
+ pass
421
+ elif phase_id:
422
+ pass
423
+ elif files:
424
+ f"files:{len(files)}"
425
+
426
+ # Run the fidelity review
427
+ result = _run_fidelity_review(
428
+ spec_id=spec_id,
429
+ task_id=task_id,
430
+ phase_id=phase_id,
431
+ files=list(files) if files else None,
432
+ ai_provider=ai_provider,
433
+ ai_timeout=ai_timeout,
434
+ consultation_cache=consultation_cache,
435
+ incremental=incremental,
436
+ base_branch=base_branch,
437
+ specs_dir=specs_dir,
438
+ llm_status=llm_status,
439
+ start_time=start_time,
440
+ )
441
+
442
+ duration_ms = (time.perf_counter() - start_time) * 1000
443
+ emit_success(
444
+ result,
445
+ telemetry={"duration_ms": round(duration_ms, 2)},
446
+ )
447
+
448
+
449
+ def _run_fidelity_review(
450
+ spec_id: str,
451
+ task_id: Optional[str],
452
+ phase_id: Optional[str],
453
+ files: Optional[List[str]],
454
+ ai_provider: Optional[str],
455
+ ai_timeout: float,
456
+ consultation_cache: bool,
457
+ incremental: bool,
458
+ base_branch: str,
459
+ specs_dir: Any,
460
+ llm_status: Dict[str, Any],
461
+ start_time: float,
462
+ ) -> Dict[str, Any]:
463
+ """
464
+ Run a fidelity review using the AI consultation layer.
465
+
466
+ Args:
467
+ spec_id: Specification ID to review against
468
+ task_id: Optional task ID for task-scoped review
469
+ phase_id: Optional phase ID for phase-scoped review
470
+ files: Optional list of files to review
471
+ ai_provider: Explicit AI provider selection
472
+ ai_timeout: Consultation timeout in seconds
473
+ consultation_cache: Whether to use consultation cache
474
+ incremental: Only review changed files
475
+ base_branch: Base branch for git diff
476
+ specs_dir: Path to specs directory
477
+ llm_status: LLM configuration status
478
+ start_time: Start time for duration tracking
479
+
480
+ Returns:
481
+ Dict with fidelity review results
482
+ """
483
+
484
+ # Import consultation layer components
485
+ try:
486
+ from foundry_mcp.core.ai_consultation import (
487
+ ConsultationOrchestrator,
488
+ ConsultationRequest,
489
+ ConsultationWorkflow,
490
+ )
491
+ except ImportError:
492
+ emit_error(
493
+ "AI consultation layer not available",
494
+ code="AI_NOT_AVAILABLE",
495
+ error_type="unavailable",
496
+ remediation="Ensure foundry_mcp.core.ai_consultation is properly installed",
497
+ )
498
+
499
+ # Load spec
500
+ try:
501
+ from foundry_mcp.core.spec import load_spec, find_spec_file
502
+
503
+ spec_file = find_spec_file(spec_id, specs_dir)
504
+ if not spec_file:
505
+ emit_error(
506
+ f"Specification not found: {spec_id}",
507
+ code="SPEC_NOT_FOUND",
508
+ error_type="not_found",
509
+ remediation="Verify the spec ID exists using 'sdd list'",
510
+ details={"spec_id": spec_id},
511
+ )
512
+ spec_data = load_spec(spec_file)
513
+ except Exception:
514
+ logger.exception(f"Failed to load spec {spec_id}")
515
+ emit_error(
516
+ "Failed to load spec",
517
+ code="SPEC_LOAD_ERROR",
518
+ error_type="error",
519
+ remediation="Check that the spec file is valid JSON",
520
+ details={"spec_id": spec_id},
521
+ )
522
+
523
+ # Determine review scope
524
+ if task_id:
525
+ review_scope = f"Task {task_id}"
526
+ elif phase_id:
527
+ review_scope = f"Phase {phase_id}"
528
+ elif files:
529
+ review_scope = f"Files: {', '.join(files)}"
530
+ else:
531
+ review_scope = "Full specification"
532
+
533
+ # Build context for fidelity review
534
+ spec_title = spec_data.get("title", spec_id)
535
+ spec_description = spec_data.get("description", "")
536
+
537
+ # Build spec requirements from task details
538
+ spec_requirements = _build_spec_requirements(spec_data, task_id, phase_id)
539
+
540
+ # Build implementation artifacts (file contents, git diff if incremental)
541
+ implementation_artifacts = _build_implementation_artifacts(
542
+ spec_data, task_id, phase_id, files, incremental, base_branch
543
+ )
544
+
545
+ # Build test results section
546
+ test_results = _build_test_results(spec_data, task_id, phase_id)
547
+
548
+ # Build journal entries section
549
+ journal_entries = _build_journal_entries(spec_data, task_id, phase_id)
550
+
551
+ # Initialize orchestrator
552
+ orchestrator = ConsultationOrchestrator(
553
+ default_timeout=ai_timeout,
554
+ )
555
+
556
+ # Check if providers are available
557
+ if not orchestrator.is_available(provider_id=ai_provider):
558
+ provider_msg = f" (requested: {ai_provider})" if ai_provider else ""
559
+ emit_error(
560
+ f"Fidelity review requested but no providers available{provider_msg}",
561
+ code="AI_NO_PROVIDER",
562
+ error_type="unavailable",
563
+ remediation="Install and configure an AI provider (gemini, cursor-agent, codex)",
564
+ details={
565
+ "spec_id": spec_id,
566
+ "requested_provider": ai_provider,
567
+ "llm_status": llm_status,
568
+ },
569
+ )
570
+
571
+ # Create consultation request
572
+ request = ConsultationRequest(
573
+ workflow=ConsultationWorkflow.FIDELITY_REVIEW,
574
+ prompt_id="FIDELITY_REVIEW_V1",
575
+ context={
576
+ "spec_id": spec_id,
577
+ "spec_title": spec_title,
578
+ "spec_description": f"**Description:** {spec_description}"
579
+ if spec_description
580
+ else "",
581
+ "review_scope": review_scope,
582
+ "spec_requirements": spec_requirements,
583
+ "implementation_artifacts": implementation_artifacts,
584
+ "test_results": test_results,
585
+ "journal_entries": journal_entries,
586
+ },
587
+ provider_id=ai_provider,
588
+ timeout=ai_timeout,
589
+ )
590
+
591
+ # Execute consultation
592
+ try:
593
+ result = orchestrator.consult(request, use_cache=consultation_cache)
594
+ except Exception:
595
+ logger.exception(f"AI fidelity consultation failed for {spec_id}")
596
+ emit_error(
597
+ "AI consultation failed",
598
+ code="AI_CONSULTATION_ERROR",
599
+ error_type="error",
600
+ remediation="Check provider configuration and try again",
601
+ details={
602
+ "spec_id": spec_id,
603
+ "review_scope": review_scope,
604
+ },
605
+ )
606
+
607
+ # Parse JSON response if possible
608
+ parsed_response = None
609
+ if result and result.content:
610
+ try:
611
+ # Try to extract JSON from markdown code blocks if present
612
+ content = result.content
613
+ if "```json" in content:
614
+ start = content.find("```json") + 7
615
+ end = content.find("```", start)
616
+ if end > start:
617
+ content = content[start:end].strip()
618
+ elif "```" in content:
619
+ start = content.find("```") + 3
620
+ end = content.find("```", start)
621
+ if end > start:
622
+ content = content[start:end].strip()
623
+ parsed_response = json.loads(content)
624
+ except (json.JSONDecodeError, ValueError):
625
+ # Fall back to raw content
626
+ pass
627
+
628
+ # Build response
629
+ return {
630
+ "spec_id": spec_id,
631
+ "title": spec_title,
632
+ "review_scope": review_scope,
633
+ "task_id": task_id,
634
+ "phase_id": phase_id,
635
+ "files": files,
636
+ "verdict": parsed_response.get("verdict", "unknown")
637
+ if parsed_response
638
+ else "unknown",
639
+ "llm_status": llm_status,
640
+ "ai_provider": result.provider_id if result else ai_provider,
641
+ "consultation_cache": consultation_cache,
642
+ "response": parsed_response
643
+ if parsed_response
644
+ else result.content
645
+ if result
646
+ else None,
647
+ "raw_response": result.content if result and not parsed_response else None,
648
+ "model": result.model_used if result else None,
649
+ "cached": result.cache_hit if result else False,
650
+ "incremental": incremental,
651
+ "base_branch": base_branch,
652
+ }