crucible-mcp 0.1.0__py3-none-any.whl → 0.3.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.
- crucible/cli.py +1119 -7
- crucible/hooks/__init__.py +15 -0
- crucible/hooks/precommit.py +660 -0
- crucible/knowledge/loader.py +70 -1
- crucible/models.py +15 -0
- crucible/server.py +1059 -6
- crucible/skills/__init__.py +23 -0
- crucible/skills/loader.py +281 -0
- crucible/tools/delegation.py +96 -10
- crucible/tools/git.py +317 -0
- crucible_mcp-0.3.0.dist-info/METADATA +153 -0
- crucible_mcp-0.3.0.dist-info/RECORD +22 -0
- {crucible_mcp-0.1.0.dist-info → crucible_mcp-0.3.0.dist-info}/WHEEL +1 -1
- crucible_mcp-0.1.0.dist-info/METADATA +0 -158
- crucible_mcp-0.1.0.dist-info/RECORD +0 -17
- {crucible_mcp-0.1.0.dist-info → crucible_mcp-0.3.0.dist-info}/entry_points.txt +0 -0
- {crucible_mcp-0.1.0.dist-info → crucible_mcp-0.3.0.dist-info}/top_level.txt +0 -0
crucible/knowledge/loader.py
CHANGED
|
@@ -16,6 +16,25 @@ KNOWLEDGE_USER = Path.home() / ".claude" / "crucible" / "knowledge"
|
|
|
16
16
|
KNOWLEDGE_PROJECT = Path(".crucible") / "knowledge"
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def load_knowledge_file(filename: str) -> Result[str, str]:
|
|
20
|
+
"""Load a single knowledge file by name.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
filename: Knowledge file name (e.g., "SECURITY.md")
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Result containing file content or error message
|
|
27
|
+
"""
|
|
28
|
+
path, source = resolve_knowledge_file(filename)
|
|
29
|
+
if path is None:
|
|
30
|
+
return err(f"Knowledge file '{filename}' not found")
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
return ok(path.read_text())
|
|
34
|
+
except OSError as e:
|
|
35
|
+
return err(f"Failed to read '{filename}': {e}")
|
|
36
|
+
|
|
37
|
+
|
|
19
38
|
def resolve_knowledge_file(filename: str) -> tuple[Path | None, str]:
|
|
20
39
|
"""Find knowledge file with cascade priority.
|
|
21
40
|
|
|
@@ -52,6 +71,55 @@ def get_all_knowledge_files() -> set[str]:
|
|
|
52
71
|
return files
|
|
53
72
|
|
|
54
73
|
|
|
74
|
+
def get_custom_knowledge_files() -> set[str]:
|
|
75
|
+
"""Get knowledge files from project and user directories only.
|
|
76
|
+
|
|
77
|
+
These are custom/team knowledge files that should always be included
|
|
78
|
+
in full_review, regardless of skill references.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Set of filenames from project and user knowledge directories
|
|
82
|
+
"""
|
|
83
|
+
files: set[str] = set()
|
|
84
|
+
|
|
85
|
+
for source_dir in [KNOWLEDGE_USER, KNOWLEDGE_PROJECT]:
|
|
86
|
+
if source_dir.exists():
|
|
87
|
+
for file_path in source_dir.iterdir():
|
|
88
|
+
if file_path.is_file() and file_path.suffix == ".md":
|
|
89
|
+
files.add(file_path.name)
|
|
90
|
+
|
|
91
|
+
return files
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_all_knowledge(
|
|
95
|
+
include_bundled: bool = False,
|
|
96
|
+
filenames: set[str] | None = None,
|
|
97
|
+
) -> tuple[list[str], str]:
|
|
98
|
+
"""Load multiple knowledge files.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
include_bundled: If True, include bundled knowledge files
|
|
102
|
+
filenames: Specific files to load (if None, loads based on include_bundled)
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Tuple of (list of loaded filenames, combined content)
|
|
106
|
+
"""
|
|
107
|
+
if filenames is None:
|
|
108
|
+
filenames = get_all_knowledge_files() if include_bundled else get_custom_knowledge_files()
|
|
109
|
+
|
|
110
|
+
loaded: list[str] = []
|
|
111
|
+
parts: list[str] = []
|
|
112
|
+
|
|
113
|
+
for filename in sorted(filenames):
|
|
114
|
+
result = load_knowledge_file(filename)
|
|
115
|
+
if result.is_ok:
|
|
116
|
+
loaded.append(filename)
|
|
117
|
+
parts.append(f"# {filename}\n\n{result.value}")
|
|
118
|
+
|
|
119
|
+
content = "\n\n---\n\n".join(parts) if parts else ""
|
|
120
|
+
return loaded, content
|
|
121
|
+
|
|
122
|
+
|
|
55
123
|
def load_principles(topic: str | None = None) -> Result[str, str]:
|
|
56
124
|
"""
|
|
57
125
|
Load engineering principles from markdown files.
|
|
@@ -66,9 +134,10 @@ def load_principles(topic: str | None = None) -> Result[str, str]:
|
|
|
66
134
|
topic_files = {
|
|
67
135
|
None: ["SECURITY.md", "TESTING.md"], # Default: security + testing basics
|
|
68
136
|
"engineering": ["TESTING.md", "ERROR_HANDLING.md", "TYPE_SAFETY.md"],
|
|
69
|
-
"security": ["SECURITY.md"],
|
|
137
|
+
"security": ["SECURITY.md", "GITIGNORE.md", "PRECOMMIT.md"],
|
|
70
138
|
"smart_contract": ["SMART_CONTRACT.md"],
|
|
71
139
|
"checklist": ["SECURITY.md", "TESTING.md", "ERROR_HANDLING.md"],
|
|
140
|
+
"repo_hygiene": ["GITIGNORE.md", "PRECOMMIT.md", "COMMITS.md"],
|
|
72
141
|
}
|
|
73
142
|
|
|
74
143
|
files_to_load = topic_files.get(topic, topic_files[None])
|
crucible/models.py
CHANGED
|
@@ -59,3 +59,18 @@ DOMAIN_HEURISTICS: dict[Domain, dict[str, list[str]]] = {
|
|
|
59
59
|
"markers": ["resource ", "provider ", "apiVersion:", "kind:"],
|
|
60
60
|
},
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class FullReviewResult:
|
|
66
|
+
"""Result from full_review tool."""
|
|
67
|
+
|
|
68
|
+
domains_detected: tuple[str, ...]
|
|
69
|
+
severity_summary: dict[str, int]
|
|
70
|
+
findings: tuple[ToolFinding, ...]
|
|
71
|
+
applicable_skills: tuple[str, ...]
|
|
72
|
+
skill_triggers_matched: dict[str, tuple[str, ...]]
|
|
73
|
+
principles_loaded: tuple[str, ...]
|
|
74
|
+
principles_content: str
|
|
75
|
+
sage_knowledge: str | None = None
|
|
76
|
+
sage_query_used: str | None = None
|