crucible-mcp 0.1.0__py3-none-any.whl → 0.2.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/tools/git.py ADDED
@@ -0,0 +1,317 @@
1
+ """Git operations for change-aware code review."""
2
+
3
+ import re
4
+ import shutil
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from crucible.errors import Result, err, ok
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class LineRange:
14
+ """A range of lines in a file."""
15
+
16
+ start: int
17
+ end: int
18
+
19
+ def contains(self, line: int) -> bool:
20
+ """Check if a line number is within this range."""
21
+ return self.start <= line <= self.end
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class GitChange:
26
+ """A changed file in git with its modified line ranges."""
27
+
28
+ path: str
29
+ status: str # A=added, M=modified, D=deleted, R=renamed
30
+ added_lines: tuple[LineRange, ...]
31
+ old_path: str | None = None
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class GitContext:
36
+ """Context about git changes for a review."""
37
+
38
+ mode: str
39
+ base_ref: str | None
40
+ changes: tuple[GitChange, ...]
41
+ commit_messages: tuple[str, ...] | None = None
42
+
43
+
44
+ def is_git_repo(path: str | Path) -> bool:
45
+ """Check if the path is inside a git repository."""
46
+ try:
47
+ result = subprocess.run(
48
+ ["git", "rev-parse", "--git-dir"],
49
+ cwd=str(path),
50
+ capture_output=True,
51
+ text=True,
52
+ timeout=5,
53
+ )
54
+ return result.returncode == 0
55
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
56
+ return False
57
+
58
+
59
+ def get_repo_root(path: str | Path) -> Result[str, str]:
60
+ """Get the root directory of the git repository."""
61
+ if not shutil.which("git"):
62
+ return err("git not found")
63
+
64
+ try:
65
+ result = subprocess.run(
66
+ ["git", "rev-parse", "--show-toplevel"],
67
+ cwd=str(path),
68
+ capture_output=True,
69
+ text=True,
70
+ timeout=5,
71
+ )
72
+ if result.returncode != 0:
73
+ return err("Not a git repository")
74
+ return ok(result.stdout.strip())
75
+ except subprocess.TimeoutExpired:
76
+ return err("git command timed out")
77
+ except OSError as e:
78
+ return err(f"Failed to run git: {e}")
79
+
80
+
81
+ def _run_git(args: list[str], cwd: str, timeout: int = 30) -> Result[str, str]:
82
+ """Run a git command and return stdout or error."""
83
+ if not shutil.which("git"):
84
+ return err("git not found")
85
+
86
+ try:
87
+ result = subprocess.run(
88
+ ["git", *args],
89
+ cwd=cwd,
90
+ capture_output=True,
91
+ text=True,
92
+ timeout=timeout,
93
+ )
94
+ if result.returncode != 0:
95
+ stderr = result.stderr.strip()
96
+ if "unknown revision" in stderr.lower() or "bad revision" in stderr.lower():
97
+ return err(f"Unknown ref: {stderr}")
98
+ return err(stderr or f"git {args[0]} failed")
99
+ return ok(result.stdout)
100
+ except subprocess.TimeoutExpired:
101
+ return err(f"git command timed out after {timeout}s")
102
+ except OSError as e:
103
+ return err(f"Failed to run git: {e}")
104
+
105
+
106
+ def parse_diff_line_ranges(diff_output: str) -> dict[str, list[LineRange]]:
107
+ """
108
+ Parse unified diff output to extract added line ranges per file.
109
+
110
+ Returns a dict mapping file paths to lists of LineRange for added/modified lines.
111
+ """
112
+ result: dict[str, list[LineRange]] = {}
113
+ current_file: str | None = None
114
+
115
+ # Match diff header: +++ b/path/to/file
116
+ file_pattern = re.compile(r"^\+\+\+ b/(.+)$")
117
+ # Match hunk header: @@ -old_start,old_count +new_start,new_count @@
118
+ hunk_pattern = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
119
+
120
+ lines = diff_output.split("\n")
121
+ i = 0
122
+ while i < len(lines):
123
+ line = lines[i]
124
+
125
+ # Check for new file
126
+ file_match = file_pattern.match(line)
127
+ if file_match:
128
+ current_file = file_match.group(1)
129
+ if current_file not in result:
130
+ result[current_file] = []
131
+ i += 1
132
+ continue
133
+
134
+ # Check for hunk header
135
+ hunk_match = hunk_pattern.match(line)
136
+ if hunk_match and current_file:
137
+ new_start = int(hunk_match.group(1))
138
+
139
+ # Track which lines within this hunk are additions
140
+ i += 1
141
+ current_line = new_start
142
+ hunk_added_lines: list[int] = []
143
+
144
+ while i < len(lines):
145
+ hunk_line = lines[i]
146
+ if hunk_line.startswith("@@") or hunk_line.startswith("diff "):
147
+ break
148
+ if hunk_line.startswith("+") and not hunk_line.startswith("+++"):
149
+ hunk_added_lines.append(current_line)
150
+ current_line += 1
151
+ elif hunk_line.startswith("-") and not hunk_line.startswith("---"):
152
+ pass # Deleted line, don't increment current_line
153
+ else:
154
+ current_line += 1
155
+ i += 1
156
+
157
+ # Convert consecutive lines to ranges
158
+ if hunk_added_lines:
159
+ ranges = _lines_to_ranges(hunk_added_lines)
160
+ result[current_file].extend(ranges)
161
+ continue
162
+
163
+ i += 1
164
+
165
+ return result
166
+
167
+
168
+ def _lines_to_ranges(lines: list[int]) -> list[LineRange]:
169
+ """Convert a sorted list of line numbers to LineRange objects."""
170
+ if not lines:
171
+ return []
172
+
173
+ ranges: list[LineRange] = []
174
+ start = lines[0]
175
+ end = lines[0]
176
+
177
+ for line in lines[1:]:
178
+ if line == end + 1:
179
+ end = line
180
+ else:
181
+ ranges.append(LineRange(start=start, end=end))
182
+ start = line
183
+ end = line
184
+
185
+ ranges.append(LineRange(start=start, end=end))
186
+ return ranges
187
+
188
+
189
+ def get_staged_changes(repo_path: str) -> Result[GitContext, str]:
190
+ """Get staged changes (ready to commit)."""
191
+ # Get diff of staged changes
192
+ diff_result = _run_git(["diff", "--cached", "--unified=0"], repo_path)
193
+ if diff_result.is_err:
194
+ return err(diff_result.error)
195
+
196
+ # Get list of staged files with status
197
+ status_result = _run_git(["diff", "--cached", "--name-status"], repo_path)
198
+ if status_result.is_err:
199
+ return err(status_result.error)
200
+
201
+ changes = _parse_changes(diff_result.value, status_result.value)
202
+ return ok(GitContext(mode="staged", base_ref=None, changes=tuple(changes)))
203
+
204
+
205
+ def get_unstaged_changes(repo_path: str) -> Result[GitContext, str]:
206
+ """Get unstaged changes (working directory)."""
207
+ diff_result = _run_git(["diff", "--unified=0"], repo_path)
208
+ if diff_result.is_err:
209
+ return err(diff_result.error)
210
+
211
+ status_result = _run_git(["diff", "--name-status"], repo_path)
212
+ if status_result.is_err:
213
+ return err(status_result.error)
214
+
215
+ changes = _parse_changes(diff_result.value, status_result.value)
216
+ return ok(GitContext(mode="unstaged", base_ref=None, changes=tuple(changes)))
217
+
218
+
219
+ def get_branch_diff(repo_path: str, base: str = "main") -> Result[GitContext, str]:
220
+ """Get diff between current branch and base branch."""
221
+ # Use three-dot diff to show changes since branching
222
+ diff_result = _run_git(["diff", f"{base}...HEAD", "--unified=0"], repo_path)
223
+ if diff_result.is_err:
224
+ return err(diff_result.error)
225
+
226
+ status_result = _run_git(["diff", f"{base}...HEAD", "--name-status"], repo_path)
227
+ if status_result.is_err:
228
+ return err(status_result.error)
229
+
230
+ # Get commit messages since base
231
+ log_result = _run_git(
232
+ ["log", f"{base}..HEAD", "--pretty=format:%s", "--reverse"], repo_path
233
+ )
234
+ commit_messages = None
235
+ if log_result.is_ok and log_result.value.strip():
236
+ commit_messages = tuple(log_result.value.strip().split("\n"))
237
+
238
+ changes = _parse_changes(diff_result.value, status_result.value)
239
+ return ok(
240
+ GitContext(
241
+ mode="branch",
242
+ base_ref=base,
243
+ changes=tuple(changes),
244
+ commit_messages=commit_messages,
245
+ )
246
+ )
247
+
248
+
249
+ def get_recent_commits(repo_path: str, count: int = 1) -> Result[GitContext, str]:
250
+ """Get changes from recent N commits."""
251
+ if count < 1:
252
+ return err("Commit count must be at least 1")
253
+
254
+ diff_result = _run_git(["diff", f"HEAD~{count}..HEAD", "--unified=0"], repo_path)
255
+ if diff_result.is_err:
256
+ return err(diff_result.error)
257
+
258
+ status_result = _run_git(["diff", f"HEAD~{count}..HEAD", "--name-status"], repo_path)
259
+ if status_result.is_err:
260
+ return err(status_result.error)
261
+
262
+ # Get commit messages
263
+ log_result = _run_git(
264
+ ["log", f"HEAD~{count}..HEAD", "--pretty=format:%s", "--reverse"], repo_path
265
+ )
266
+ commit_messages = None
267
+ if log_result.is_ok and log_result.value.strip():
268
+ commit_messages = tuple(log_result.value.strip().split("\n"))
269
+
270
+ changes = _parse_changes(diff_result.value, status_result.value)
271
+ return ok(
272
+ GitContext(
273
+ mode="commits",
274
+ base_ref=f"HEAD~{count}",
275
+ changes=tuple(changes),
276
+ commit_messages=commit_messages,
277
+ )
278
+ )
279
+
280
+
281
+ def _parse_changes(diff_output: str, status_output: str) -> list[GitChange]:
282
+ """Parse git diff and status output into GitChange objects."""
283
+ # Parse line ranges from diff
284
+ line_ranges = parse_diff_line_ranges(diff_output)
285
+
286
+ # Parse file statuses
287
+ changes: list[GitChange] = []
288
+ for line in status_output.strip().split("\n"):
289
+ if not line:
290
+ continue
291
+
292
+ parts = line.split("\t")
293
+ if len(parts) < 2:
294
+ continue
295
+
296
+ status = parts[0][0] # First char of status (A, M, D, R)
297
+ path = parts[-1] # Last part is always the current path
298
+ old_path = parts[1] if status == "R" and len(parts) > 2 else None
299
+
300
+ # Get added lines for this file
301
+ added_lines = tuple(line_ranges.get(path, []))
302
+
303
+ changes.append(
304
+ GitChange(
305
+ path=path,
306
+ status=status,
307
+ added_lines=added_lines,
308
+ old_path=old_path,
309
+ )
310
+ )
311
+
312
+ return changes
313
+
314
+
315
+ def get_changed_files(context: GitContext) -> list[str]:
316
+ """Get list of changed file paths from a git context (excluding deleted)."""
317
+ return [c.path for c in context.changes if c.status != "D"]
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: crucible-mcp
3
+ Version: 0.2.0
4
+ Summary: Code review MCP server for Claude. Not affiliated with Atlassian.
5
+ Author: be.nvy
6
+ License-Expression: MIT
7
+ Keywords: mcp,code-review,static-analysis,claude
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: mcp>=1.0.0
11
+ Requires-Dist: pyyaml>=6.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8.0; extra == "dev"
14
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
15
+ Requires-Dist: mypy>=1.8; extra == "dev"
16
+ Requires-Dist: ruff>=0.3; extra == "dev"
17
+
18
+ # Crucible
19
+
20
+ Load your coding patterns into Claude Code.
21
+
22
+ ```
23
+ ├── Personas: Domain-specific thinking (how to approach problems)
24
+ ├── Knowledge: Coding patterns and principles (what to apply)
25
+ ├── Cascade: Project → User → Bundled (customizable at every level)
26
+ └── Context-aware: Loads relevant skills based on what you're working on
27
+ ```
28
+
29
+ **Personas for domains. Knowledge for patterns. All customizable.**
30
+
31
+ > Not affiliated with Atlassian's Crucible.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install crucible-mcp
37
+ ```
38
+
39
+ Add to Claude Code (`.mcp.json`):
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "crucible": {
45
+ "command": "crucible-mcp"
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ With hot reload (recommended for customization):
52
+
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "crucible": {
57
+ "command": "mcpmon",
58
+ "args": ["--watch", "~/.crucible/", "--", "crucible-mcp"]
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ ## How It Works
65
+
66
+ ```
67
+ Code → Detect Domain → Load Personas + Knowledge → Claude with YOUR patterns
68
+
69
+ .sol file → web3 domain → security-engineer + SMART_CONTRACT.md → Knows your security rules
70
+ ```
71
+
72
+ ## MCP Tools
73
+
74
+ | Tool | Purpose |
75
+ |------|---------|
76
+ | `quick_review(path)` | Run analysis, return findings + domains |
77
+ | `get_principles(topic)` | Load engineering knowledge |
78
+ | `delegate_*` | Direct tool access (semgrep, ruff, slither, bandit) |
79
+ | `check_tools()` | Show installed analysis tools |
80
+
81
+ ## CLI
82
+
83
+ ```bash
84
+ crucible skills list # List all skills
85
+ crucible skills show <skill> # Show which version is active
86
+ crucible skills init <skill> # Copy to .crucible/ for customization
87
+
88
+ crucible knowledge list # List all knowledge files
89
+ crucible knowledge init <file> # Copy for customization
90
+ ```
91
+
92
+ See [ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full flow.
93
+
94
+ ## Customization
95
+
96
+ Override skills and knowledge for your project or personal preferences:
97
+
98
+ ```bash
99
+ # Customize a skill for your project
100
+ crucible skills init security-engineer
101
+ # Creates .crucible/skills/security-engineer/SKILL.md
102
+
103
+ # Add project-specific concerns, team conventions, etc.
104
+ ```
105
+
106
+ Resolution order (first found wins):
107
+ 1. `.crucible/` — Project overrides
108
+ 2. `~/.claude/crucible/` — User preferences
109
+ 3. Bundled — Package defaults
110
+
111
+ See [CUSTOMIZATION.md](docs/CUSTOMIZATION.md) for the full guide.
112
+
113
+ ## What's Included
114
+
115
+ **18 Personas** — Domain-specific thinking: security, performance, accessibility, web3, backend, and more.
116
+
117
+ See [SKILLS.md](docs/SKILLS.md) for the full list.
118
+
119
+ **12 Knowledge Files** — Coding patterns and principles for security, testing, APIs, databases, smart contracts, etc.
120
+
121
+ See [KNOWLEDGE.md](docs/KNOWLEDGE.md) for topics covered.
122
+
123
+ ## Documentation
124
+
125
+ | Doc | What's In It |
126
+ |-----|--------------|
127
+ | [FEATURES.md](docs/FEATURES.md) | Complete feature reference |
128
+ | [ARCHITECTURE.md](docs/ARCHITECTURE.md) | How MCP, tools, skills, and knowledge fit together |
129
+ | [CUSTOMIZATION.md](docs/CUSTOMIZATION.md) | Override skills and knowledge for your project |
130
+ | [SKILLS.md](docs/SKILLS.md) | All 18 personas with triggers and focus areas |
131
+ | [KNOWLEDGE.md](docs/KNOWLEDGE.md) | All 12 knowledge files with topics covered |
132
+ | [CONTRIBUTING.md](docs/CONTRIBUTING.md) | Adding tools, skills, and knowledge |
133
+
134
+ ## Development
135
+
136
+ ```bash
137
+ pip install -e ".[dev]"
138
+ pytest # Run tests (494 tests)
139
+ ruff check src/ --fix # Lint
140
+ ```
@@ -0,0 +1,22 @@
1
+ crucible/__init__.py,sha256=ZLZQWKmjTHaeeDijcOl3xmaEgoI2W3a8FCFwcieZGv0,77
2
+ crucible/cli.py,sha256=eCOHbV1jn9SdjtRVB8-PnHah0e-mkrxSOFjMYpMTUCY,51651
3
+ crucible/errors.py,sha256=HrX_yvJEhXJoKodXGo_iY9wqx2J3ONYy0a_LbrVC5As,819
4
+ crucible/models.py,sha256=jaxbiPc1E7bJxKPLadZe1dbSJdq-WINsxjveeSNNqeg,2066
5
+ crucible/server.py,sha256=GnT_wT9QJE5gAQYmYzSIuaj5hG2hKfOrHHK104tPtqA,34493
6
+ crucible/domain/__init__.py,sha256=2fsoB5wH2Pl3vtGRt4voYOSZ04-zLoW8pNq6nvzVMgU,118
7
+ crucible/domain/detection.py,sha256=TNeLB_VQgS1AsT5BKDf_tIpGa47THrFoRXwU4u54VB0,1797
8
+ crucible/hooks/__init__.py,sha256=k5oEWhTJKEQi-QWBfTbp1p6HaKg55_wVCBVD5pZzdqw,271
9
+ crucible/hooks/precommit.py,sha256=OAwvjEACopcrTmWmZMO0S8TqZkvFY_392pJBFCHGSaQ,21561
10
+ crucible/knowledge/__init__.py,sha256=unb7kyO1MtB3Zt-TGx_O8LE79KyrGrNHoFFHgUWUvGU,40
11
+ crucible/knowledge/loader.py,sha256=DD4gqU6xkssaWvEkbymMOu6YtHab7YLEk-tU9cPTeaE,6666
12
+ crucible/skills/__init__.py,sha256=L3heXWF0T3aR9yYLFphs1LNlkxAFSPkPuRFMH-S1taI,495
13
+ crucible/skills/loader.py,sha256=iC0_V1s6CIse5NXyFGtpLbON8xDxYh8xXmHH7hAX5O0,8642
14
+ crucible/synthesis/__init__.py,sha256=CYrkZG4bdAjp8XdOh1smfKscd3YU5lZlaDLGwLE9c-0,46
15
+ crucible/tools/__init__.py,sha256=gFRThTk1E-fHzpe8bB5rtBG6Z6G-ysPzjVEHfKGbEYU,400
16
+ crucible/tools/delegation.py,sha256=_x1y76No3qkmGjjROVvMx1pSKKwU59aRu5R-r07lVFU,12871
17
+ crucible/tools/git.py,sha256=EmxRUt0jSFLa_mm_2Czt5rHdiFC0YK9IpaPDfRwlXVo,10051
18
+ crucible_mcp-0.2.0.dist-info/METADATA,sha256=Jcnnfw7AxZ-vtqNNOCR9j43MaRxzvXn6oW05nR1wccI,3890
19
+ crucible_mcp-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
20
+ crucible_mcp-0.2.0.dist-info/entry_points.txt,sha256=18BZaH1OlFSFYtKuHq0Z8yYX8Wmx7Ikfqay-P00ZX3Q,83
21
+ crucible_mcp-0.2.0.dist-info/top_level.txt,sha256=4hzuFgqbFPOO-WiU_DYxTm8VYIxTXh7Wlp0gRcWR0Cs,9
22
+ crucible_mcp-0.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,158 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: crucible-mcp
3
- Version: 0.1.0
4
- Summary: Code review MCP server for Claude. Not affiliated with Atlassian.
5
- Author: be.nvy
6
- License-Expression: MIT
7
- Keywords: mcp,code-review,static-analysis,claude
8
- Requires-Python: >=3.11
9
- Description-Content-Type: text/markdown
10
- Requires-Dist: mcp>=1.0.0
11
- Requires-Dist: pyyaml>=6.0
12
- Provides-Extra: dev
13
- Requires-Dist: pytest>=8.0; extra == "dev"
14
- Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
15
- Requires-Dist: mypy>=1.8; extra == "dev"
16
- Requires-Dist: ruff>=0.3; extra == "dev"
17
-
18
- # Crucible
19
-
20
- Code review MCP server for Claude. Runs static analysis and loads review skills based on what kind of code you're looking at.
21
-
22
- > **Note:** This project is not affiliated with Atlassian or their Crucible code review tool. Just an unfortunate naming collision.
23
-
24
- ```
25
- ┌─────────────────────────────────────────────────────────────────────────────┐
26
- │ Your Code ──→ Crucible ──→ Claude │
27
- │ (analysis) (synthesis) │
28
- │ │
29
- │ .sol file ──→ slither, semgrep ──→ web3-engineer skill loaded │
30
- │ .py file ──→ ruff, bandit ──→ backend-engineer skill loaded │
31
- └─────────────────────────────────────────────────────────────────────────────┘
32
- ```
33
-
34
- **MCP provides data. Skills provide perspective. Claude orchestrates.**
35
-
36
- ## Quick Start
37
-
38
- ```bash
39
- # Install from PyPI
40
- pip install crucible-mcp
41
-
42
- # Or install from source
43
- pip install -e ".[dev]"
44
-
45
- # Install skills to ~/.claude/crucible/skills/
46
- crucible skills install
47
-
48
- # Install analysis tools for your stack
49
- pip install semgrep ruff # Python
50
- pip install slither-analyzer # Solidity
51
- pip install bandit # Python security
52
- ```
53
-
54
- > **Tools are separate by design.** Different workflows need different analyzers. Install what you need, skip what you don't. Crucible gracefully handles missing tools.
55
-
56
- ## MCP Setup
57
-
58
- Works with any MCP client (Claude Code, Cursor, etc.). Add to your `.mcp.json`:
59
-
60
- ```json
61
- {
62
- "mcpServers": {
63
- "crucible": {
64
- "command": "crucible-mcp"
65
- }
66
- }
67
- }
68
- ```
69
-
70
- Then in Claude:
71
-
72
- ```
73
- Review src/Vault.sol
74
-
75
- → Crucible: domains_detected: [solidity, smart_contract, web3]
76
- → Crucible: severity_summary: {critical: 1, high: 3}
77
- → Claude loads: web3-engineer, security-engineer skills
78
- → Claude synthesizes multi-perspective review
79
- ```
80
-
81
- ## MCP Tools
82
-
83
- | Tool | Purpose |
84
- |------|---------|
85
- | `quick_review(path)` | Run analysis, return findings + domains |
86
- | `get_principles(topic)` | Load engineering knowledge |
87
- | `delegate_*` | Direct tool access (semgrep, ruff, slither, bandit) |
88
- | `check_tools()` | Show installed analysis tools |
89
-
90
- ## CLI
91
-
92
- ```bash
93
- crucible skills list # List all skills
94
- crucible skills show <skill> # Show which version is active
95
- crucible skills init <skill> # Copy to .crucible/ for customization
96
-
97
- crucible knowledge list # List all knowledge files
98
- crucible knowledge init <file> # Copy for customization
99
- ```
100
-
101
- ## How It Works
102
-
103
- Crucible detects what kind of code you're reviewing, runs the right analysis tools, and returns findings with domain metadata. Claude uses this to load appropriate review skills.
104
-
105
- ```
106
- .sol file → slither + semgrep → web3-engineer, gas-optimizer skills
107
- .py file → ruff + bandit → backend-engineer, security-engineer skills
108
- ```
109
-
110
- See [ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full flow.
111
-
112
- ## Customization
113
-
114
- Override skills and knowledge for your project or personal preferences:
115
-
116
- ```bash
117
- # Customize a skill for your project
118
- crucible skills init security-engineer
119
- # Creates .crucible/skills/security-engineer/SKILL.md
120
-
121
- # Add project-specific concerns, team conventions, etc.
122
- ```
123
-
124
- Resolution order (first found wins):
125
- 1. `.crucible/` — Project overrides
126
- 2. `~/.claude/crucible/` — User preferences
127
- 3. Bundled — Package defaults
128
-
129
- See [CUSTOMIZATION.md](docs/CUSTOMIZATION.md) for the full guide.
130
-
131
- ## What's Included
132
-
133
- **18 Review Skills** — Different review perspectives (security, performance, accessibility, web3, etc.)
134
-
135
- See [SKILLS.md](docs/SKILLS.md) for the full list with triggers and focus areas.
136
-
137
- **12 Knowledge Files** — Engineering principles for security, testing, APIs, databases, smart contracts, etc.
138
-
139
- See [KNOWLEDGE.md](docs/KNOWLEDGE.md) for topics covered and skill linkages.
140
-
141
- ## Documentation
142
-
143
- | Doc | What's In It |
144
- |-----|--------------|
145
- | [FEATURES.md](docs/FEATURES.md) | Complete feature reference |
146
- | [ARCHITECTURE.md](docs/ARCHITECTURE.md) | How MCP, tools, skills, and knowledge fit together |
147
- | [CUSTOMIZATION.md](docs/CUSTOMIZATION.md) | Override skills and knowledge for your project |
148
- | [SKILLS.md](docs/SKILLS.md) | All 18 review personas with triggers and key questions |
149
- | [KNOWLEDGE.md](docs/KNOWLEDGE.md) | All 12 knowledge files with topics covered |
150
- | [CONTRIBUTING.md](docs/CONTRIBUTING.md) | Adding tools, skills, and knowledge |
151
-
152
- ## Development
153
-
154
- ```bash
155
- pip install -e ".[dev]"
156
- pytest # Run tests (263 tests)
157
- ruff check src/ --fix # Lint
158
- ```
@@ -1,17 +0,0 @@
1
- crucible/__init__.py,sha256=ZLZQWKmjTHaeeDijcOl3xmaEgoI2W3a8FCFwcieZGv0,77
2
- crucible/cli.py,sha256=2-aHRpNoSyLSsiDPBYGyg-16dsylHL-opTVUpshKgAE,17149
3
- crucible/errors.py,sha256=HrX_yvJEhXJoKodXGo_iY9wqx2J3ONYy0a_LbrVC5As,819
4
- crucible/models.py,sha256=9bEsNUvKcmq0r_-pCiZ8lv7RaelDQcPgiDia5eghxl4,1624
5
- crucible/server.py,sha256=9_rxelihpZEpYc4axoRNP5rh_PsuzJZpj6qwCdBtz4M,12857
6
- crucible/domain/__init__.py,sha256=2fsoB5wH2Pl3vtGRt4voYOSZ04-zLoW8pNq6nvzVMgU,118
7
- crucible/domain/detection.py,sha256=TNeLB_VQgS1AsT5BKDf_tIpGa47THrFoRXwU4u54VB0,1797
8
- crucible/knowledge/__init__.py,sha256=unb7kyO1MtB3Zt-TGx_O8LE79KyrGrNHoFFHgUWUvGU,40
9
- crucible/knowledge/loader.py,sha256=VuMSZCEkKTP0vTGkju9tHIzokXfq5RRWHYjSo54WDPs,4525
10
- crucible/synthesis/__init__.py,sha256=CYrkZG4bdAjp8XdOh1smfKscd3YU5lZlaDLGwLE9c-0,46
11
- crucible/tools/__init__.py,sha256=gFRThTk1E-fHzpe8bB5rtBG6Z6G-ysPzjVEHfKGbEYU,400
12
- crucible/tools/delegation.py,sha256=A5NM7snJOptwhuaqxfMfPmVoAvWjzpyx3vZoyvRIh_A,10183
13
- crucible_mcp-0.1.0.dist-info/METADATA,sha256=-wOJb1OYlrxQ3Z872Nc3EwIG0KwHkeImrsezKV1cZNM,5529
14
- crucible_mcp-0.1.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
15
- crucible_mcp-0.1.0.dist-info/entry_points.txt,sha256=18BZaH1OlFSFYtKuHq0Z8yYX8Wmx7Ikfqay-P00ZX3Q,83
16
- crucible_mcp-0.1.0.dist-info/top_level.txt,sha256=4hzuFgqbFPOO-WiU_DYxTm8VYIxTXh7Wlp0gRcWR0Cs,9
17
- crucible_mcp-0.1.0.dist-info/RECORD,,