craftsman-gate 4.1.0__tar.gz
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.
- craftsman_gate-4.1.0/.gitattributes +12 -0
- craftsman_gate-4.1.0/LICENSE +21 -0
- craftsman_gate-4.1.0/MANIFEST.in +6 -0
- craftsman_gate-4.1.0/NOTICE +12 -0
- craftsman_gate-4.1.0/PKG-INFO +165 -0
- craftsman_gate-4.1.0/README.md +131 -0
- craftsman_gate-4.1.0/craftsman/__init__.py +7 -0
- craftsman_gate-4.1.0/craftsman/antigravity.py +242 -0
- craftsman_gate-4.1.0/craftsman/assets.py +204 -0
- craftsman_gate-4.1.0/craftsman/cli.py +456 -0
- craftsman_gate-4.1.0/craftsman/diffscan.py +1361 -0
- craftsman_gate-4.1.0/craftsman/diffscan_signals.py +228 -0
- craftsman_gate-4.1.0/craftsman/doctor.py +886 -0
- craftsman_gate-4.1.0/craftsman/evidence.py +547 -0
- craftsman_gate-4.1.0/craftsman/export.py +228 -0
- craftsman_gate-4.1.0/craftsman/files.py +141 -0
- craftsman_gate-4.1.0/craftsman/gate.py +681 -0
- craftsman_gate-4.1.0/craftsman/gatecheck.py +754 -0
- craftsman_gate-4.1.0/craftsman/generated.py +147 -0
- craftsman_gate-4.1.0/craftsman/hook_command.py +83 -0
- craftsman_gate-4.1.0/craftsman/hooks.py +727 -0
- craftsman_gate-4.1.0/craftsman/install.py +378 -0
- craftsman_gate-4.1.0/craftsman/ledger.py +1295 -0
- craftsman_gate-4.1.0/craftsman/legacy_githooks.py +147 -0
- craftsman_gate-4.1.0/craftsman/lenses.py +223 -0
- craftsman_gate-4.1.0/craftsman/manifest.py +254 -0
- craftsman_gate-4.1.0/craftsman/packet.py +751 -0
- craftsman_gate-4.1.0/craftsman/paths.py +99 -0
- craftsman_gate-4.1.0/craftsman/platforms/__init__.py +1 -0
- craftsman_gate-4.1.0/craftsman/platforms/agents_md.py +77 -0
- craftsman_gate-4.1.0/craftsman/platforms/antigravity.py +151 -0
- craftsman_gate-4.1.0/craftsman/platforms/claude.py +414 -0
- craftsman_gate-4.1.0/craftsman/platforms/copilot.py +459 -0
- craftsman_gate-4.1.0/craftsman/playbooks.py +113 -0
- craftsman_gate-4.1.0/craftsman/quality.py +368 -0
- craftsman_gate-4.1.0/craftsman/records.py +1216 -0
- craftsman_gate-4.1.0/craftsman/review.py +2121 -0
- craftsman_gate-4.1.0/craftsman/runner.py +313 -0
- craftsman_gate-4.1.0/craftsman/scope.py +559 -0
- craftsman_gate-4.1.0/craftsman/shell.py +710 -0
- craftsman_gate-4.1.0/craftsman/staging.py +365 -0
- craftsman_gate-4.1.0/craftsman/streams.py +24 -0
- craftsman_gate-4.1.0/craftsman/text.py +112 -0
- craftsman_gate-4.1.0/craftsman/transcript.py +234 -0
- craftsman_gate-4.1.0/craftsman/trigger.py +88 -0
- craftsman_gate-4.1.0/craftsman/triviality.py +193 -0
- craftsman_gate-4.1.0/craftsman/uninstall.py +240 -0
- craftsman_gate-4.1.0/craftsman/workspace.py +52 -0
- craftsman_gate-4.1.0/craftsman/worktree_guard.py +35 -0
- craftsman_gate-4.1.0/craftsman_gate.egg-info/PKG-INFO +165 -0
- craftsman_gate-4.1.0/craftsman_gate.egg-info/SOURCES.txt +131 -0
- craftsman_gate-4.1.0/craftsman_gate.egg-info/dependency_links.txt +1 -0
- craftsman_gate-4.1.0/craftsman_gate.egg-info/entry_points.txt +2 -0
- craftsman_gate-4.1.0/craftsman_gate.egg-info/requires.txt +5 -0
- craftsman_gate-4.1.0/craftsman_gate.egg-info/top_level.txt +1 -0
- craftsman_gate-4.1.0/hooks.json +27 -0
- craftsman_gate-4.1.0/plugin.json +25 -0
- craftsman_gate-4.1.0/pyproject.toml +84 -0
- craftsman_gate-4.1.0/rules/ultra_mode.md +36 -0
- craftsman_gate-4.1.0/scripts/copilot-export.py +16 -0
- craftsman_gate-4.1.0/scripts/craftsman-cli.py +16 -0
- craftsman_gate-4.1.0/scripts/craftsman-scope-guard.py +16 -0
- craftsman_gate-4.1.0/scripts/craftsman-stop-gate.py +16 -0
- craftsman_gate-4.1.0/setup.cfg +4 -0
- craftsman_gate-4.1.0/setup.py +36 -0
- craftsman_gate-4.1.0/skills/audit/SKILL.md +51 -0
- craftsman_gate-4.1.0/skills/audit-comments/SKILL.md +50 -0
- craftsman_gate-4.1.0/skills/audit-errors/SKILL.md +64 -0
- craftsman_gate-4.1.0/skills/audit-security/SKILL.md +66 -0
- craftsman_gate-4.1.0/skills/audit-tests/SKILL.md +60 -0
- craftsman_gate-4.1.0/skills/audit-types/SKILL.md +59 -0
- craftsman_gate-4.1.0/skills/batch/SKILL.md +52 -0
- craftsman_gate-4.1.0/skills/code-quality/SKILL.md +202 -0
- craftsman_gate-4.1.0/skills/code-review/SKILL.md +516 -0
- craftsman_gate-4.1.0/skills/code-review/lenses.md +345 -0
- craftsman_gate-4.1.0/skills/craftsman/SKILL.md +186 -0
- craftsman_gate-4.1.0/skills/craftsman/scope.md +8 -0
- craftsman_gate-4.1.0/skills/extreme-optimizer/SKILL.md +128 -0
- craftsman_gate-4.1.0/skills/feature-dev/SKILL.md +63 -0
- craftsman_gate-4.1.0/skills/high-level-commit-message/SKILL.md +145 -0
- craftsman_gate-4.1.0/skills/leetcode-style-code-reviewer/SKILL.md +379 -0
- craftsman_gate-4.1.0/skills/modernize/SKILL.md +62 -0
- craftsman_gate-4.1.0/skills/simplify/SKILL.md +68 -0
- craftsman_gate-4.1.0/skills/verify/SKILL.md +92 -0
- craftsman_gate-4.1.0/templates/AGENTS.md +22 -0
- craftsman_gate-4.1.0/templates/agents/craftsman-reviewer.md +145 -0
- craftsman_gate-4.1.0/templates/agents/craftsman-verifier.md +163 -0
- craftsman_gate-4.1.0/templates/agents/craftsman.md +27 -0
- craftsman_gate-4.1.0/tests/test_antigravity.py +294 -0
- craftsman_gate-4.1.0/tests/test_assets.py +179 -0
- craftsman_gate-4.1.0/tests/test_cli.py +178 -0
- craftsman_gate-4.1.0/tests/test_diffscan.py +1224 -0
- craftsman_gate-4.1.0/tests/test_diffscan_signals.py +191 -0
- craftsman_gate-4.1.0/tests/test_docs_links.py +144 -0
- craftsman_gate-4.1.0/tests/test_doctor.py +534 -0
- craftsman_gate-4.1.0/tests/test_evidence.py +89 -0
- craftsman_gate-4.1.0/tests/test_export_cli.py +190 -0
- craftsman_gate-4.1.0/tests/test_files.py +140 -0
- craftsman_gate-4.1.0/tests/test_gate.py +515 -0
- craftsman_gate-4.1.0/tests/test_gatecheck.py +676 -0
- craftsman_gate-4.1.0/tests/test_generated.py +104 -0
- craftsman_gate-4.1.0/tests/test_hooks.py +902 -0
- craftsman_gate-4.1.0/tests/test_install.py +819 -0
- craftsman_gate-4.1.0/tests/test_ledger.py +934 -0
- craftsman_gate-4.1.0/tests/test_lenses.py +152 -0
- craftsman_gate-4.1.0/tests/test_manifest.py +278 -0
- craftsman_gate-4.1.0/tests/test_packet.py +406 -0
- craftsman_gate-4.1.0/tests/test_packet_shards.py +412 -0
- craftsman_gate-4.1.0/tests/test_partial_report.py +272 -0
- craftsman_gate-4.1.0/tests/test_platform_agents_md.py +59 -0
- craftsman_gate-4.1.0/tests/test_platform_claude.py +367 -0
- craftsman_gate-4.1.0/tests/test_platform_copilot.py +282 -0
- craftsman_gate-4.1.0/tests/test_playbooks.py +178 -0
- craftsman_gate-4.1.0/tests/test_pruning.py +765 -0
- craftsman_gate-4.1.0/tests/test_quality.py +224 -0
- craftsman_gate-4.1.0/tests/test_quick_cover.py +549 -0
- craftsman_gate-4.1.0/tests/test_records.py +232 -0
- craftsman_gate-4.1.0/tests/test_review.py +1587 -0
- craftsman_gate-4.1.0/tests/test_review_scope.py +607 -0
- craftsman_gate-4.1.0/tests/test_runner.py +181 -0
- craftsman_gate-4.1.0/tests/test_scope.py +1110 -0
- craftsman_gate-4.1.0/tests/test_shell.py +139 -0
- craftsman_gate-4.1.0/tests/test_staging.py +130 -0
- craftsman_gate-4.1.0/tests/test_streams.py +59 -0
- craftsman_gate-4.1.0/tests/test_text.py +429 -0
- craftsman_gate-4.1.0/tests/test_transcript.py +134 -0
- craftsman_gate-4.1.0/tests/test_trigger.py +182 -0
- craftsman_gate-4.1.0/tests/test_triviality.py +89 -0
- craftsman_gate-4.1.0/tests/test_uninstall.py +279 -0
- craftsman_gate-4.1.0/tests/test_version.py +82 -0
- craftsman_gate-4.1.0/tests/test_working_tree_hygiene.py +346 -0
- craftsman_gate-4.1.0/tests/test_workspace.py +67 -0
- craftsman_gate-4.1.0/tests/test_worktree_guard.py +354 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rafał Buczyński
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Craftsman
|
|
2
|
+
Copyright (c) 2026 Rafał Buczyński
|
|
3
|
+
|
|
4
|
+
Make AI coding agents prove they reviewed and tested their change.
|
|
5
|
+
|
|
6
|
+
This software includes:
|
|
7
|
+
- Quality gate and review verification runtime (`craftsman/`).
|
|
8
|
+
- Lifecycle hook scripts and adapters for Claude Code, GitHub Copilot, and Google Antigravity.
|
|
9
|
+
- Curated software engineering review skills and agent templates.
|
|
10
|
+
- Test suites and benchmarks.
|
|
11
|
+
|
|
12
|
+
All components are licensed under the MIT License (see LICENSE).
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: craftsman-gate
|
|
3
|
+
Version: 4.1.0
|
|
4
|
+
Summary: Make AI coding agents prove they reviewed and tested their change
|
|
5
|
+
Author-email: Rafał Buczyński <buczynski.rafal2@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/BuczynskiRafal/craftsman
|
|
8
|
+
Project-URL: Repository, https://github.com/BuczynskiRafal/craftsman
|
|
9
|
+
Project-URL: Issues, https://github.com/BuczynskiRafal/craftsman/issues
|
|
10
|
+
Keywords: ai-agents,claude-code,github-copilot,codex,cursor,antigravity,quality-gate,developer-tools,code-review,ai-code-review,llm,coding-agents,agents-md
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
License-File: NOTICE
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# Craftsman
|
|
36
|
+
|
|
37
|
+
[](https://github.com/BuczynskiRafal/craftsman/actions/workflows/ci.yml)
|
|
38
|
+
[](https://github.com/BuczynskiRafal/craftsman/blob/main/plugin.json)
|
|
39
|
+
[](https://github.com/BuczynskiRafal/craftsman/blob/main/pyproject.toml)
|
|
40
|
+
[](https://github.com/BuczynskiRafal/craftsman/blob/main/pyproject.toml)
|
|
41
|
+
[](https://github.com/BuczynskiRafal/craftsman/blob/main/LICENSE)
|
|
42
|
+
|
|
43
|
+
Craftsman adds a layer to an AI coding agent that checks the quality and simplicity of the code it
|
|
44
|
+
delivers. The agent cannot finish its work until an independent reviewer has examined the change.
|
|
45
|
+
Craftsman enforces a review loop in which every reported defect is verified and resolved, and at the
|
|
46
|
+
end the tests must pass on the final version of the code.
|
|
47
|
+
|
|
48
|
+
Craftsman keeps a ledger in the `.git` directory, where it collects evidence from review,
|
|
49
|
+
verification, and tests. When the agent tries to finish, a platform hook checks the ledger and, if
|
|
50
|
+
anything is missing, sends the agent back with a list of what is missing.
|
|
51
|
+
|
|
52
|
+
It works with Claude Code, GitHub Copilot, and Google Antigravity. In Codex and Cursor, the same
|
|
53
|
+
protocol runs without hooks.
|
|
54
|
+
|
|
55
|
+
## How It Works
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
1. `/craftsman <task>` records the base commit that the diff is computed from.
|
|
60
|
+
2. The agent implements the change and cleans it up with `/simplify`.
|
|
61
|
+
3. Craftsman splits the diff into hunks and attaches to each one the context the diff does not show:
|
|
62
|
+
the rest of the function, its call sites, and risk signals. Defects usually sit where the change
|
|
63
|
+
meets that code.
|
|
64
|
+
4. Reviewer subagents that did not write the change examine every hunk through several lenses (for
|
|
65
|
+
example contracts, error handling, tests, security) and record suspected defects.
|
|
66
|
+
5. A separate verifier tries to disprove or reproduce each suspected defect. Searching and judging
|
|
67
|
+
are kept apart so that the reviewer does not drop leads too early.
|
|
68
|
+
6. The agent fixes confirmed defects. Every piece of evidence in the ledger is tied to a diff
|
|
69
|
+
fingerprint, so a code change invalidates the old review; the next round covers only the changed
|
|
70
|
+
parts.
|
|
71
|
+
7. The agent runs the tests through `craftsman run`, which records the exit code.
|
|
72
|
+
8. The stop hook lets the agent finish once the ledger is complete for the current version of the
|
|
73
|
+
code. The agent prints a report: what was reviewed, what was found, what was fixed, what was
|
|
74
|
+
dismissed, and what the gate did not assess.
|
|
75
|
+
|
|
76
|
+
The level of rigor depends on the gear: `quick` for small changes (review in the current context),
|
|
77
|
+
`standard` by default (subagents and a verifier), `deep` for risky changes (all lenses, tests, and
|
|
78
|
+
e2e).
|
|
79
|
+
|
|
80
|
+
`/code-review` runs only the review of an existing change, without fixes.
|
|
81
|
+
|
|
82
|
+
## Platforms
|
|
83
|
+
|
|
84
|
+
| Platform | What blocks completion |
|
|
85
|
+
| :--- | :--- |
|
|
86
|
+
| Claude Code | `Stop` hook |
|
|
87
|
+
| GitHub Copilot in VS Code | `Stop` hook |
|
|
88
|
+
| GitHub Copilot CLI | `agentStop` hook |
|
|
89
|
+
| Google Antigravity | `Stop` hook |
|
|
90
|
+
| Codex, Cursor, and other tools that read `AGENTS.md` | `craftsman gate --complete` |
|
|
91
|
+
|
|
92
|
+
Where the platform cannot confirm that a separate subagent performed the review, the report marks it
|
|
93
|
+
as `SELF-REVIEWED`.
|
|
94
|
+
|
|
95
|
+
## Measurements
|
|
96
|
+
|
|
97
|
+
Pilot on [SWR-Bench](https://doi.org/10.1145/3808144): 5 PRs from `pytest`, `flask`, and `requests`,
|
|
98
|
+
9 confirmed defects, 1 repetition per configuration. The sample is too small for statistical
|
|
99
|
+
conclusions. Details and Claude Code runs: [docs/benchmark.md](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/benchmark.md).
|
|
100
|
+
|
|
101
|
+
| Configuration | Defects: without → with Craftsman | Avg. time per PR |
|
|
102
|
+
| :--- | :---: | :---: |
|
|
103
|
+
| Antigravity · Gemini 4 Argon `low` | 2/9 → 4/9 | 1.5 → 4.7 min |
|
|
104
|
+
| Antigravity · Gemini 4 Argon `medium` | 2/9 → 5/9 | 2.0 → 6.7 min |
|
|
105
|
+
| Antigravity · Gemini 4 Argon `high` | 3/9 → 7/9 | 15.6 → 26.3 min |
|
|
106
|
+
|
|
107
|
+
## Installation
|
|
108
|
+
|
|
109
|
+
Requires Python 3.10+, with no external dependencies.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pipx install craftsman-gate # or: uv tool install craftsman-gate
|
|
113
|
+
craftsman install # detects Claude Code, Copilot, and Antigravity
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Or as a plugin:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
/plugin marketplace add BuczynskiRafal/craftsman # Claude Code
|
|
120
|
+
/plugin install craftsman@craftsman
|
|
121
|
+
agy plugin install https://github.com/BuczynskiRafal/craftsman # Antigravity
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Usage
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
/craftsman add pagination to the catalog search
|
|
128
|
+
/craftsman --deep migrate the session store to Redis
|
|
129
|
+
/code-review
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`craftsman status` shows the current gear, round, and missing evidence.
|
|
133
|
+
|
|
134
|
+
## Documentation
|
|
135
|
+
|
|
136
|
+
- [How it works](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/how-it-works.md): ledger, lenses, gears, platform enforcement
|
|
137
|
+
- [Installation](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/installation.md): flags, platform setup, uninstall, `craftsman doctor`
|
|
138
|
+
- [Usage](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/usage.md): commands, review-only mode
|
|
139
|
+
- [Guards](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/guards.md): gate state machine, search scope guard
|
|
140
|
+
- [Skills](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/skills.md): catalog of bundled skills
|
|
141
|
+
- [Benchmark](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/benchmark.md): methodology, results, replication
|
|
142
|
+
- [Architecture](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/harness-architecture.md): project internals for contributors
|
|
143
|
+
|
|
144
|
+
## Contributing
|
|
145
|
+
|
|
146
|
+
See [CONTRIBUTING.md](https://github.com/BuczynskiRafal/craftsman/blob/main/CONTRIBUTING.md).
|
|
147
|
+
|
|
148
|
+
## Citation
|
|
149
|
+
|
|
150
|
+
```bibtex
|
|
151
|
+
@article{zeng2026swrbench,
|
|
152
|
+
author = {Zeng, Zhengran and Shi, Ruikai and Han, Keke and Li, Yixin and Sun, Kaicheng and Wang, Yidong and Yu, Zhuohao and Xie, Rui and Ye, Wei and Zhang, Shikun},
|
|
153
|
+
title = {{SWR-Bench}: Assessing {LLM} Performance in Real-World Code Review Comment Generation},
|
|
154
|
+
journal = {Proceedings of the ACM on Software Engineering},
|
|
155
|
+
volume = {3},
|
|
156
|
+
number = {FSE},
|
|
157
|
+
articleno = {FSE137},
|
|
158
|
+
year = {2026},
|
|
159
|
+
doi = {10.1145/3808144}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT, see [LICENSE](https://github.com/BuczynskiRafal/craftsman/blob/main/LICENSE) and attribution in [NOTICE](https://github.com/BuczynskiRafal/craftsman/blob/main/NOTICE).
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Craftsman
|
|
2
|
+
|
|
3
|
+
[](https://github.com/BuczynskiRafal/craftsman/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/BuczynskiRafal/craftsman/blob/main/plugin.json)
|
|
5
|
+
[](https://github.com/BuczynskiRafal/craftsman/blob/main/pyproject.toml)
|
|
6
|
+
[](https://github.com/BuczynskiRafal/craftsman/blob/main/pyproject.toml)
|
|
7
|
+
[](https://github.com/BuczynskiRafal/craftsman/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
Craftsman adds a layer to an AI coding agent that checks the quality and simplicity of the code it
|
|
10
|
+
delivers. The agent cannot finish its work until an independent reviewer has examined the change.
|
|
11
|
+
Craftsman enforces a review loop in which every reported defect is verified and resolved, and at the
|
|
12
|
+
end the tests must pass on the final version of the code.
|
|
13
|
+
|
|
14
|
+
Craftsman keeps a ledger in the `.git` directory, where it collects evidence from review,
|
|
15
|
+
verification, and tests. When the agent tries to finish, a platform hook checks the ledger and, if
|
|
16
|
+
anything is missing, sends the agent back with a list of what is missing.
|
|
17
|
+
|
|
18
|
+
It works with Claude Code, GitHub Copilot, and Google Antigravity. In Codex and Cursor, the same
|
|
19
|
+
protocol runs without hooks.
|
|
20
|
+
|
|
21
|
+
## How It Works
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
1. `/craftsman <task>` records the base commit that the diff is computed from.
|
|
26
|
+
2. The agent implements the change and cleans it up with `/simplify`.
|
|
27
|
+
3. Craftsman splits the diff into hunks and attaches to each one the context the diff does not show:
|
|
28
|
+
the rest of the function, its call sites, and risk signals. Defects usually sit where the change
|
|
29
|
+
meets that code.
|
|
30
|
+
4. Reviewer subagents that did not write the change examine every hunk through several lenses (for
|
|
31
|
+
example contracts, error handling, tests, security) and record suspected defects.
|
|
32
|
+
5. A separate verifier tries to disprove or reproduce each suspected defect. Searching and judging
|
|
33
|
+
are kept apart so that the reviewer does not drop leads too early.
|
|
34
|
+
6. The agent fixes confirmed defects. Every piece of evidence in the ledger is tied to a diff
|
|
35
|
+
fingerprint, so a code change invalidates the old review; the next round covers only the changed
|
|
36
|
+
parts.
|
|
37
|
+
7. The agent runs the tests through `craftsman run`, which records the exit code.
|
|
38
|
+
8. The stop hook lets the agent finish once the ledger is complete for the current version of the
|
|
39
|
+
code. The agent prints a report: what was reviewed, what was found, what was fixed, what was
|
|
40
|
+
dismissed, and what the gate did not assess.
|
|
41
|
+
|
|
42
|
+
The level of rigor depends on the gear: `quick` for small changes (review in the current context),
|
|
43
|
+
`standard` by default (subagents and a verifier), `deep` for risky changes (all lenses, tests, and
|
|
44
|
+
e2e).
|
|
45
|
+
|
|
46
|
+
`/code-review` runs only the review of an existing change, without fixes.
|
|
47
|
+
|
|
48
|
+
## Platforms
|
|
49
|
+
|
|
50
|
+
| Platform | What blocks completion |
|
|
51
|
+
| :--- | :--- |
|
|
52
|
+
| Claude Code | `Stop` hook |
|
|
53
|
+
| GitHub Copilot in VS Code | `Stop` hook |
|
|
54
|
+
| GitHub Copilot CLI | `agentStop` hook |
|
|
55
|
+
| Google Antigravity | `Stop` hook |
|
|
56
|
+
| Codex, Cursor, and other tools that read `AGENTS.md` | `craftsman gate --complete` |
|
|
57
|
+
|
|
58
|
+
Where the platform cannot confirm that a separate subagent performed the review, the report marks it
|
|
59
|
+
as `SELF-REVIEWED`.
|
|
60
|
+
|
|
61
|
+
## Measurements
|
|
62
|
+
|
|
63
|
+
Pilot on [SWR-Bench](https://doi.org/10.1145/3808144): 5 PRs from `pytest`, `flask`, and `requests`,
|
|
64
|
+
9 confirmed defects, 1 repetition per configuration. The sample is too small for statistical
|
|
65
|
+
conclusions. Details and Claude Code runs: [docs/benchmark.md](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/benchmark.md).
|
|
66
|
+
|
|
67
|
+
| Configuration | Defects: without → with Craftsman | Avg. time per PR |
|
|
68
|
+
| :--- | :---: | :---: |
|
|
69
|
+
| Antigravity · Gemini 4 Argon `low` | 2/9 → 4/9 | 1.5 → 4.7 min |
|
|
70
|
+
| Antigravity · Gemini 4 Argon `medium` | 2/9 → 5/9 | 2.0 → 6.7 min |
|
|
71
|
+
| Antigravity · Gemini 4 Argon `high` | 3/9 → 7/9 | 15.6 → 26.3 min |
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
Requires Python 3.10+, with no external dependencies.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pipx install craftsman-gate # or: uv tool install craftsman-gate
|
|
79
|
+
craftsman install # detects Claude Code, Copilot, and Antigravity
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Or as a plugin:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
/plugin marketplace add BuczynskiRafal/craftsman # Claude Code
|
|
86
|
+
/plugin install craftsman@craftsman
|
|
87
|
+
agy plugin install https://github.com/BuczynskiRafal/craftsman # Antigravity
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Usage
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
/craftsman add pagination to the catalog search
|
|
94
|
+
/craftsman --deep migrate the session store to Redis
|
|
95
|
+
/code-review
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`craftsman status` shows the current gear, round, and missing evidence.
|
|
99
|
+
|
|
100
|
+
## Documentation
|
|
101
|
+
|
|
102
|
+
- [How it works](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/how-it-works.md): ledger, lenses, gears, platform enforcement
|
|
103
|
+
- [Installation](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/installation.md): flags, platform setup, uninstall, `craftsman doctor`
|
|
104
|
+
- [Usage](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/usage.md): commands, review-only mode
|
|
105
|
+
- [Guards](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/guards.md): gate state machine, search scope guard
|
|
106
|
+
- [Skills](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/skills.md): catalog of bundled skills
|
|
107
|
+
- [Benchmark](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/benchmark.md): methodology, results, replication
|
|
108
|
+
- [Architecture](https://github.com/BuczynskiRafal/craftsman/blob/main/docs/harness-architecture.md): project internals for contributors
|
|
109
|
+
|
|
110
|
+
## Contributing
|
|
111
|
+
|
|
112
|
+
See [CONTRIBUTING.md](https://github.com/BuczynskiRafal/craftsman/blob/main/CONTRIBUTING.md).
|
|
113
|
+
|
|
114
|
+
## Citation
|
|
115
|
+
|
|
116
|
+
```bibtex
|
|
117
|
+
@article{zeng2026swrbench,
|
|
118
|
+
author = {Zeng, Zhengran and Shi, Ruikai and Han, Keke and Li, Yixin and Sun, Kaicheng and Wang, Yidong and Yu, Zhuohao and Xie, Rui and Ye, Wei and Zhang, Shikun},
|
|
119
|
+
title = {{SWR-Bench}: Assessing {LLM} Performance in Real-World Code Review Comment Generation},
|
|
120
|
+
journal = {Proceedings of the ACM on Software Engineering},
|
|
121
|
+
volume = {3},
|
|
122
|
+
number = {FSE},
|
|
123
|
+
articleno = {FSE137},
|
|
124
|
+
year = {2026},
|
|
125
|
+
doi = {10.1145/3808144}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT, see [LICENSE](https://github.com/BuczynskiRafal/craftsman/blob/main/LICENSE) and attribution in [NOTICE](https://github.com/BuczynskiRafal/craftsman/blob/main/NOTICE).
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""The Antigravity Stop hook: `scripts/craftsman-stop-gate.py`, wired by `hooks.json`.
|
|
2
|
+
|
|
3
|
+
Antigravity has no prompt or subagent hooks, so this hook reads the session transcript instead:
|
|
4
|
+
- a `/craftsman` or `ultracraft` request opens a gate when the agent did not open one itself;
|
|
5
|
+
- `craftsman-reviewer` subagents that finished since the request are recorded in the evidence
|
|
6
|
+
ledger, which is the proof of reviewer isolation for Antigravity sessions;
|
|
7
|
+
- an unanswered `ask_question` lets the agent stop to wait for the user.
|
|
8
|
+
Then the gate's shared stop decision applies (`craftsman.gate.decide_stop`).
|
|
9
|
+
|
|
10
|
+
The hook reads a JSON object on stdin with `terminationReason`, `fullyIdle`,
|
|
11
|
+
`artifactDirectoryPath` (the session directory, `<appDataDir>/brain/<conversation id>`) and
|
|
12
|
+
`workspacePaths`, and answers `{"decision": "continue" | "allow", "reason": ...}`. When the gate
|
|
13
|
+
cannot be evaluated, it lets the agent stop and says why on stderr.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from collections.abc import Mapping, Sequence
|
|
21
|
+
from dataclasses import dataclass, replace
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Literal
|
|
25
|
+
|
|
26
|
+
from craftsman.diffscan import DiffScanError, resolve_base_sha, snapshot, worktree_root
|
|
27
|
+
from craftsman.gate import (
|
|
28
|
+
GateOwner,
|
|
29
|
+
GateState,
|
|
30
|
+
GateStateError,
|
|
31
|
+
GateTier,
|
|
32
|
+
archive_ledger_for_new_base,
|
|
33
|
+
decide_stop,
|
|
34
|
+
state_path_for,
|
|
35
|
+
update_state,
|
|
36
|
+
)
|
|
37
|
+
from craftsman.ledger import add_agent_event, ledger_path_for, mutate
|
|
38
|
+
from craftsman.records import REVIEWER_AGENT, AgentEvent, Ledger, LedgerError, format_timestamp
|
|
39
|
+
from craftsman.streams import configure_std_streams
|
|
40
|
+
from craftsman.transcript import (
|
|
41
|
+
Entry,
|
|
42
|
+
TranscriptError,
|
|
43
|
+
finished_reviewers,
|
|
44
|
+
is_waiting_on_question,
|
|
45
|
+
latest_user_input,
|
|
46
|
+
read_transcript_entries,
|
|
47
|
+
step_of,
|
|
48
|
+
)
|
|
49
|
+
from craftsman.trigger import detect_trigger, task_from_prompt
|
|
50
|
+
|
|
51
|
+
PLATFORM: Literal["antigravity"] = "antigravity"
|
|
52
|
+
TRANSCRIPT = Path(".system_generated") / "logs" / "transcript.jsonl"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class PayloadError(ValueError):
|
|
56
|
+
"""The Stop payload lacks what the hook needs to find the session and its work tree."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class StopPayload:
|
|
61
|
+
session_dir: Path
|
|
62
|
+
workspace: Path
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def parse(cls, payload: Mapping[str, Any]) -> StopPayload:
|
|
66
|
+
session_dir = payload.get("artifactDirectoryPath")
|
|
67
|
+
workspaces = payload.get("workspacePaths")
|
|
68
|
+
if not isinstance(session_dir, str) or not session_dir.strip():
|
|
69
|
+
raise PayloadError("the Stop payload has no artifactDirectoryPath")
|
|
70
|
+
if not isinstance(workspaces, list) or not workspaces or not isinstance(workspaces[0], str):
|
|
71
|
+
raise PayloadError("the Stop payload has no workspacePaths")
|
|
72
|
+
return cls(Path(session_dir).expanduser(), Path(workspaces[0]).expanduser())
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def caller(self) -> GateOwner:
|
|
76
|
+
return GateOwner(PLATFORM, self.session_dir.name)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def session_id(payload: Mapping[str, Any]) -> str | None:
|
|
80
|
+
"""The conversation id of an Antigravity hook payload: the name of its session directory."""
|
|
81
|
+
session_dir = payload.get("artifactDirectoryPath")
|
|
82
|
+
if isinstance(session_dir, str) and session_dir.strip():
|
|
83
|
+
return Path(session_dir).expanduser().name
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_final_stop(payload: Mapping[str, Any]) -> bool:
|
|
88
|
+
"""True when the agent ends its turn, rather than pausing between tool calls."""
|
|
89
|
+
reason, idle = payload.get("terminationReason"), payload.get("fullyIdle")
|
|
90
|
+
if not isinstance(reason, str) or not isinstance(idle, bool):
|
|
91
|
+
raise PayloadError("the Stop payload needs `terminationReason` and `fullyIdle`")
|
|
92
|
+
return reason == "NO_TOOL_CALL" and idle
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def on_stop(stop: StopPayload) -> str | None:
|
|
96
|
+
"""The reason to keep the agent working, or None to let it stop."""
|
|
97
|
+
root = worktree_root(stop.workspace)
|
|
98
|
+
entries = read_transcript_entries(stop.session_dir / TRANSCRIPT)
|
|
99
|
+
|
|
100
|
+
def decide(state: GateState | None) -> tuple[GateState | None, str | None]:
|
|
101
|
+
state = _opened_by_request(state, root, entries, stop.caller)
|
|
102
|
+
if state is None or not state.active or not state.owner.admits(stop.caller):
|
|
103
|
+
return state, None
|
|
104
|
+
state = _claimed(state, entries, stop.caller)
|
|
105
|
+
_record_finished_reviewers(root, stop.session_dir, entries, state.request_step or 0)
|
|
106
|
+
if is_waiting_on_question(entries):
|
|
107
|
+
return state, None
|
|
108
|
+
decision = decide_stop(state, root)
|
|
109
|
+
if decision.notice is not None:
|
|
110
|
+
sys.stderr.write(decision.notice + "\n")
|
|
111
|
+
return decision.state, decision.block_reason
|
|
112
|
+
|
|
113
|
+
return update_state(state_path_for(root), decide)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _opened_by_request(
|
|
117
|
+
state: GateState | None, root: Path, entries: Sequence[Entry], caller: GateOwner
|
|
118
|
+
) -> GateState | None:
|
|
119
|
+
"""The gate, opened for a `/craftsman` request the agent did not open a gate for."""
|
|
120
|
+
if state is not None and state.active:
|
|
121
|
+
return state
|
|
122
|
+
if state is not None and state.owner.admits(caller):
|
|
123
|
+
if state.request_step is None:
|
|
124
|
+
# Closed with `craftsman gate --complete` before any stop: the request was handled.
|
|
125
|
+
latest = latest_user_input(entries)
|
|
126
|
+
return state if latest is None else replace(state, request_step=latest[0])
|
|
127
|
+
handled_step = state.request_step
|
|
128
|
+
else:
|
|
129
|
+
handled_step = 0
|
|
130
|
+
request = _latest_trigger(entries, handled_step)
|
|
131
|
+
if request is None:
|
|
132
|
+
return state
|
|
133
|
+
step, tier, prompt = request
|
|
134
|
+
base_sha = resolve_base_sha(root, None)
|
|
135
|
+
if snapshot(root, base_sha).is_empty:
|
|
136
|
+
return state # nothing changed yet, so there is nothing to gate
|
|
137
|
+
archive_ledger_for_new_base(root, base_sha)
|
|
138
|
+
return GateState.open(caller, base_sha, tier, task_from_prompt(prompt), request_step=step)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _latest_trigger(entries: Sequence[Entry], after_step: int) -> tuple[int, GateTier, str] | None:
|
|
142
|
+
"""The step, tier and text of the latest user input after `after_step` that invokes Craftsman."""
|
|
143
|
+
for position in range(len(entries), 0, -1):
|
|
144
|
+
entry = entries[position - 1]
|
|
145
|
+
step = step_of(entry, position)
|
|
146
|
+
if step <= after_step:
|
|
147
|
+
return None
|
|
148
|
+
if entry.get("type") != "USER_INPUT":
|
|
149
|
+
continue
|
|
150
|
+
prompt = str(entry.get("content") or "")
|
|
151
|
+
tier = detect_trigger(prompt)
|
|
152
|
+
if tier is not None:
|
|
153
|
+
return step, tier, prompt
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _claimed(state: GateState, entries: Sequence[Entry], caller: GateOwner) -> GateState:
|
|
158
|
+
"""The gate as this session's stop hook serves it: owned, transcript-isolated, anchored."""
|
|
159
|
+
request_step = state.request_step
|
|
160
|
+
if request_step is None:
|
|
161
|
+
latest = latest_user_input(entries)
|
|
162
|
+
request_step = latest[0] if latest is not None else 0
|
|
163
|
+
return replace(
|
|
164
|
+
state,
|
|
165
|
+
owner=state.owner.claimed_by(caller),
|
|
166
|
+
isolation_evidence="transcript",
|
|
167
|
+
request_step=request_step,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _record_finished_reviewers(
|
|
172
|
+
root: Path, session_dir: Path, entries: Sequence[Entry], after_step: int
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Record each finished `craftsman-reviewer` subagent once, at the time its own run ended.
|
|
175
|
+
|
|
176
|
+
A subagent's session directory is a sibling of its parent's, named by its conversation id;
|
|
177
|
+
a subagent without one left no trace of having run, so it is not recorded.
|
|
178
|
+
"""
|
|
179
|
+
events = [
|
|
180
|
+
event
|
|
181
|
+
for sub_id in finished_reviewers(entries, after_step)
|
|
182
|
+
if (event := _reviewer_stop(session_dir.parent / sub_id, sub_id)) is not None
|
|
183
|
+
]
|
|
184
|
+
if events:
|
|
185
|
+
mutate(ledger_path_for(root), lambda ledger: (_with_new_events(ledger, events), None))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _reviewer_stop(subagent_dir: Path, sub_id: str) -> AgentEvent | None:
|
|
189
|
+
try:
|
|
190
|
+
finished_at = (subagent_dir / TRANSCRIPT).stat().st_mtime
|
|
191
|
+
except FileNotFoundError:
|
|
192
|
+
return None
|
|
193
|
+
return AgentEvent(
|
|
194
|
+
platform=PLATFORM,
|
|
195
|
+
event="stop",
|
|
196
|
+
agent=REVIEWER_AGENT,
|
|
197
|
+
agent_id=sub_id,
|
|
198
|
+
at=format_timestamp(datetime.fromtimestamp(finished_at, timezone.utc)),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _with_new_events(ledger: Ledger, events: Sequence[AgentEvent]) -> Ledger:
|
|
203
|
+
recorded = {(event.platform, event.agent_id) for event in ledger.agents if event.event == "stop"}
|
|
204
|
+
for event in events:
|
|
205
|
+
if (event.platform, event.agent_id) not in recorded:
|
|
206
|
+
ledger = add_agent_event(ledger, event)
|
|
207
|
+
return ledger
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def answer(payload: object) -> dict[str, str]:
|
|
211
|
+
"""The hook's answer to one Stop payload."""
|
|
212
|
+
if not isinstance(payload, dict):
|
|
213
|
+
return _allow_after_error("the Stop payload is not a JSON object")
|
|
214
|
+
try:
|
|
215
|
+
if not is_final_stop(payload):
|
|
216
|
+
return {"decision": "allow"}
|
|
217
|
+
reason = on_stop(StopPayload.parse(payload))
|
|
218
|
+
except (PayloadError, DiffScanError, LedgerError, GateStateError, TranscriptError, OSError) as exc:
|
|
219
|
+
return _allow_after_error(str(exc))
|
|
220
|
+
return {"decision": "continue", "reason": reason} if reason is not None else {"decision": "allow"}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _allow_after_error(problem: str) -> dict[str, str]:
|
|
224
|
+
sys.stderr.write(
|
|
225
|
+
f"[craftsman-stop-gate] The quality gate was not checked: {problem}. "
|
|
226
|
+
"Run `craftsman review check` yourself.\n"
|
|
227
|
+
)
|
|
228
|
+
return {"decision": "allow"}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def main() -> int:
|
|
232
|
+
configure_std_streams()
|
|
233
|
+
try:
|
|
234
|
+
out = answer(json.loads(sys.stdin.read()))
|
|
235
|
+
except (OSError, ValueError) as exc:
|
|
236
|
+
out = _allow_after_error(f"unreadable hook input ({exc})")
|
|
237
|
+
sys.stdout.write(json.dumps(out, ensure_ascii=False) + "\n")
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
raise SystemExit(main())
|