foundry-mcp 0.3.3__py3-none-any.whl → 0.8.10__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/__init__.py +0 -13
- foundry_mcp/cli/commands/plan.py +10 -3
- foundry_mcp/cli/commands/review.py +19 -4
- foundry_mcp/cli/commands/session.py +1 -8
- foundry_mcp/cli/commands/specs.py +38 -208
- foundry_mcp/cli/context.py +39 -0
- foundry_mcp/cli/output.py +3 -3
- foundry_mcp/config.py +615 -11
- foundry_mcp/core/ai_consultation.py +146 -9
- foundry_mcp/core/batch_operations.py +1196 -0
- foundry_mcp/core/discovery.py +7 -7
- foundry_mcp/core/error_store.py +2 -2
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/llm_config.py +28 -2
- foundry_mcp/core/metrics_store.py +2 -2
- foundry_mcp/core/naming.py +25 -2
- foundry_mcp/core/progress.py +70 -0
- foundry_mcp/core/prometheus.py +0 -13
- 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/__init__.py +12 -0
- foundry_mcp/core/providers/base.py +39 -0
- foundry_mcp/core/providers/claude.py +51 -48
- foundry_mcp/core/providers/codex.py +70 -60
- foundry_mcp/core/providers/cursor_agent.py +25 -47
- foundry_mcp/core/providers/detectors.py +34 -7
- foundry_mcp/core/providers/gemini.py +69 -58
- foundry_mcp/core/providers/opencode.py +101 -47
- foundry_mcp/core/providers/package-lock.json +4 -4
- foundry_mcp/core/providers/package.json +1 -1
- foundry_mcp/core/providers/validation.py +128 -0
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1220 -0
- foundry_mcp/core/research/providers/__init__.py +40 -0
- foundry_mcp/core/research/providers/base.py +242 -0
- foundry_mcp/core/research/providers/google.py +507 -0
- foundry_mcp/core/research/providers/perplexity.py +442 -0
- foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
- foundry_mcp/core/research/providers/tavily.py +383 -0
- foundry_mcp/core/research/workflows/__init__.py +25 -0
- foundry_mcp/core/research/workflows/base.py +298 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +539 -0
- foundry_mcp/core/research/workflows/deep_research.py +4020 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/responses.py +690 -0
- foundry_mcp/core/spec.py +2439 -236
- foundry_mcp/core/task.py +1205 -31
- foundry_mcp/core/testing.py +512 -123
- foundry_mcp/core/validation.py +319 -43
- 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 +0 -14
- foundry_mcp/tools/unified/__init__.py +39 -18
- foundry_mcp/tools/unified/authoring.py +2371 -248
- foundry_mcp/tools/unified/documentation_helpers.py +69 -6
- foundry_mcp/tools/unified/environment.py +434 -32
- foundry_mcp/tools/unified/error.py +18 -1
- foundry_mcp/tools/unified/lifecycle.py +8 -0
- foundry_mcp/tools/unified/plan.py +133 -2
- foundry_mcp/tools/unified/provider.py +0 -40
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +374 -17
- foundry_mcp/tools/unified/review_helpers.py +16 -1
- foundry_mcp/tools/unified/server.py +9 -24
- foundry_mcp/tools/unified/spec.py +367 -0
- foundry_mcp/tools/unified/task.py +1664 -30
- foundry_mcp/tools/unified/test.py +69 -8
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.8.10.dist-info}/METADATA +8 -1
- foundry_mcp-0.8.10.dist-info/RECORD +153 -0
- foundry_mcp/cli/flags.py +0 -266
- foundry_mcp/core/feature_flags.py +0 -592
- foundry_mcp-0.3.3.dist-info/RECORD +0 -135
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.8.10.dist-info}/WHEEL +0 -0
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.8.10.dist-info}/entry_points.txt +0 -0
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.8.10.dist-info}/licenses/LICENSE +0 -0
foundry_mcp/core/responses.py
CHANGED
|
@@ -113,14 +113,23 @@ class ErrorCode(str, Enum):
|
|
|
113
113
|
VALIDATION_ERROR = "VALIDATION_ERROR"
|
|
114
114
|
INVALID_FORMAT = "INVALID_FORMAT"
|
|
115
115
|
MISSING_REQUIRED = "MISSING_REQUIRED"
|
|
116
|
+
INVALID_PARENT = "INVALID_PARENT"
|
|
117
|
+
INVALID_POSITION = "INVALID_POSITION"
|
|
118
|
+
INVALID_REGEX_PATTERN = "INVALID_REGEX_PATTERN"
|
|
119
|
+
PATTERN_TOO_BROAD = "PATTERN_TOO_BROAD"
|
|
116
120
|
|
|
117
121
|
# Resource errors
|
|
118
122
|
NOT_FOUND = "NOT_FOUND"
|
|
119
123
|
SPEC_NOT_FOUND = "SPEC_NOT_FOUND"
|
|
120
124
|
TASK_NOT_FOUND = "TASK_NOT_FOUND"
|
|
121
125
|
PHASE_NOT_FOUND = "PHASE_NOT_FOUND"
|
|
126
|
+
DEPENDENCY_NOT_FOUND = "DEPENDENCY_NOT_FOUND"
|
|
127
|
+
BACKUP_NOT_FOUND = "BACKUP_NOT_FOUND"
|
|
128
|
+
NO_MATCHES_FOUND = "NO_MATCHES_FOUND"
|
|
122
129
|
DUPLICATE_ENTRY = "DUPLICATE_ENTRY"
|
|
123
130
|
CONFLICT = "CONFLICT"
|
|
131
|
+
CIRCULAR_DEPENDENCY = "CIRCULAR_DEPENDENCY"
|
|
132
|
+
SELF_REFERENCE = "SELF_REFERENCE"
|
|
124
133
|
|
|
125
134
|
# Access errors
|
|
126
135
|
UNAUTHORIZED = "UNAUTHORIZED"
|
|
@@ -131,6 +140,11 @@ class ErrorCode(str, Enum):
|
|
|
131
140
|
# System errors
|
|
132
141
|
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
133
142
|
UNAVAILABLE = "UNAVAILABLE"
|
|
143
|
+
RESOURCE_BUSY = "RESOURCE_BUSY"
|
|
144
|
+
BACKUP_CORRUPTED = "BACKUP_CORRUPTED"
|
|
145
|
+
ROLLBACK_FAILED = "ROLLBACK_FAILED"
|
|
146
|
+
COMPARISON_FAILED = "COMPARISON_FAILED"
|
|
147
|
+
OPERATION_FAILED = "OPERATION_FAILED"
|
|
134
148
|
|
|
135
149
|
# AI/LLM Provider errors
|
|
136
150
|
AI_NO_PROVIDER = "AI_NO_PROVIDER"
|
|
@@ -604,6 +618,443 @@ def unavailable_error(
|
|
|
604
618
|
)
|
|
605
619
|
|
|
606
620
|
|
|
621
|
+
# ---------------------------------------------------------------------------
|
|
622
|
+
# Spec Modification Error Helpers
|
|
623
|
+
# ---------------------------------------------------------------------------
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def circular_dependency_error(
|
|
627
|
+
task_id: str,
|
|
628
|
+
target_id: str,
|
|
629
|
+
*,
|
|
630
|
+
cycle_path: Optional[Sequence[str]] = None,
|
|
631
|
+
remediation: Optional[str] = None,
|
|
632
|
+
request_id: Optional[str] = None,
|
|
633
|
+
) -> ToolResponse:
|
|
634
|
+
"""Create an error response for circular dependency detection.
|
|
635
|
+
|
|
636
|
+
Use when a move or dependency operation would create a cycle.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
task_id: The task being moved or modified.
|
|
640
|
+
target_id: The target parent or dependency that would create a cycle.
|
|
641
|
+
cycle_path: Optional sequence showing the dependency cycle path.
|
|
642
|
+
remediation: Guidance on how to resolve.
|
|
643
|
+
request_id: Correlation identifier.
|
|
644
|
+
|
|
645
|
+
Example:
|
|
646
|
+
>>> circular_dependency_error("task-3", "task-1", cycle_path=["task-1", "task-2", "task-3"])
|
|
647
|
+
"""
|
|
648
|
+
data: Dict[str, Any] = {
|
|
649
|
+
"task_id": task_id,
|
|
650
|
+
"target_id": target_id,
|
|
651
|
+
}
|
|
652
|
+
if cycle_path:
|
|
653
|
+
data["cycle_path"] = list(cycle_path)
|
|
654
|
+
|
|
655
|
+
return error_response(
|
|
656
|
+
f"Circular dependency detected: {task_id} cannot depend on {target_id}",
|
|
657
|
+
error_code=ErrorCode.CIRCULAR_DEPENDENCY,
|
|
658
|
+
error_type=ErrorType.CONFLICT,
|
|
659
|
+
data=data,
|
|
660
|
+
remediation=remediation
|
|
661
|
+
or "Remove an existing dependency to break the cycle before adding this one.",
|
|
662
|
+
request_id=request_id,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def invalid_parent_error(
|
|
667
|
+
task_id: str,
|
|
668
|
+
target_parent: str,
|
|
669
|
+
reason: str,
|
|
670
|
+
*,
|
|
671
|
+
valid_parents: Optional[Sequence[str]] = None,
|
|
672
|
+
remediation: Optional[str] = None,
|
|
673
|
+
request_id: Optional[str] = None,
|
|
674
|
+
) -> ToolResponse:
|
|
675
|
+
"""Create an error response for invalid parent in move operation.
|
|
676
|
+
|
|
677
|
+
Use when a task cannot be moved to the specified parent.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
task_id: The task being moved.
|
|
681
|
+
target_parent: The invalid target parent.
|
|
682
|
+
reason: Why the parent is invalid (e.g., "is a task, not a phase").
|
|
683
|
+
valid_parents: Optional list of valid parent IDs.
|
|
684
|
+
remediation: Guidance on how to resolve.
|
|
685
|
+
request_id: Correlation identifier.
|
|
686
|
+
|
|
687
|
+
Example:
|
|
688
|
+
>>> invalid_parent_error("task-3-1", "task-2-1", "target is a task, not a phase")
|
|
689
|
+
"""
|
|
690
|
+
data: Dict[str, Any] = {
|
|
691
|
+
"task_id": task_id,
|
|
692
|
+
"target_parent": target_parent,
|
|
693
|
+
"reason": reason,
|
|
694
|
+
}
|
|
695
|
+
if valid_parents:
|
|
696
|
+
data["valid_parents"] = list(valid_parents)
|
|
697
|
+
|
|
698
|
+
return error_response(
|
|
699
|
+
f"Invalid parent '{target_parent}' for task '{task_id}': {reason}",
|
|
700
|
+
error_code=ErrorCode.INVALID_PARENT,
|
|
701
|
+
error_type=ErrorType.VALIDATION,
|
|
702
|
+
data=data,
|
|
703
|
+
remediation=remediation or "Specify a valid phase or parent task as the target.",
|
|
704
|
+
request_id=request_id,
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def self_reference_error(
|
|
709
|
+
task_id: str,
|
|
710
|
+
operation: str,
|
|
711
|
+
*,
|
|
712
|
+
remediation: Optional[str] = None,
|
|
713
|
+
request_id: Optional[str] = None,
|
|
714
|
+
) -> ToolResponse:
|
|
715
|
+
"""Create an error response for self-referencing operations.
|
|
716
|
+
|
|
717
|
+
Use when a task references itself in dependencies or move operations.
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
task_id: The task that references itself.
|
|
721
|
+
operation: The operation attempted (e.g., "add-dependency", "move").
|
|
722
|
+
remediation: Guidance on how to resolve.
|
|
723
|
+
request_id: Correlation identifier.
|
|
724
|
+
|
|
725
|
+
Example:
|
|
726
|
+
>>> self_reference_error("task-1-1", "add-dependency")
|
|
727
|
+
"""
|
|
728
|
+
return error_response(
|
|
729
|
+
f"Task '{task_id}' cannot reference itself in {operation}",
|
|
730
|
+
error_code=ErrorCode.SELF_REFERENCE,
|
|
731
|
+
error_type=ErrorType.VALIDATION,
|
|
732
|
+
data={"task_id": task_id, "operation": operation},
|
|
733
|
+
remediation=remediation or "Specify a different task ID as the target.",
|
|
734
|
+
request_id=request_id,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def dependency_not_found_error(
|
|
739
|
+
task_id: str,
|
|
740
|
+
dependency_id: str,
|
|
741
|
+
*,
|
|
742
|
+
remediation: Optional[str] = None,
|
|
743
|
+
request_id: Optional[str] = None,
|
|
744
|
+
) -> ToolResponse:
|
|
745
|
+
"""Create an error response for missing dependency in remove operation.
|
|
746
|
+
|
|
747
|
+
Use when trying to remove a dependency that doesn't exist.
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
task_id: The task being modified.
|
|
751
|
+
dependency_id: The dependency that wasn't found.
|
|
752
|
+
remediation: Guidance on how to resolve.
|
|
753
|
+
request_id: Correlation identifier.
|
|
754
|
+
|
|
755
|
+
Example:
|
|
756
|
+
>>> dependency_not_found_error("task-1-1", "task-2-1")
|
|
757
|
+
"""
|
|
758
|
+
return error_response(
|
|
759
|
+
f"Dependency '{dependency_id}' not found on task '{task_id}'",
|
|
760
|
+
error_code=ErrorCode.DEPENDENCY_NOT_FOUND,
|
|
761
|
+
error_type=ErrorType.NOT_FOUND,
|
|
762
|
+
data={"task_id": task_id, "dependency_id": dependency_id},
|
|
763
|
+
remediation=remediation
|
|
764
|
+
or "Check existing dependencies using task info before removing.",
|
|
765
|
+
request_id=request_id,
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def invalid_position_error(
|
|
770
|
+
item_id: str,
|
|
771
|
+
position: int,
|
|
772
|
+
max_position: int,
|
|
773
|
+
*,
|
|
774
|
+
remediation: Optional[str] = None,
|
|
775
|
+
request_id: Optional[str] = None,
|
|
776
|
+
) -> ToolResponse:
|
|
777
|
+
"""Create an error response for invalid position in move/reorder operation.
|
|
778
|
+
|
|
779
|
+
Use when the specified position is out of valid range.
|
|
780
|
+
|
|
781
|
+
Args:
|
|
782
|
+
item_id: The item being moved (phase or task ID).
|
|
783
|
+
position: The invalid position specified.
|
|
784
|
+
max_position: The maximum valid position.
|
|
785
|
+
remediation: Guidance on how to resolve.
|
|
786
|
+
request_id: Correlation identifier.
|
|
787
|
+
|
|
788
|
+
Example:
|
|
789
|
+
>>> invalid_position_error("phase-3", 10, 5)
|
|
790
|
+
"""
|
|
791
|
+
return error_response(
|
|
792
|
+
f"Invalid position {position} for '{item_id}': must be 1-{max_position}",
|
|
793
|
+
error_code=ErrorCode.INVALID_POSITION,
|
|
794
|
+
error_type=ErrorType.VALIDATION,
|
|
795
|
+
data={
|
|
796
|
+
"item_id": item_id,
|
|
797
|
+
"position": position,
|
|
798
|
+
"max_position": max_position,
|
|
799
|
+
"valid_range": f"1-{max_position}",
|
|
800
|
+
},
|
|
801
|
+
remediation=remediation or f"Specify a position between 1 and {max_position}.",
|
|
802
|
+
request_id=request_id,
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def invalid_regex_error(
|
|
807
|
+
pattern: str,
|
|
808
|
+
error_detail: str,
|
|
809
|
+
*,
|
|
810
|
+
remediation: Optional[str] = None,
|
|
811
|
+
request_id: Optional[str] = None,
|
|
812
|
+
) -> ToolResponse:
|
|
813
|
+
"""Create an error response for invalid regex pattern.
|
|
814
|
+
|
|
815
|
+
Use when a find/replace pattern is not valid regex.
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
pattern: The invalid regex pattern.
|
|
819
|
+
error_detail: The regex error message.
|
|
820
|
+
remediation: Guidance on how to fix the pattern.
|
|
821
|
+
request_id: Correlation identifier.
|
|
822
|
+
|
|
823
|
+
Example:
|
|
824
|
+
>>> invalid_regex_error("[unclosed", "unterminated character set")
|
|
825
|
+
"""
|
|
826
|
+
return error_response(
|
|
827
|
+
f"Invalid regex pattern: {error_detail}",
|
|
828
|
+
error_code=ErrorCode.INVALID_REGEX_PATTERN,
|
|
829
|
+
error_type=ErrorType.VALIDATION,
|
|
830
|
+
data={"pattern": pattern, "error_detail": error_detail},
|
|
831
|
+
remediation=remediation
|
|
832
|
+
or "Check regex syntax. Use raw strings and escape special characters.",
|
|
833
|
+
request_id=request_id,
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def pattern_too_broad_error(
|
|
838
|
+
pattern: str,
|
|
839
|
+
match_count: int,
|
|
840
|
+
max_matches: int,
|
|
841
|
+
*,
|
|
842
|
+
remediation: Optional[str] = None,
|
|
843
|
+
request_id: Optional[str] = None,
|
|
844
|
+
) -> ToolResponse:
|
|
845
|
+
"""Create an error response for overly broad patterns.
|
|
846
|
+
|
|
847
|
+
Use when a find/replace pattern matches too many items.
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
pattern: The pattern that matched too broadly.
|
|
851
|
+
match_count: Number of matches found.
|
|
852
|
+
max_matches: Maximum allowed matches.
|
|
853
|
+
remediation: Guidance on how to narrow the pattern.
|
|
854
|
+
request_id: Correlation identifier.
|
|
855
|
+
|
|
856
|
+
Example:
|
|
857
|
+
>>> pattern_too_broad_error(".*", 500, 100)
|
|
858
|
+
"""
|
|
859
|
+
return error_response(
|
|
860
|
+
f"Pattern too broad: {match_count} matches exceeds limit of {max_matches}",
|
|
861
|
+
error_code=ErrorCode.PATTERN_TOO_BROAD,
|
|
862
|
+
error_type=ErrorType.VALIDATION,
|
|
863
|
+
data={
|
|
864
|
+
"pattern": pattern,
|
|
865
|
+
"match_count": match_count,
|
|
866
|
+
"max_matches": max_matches,
|
|
867
|
+
},
|
|
868
|
+
remediation=remediation
|
|
869
|
+
or "Use a more specific pattern or apply to a narrower scope.",
|
|
870
|
+
request_id=request_id,
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def no_matches_error(
|
|
875
|
+
pattern: str,
|
|
876
|
+
scope: str,
|
|
877
|
+
*,
|
|
878
|
+
remediation: Optional[str] = None,
|
|
879
|
+
request_id: Optional[str] = None,
|
|
880
|
+
) -> ToolResponse:
|
|
881
|
+
"""Create an error response for patterns with no matches.
|
|
882
|
+
|
|
883
|
+
Use when a find/replace pattern matches nothing.
|
|
884
|
+
|
|
885
|
+
Args:
|
|
886
|
+
pattern: The pattern that found no matches.
|
|
887
|
+
scope: Where the search was performed (e.g., "spec", "phase-1").
|
|
888
|
+
remediation: Guidance on what to check.
|
|
889
|
+
request_id: Correlation identifier.
|
|
890
|
+
|
|
891
|
+
Example:
|
|
892
|
+
>>> no_matches_error("deprecated_function", "spec my-spec-001")
|
|
893
|
+
"""
|
|
894
|
+
return error_response(
|
|
895
|
+
f"No matches found for pattern '{pattern}' in {scope}",
|
|
896
|
+
error_code=ErrorCode.NO_MATCHES_FOUND,
|
|
897
|
+
error_type=ErrorType.NOT_FOUND,
|
|
898
|
+
data={"pattern": pattern, "scope": scope},
|
|
899
|
+
remediation=remediation
|
|
900
|
+
or "Verify the pattern and scope. Use dry-run to preview matches.",
|
|
901
|
+
request_id=request_id,
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def backup_not_found_error(
|
|
906
|
+
spec_id: str,
|
|
907
|
+
backup_id: Optional[str] = None,
|
|
908
|
+
*,
|
|
909
|
+
available_backups: Optional[Sequence[str]] = None,
|
|
910
|
+
remediation: Optional[str] = None,
|
|
911
|
+
request_id: Optional[str] = None,
|
|
912
|
+
) -> ToolResponse:
|
|
913
|
+
"""Create an error response for missing backup.
|
|
914
|
+
|
|
915
|
+
Use when a rollback or diff references a non-existent backup.
|
|
916
|
+
|
|
917
|
+
Args:
|
|
918
|
+
spec_id: The spec whose backup is missing.
|
|
919
|
+
backup_id: The specific backup ID that wasn't found.
|
|
920
|
+
available_backups: Optional list of available backup IDs.
|
|
921
|
+
remediation: Guidance on how to resolve.
|
|
922
|
+
request_id: Correlation identifier.
|
|
923
|
+
|
|
924
|
+
Example:
|
|
925
|
+
>>> backup_not_found_error("my-spec-001", "backup-2024-01-15")
|
|
926
|
+
"""
|
|
927
|
+
data: Dict[str, Any] = {"spec_id": spec_id}
|
|
928
|
+
if backup_id:
|
|
929
|
+
data["backup_id"] = backup_id
|
|
930
|
+
if available_backups:
|
|
931
|
+
data["available_backups"] = list(available_backups)
|
|
932
|
+
|
|
933
|
+
message = f"Backup not found for spec '{spec_id}'"
|
|
934
|
+
if backup_id:
|
|
935
|
+
message = f"Backup '{backup_id}' not found for spec '{spec_id}'"
|
|
936
|
+
|
|
937
|
+
return error_response(
|
|
938
|
+
message,
|
|
939
|
+
error_code=ErrorCode.BACKUP_NOT_FOUND,
|
|
940
|
+
error_type=ErrorType.NOT_FOUND,
|
|
941
|
+
data=data,
|
|
942
|
+
remediation=remediation or "List available backups using spec action='history'.",
|
|
943
|
+
request_id=request_id,
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def backup_corrupted_error(
|
|
948
|
+
spec_id: str,
|
|
949
|
+
backup_id: str,
|
|
950
|
+
error_detail: str,
|
|
951
|
+
*,
|
|
952
|
+
remediation: Optional[str] = None,
|
|
953
|
+
request_id: Optional[str] = None,
|
|
954
|
+
) -> ToolResponse:
|
|
955
|
+
"""Create an error response for corrupted backup.
|
|
956
|
+
|
|
957
|
+
Use when a backup file exists but cannot be loaded.
|
|
958
|
+
|
|
959
|
+
Args:
|
|
960
|
+
spec_id: The spec whose backup is corrupted.
|
|
961
|
+
backup_id: The corrupted backup identifier.
|
|
962
|
+
error_detail: Description of the corruption.
|
|
963
|
+
remediation: Guidance on how to recover.
|
|
964
|
+
request_id: Correlation identifier.
|
|
965
|
+
|
|
966
|
+
Example:
|
|
967
|
+
>>> backup_corrupted_error("my-spec", "backup-001", "Invalid JSON structure")
|
|
968
|
+
"""
|
|
969
|
+
return error_response(
|
|
970
|
+
f"Backup '{backup_id}' for spec '{spec_id}' is corrupted: {error_detail}",
|
|
971
|
+
error_code=ErrorCode.BACKUP_CORRUPTED,
|
|
972
|
+
error_type=ErrorType.INTERNAL,
|
|
973
|
+
data={
|
|
974
|
+
"spec_id": spec_id,
|
|
975
|
+
"backup_id": backup_id,
|
|
976
|
+
"error_detail": error_detail,
|
|
977
|
+
},
|
|
978
|
+
remediation=remediation
|
|
979
|
+
or "Try an earlier backup or restore from version control.",
|
|
980
|
+
request_id=request_id,
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def rollback_failed_error(
|
|
985
|
+
spec_id: str,
|
|
986
|
+
backup_id: str,
|
|
987
|
+
error_detail: str,
|
|
988
|
+
*,
|
|
989
|
+
remediation: Optional[str] = None,
|
|
990
|
+
request_id: Optional[str] = None,
|
|
991
|
+
) -> ToolResponse:
|
|
992
|
+
"""Create an error response for failed rollback operation.
|
|
993
|
+
|
|
994
|
+
Use when a rollback operation fails after starting.
|
|
995
|
+
|
|
996
|
+
Args:
|
|
997
|
+
spec_id: The spec being rolled back.
|
|
998
|
+
backup_id: The backup being restored from.
|
|
999
|
+
error_detail: What went wrong during rollback.
|
|
1000
|
+
remediation: Guidance on how to recover.
|
|
1001
|
+
request_id: Correlation identifier.
|
|
1002
|
+
|
|
1003
|
+
Example:
|
|
1004
|
+
>>> rollback_failed_error("my-spec", "backup-001", "Write permission denied")
|
|
1005
|
+
"""
|
|
1006
|
+
return error_response(
|
|
1007
|
+
f"Rollback failed for spec '{spec_id}' from backup '{backup_id}': {error_detail}",
|
|
1008
|
+
error_code=ErrorCode.ROLLBACK_FAILED,
|
|
1009
|
+
error_type=ErrorType.INTERNAL,
|
|
1010
|
+
data={
|
|
1011
|
+
"spec_id": spec_id,
|
|
1012
|
+
"backup_id": backup_id,
|
|
1013
|
+
"error_detail": error_detail,
|
|
1014
|
+
},
|
|
1015
|
+
remediation=remediation
|
|
1016
|
+
or "Check file permissions. A safety backup was created before rollback attempt.",
|
|
1017
|
+
request_id=request_id,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def comparison_failed_error(
|
|
1022
|
+
source: str,
|
|
1023
|
+
target: str,
|
|
1024
|
+
error_detail: str,
|
|
1025
|
+
*,
|
|
1026
|
+
remediation: Optional[str] = None,
|
|
1027
|
+
request_id: Optional[str] = None,
|
|
1028
|
+
) -> ToolResponse:
|
|
1029
|
+
"""Create an error response for failed diff/comparison operation.
|
|
1030
|
+
|
|
1031
|
+
Use when a spec comparison operation fails.
|
|
1032
|
+
|
|
1033
|
+
Args:
|
|
1034
|
+
source: The source spec or backup being compared.
|
|
1035
|
+
target: The target spec or backup being compared.
|
|
1036
|
+
error_detail: What went wrong during comparison.
|
|
1037
|
+
remediation: Guidance on how to resolve.
|
|
1038
|
+
request_id: Correlation identifier.
|
|
1039
|
+
|
|
1040
|
+
Example:
|
|
1041
|
+
>>> comparison_failed_error("my-spec-v1", "my-spec-v2", "Schema version mismatch")
|
|
1042
|
+
"""
|
|
1043
|
+
return error_response(
|
|
1044
|
+
f"Comparison failed between '{source}' and '{target}': {error_detail}",
|
|
1045
|
+
error_code=ErrorCode.COMPARISON_FAILED,
|
|
1046
|
+
error_type=ErrorType.INTERNAL,
|
|
1047
|
+
data={
|
|
1048
|
+
"source": source,
|
|
1049
|
+
"target": target,
|
|
1050
|
+
"error_detail": error_detail,
|
|
1051
|
+
},
|
|
1052
|
+
remediation=remediation
|
|
1053
|
+
or "Ensure both specs are valid and use compatible schema versions.",
|
|
1054
|
+
request_id=request_id,
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
|
|
607
1058
|
# ---------------------------------------------------------------------------
|
|
608
1059
|
# AI/LLM Provider Error Helpers
|
|
609
1060
|
# ---------------------------------------------------------------------------
|
|
@@ -881,6 +1332,13 @@ def ai_cache_stale_error(
|
|
|
881
1332
|
# ---------------------------------------------------------------------------
|
|
882
1333
|
|
|
883
1334
|
|
|
1335
|
+
try:
|
|
1336
|
+
from pydantic import BaseModel, Field
|
|
1337
|
+
PYDANTIC_AVAILABLE = True
|
|
1338
|
+
except ImportError:
|
|
1339
|
+
PYDANTIC_AVAILABLE = False
|
|
1340
|
+
|
|
1341
|
+
|
|
884
1342
|
def sanitize_error_message(
|
|
885
1343
|
exc: Exception,
|
|
886
1344
|
context: str = "",
|
|
@@ -932,3 +1390,235 @@ def sanitize_error_message(
|
|
|
932
1390
|
# Generic fallback - don't expose exception message
|
|
933
1391
|
suffix = f" ({type_name})" if include_type else ""
|
|
934
1392
|
return f"An internal error occurred{suffix}"
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
# ---------------------------------------------------------------------------
|
|
1396
|
+
# Batch Operation Response Schemas (Pydantic)
|
|
1397
|
+
# ---------------------------------------------------------------------------
|
|
1398
|
+
# These schemas provide type-safe definitions for batch operation responses.
|
|
1399
|
+
# They ensure contract stability and enable validation of batch operation data.
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
if PYDANTIC_AVAILABLE:
|
|
1403
|
+
|
|
1404
|
+
class DependencyNode(BaseModel):
|
|
1405
|
+
"""A node in the dependency graph representing a task."""
|
|
1406
|
+
|
|
1407
|
+
id: str = Field(..., description="Task identifier")
|
|
1408
|
+
title: str = Field(default="", description="Task title")
|
|
1409
|
+
status: str = Field(default="", description="Task status")
|
|
1410
|
+
file_path: Optional[str] = Field(
|
|
1411
|
+
default=None, description="File path associated with the task"
|
|
1412
|
+
)
|
|
1413
|
+
is_target: bool = Field(
|
|
1414
|
+
default=False, description="Whether this is a target task in the batch"
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
class DependencyEdge(BaseModel):
|
|
1418
|
+
"""An edge in the dependency graph representing a dependency relationship."""
|
|
1419
|
+
|
|
1420
|
+
from_id: str = Field(..., alias="from", description="Source task ID")
|
|
1421
|
+
to_id: str = Field(..., alias="to", description="Target task ID")
|
|
1422
|
+
edge_type: str = Field(
|
|
1423
|
+
default="blocks", alias="type", description="Type of dependency (blocks)"
|
|
1424
|
+
)
|
|
1425
|
+
|
|
1426
|
+
model_config = {"populate_by_name": True}
|
|
1427
|
+
|
|
1428
|
+
class DependencyGraph(BaseModel):
|
|
1429
|
+
"""Dependency graph structure for batch tasks.
|
|
1430
|
+
|
|
1431
|
+
Contains nodes (tasks) and edges (dependency relationships) to visualize
|
|
1432
|
+
task dependencies for parallel execution planning.
|
|
1433
|
+
"""
|
|
1434
|
+
|
|
1435
|
+
nodes: list[DependencyNode] = Field(
|
|
1436
|
+
default_factory=list, description="Task nodes in the graph"
|
|
1437
|
+
)
|
|
1438
|
+
edges: list[DependencyEdge] = Field(
|
|
1439
|
+
default_factory=list, description="Dependency edges between tasks"
|
|
1440
|
+
)
|
|
1441
|
+
|
|
1442
|
+
class BatchTaskDependencies(BaseModel):
|
|
1443
|
+
"""Dependency status for a task in a batch."""
|
|
1444
|
+
|
|
1445
|
+
task_id: str = Field(..., description="Task identifier")
|
|
1446
|
+
can_start: bool = Field(
|
|
1447
|
+
default=True, description="Whether the task can be started"
|
|
1448
|
+
)
|
|
1449
|
+
blocked_by: list[str] = Field(
|
|
1450
|
+
default_factory=list, description="IDs of tasks blocking this one"
|
|
1451
|
+
)
|
|
1452
|
+
soft_depends: list[str] = Field(
|
|
1453
|
+
default_factory=list, description="IDs of soft dependencies"
|
|
1454
|
+
)
|
|
1455
|
+
blocks: list[str] = Field(
|
|
1456
|
+
default_factory=list, description="IDs of tasks this one blocks"
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
class BatchTaskContext(BaseModel):
|
|
1460
|
+
"""Context for a single task in a batch prepare response.
|
|
1461
|
+
|
|
1462
|
+
Contains all information needed to execute a task in parallel with others.
|
|
1463
|
+
"""
|
|
1464
|
+
|
|
1465
|
+
task_id: str = Field(..., description="Unique task identifier")
|
|
1466
|
+
title: str = Field(default="", description="Task title")
|
|
1467
|
+
task_type: str = Field(
|
|
1468
|
+
default="task", alias="type", description="Task type (task, subtask, verify)"
|
|
1469
|
+
)
|
|
1470
|
+
status: str = Field(default="pending", description="Current task status")
|
|
1471
|
+
metadata: Dict[str, Any] = Field(
|
|
1472
|
+
default_factory=dict,
|
|
1473
|
+
description="Task metadata including file_path, description, etc.",
|
|
1474
|
+
)
|
|
1475
|
+
dependencies: Optional[BatchTaskDependencies] = Field(
|
|
1476
|
+
default=None, description="Dependency status for the task"
|
|
1477
|
+
)
|
|
1478
|
+
phase: Optional[Dict[str, Any]] = Field(
|
|
1479
|
+
default=None, description="Phase context (id, title, progress)"
|
|
1480
|
+
)
|
|
1481
|
+
parent: Optional[Dict[str, Any]] = Field(
|
|
1482
|
+
default=None, description="Parent task context (id, title, position_label)"
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
model_config = {"populate_by_name": True}
|
|
1486
|
+
|
|
1487
|
+
class StaleTaskInfo(BaseModel):
|
|
1488
|
+
"""Information about a stale in_progress task."""
|
|
1489
|
+
|
|
1490
|
+
task_id: str = Field(..., description="Task identifier")
|
|
1491
|
+
title: str = Field(default="", description="Task title")
|
|
1492
|
+
|
|
1493
|
+
class BatchPrepareResponse(BaseModel):
|
|
1494
|
+
"""Response schema for prepare_batch_context operation.
|
|
1495
|
+
|
|
1496
|
+
Contains independent tasks that can be executed in parallel along with
|
|
1497
|
+
context, dependency information, and warnings.
|
|
1498
|
+
"""
|
|
1499
|
+
|
|
1500
|
+
tasks: list[BatchTaskContext] = Field(
|
|
1501
|
+
default_factory=list, description="Tasks ready for parallel execution"
|
|
1502
|
+
)
|
|
1503
|
+
task_count: int = Field(default=0, description="Number of tasks in the batch")
|
|
1504
|
+
spec_complete: bool = Field(
|
|
1505
|
+
default=False, description="Whether the spec has no remaining tasks"
|
|
1506
|
+
)
|
|
1507
|
+
all_blocked: bool = Field(
|
|
1508
|
+
default=False, description="Whether all remaining tasks are blocked"
|
|
1509
|
+
)
|
|
1510
|
+
warnings: list[str] = Field(
|
|
1511
|
+
default_factory=list, description="Non-fatal warnings about the batch"
|
|
1512
|
+
)
|
|
1513
|
+
stale_tasks: list[StaleTaskInfo] = Field(
|
|
1514
|
+
default_factory=list, description="In-progress tasks exceeding time threshold"
|
|
1515
|
+
)
|
|
1516
|
+
dependency_graph: DependencyGraph = Field(
|
|
1517
|
+
default_factory=DependencyGraph,
|
|
1518
|
+
description="Dependency graph for batch tasks",
|
|
1519
|
+
)
|
|
1520
|
+
token_estimate: Optional[int] = Field(
|
|
1521
|
+
default=None, description="Estimated token count for the batch context"
|
|
1522
|
+
)
|
|
1523
|
+
|
|
1524
|
+
class BatchStartResponse(BaseModel):
|
|
1525
|
+
"""Response schema for start_batch operation.
|
|
1526
|
+
|
|
1527
|
+
Confirms which tasks were atomically started and when.
|
|
1528
|
+
"""
|
|
1529
|
+
|
|
1530
|
+
started: list[str] = Field(
|
|
1531
|
+
default_factory=list, description="IDs of tasks successfully started"
|
|
1532
|
+
)
|
|
1533
|
+
started_count: int = Field(
|
|
1534
|
+
default=0, description="Number of tasks started"
|
|
1535
|
+
)
|
|
1536
|
+
started_at: Optional[str] = Field(
|
|
1537
|
+
default=None, description="ISO timestamp when tasks were started"
|
|
1538
|
+
)
|
|
1539
|
+
errors: Optional[list[str]] = Field(
|
|
1540
|
+
default=None, description="Validation errors if operation failed"
|
|
1541
|
+
)
|
|
1542
|
+
|
|
1543
|
+
class BatchTaskCompletion(BaseModel):
|
|
1544
|
+
"""Input schema for a single task completion in complete_batch.
|
|
1545
|
+
|
|
1546
|
+
Used to specify outcome for each task being completed.
|
|
1547
|
+
"""
|
|
1548
|
+
|
|
1549
|
+
task_id: str = Field(..., description="Task identifier to complete")
|
|
1550
|
+
success: bool = Field(
|
|
1551
|
+
..., description="True if task succeeded, False if failed"
|
|
1552
|
+
)
|
|
1553
|
+
completion_note: str = Field(
|
|
1554
|
+
default="", description="Note describing what was accomplished or why it failed"
|
|
1555
|
+
)
|
|
1556
|
+
|
|
1557
|
+
class BatchTaskResult(BaseModel):
|
|
1558
|
+
"""Result for a single task in the complete_batch response."""
|
|
1559
|
+
|
|
1560
|
+
status: str = Field(
|
|
1561
|
+
..., description="Result status: completed, failed, skipped, error"
|
|
1562
|
+
)
|
|
1563
|
+
completed_at: Optional[str] = Field(
|
|
1564
|
+
default=None, description="ISO timestamp when completed (if successful)"
|
|
1565
|
+
)
|
|
1566
|
+
failed_at: Optional[str] = Field(
|
|
1567
|
+
default=None, description="ISO timestamp when failed (if unsuccessful)"
|
|
1568
|
+
)
|
|
1569
|
+
retry_count: Optional[int] = Field(
|
|
1570
|
+
default=None, description="Updated retry count (if failed)"
|
|
1571
|
+
)
|
|
1572
|
+
error: Optional[str] = Field(
|
|
1573
|
+
default=None, description="Error message (if status is error or skipped)"
|
|
1574
|
+
)
|
|
1575
|
+
|
|
1576
|
+
class BatchCompleteResponse(BaseModel):
|
|
1577
|
+
"""Response schema for complete_batch operation.
|
|
1578
|
+
|
|
1579
|
+
Contains per-task results and summary counts for the batch completion.
|
|
1580
|
+
"""
|
|
1581
|
+
|
|
1582
|
+
results: Dict[str, BatchTaskResult] = Field(
|
|
1583
|
+
default_factory=dict,
|
|
1584
|
+
description="Per-task results keyed by task_id",
|
|
1585
|
+
)
|
|
1586
|
+
completed_count: int = Field(
|
|
1587
|
+
default=0, description="Number of tasks successfully completed"
|
|
1588
|
+
)
|
|
1589
|
+
failed_count: int = Field(
|
|
1590
|
+
default=0, description="Number of tasks that failed"
|
|
1591
|
+
)
|
|
1592
|
+
total_processed: int = Field(
|
|
1593
|
+
default=0, description="Total number of completions processed"
|
|
1594
|
+
)
|
|
1595
|
+
|
|
1596
|
+
# Export Pydantic models
|
|
1597
|
+
__all_pydantic__ = [
|
|
1598
|
+
"DependencyNode",
|
|
1599
|
+
"DependencyEdge",
|
|
1600
|
+
"DependencyGraph",
|
|
1601
|
+
"BatchTaskDependencies",
|
|
1602
|
+
"BatchTaskContext",
|
|
1603
|
+
"StaleTaskInfo",
|
|
1604
|
+
"BatchPrepareResponse",
|
|
1605
|
+
"BatchStartResponse",
|
|
1606
|
+
"BatchTaskCompletion",
|
|
1607
|
+
"BatchTaskResult",
|
|
1608
|
+
"BatchCompleteResponse",
|
|
1609
|
+
]
|
|
1610
|
+
|
|
1611
|
+
else:
|
|
1612
|
+
# Pydantic not available - provide None placeholders
|
|
1613
|
+
DependencyNode = None # type: ignore[misc,assignment]
|
|
1614
|
+
DependencyEdge = None # type: ignore[misc,assignment]
|
|
1615
|
+
DependencyGraph = None # type: ignore[misc,assignment]
|
|
1616
|
+
BatchTaskDependencies = None # type: ignore[misc,assignment]
|
|
1617
|
+
BatchTaskContext = None # type: ignore[misc,assignment]
|
|
1618
|
+
StaleTaskInfo = None # type: ignore[misc,assignment]
|
|
1619
|
+
BatchPrepareResponse = None # type: ignore[misc,assignment]
|
|
1620
|
+
BatchStartResponse = None # type: ignore[misc,assignment]
|
|
1621
|
+
BatchTaskCompletion = None # type: ignore[misc,assignment]
|
|
1622
|
+
BatchTaskResult = None # type: ignore[misc,assignment]
|
|
1623
|
+
BatchCompleteResponse = None # type: ignore[misc,assignment]
|
|
1624
|
+
__all_pydantic__ = []
|