foundry-mcp 0.3.3__py3-none-any.whl → 0.7.0__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.
- foundry_mcp/__init__.py +7 -1
- foundry_mcp/cli/commands/plan.py +10 -3
- foundry_mcp/cli/commands/review.py +19 -4
- foundry_mcp/cli/commands/specs.py +38 -208
- foundry_mcp/cli/output.py +3 -3
- foundry_mcp/config.py +235 -5
- foundry_mcp/core/ai_consultation.py +146 -9
- foundry_mcp/core/discovery.py +6 -6
- foundry_mcp/core/error_store.py +2 -2
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/llm_config.py +20 -2
- foundry_mcp/core/metrics_store.py +2 -2
- foundry_mcp/core/progress.py +70 -0
- foundry_mcp/core/prompts/fidelity_review.py +149 -4
- foundry_mcp/core/prompts/markdown_plan_review.py +5 -1
- foundry_mcp/core/prompts/plan_review.py +5 -1
- foundry_mcp/core/providers/claude.py +6 -47
- foundry_mcp/core/providers/codex.py +6 -57
- foundry_mcp/core/providers/cursor_agent.py +3 -44
- foundry_mcp/core/providers/gemini.py +6 -57
- foundry_mcp/core/providers/opencode.py +35 -5
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +425 -0
- foundry_mcp/core/research/models.py +437 -0
- foundry_mcp/core/research/workflows/__init__.py +22 -0
- foundry_mcp/core/research/workflows/base.py +204 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +396 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/responses.py +450 -0
- foundry_mcp/core/spec.py +2438 -236
- foundry_mcp/core/task.py +1064 -19
- foundry_mcp/core/testing.py +512 -123
- foundry_mcp/core/validation.py +313 -42
- foundry_mcp/dashboard/components/charts.py +0 -57
- foundry_mcp/dashboard/launcher.py +11 -0
- foundry_mcp/dashboard/views/metrics.py +25 -35
- foundry_mcp/dashboard/views/overview.py +1 -65
- foundry_mcp/resources/specs.py +25 -25
- foundry_mcp/schemas/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +33 -5
- foundry_mcp/server.py +38 -0
- foundry_mcp/tools/unified/__init__.py +4 -2
- foundry_mcp/tools/unified/authoring.py +2423 -267
- foundry_mcp/tools/unified/documentation_helpers.py +69 -6
- foundry_mcp/tools/unified/environment.py +235 -6
- foundry_mcp/tools/unified/error.py +18 -1
- foundry_mcp/tools/unified/lifecycle.py +8 -0
- foundry_mcp/tools/unified/plan.py +113 -1
- foundry_mcp/tools/unified/research.py +658 -0
- foundry_mcp/tools/unified/review.py +370 -16
- foundry_mcp/tools/unified/spec.py +367 -0
- foundry_mcp/tools/unified/task.py +1163 -48
- foundry_mcp/tools/unified/test.py +69 -8
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/METADATA +7 -1
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/RECORD +60 -48
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/WHEEL +0 -0
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/entry_points.txt +0 -0
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
11
|
import time
|
|
12
|
+
from datetime import datetime
|
|
12
13
|
from dataclasses import asdict
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any, Dict, List, Optional
|
|
@@ -19,9 +20,14 @@ from foundry_mcp.config import ServerConfig
|
|
|
19
20
|
from foundry_mcp.core.ai_consultation import (
|
|
20
21
|
ConsultationOrchestrator,
|
|
21
22
|
ConsultationRequest,
|
|
23
|
+
ConsultationResult,
|
|
22
24
|
ConsultationWorkflow,
|
|
23
25
|
ConsensusResult,
|
|
24
26
|
)
|
|
27
|
+
from foundry_mcp.core.prompts.fidelity_review import (
|
|
28
|
+
FIDELITY_SYNTHESIZED_RESPONSE_SCHEMA,
|
|
29
|
+
)
|
|
30
|
+
from foundry_mcp.core.llm_config import get_consultation_config
|
|
25
31
|
from foundry_mcp.core.naming import canonical_tool
|
|
26
32
|
from foundry_mcp.core.observability import get_metrics, mcp_tool
|
|
27
33
|
from foundry_mcp.core.providers import get_provider_statuses
|
|
@@ -82,7 +88,11 @@ def _parse_json_content(content: str) -> Optional[dict]:
|
|
|
82
88
|
|
|
83
89
|
def _handle_spec_review(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
|
|
84
90
|
spec_id = payload.get("spec_id")
|
|
85
|
-
review_type
|
|
91
|
+
# Get default review_type from consultation config (used when not provided or None)
|
|
92
|
+
consultation_config = get_consultation_config()
|
|
93
|
+
workflow_config = consultation_config.get_workflow_config("plan_review")
|
|
94
|
+
default_review_type = workflow_config.default_review_type
|
|
95
|
+
review_type = payload.get("review_type") or default_review_type
|
|
86
96
|
|
|
87
97
|
if not isinstance(spec_id, str) or not spec_id.strip():
|
|
88
98
|
return asdict(
|
|
@@ -360,6 +370,159 @@ def _handle_parse_feedback(*, config: ServerConfig, payload: Dict[str, Any]) ->
|
|
|
360
370
|
)
|
|
361
371
|
|
|
362
372
|
|
|
373
|
+
def _format_fidelity_markdown(
|
|
374
|
+
parsed: Dict[str, Any],
|
|
375
|
+
spec_id: str,
|
|
376
|
+
spec_title: str,
|
|
377
|
+
scope: str,
|
|
378
|
+
task_id: Optional[str] = None,
|
|
379
|
+
phase_id: Optional[str] = None,
|
|
380
|
+
provider_id: Optional[str] = None,
|
|
381
|
+
) -> str:
|
|
382
|
+
"""Format fidelity review JSON as human-readable markdown."""
|
|
383
|
+
# Build scope detail
|
|
384
|
+
scope_detail = scope
|
|
385
|
+
if task_id:
|
|
386
|
+
scope_detail += f" (task: {task_id})"
|
|
387
|
+
elif phase_id:
|
|
388
|
+
scope_detail += f" (phase: {phase_id})"
|
|
389
|
+
|
|
390
|
+
lines = [
|
|
391
|
+
f"# Fidelity Review: {spec_title}",
|
|
392
|
+
"",
|
|
393
|
+
f"**Spec ID:** {spec_id}",
|
|
394
|
+
f"**Scope:** {scope_detail}",
|
|
395
|
+
f"**Verdict:** {parsed.get('verdict', 'unknown')}",
|
|
396
|
+
f"**Date:** {datetime.now().isoformat()}",
|
|
397
|
+
]
|
|
398
|
+
if provider_id:
|
|
399
|
+
lines.append(f"**Provider:** {provider_id}")
|
|
400
|
+
lines.append("")
|
|
401
|
+
|
|
402
|
+
# Summary section
|
|
403
|
+
if parsed.get("summary"):
|
|
404
|
+
lines.extend(["## Summary", "", parsed["summary"], ""])
|
|
405
|
+
|
|
406
|
+
# Requirement Alignment
|
|
407
|
+
req_align = parsed.get("requirement_alignment", {})
|
|
408
|
+
if req_align:
|
|
409
|
+
lines.extend([
|
|
410
|
+
"## Requirement Alignment",
|
|
411
|
+
f"**Status:** {req_align.get('answer', 'unknown')}",
|
|
412
|
+
"",
|
|
413
|
+
req_align.get("details", ""),
|
|
414
|
+
"",
|
|
415
|
+
])
|
|
416
|
+
|
|
417
|
+
# Success Criteria
|
|
418
|
+
success = parsed.get("success_criteria", {})
|
|
419
|
+
if success:
|
|
420
|
+
lines.extend([
|
|
421
|
+
"## Success Criteria",
|
|
422
|
+
f"**Status:** {success.get('met', 'unknown')}",
|
|
423
|
+
"",
|
|
424
|
+
success.get("details", ""),
|
|
425
|
+
"",
|
|
426
|
+
])
|
|
427
|
+
|
|
428
|
+
# Deviations
|
|
429
|
+
deviations = parsed.get("deviations", [])
|
|
430
|
+
if deviations:
|
|
431
|
+
lines.extend(["## Deviations", ""])
|
|
432
|
+
for dev in deviations:
|
|
433
|
+
severity = dev.get("severity", "unknown")
|
|
434
|
+
description = dev.get("description", "")
|
|
435
|
+
justification = dev.get("justification", "")
|
|
436
|
+
lines.append(f"- **[{severity.upper()}]** {description}")
|
|
437
|
+
if justification:
|
|
438
|
+
lines.append(f" - Justification: {justification}")
|
|
439
|
+
lines.append("")
|
|
440
|
+
|
|
441
|
+
# Test Coverage
|
|
442
|
+
test_cov = parsed.get("test_coverage", {})
|
|
443
|
+
if test_cov:
|
|
444
|
+
lines.extend([
|
|
445
|
+
"## Test Coverage",
|
|
446
|
+
f"**Status:** {test_cov.get('status', 'unknown')}",
|
|
447
|
+
"",
|
|
448
|
+
test_cov.get("details", ""),
|
|
449
|
+
"",
|
|
450
|
+
])
|
|
451
|
+
|
|
452
|
+
# Code Quality
|
|
453
|
+
code_quality = parsed.get("code_quality", {})
|
|
454
|
+
if code_quality:
|
|
455
|
+
lines.extend(["## Code Quality", ""])
|
|
456
|
+
if code_quality.get("details"):
|
|
457
|
+
lines.append(code_quality["details"])
|
|
458
|
+
lines.append("")
|
|
459
|
+
for issue in code_quality.get("issues", []):
|
|
460
|
+
lines.append(f"- {issue}")
|
|
461
|
+
lines.append("")
|
|
462
|
+
|
|
463
|
+
# Documentation
|
|
464
|
+
doc = parsed.get("documentation", {})
|
|
465
|
+
if doc:
|
|
466
|
+
lines.extend([
|
|
467
|
+
"## Documentation",
|
|
468
|
+
f"**Status:** {doc.get('status', 'unknown')}",
|
|
469
|
+
"",
|
|
470
|
+
doc.get("details", ""),
|
|
471
|
+
"",
|
|
472
|
+
])
|
|
473
|
+
|
|
474
|
+
# Issues
|
|
475
|
+
issues = parsed.get("issues", [])
|
|
476
|
+
if issues:
|
|
477
|
+
lines.extend(["## Issues", ""])
|
|
478
|
+
for issue in issues:
|
|
479
|
+
lines.append(f"- {issue}")
|
|
480
|
+
lines.append("")
|
|
481
|
+
|
|
482
|
+
# Recommendations
|
|
483
|
+
recommendations = parsed.get("recommendations", [])
|
|
484
|
+
if recommendations:
|
|
485
|
+
lines.extend(["## Recommendations", ""])
|
|
486
|
+
for rec in recommendations:
|
|
487
|
+
lines.append(f"- {rec}")
|
|
488
|
+
lines.append("")
|
|
489
|
+
|
|
490
|
+
# Verdict consensus (if synthesized)
|
|
491
|
+
verdict_consensus = parsed.get("verdict_consensus", {})
|
|
492
|
+
if verdict_consensus:
|
|
493
|
+
lines.extend(["## Verdict Consensus", ""])
|
|
494
|
+
votes = verdict_consensus.get("votes", {})
|
|
495
|
+
for verdict_type, models in votes.items():
|
|
496
|
+
if models:
|
|
497
|
+
lines.append(f"- **{verdict_type}:** {', '.join(models)}")
|
|
498
|
+
agreement = verdict_consensus.get("agreement_level", "")
|
|
499
|
+
if agreement:
|
|
500
|
+
lines.append(f"\n**Agreement Level:** {agreement}")
|
|
501
|
+
notes = verdict_consensus.get("notes", "")
|
|
502
|
+
if notes:
|
|
503
|
+
lines.extend(["", notes])
|
|
504
|
+
lines.append("")
|
|
505
|
+
|
|
506
|
+
# Synthesis metadata
|
|
507
|
+
synth_meta = parsed.get("synthesis_metadata", {})
|
|
508
|
+
if synth_meta:
|
|
509
|
+
lines.extend(["## Synthesis Metadata", ""])
|
|
510
|
+
if synth_meta.get("models_consulted"):
|
|
511
|
+
lines.append(f"- Models consulted: {', '.join(synth_meta['models_consulted'])}")
|
|
512
|
+
if synth_meta.get("models_succeeded"):
|
|
513
|
+
lines.append(f"- Models succeeded: {', '.join(synth_meta['models_succeeded'])}")
|
|
514
|
+
if synth_meta.get("synthesis_provider"):
|
|
515
|
+
lines.append(f"- Synthesis provider: {synth_meta['synthesis_provider']}")
|
|
516
|
+
lines.append("")
|
|
517
|
+
|
|
518
|
+
lines.extend([
|
|
519
|
+
"---",
|
|
520
|
+
"*Generated by Foundry MCP Fidelity Review*",
|
|
521
|
+
])
|
|
522
|
+
|
|
523
|
+
return "\n".join(lines)
|
|
524
|
+
|
|
525
|
+
|
|
363
526
|
def _handle_fidelity(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
|
|
364
527
|
"""Best-effort fidelity review.
|
|
365
528
|
|
|
@@ -512,9 +675,25 @@ def _handle_fidelity(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
|
|
|
512
675
|
|
|
513
676
|
scope = "task" if task_id else ("phase" if phase_id else "spec")
|
|
514
677
|
|
|
678
|
+
# Setup fidelity reviews directory and file naming
|
|
679
|
+
fidelity_reviews_dir = Path(specs_dir) / ".fidelity-reviews"
|
|
680
|
+
base_name = f"{spec_id}-{scope}"
|
|
681
|
+
if task_id:
|
|
682
|
+
base_name += f"-{task_id}"
|
|
683
|
+
elif phase_id:
|
|
684
|
+
base_name += f"-{phase_id}"
|
|
685
|
+
provider_review_paths: List[Dict[str, Any]] = []
|
|
686
|
+
review_path: Optional[str] = None
|
|
687
|
+
|
|
515
688
|
spec_requirements = _build_spec_requirements(spec_data, task_id, phase_id)
|
|
516
689
|
implementation_artifacts = _build_implementation_artifacts(
|
|
517
|
-
spec_data,
|
|
690
|
+
spec_data,
|
|
691
|
+
task_id,
|
|
692
|
+
phase_id,
|
|
693
|
+
files,
|
|
694
|
+
incremental,
|
|
695
|
+
base_branch,
|
|
696
|
+
workspace_root=ws_path,
|
|
518
697
|
)
|
|
519
698
|
test_results = (
|
|
520
699
|
_build_test_results(spec_data, task_id, phase_id) if include_tests else ""
|
|
@@ -555,27 +734,202 @@ def _handle_fidelity(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
|
|
|
555
734
|
|
|
556
735
|
result = orchestrator.consult(request, use_cache=True)
|
|
557
736
|
is_consensus = isinstance(result, ConsensusResult)
|
|
558
|
-
|
|
737
|
+
synthesis_performed = False
|
|
738
|
+
synthesis_error = None
|
|
739
|
+
successful_providers: List[str] = []
|
|
740
|
+
failed_providers: List[Dict[str, Any]] = []
|
|
741
|
+
|
|
742
|
+
if is_consensus:
|
|
743
|
+
# Extract provider details for visibility
|
|
744
|
+
failed_providers = [
|
|
745
|
+
{"provider_id": r.provider_id, "error": r.error}
|
|
746
|
+
for r in result.responses
|
|
747
|
+
if not r.success
|
|
748
|
+
]
|
|
749
|
+
# Filter for truly successful responses (success=True AND non-empty content)
|
|
750
|
+
successful_responses = [
|
|
751
|
+
r for r in result.responses if r.success and r.content.strip()
|
|
752
|
+
]
|
|
753
|
+
successful_providers = [r.provider_id for r in successful_responses]
|
|
754
|
+
|
|
755
|
+
if len(successful_responses) >= 2:
|
|
756
|
+
# Multi-model mode: run synthesis to consolidate reviews
|
|
757
|
+
model_reviews_json = ""
|
|
758
|
+
for response in successful_responses:
|
|
759
|
+
model_reviews_json += (
|
|
760
|
+
f"\n---\n## Review by {response.provider_id}\n\n"
|
|
761
|
+
f"```json\n{response.content}\n```\n"
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
# Write individual provider review files
|
|
765
|
+
try:
|
|
766
|
+
fidelity_reviews_dir.mkdir(parents=True, exist_ok=True)
|
|
767
|
+
for response in successful_responses:
|
|
768
|
+
provider_parsed = _parse_json_content(response.content)
|
|
769
|
+
provider_file = fidelity_reviews_dir / f"{base_name}-{response.provider_id}.md"
|
|
770
|
+
if provider_parsed:
|
|
771
|
+
provider_md = _format_fidelity_markdown(
|
|
772
|
+
provider_parsed,
|
|
773
|
+
spec_id,
|
|
774
|
+
spec_data.get("title", spec_id),
|
|
775
|
+
scope,
|
|
776
|
+
task_id=task_id,
|
|
777
|
+
phase_id=phase_id,
|
|
778
|
+
provider_id=response.provider_id,
|
|
779
|
+
)
|
|
780
|
+
provider_file.write_text(provider_md, encoding="utf-8")
|
|
781
|
+
provider_review_paths.append({
|
|
782
|
+
"provider_id": response.provider_id,
|
|
783
|
+
"path": str(provider_file),
|
|
784
|
+
})
|
|
785
|
+
else:
|
|
786
|
+
# JSON parsing failed - write raw content as fallback
|
|
787
|
+
logger.warning(
|
|
788
|
+
"Provider %s returned non-JSON content, writing raw response",
|
|
789
|
+
response.provider_id,
|
|
790
|
+
)
|
|
791
|
+
raw_md = (
|
|
792
|
+
f"# Fidelity Review (Raw): {spec_id}\n\n"
|
|
793
|
+
f"**Provider:** {response.provider_id}\n"
|
|
794
|
+
f"**Note:** Response could not be parsed as JSON\n\n"
|
|
795
|
+
f"## Raw Response\n\n```\n{response.content}\n```\n"
|
|
796
|
+
)
|
|
797
|
+
provider_file.write_text(raw_md, encoding="utf-8")
|
|
798
|
+
provider_review_paths.append({
|
|
799
|
+
"provider_id": response.provider_id,
|
|
800
|
+
"path": str(provider_file),
|
|
801
|
+
"parse_error": True,
|
|
802
|
+
})
|
|
803
|
+
except Exception as e:
|
|
804
|
+
logger.warning("Failed to write provider review files: %s", e)
|
|
805
|
+
|
|
806
|
+
logger.info(
|
|
807
|
+
"Running fidelity synthesis for %d provider reviews: %s",
|
|
808
|
+
len(successful_responses),
|
|
809
|
+
successful_providers,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
synthesis_request = ConsultationRequest(
|
|
813
|
+
workflow=ConsultationWorkflow.FIDELITY_REVIEW,
|
|
814
|
+
prompt_id="FIDELITY_SYNTHESIS_PROMPT_V1",
|
|
815
|
+
context={
|
|
816
|
+
"spec_id": spec_id,
|
|
817
|
+
"spec_title": spec_data.get("title", spec_id),
|
|
818
|
+
"review_scope": scope,
|
|
819
|
+
"num_models": len(successful_responses),
|
|
820
|
+
"model_reviews": model_reviews_json,
|
|
821
|
+
"response_schema": FIDELITY_SYNTHESIZED_RESPONSE_SCHEMA,
|
|
822
|
+
},
|
|
823
|
+
provider_id=successful_providers[0],
|
|
824
|
+
model=model,
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
try:
|
|
828
|
+
synthesis_result = orchestrator.consult(synthesis_request, use_cache=True)
|
|
829
|
+
except Exception as e:
|
|
830
|
+
logger.error("Fidelity synthesis call crashed: %s", e, exc_info=True)
|
|
831
|
+
synthesis_result = None
|
|
832
|
+
|
|
833
|
+
# Handle both ConsultationResult and ConsensusResult from synthesis
|
|
834
|
+
synthesis_success = False
|
|
835
|
+
synthesis_content = None
|
|
836
|
+
if synthesis_result:
|
|
837
|
+
if isinstance(synthesis_result, ConsultationResult) and synthesis_result.success:
|
|
838
|
+
synthesis_content = synthesis_result.content
|
|
839
|
+
synthesis_success = bool(synthesis_content and synthesis_content.strip())
|
|
840
|
+
elif isinstance(synthesis_result, ConsensusResult) and synthesis_result.success:
|
|
841
|
+
synthesis_content = synthesis_result.primary_content
|
|
842
|
+
synthesis_success = bool(synthesis_content and synthesis_content.strip())
|
|
843
|
+
|
|
844
|
+
if synthesis_success and synthesis_content:
|
|
845
|
+
content = synthesis_content
|
|
846
|
+
synthesis_performed = True
|
|
847
|
+
else:
|
|
848
|
+
# Synthesis failed - fall back to first provider's content
|
|
849
|
+
error_detail = "unknown"
|
|
850
|
+
if synthesis_result is None:
|
|
851
|
+
error_detail = "synthesis crashed (see logs)"
|
|
852
|
+
elif isinstance(synthesis_result, ConsultationResult):
|
|
853
|
+
error_detail = synthesis_result.error or "empty response"
|
|
854
|
+
elif isinstance(synthesis_result, ConsensusResult):
|
|
855
|
+
error_detail = "empty synthesis content"
|
|
856
|
+
logger.warning(
|
|
857
|
+
"Fidelity synthesis call failed (%s), falling back to first provider's content",
|
|
858
|
+
error_detail,
|
|
859
|
+
)
|
|
860
|
+
content = result.primary_content
|
|
861
|
+
synthesis_error = error_detail
|
|
862
|
+
else:
|
|
863
|
+
# Single successful provider - use its content directly (no synthesis needed)
|
|
864
|
+
content = result.primary_content
|
|
865
|
+
else:
|
|
866
|
+
content = result.content
|
|
559
867
|
|
|
560
868
|
parsed = _parse_json_content(content)
|
|
561
869
|
verdict = parsed.get("verdict") if parsed else "unknown"
|
|
562
870
|
|
|
871
|
+
# Write main fidelity review file
|
|
872
|
+
if parsed:
|
|
873
|
+
try:
|
|
874
|
+
fidelity_reviews_dir.mkdir(parents=True, exist_ok=True)
|
|
875
|
+
main_md = _format_fidelity_markdown(
|
|
876
|
+
parsed,
|
|
877
|
+
spec_id,
|
|
878
|
+
spec_data.get("title", spec_id),
|
|
879
|
+
scope,
|
|
880
|
+
task_id=task_id,
|
|
881
|
+
phase_id=phase_id,
|
|
882
|
+
)
|
|
883
|
+
review_file = fidelity_reviews_dir / f"{base_name}.md"
|
|
884
|
+
review_file.write_text(main_md, encoding="utf-8")
|
|
885
|
+
review_path = str(review_file)
|
|
886
|
+
except Exception as e:
|
|
887
|
+
logger.warning("Failed to write main fidelity review file: %s", e)
|
|
888
|
+
|
|
563
889
|
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
564
890
|
|
|
891
|
+
# Build consensus info with synthesis details
|
|
892
|
+
consensus_info: Dict[str, Any] = {
|
|
893
|
+
"mode": "multi_model" if is_consensus else "single_model",
|
|
894
|
+
"threshold": consensus_threshold,
|
|
895
|
+
"provider_id": getattr(result, "provider_id", None),
|
|
896
|
+
"model_used": getattr(result, "model_used", None),
|
|
897
|
+
"synthesis_performed": synthesis_performed,
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if is_consensus:
|
|
901
|
+
consensus_info["successful_providers"] = successful_providers
|
|
902
|
+
consensus_info["failed_providers"] = failed_providers
|
|
903
|
+
if synthesis_error:
|
|
904
|
+
consensus_info["synthesis_error"] = synthesis_error
|
|
905
|
+
|
|
906
|
+
# Include additional synthesized fields if available
|
|
907
|
+
response_data: Dict[str, Any] = {
|
|
908
|
+
"spec_id": spec_id,
|
|
909
|
+
"title": spec_data.get("title", spec_id),
|
|
910
|
+
"scope": scope,
|
|
911
|
+
"verdict": verdict,
|
|
912
|
+
"deviations": parsed.get("deviations") if parsed else [],
|
|
913
|
+
"recommendations": parsed.get("recommendations") if parsed else [],
|
|
914
|
+
"consensus": consensus_info,
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
# Add file paths if reviews were written
|
|
918
|
+
if review_path:
|
|
919
|
+
response_data["review_path"] = review_path
|
|
920
|
+
if provider_review_paths:
|
|
921
|
+
response_data["provider_reviews"] = provider_review_paths
|
|
922
|
+
|
|
923
|
+
# Add synthesis-specific fields if synthesis was performed
|
|
924
|
+
if synthesis_performed and parsed:
|
|
925
|
+
if "verdict_consensus" in parsed:
|
|
926
|
+
response_data["verdict_consensus"] = parsed["verdict_consensus"]
|
|
927
|
+
if "synthesis_metadata" in parsed:
|
|
928
|
+
response_data["synthesis_metadata"] = parsed["synthesis_metadata"]
|
|
929
|
+
|
|
565
930
|
return asdict(
|
|
566
931
|
success_response(
|
|
567
|
-
|
|
568
|
-
title=spec_data.get("title", spec_id),
|
|
569
|
-
scope=scope,
|
|
570
|
-
verdict=verdict,
|
|
571
|
-
deviations=(parsed.get("deviations") if parsed else []),
|
|
572
|
-
recommendations=(parsed.get("recommendations") if parsed else []),
|
|
573
|
-
consensus={
|
|
574
|
-
"mode": "multi_model" if is_consensus else "single_model",
|
|
575
|
-
"threshold": consensus_threshold,
|
|
576
|
-
"provider_id": getattr(result, "provider_id", None),
|
|
577
|
-
"model_used": getattr(result, "model_used", None),
|
|
578
|
-
},
|
|
932
|
+
**response_data,
|
|
579
933
|
telemetry={"duration_ms": round(duration_ms, 2)},
|
|
580
934
|
)
|
|
581
935
|
)
|
|
@@ -633,7 +987,7 @@ def register_unified_review_tool(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
633
987
|
def review(
|
|
634
988
|
action: str,
|
|
635
989
|
spec_id: Optional[str] = None,
|
|
636
|
-
review_type: str =
|
|
990
|
+
review_type: Optional[str] = None,
|
|
637
991
|
tools: Optional[str] = None,
|
|
638
992
|
model: Optional[str] = None,
|
|
639
993
|
ai_provider: Optional[str] = None,
|