code-review-forge 2.0.0a1__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.
Files changed (62) hide show
  1. code_forge/__init__.py +14 -0
  2. code_forge/__main__.py +8 -0
  3. code_forge/autofix.py +78 -0
  4. code_forge/baseline.py +216 -0
  5. code_forge/cli.py +983 -0
  6. code_forge/delta.py +65 -0
  7. code_forge/diagnose.py +109 -0
  8. code_forge/diff.py +82 -0
  9. code_forge/disposition.py +32 -0
  10. code_forge/e2e_check.py +641 -0
  11. code_forge/env_resolver.py +91 -0
  12. code_forge/errors.py +34 -0
  13. code_forge/exit_codes.py +37 -0
  14. code_forge/factories.py +191 -0
  15. code_forge/falsify.py +85 -0
  16. code_forge/gate_check.py +466 -0
  17. code_forge/git.py +351 -0
  18. code_forge/hold.py +126 -0
  19. code_forge/install_hooks.py +331 -0
  20. code_forge/lock.py +162 -0
  21. code_forge/machine.py +792 -0
  22. code_forge/mode_resolver.py +60 -0
  23. code_forge/mutation.py +380 -0
  24. code_forge/parsers/__init__.py +56 -0
  25. code_forge/parsers/_sarif.py +77 -0
  26. code_forge/parsers/base.py +65 -0
  27. code_forge/parsers/checkpatch.py +66 -0
  28. code_forge/parsers/clippy.py +85 -0
  29. code_forge/parsers/non_ascii.py +47 -0
  30. code_forge/parsers/ruff.py +18 -0
  31. code_forge/parsers/semgrep.py +18 -0
  32. code_forge/parsers/shellcheck.py +56 -0
  33. code_forge/registry.py +153 -0
  34. code_forge/reporter.py +133 -0
  35. code_forge/runner.py +205 -0
  36. code_forge/sarif.py +226 -0
  37. code_forge/skills/adversarial-qe/SKILL.md +272 -0
  38. code_forge/skills/code-forge/SKILL.md +1193 -0
  39. code_forge/skills/code-review-expert/SKILL.md +162 -0
  40. code_forge/skills/code-review-expert/references/code-quality-checklist.md +130 -0
  41. code_forge/skills/code-review-expert/references/removal-plan.md +52 -0
  42. code_forge/skills/code-review-expert/references/security-checklist.md +118 -0
  43. code_forge/skills/code-review-expert/references/solid-checklist.md +65 -0
  44. code_forge/skills/kernel-fp-verify/SKILL.md +101 -0
  45. code_forge/skills/qodo-review/SKILL.md +135 -0
  46. code_forge/skills/smoke-test/SKILL.md +253 -0
  47. code_forge/skills/smoke-test/references/boundary-cases.md +114 -0
  48. code_forge/skills/smoke-test/references/concurrency-patterns.md +306 -0
  49. code_forge/skills/smoke-test/references/injection-payloads.md +124 -0
  50. code_forge/skills/smoke-test/test-library/shell/README.md +271 -0
  51. code_forge/skills/smoke-test/test-library/shell/primitives.sh +352 -0
  52. code_forge/skills/smoke-test/test-library/shell/primitives_test.sh +324 -0
  53. code_forge/snapshot.py +196 -0
  54. code_forge/source.py +64 -0
  55. code_forge/state.py +246 -0
  56. code_forge/verdict.py +43 -0
  57. code_review_forge-2.0.0a1.dist-info/METADATA +237 -0
  58. code_review_forge-2.0.0a1.dist-info/RECORD +62 -0
  59. code_review_forge-2.0.0a1.dist-info/WHEEL +5 -0
  60. code_review_forge-2.0.0a1.dist-info/entry_points.txt +2 -0
  61. code_review_forge-2.0.0a1.dist-info/licenses/LICENSE +179 -0
  62. code_review_forge-2.0.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,331 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """install-hooks subcommand: write .git/hooks/pre-commit hook.
4
+
5
+ Resolves the hooks directory (worktree-safe), backs up and chains any
6
+ existing hook, embeds an absolute code-forge path, and aborts when
7
+ core.hooksPath is set. Idempotent on re-install.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+ import shlex
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import IO, Callable, Mapping, Optional
19
+
20
+ from .exit_codes import EXIT_FAIL, EXIT_PASS
21
+
22
+
23
+ def resolve_hooks_dir(
24
+ cwd: Path,
25
+ run_cmd: Callable = subprocess.run,
26
+ ) -> Path:
27
+ """Resolve git hooks directory via git rev-parse --git-path hooks.
28
+
29
+ Args:
30
+ cwd: working directory (repo root)
31
+ run_cmd: subprocess.run callable (injected for testing)
32
+
33
+ Returns:
34
+ Absolute Path to hooks directory
35
+
36
+ Raises:
37
+ RuntimeError: if not in a git repo or git command fails
38
+ """
39
+ try:
40
+ result = run_cmd(
41
+ ["git", "rev-parse", "--git-path", "hooks"],
42
+ capture_output=True,
43
+ text=True,
44
+ check=True,
45
+ timeout=5,
46
+ cwd=str(cwd),
47
+ )
48
+ hooks_path = result.stdout.strip()
49
+ except subprocess.CalledProcessError as e:
50
+ raise RuntimeError(
51
+ "Not in a git repository or git command failed: %s" % e
52
+ ) from e
53
+ except subprocess.TimeoutExpired as e:
54
+ raise RuntimeError(
55
+ "git rev-parse timed out: %s" % e
56
+ ) from e
57
+
58
+ # If relative, resolve against repo root
59
+ hooks_path_obj = Path(hooks_path)
60
+ if not hooks_path_obj.is_absolute():
61
+ # Get repo root via git rev-parse --show-toplevel
62
+ try:
63
+ root_result = run_cmd(
64
+ ["git", "rev-parse", "--show-toplevel"],
65
+ capture_output=True,
66
+ text=True,
67
+ check=True,
68
+ timeout=5,
69
+ cwd=str(cwd),
70
+ )
71
+ repo_root = Path(root_result.stdout.strip())
72
+ hooks_path_obj = repo_root / hooks_path_obj
73
+ except subprocess.CalledProcessError as e:
74
+ raise RuntimeError(
75
+ "git rev-parse --show-toplevel failed: %s" % e
76
+ ) from e
77
+
78
+ return hooks_path_obj.resolve()
79
+
80
+
81
+ def check_hooks_path_override(
82
+ cwd: Path,
83
+ run_cmd: Callable = subprocess.run,
84
+ ) -> str | None:
85
+ """Check if core.hooksPath is set.
86
+
87
+ Args:
88
+ cwd: working directory
89
+ run_cmd: subprocess.run callable (injected for testing)
90
+
91
+ Returns:
92
+ The value of core.hooksPath if set and non-empty, None otherwise
93
+ """
94
+ try:
95
+ result = run_cmd(
96
+ ["git", "config", "--get", "core.hooksPath"],
97
+ capture_output=True,
98
+ text=True,
99
+ check=False,
100
+ timeout=5,
101
+ cwd=str(cwd),
102
+ )
103
+ # returncode 1 means "not set", which is normal
104
+ if result.returncode == 0:
105
+ value = result.stdout.strip()
106
+ return value if value else None
107
+ return None
108
+ except subprocess.TimeoutExpired:
109
+ return None
110
+
111
+
112
+ def resolve_forge_path() -> str:
113
+ """Resolve absolute code-forge path for hook embedding.
114
+
115
+ Returns:
116
+ Absolute path string that will be embedded in hook.
117
+ For single executable: "/usr/local/bin/code-forge gate-check"
118
+ For python module: "/usr/bin/python3 -m code_forge gate-check"
119
+
120
+ Raises:
121
+ RuntimeError: if no valid code-forge executable path found
122
+ """
123
+ logger = logging.getLogger("code_forge")
124
+
125
+ # Try shutil.which('code-forge') first
126
+ forge_exe = shutil.which("code-forge")
127
+ if forge_exe is not None and os.access(forge_exe, os.X_OK):
128
+ # Run liveness check
129
+ try:
130
+ result = subprocess.run(
131
+ [forge_exe, "--version"],
132
+ capture_output=True,
133
+ text=True,
134
+ timeout=1,
135
+ check=False,
136
+ )
137
+ if (
138
+ result.returncode == 0
139
+ and result.stdout.strip().startswith("code-forge ")
140
+ ):
141
+ return "%s gate-check" % shlex.quote(forge_exe)
142
+ else:
143
+ logger.warning(
144
+ "code-forge at %s failed --version check; "
145
+ "falling back to sys.executable",
146
+ forge_exe,
147
+ )
148
+ except subprocess.TimeoutExpired:
149
+ logger.warning(
150
+ "code-forge at %s --version timed out; "
151
+ "falling back to sys.executable",
152
+ forge_exe,
153
+ )
154
+ except Exception as e:
155
+ logger.warning(
156
+ "code-forge at %s --version raised %s; "
157
+ "falling back to sys.executable",
158
+ forge_exe,
159
+ e,
160
+ )
161
+
162
+ # Fallback to sys.executable + ' -m code_forge'
163
+ if sys.executable and os.access(sys.executable, os.X_OK):
164
+ return "%s -m code_forge gate-check" % shlex.quote(sys.executable)
165
+
166
+ raise RuntimeError(
167
+ "Cannot resolve code-forge path: 'code-forge' not on PATH and "
168
+ "sys.executable is not valid"
169
+ )
170
+
171
+
172
+ def generate_hook_content(
173
+ forge_invocation: str,
174
+ chain_path: Path | None,
175
+ ) -> str:
176
+ """Generate pre-commit hook shell script content.
177
+
178
+ Args:
179
+ forge_invocation: absolute code-forge path + args (e.g. "/usr/local/bin/code-forge gate-check")
180
+ chain_path: Path to existing hook backup, or None
181
+
182
+ Returns:
183
+ Shell script content as string
184
+ """
185
+ if chain_path is not None:
186
+ return f"""#!/bin/sh
187
+ # code-forge pre-commit gate-check (installed by code-forge install-hooks)
188
+ # Chained existing hook: {chain_path}
189
+ "{chain_path}" "$@" || exit 1
190
+ exec {forge_invocation}
191
+ """
192
+ else:
193
+ return f"""#!/bin/sh
194
+ # code-forge pre-commit gate-check (installed by code-forge install-hooks)
195
+ exec {forge_invocation}
196
+ """
197
+
198
+
199
+ def run_install_hooks(
200
+ args=None,
201
+ env: Optional[Mapping[str, str]] = None,
202
+ cwd: Optional[Path] = None,
203
+ stdout: Optional[IO] = None,
204
+ stderr: Optional[IO] = None,
205
+ ) -> int:
206
+ """Main install-hooks entry point.
207
+
208
+ Args:
209
+ args: parsed argparse Namespace; reads args.quiet if present
210
+ env: environment variables (os.environ if None)
211
+ cwd: working directory (Path.cwd() if None)
212
+ stdout: output stream (sys.stdout if None)
213
+ stderr: error stream (sys.stderr if None)
214
+
215
+ Returns:
216
+ EXIT_PASS (0) on success, EXIT_FAIL (1) on any error
217
+ """
218
+ if env is None:
219
+ env = os.environ
220
+ if cwd is None:
221
+ cwd = Path.cwd()
222
+ if stdout is None:
223
+ stdout = sys.stdout
224
+ if stderr is None:
225
+ stderr = sys.stderr
226
+
227
+ quiet = getattr(args, "quiet", False)
228
+
229
+ def info(msg):
230
+ if not quiet:
231
+ print(msg, file=stderr)
232
+
233
+ try:
234
+ # Step a: check core.hooksPath override
235
+ hooks_path_override = check_hooks_path_override(cwd)
236
+ if hooks_path_override is not None:
237
+ print(
238
+ "code-forge: error: core.hooksPath is set to '%s'. "
239
+ "code-forge install-hooks cannot install to a custom hooks path. "
240
+ "Add 'exec /path/to/code-forge gate-check' to your pre-commit hook manually."
241
+ % hooks_path_override,
242
+ file=stderr,
243
+ )
244
+ return EXIT_FAIL
245
+
246
+ # Step b: resolve hooks directory
247
+ hooks_dir = resolve_hooks_dir(cwd)
248
+
249
+ # Step c: check for .pre-commit-config.yaml
250
+ pre_commit_config = cwd / ".pre-commit-config.yaml"
251
+ if pre_commit_config.exists():
252
+ info(
253
+ "code-forge: warning: pre-commit framework detected. "
254
+ "code-forge hook will chain after existing hooks."
255
+ )
256
+
257
+ # Step d: resolve code-forge absolute path
258
+ forge_invocation = resolve_forge_path()
259
+
260
+ # Step e: check for existing pre-commit hook
261
+ hook_path = hooks_dir / "pre-commit"
262
+ backup_path = hooks_dir / "pre-commit.code-forge-backup"
263
+ chain_path = None
264
+
265
+ if hook_path.exists():
266
+ # Read first 3 lines to check if it's a code-forge-generated hook
267
+ try:
268
+ with open(hook_path, "r", encoding="utf-8") as f:
269
+ first_lines = [f.readline() for _ in range(3)]
270
+ hook_header = "".join(first_lines)
271
+ is_forge_hook = (
272
+ "code-forge gate-check" in hook_header
273
+ or "installed by code-forge install-hooks" in hook_header
274
+ )
275
+ except (OSError, UnicodeDecodeError):
276
+ is_forge_hook = False
277
+
278
+ if is_forge_hook:
279
+ # Idempotent re-install: skip backup, overwrite
280
+ info(
281
+ "code-forge: re-installing hook (existing is code-forge-generated)"
282
+ )
283
+ else:
284
+ # Backup existing non-code-forge hook
285
+ if backup_path.exists():
286
+ # Backup already exists from a prior install.
287
+ # Current hook_path has unknown content that would be
288
+ # overwritten without a backup. Block and ask the user
289
+ # to resolve manually.
290
+ print(
291
+ "code-forge: error: pre-commit.code-forge-backup already "
292
+ "exists at %s and a non-code-forge hook is at %s. "
293
+ "Remove one of them manually, then re-run "
294
+ "code-forge install-hooks."
295
+ % (backup_path, hook_path),
296
+ file=stderr,
297
+ )
298
+ return EXIT_FAIL
299
+ else:
300
+ # Move existing hook to backup
301
+ shutil.move(str(hook_path), str(backup_path))
302
+ info(
303
+ "code-forge: existing hook backed up to %s"
304
+ % backup_path
305
+ )
306
+ chain_path = backup_path
307
+
308
+ # Step f: generate hook content
309
+ hook_content = generate_hook_content(forge_invocation, chain_path)
310
+
311
+ # Step g: write hook file
312
+ hooks_dir.mkdir(parents=True, exist_ok=True)
313
+ with open(hook_path, "w", encoding="utf-8") as f:
314
+ f.write(hook_content)
315
+
316
+ # Step h: chmod 0o755
317
+ os.chmod(hook_path, 0o755)
318
+
319
+ # Step i: success message
320
+ info(
321
+ "code-forge: pre-commit hook installed at %s" % hook_path
322
+ )
323
+
324
+ return EXIT_PASS
325
+
326
+ except Exception as e:
327
+ print(
328
+ "code-forge: error: install-hooks failed: %s" % e,
329
+ file=stderr,
330
+ )
331
+ return EXIT_FAIL
code_forge/lock.py ADDED
@@ -0,0 +1,162 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """STATE-11 file lock with PID liveness probing.
4
+
5
+ Race-safe atomic acquire via O_CREAT|O_EXCL. Stale-PID recovery via
6
+ os.kill(pid, 0) liveness check.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import signal
12
+ from pathlib import Path
13
+ from types import TracebackType
14
+ from typing import Optional, Type
15
+
16
+
17
+ class ForgeLockBusy(Exception):
18
+ """Raised when another live forge process holds the lock.
19
+
20
+ .pid attribute carries the holder PID for caller logging.
21
+ """
22
+
23
+ def __init__(self, pid: int, path: Path):
24
+ self.pid = pid
25
+ self.path = path
26
+ super().__init__(
27
+ "another forge process is running (PID %d, lock %s)"
28
+ % (pid, path)
29
+ )
30
+
31
+
32
+ class ForgeLock:
33
+ """Context manager + explicit release().
34
+
35
+ Usage:
36
+ with ForgeLock(Path(".code-forge/forge.lock")) as lock:
37
+ ... do forge work ...
38
+ # released on normal exit OR on exception in body OR on
39
+ # SIGINT/SIGTERM.
40
+ """
41
+
42
+ def __init__(self, path: Path):
43
+ self.path = path
44
+ self._held = False
45
+ self._original_sigint = None
46
+ self._original_sigterm = None
47
+
48
+ def __enter__(self) -> "ForgeLock":
49
+ acquire_lock(self.path)
50
+ self._held = True
51
+ self._install_signal_handlers()
52
+ return self
53
+
54
+ def __exit__(
55
+ self,
56
+ exc_type: Optional[Type[BaseException]],
57
+ exc: Optional[BaseException],
58
+ tb: Optional[TracebackType],
59
+ ) -> None:
60
+ self.release()
61
+
62
+ def release(self) -> None:
63
+ if self._held and self.path.exists():
64
+ try:
65
+ stored = int(self.path.read_text().strip())
66
+ if stored == os.getpid():
67
+ self.path.unlink()
68
+ except (ValueError, FileNotFoundError):
69
+ pass
70
+ self._held = False
71
+ self._restore_signal_handlers()
72
+
73
+ def _install_signal_handlers(self) -> None:
74
+ """Save + chain previous handler per R1 B1."""
75
+ def _make_chained_handler(prev):
76
+ def _handler(signum, frame):
77
+ try:
78
+ self.release()
79
+ except Exception: # noqa: BLE001
80
+ pass
81
+ if callable(prev):
82
+ prev(signum, frame)
83
+ return
84
+ if prev == signal.SIG_IGN:
85
+ return
86
+ raise KeyboardInterrupt
87
+ return _handler
88
+
89
+ prev_sigint = signal.getsignal(signal.SIGINT)
90
+ prev_sigterm = signal.getsignal(signal.SIGTERM)
91
+ self._original_sigint = prev_sigint
92
+ self._original_sigterm = prev_sigterm
93
+ signal.signal(
94
+ signal.SIGINT, _make_chained_handler(prev_sigint)
95
+ )
96
+ signal.signal(
97
+ signal.SIGTERM, _make_chained_handler(prev_sigterm)
98
+ )
99
+
100
+ def _restore_signal_handlers(self) -> None:
101
+ if self._original_sigint is not None:
102
+ signal.signal(signal.SIGINT, self._original_sigint)
103
+ self._original_sigint = None
104
+ if self._original_sigterm is not None:
105
+ signal.signal(signal.SIGTERM, self._original_sigterm)
106
+ self._original_sigterm = None
107
+
108
+
109
+ def acquire_lock(path: Path) -> None:
110
+ """Acquire lock exclusively. Recovers from stale PID locks.
111
+
112
+ On EEXIST: probe PID liveness; alive -> ForgeLockBusy; dead -> remove
113
+ + retry (single retry; second EEXIST -> ForgeLockBusy with pid=-1).
114
+
115
+ Raises:
116
+ ForgeLockBusy: another live forge holds the lock.
117
+ OSError: filesystem error (parent dir missing, EACCES, etc).
118
+ """
119
+ path.parent.mkdir(parents=True, exist_ok=True)
120
+ for attempt in range(2):
121
+ try:
122
+ fd = os.open(
123
+ str(path),
124
+ os.O_CREAT | os.O_EXCL | os.O_WRONLY,
125
+ 0o644,
126
+ )
127
+ try:
128
+ os.write(fd, ("%d\n" % os.getpid()).encode("ascii"))
129
+ finally:
130
+ os.close(fd)
131
+ return
132
+ except FileExistsError:
133
+ if attempt == 1:
134
+ raise ForgeLockBusy(-1, path)
135
+ _handle_existing_lock(path)
136
+ except IsADirectoryError:
137
+ raise OSError(
138
+ "lock path %s is a directory, not a file "
139
+ "(remove it manually)" % path
140
+ )
141
+
142
+
143
+ def _handle_existing_lock(path: Path) -> None:
144
+ """Read PID; check liveness; remove if dead."""
145
+ try:
146
+ pid = int(path.read_text().strip())
147
+ except (ValueError, FileNotFoundError):
148
+ try:
149
+ path.unlink()
150
+ except FileNotFoundError:
151
+ pass
152
+ return
153
+ try:
154
+ os.kill(pid, 0)
155
+ raise ForgeLockBusy(pid, path)
156
+ except ProcessLookupError:
157
+ try:
158
+ path.unlink()
159
+ except FileNotFoundError:
160
+ pass
161
+ except PermissionError:
162
+ raise ForgeLockBusy(pid, path)