guard-cli 0.1.0__tar.gz → 0.1.1__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.
- {guard_cli-0.1.0 → guard_cli-0.1.1}/PKG-INFO +18 -2
- {guard_cli-0.1.0 → guard_cli-0.1.1}/README.md +16 -0
- guard_cli-0.1.1/guard_cli/__init__.py +7 -0
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/agent_integration.py +1 -1
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/api.py +27 -10
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/backend.py +80 -21
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/cli.py +19 -19
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/db.py +39 -1
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/doctor_cmd.py +65 -13
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/feedback.py +2 -1
- guard_cli-0.1.1/guard_cli/hook.py +491 -0
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/ingest/coverage.py +1 -1
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/init_cmd.py +22 -10
- guard_cli-0.1.1/guard_cli/integrations/antigravity.py +154 -0
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/integrations/claude.py +4 -1
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/state.py +45 -1
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/thin_slice.py +14 -5
- {guard_cli-0.1.0 → guard_cli-0.1.1}/guard_cli.egg-info/PKG-INFO +18 -2
- {guard_cli-0.1.0 → guard_cli-0.1.1}/guard_cli.egg-info/SOURCES.txt +8 -19
- guard_cli-0.1.1/guard_cli.egg-info/entry_points.txt +3 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/guard_cli.egg-info/top_level.txt +0 -1
- {guard_cli-0.1.0 → guard_cli-0.1.1}/pyproject.toml +6 -5
- guard_cli-0.1.1/tests/test_antigravity_integration.py +143 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_api_and_hook.py +22 -24
- guard_cli-0.1.1/tests/test_commit_bypass.py +72 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_doctor_cmd.py +1 -1
- guard_cli-0.1.1/tests/test_embedded_platform.py +63 -0
- guard_cli-0.1.1/tests/test_event_reasons.py +123 -0
- guard_cli-0.1.1/tests/test_fingerprint_invalidation.py +121 -0
- guard_cli-0.1.1/tests/test_hook_timeout.py +91 -0
- guard_cli-0.1.0/guard/__init__.py +0 -2
- guard_cli-0.1.0/guard/hook.py +0 -281
- guard_cli-0.1.0/guard_cli/__init__.py +0 -37
- guard_cli-0.1.0/guard_cli/api.py +0 -2
- guard_cli-0.1.0/guard_cli/backend.py +0 -2
- guard_cli-0.1.0/guard_cli/cli.py +0 -3
- guard_cli-0.1.0/guard_cli/db.py +0 -2
- guard_cli-0.1.0/guard_cli/decision.py +0 -2
- guard_cli-0.1.0/guard_cli/doctor_cmd.py +0 -2
- guard_cli-0.1.0/guard_cli/events.py +0 -2
- guard_cli-0.1.0/guard_cli/feedback.py +0 -2
- guard_cli-0.1.0/guard_cli/hook.py +0 -2
- guard_cli-0.1.0/guard_cli/ingest/__init__.py +0 -1
- guard_cli-0.1.0/guard_cli/ingest/coverage.py +0 -1
- guard_cli-0.1.0/guard_cli/ingest/static.py +0 -1
- guard_cli-0.1.0/guard_cli/init_cmd.py +0 -2
- guard_cli-0.1.0/guard_cli/integrations/__init__.py +0 -1
- guard_cli-0.1.0/guard_cli/integrations/claude.py +0 -1
- guard_cli-0.1.0/guard_cli/state.py +0 -2
- guard_cli-0.1.0/guard_cli/thin_slice.py +0 -2
- guard_cli-0.1.0/guard_cli.egg-info/entry_points.txt +0 -3
- {guard_cli-0.1.0 → guard_cli-0.1.1}/LICENSE +0 -0
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/decision.py +0 -0
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/events.py +0 -0
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/ingest/__init__.py +0 -0
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/ingest/static.py +0 -0
- {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/integrations/__init__.py +0 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/guard_cli.egg-info/dependency_links.txt +0 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/guard_cli.egg-info/requires.txt +0 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/setup.cfg +0 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_backend.py +0 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_coverage.py +0 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_db.py +0 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_decision.py +0 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_feedback.py +0 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_init_cmd.py +0 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_integration_init_doctor.py +0 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_static.py +0 -0
- {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_thin_slice.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: guard-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Graph-powered safety layer for AI coding agents built on FalkorDB
|
|
5
|
-
License: MIT
|
|
5
|
+
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
@@ -193,6 +193,22 @@ Initializes Guard in a repository, detects backends, and configures integrations
|
|
|
193
193
|
guard init [--backend server|docker|embedded] [--skip-coverage]
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
+
#### Backends & Platform Support
|
|
197
|
+
|
|
198
|
+
Guard supports three FalkorDB backend topologies:
|
|
199
|
+
1. **Server (`--backend server`)**: Connects to an existing FalkorDB instance (default: `localhost:6379`). Works across all platforms.
|
|
200
|
+
2. **Docker (`--backend docker`)**: Automatically launches and manages a dedicated `guard-falkordb` container on port 6390. Works on any platform with Docker installed.
|
|
201
|
+
3. **Embedded (`--backend embedded`)**: Zero-configuration, in-process FalkorDBLite instance.
|
|
202
|
+
- **Supported OS & Platform Requirements**:
|
|
203
|
+
- **Python**: 3.12 or newer
|
|
204
|
+
- **Linux**: `glibc` 2.39 or newer (e.g., Ubuntu 24.04+, Fedora 40+)
|
|
205
|
+
- **macOS**: macOS 15 (Sequoia) or newer
|
|
206
|
+
- **Platform Fallback**: On unsupported platforms (such as Linux with glibc < 2.39 or macOS < 15), Guard prints a clear message:
|
|
207
|
+
```text
|
|
208
|
+
no embedded build for this platform, use Docker or a server
|
|
209
|
+
```
|
|
210
|
+
and guides you to use `--backend docker` or `--backend server` instead of failing with a build error.
|
|
211
|
+
|
|
196
212
|
### `guard doctor`
|
|
197
213
|
Verifies system health, backend reachability, graph counts, and settings:
|
|
198
214
|
```bash
|
|
@@ -172,6 +172,22 @@ Initializes Guard in a repository, detects backends, and configures integrations
|
|
|
172
172
|
guard init [--backend server|docker|embedded] [--skip-coverage]
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
+
#### Backends & Platform Support
|
|
176
|
+
|
|
177
|
+
Guard supports three FalkorDB backend topologies:
|
|
178
|
+
1. **Server (`--backend server`)**: Connects to an existing FalkorDB instance (default: `localhost:6379`). Works across all platforms.
|
|
179
|
+
2. **Docker (`--backend docker`)**: Automatically launches and manages a dedicated `guard-falkordb` container on port 6390. Works on any platform with Docker installed.
|
|
180
|
+
3. **Embedded (`--backend embedded`)**: Zero-configuration, in-process FalkorDBLite instance.
|
|
181
|
+
- **Supported OS & Platform Requirements**:
|
|
182
|
+
- **Python**: 3.12 or newer
|
|
183
|
+
- **Linux**: `glibc` 2.39 or newer (e.g., Ubuntu 24.04+, Fedora 40+)
|
|
184
|
+
- **macOS**: macOS 15 (Sequoia) or newer
|
|
185
|
+
- **Platform Fallback**: On unsupported platforms (such as Linux with glibc < 2.39 or macOS < 15), Guard prints a clear message:
|
|
186
|
+
```text
|
|
187
|
+
no embedded build for this platform, use Docker or a server
|
|
188
|
+
```
|
|
189
|
+
and guides you to use `--backend docker` or `--backend server` instead of failing with a build error.
|
|
190
|
+
|
|
175
191
|
### `guard doctor`
|
|
176
192
|
Verifies system health, backend reachability, graph counts, and settings:
|
|
177
193
|
```bash
|
|
@@ -19,17 +19,17 @@ import subprocess
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
|
-
from
|
|
23
|
-
from
|
|
24
|
-
from
|
|
25
|
-
from
|
|
22
|
+
from guard_cli import db
|
|
23
|
+
from guard_cli import feedback
|
|
24
|
+
from guard_cli.events import log_event
|
|
25
|
+
from guard_cli.thin_slice import (
|
|
26
26
|
guard_check,
|
|
27
27
|
parse_unified_diff,
|
|
28
28
|
resolve_changed_functions,
|
|
29
29
|
thin_slice_decision,
|
|
30
30
|
CheckResult,
|
|
31
31
|
)
|
|
32
|
-
from
|
|
32
|
+
from guard_cli.agent_integration import (
|
|
33
33
|
REPO_ROOT,
|
|
34
34
|
apply_patch,
|
|
35
35
|
reset_repo,
|
|
@@ -91,6 +91,7 @@ def check(
|
|
|
91
91
|
strict: bool = False,
|
|
92
92
|
no_confidence_filter: bool = False,
|
|
93
93
|
no_dynamic_widening: bool = False,
|
|
94
|
+
baseline_sha: str | None = None,
|
|
94
95
|
) -> dict[str, Any]:
|
|
95
96
|
"""Evaluates proposed changes and returns test selection decision.
|
|
96
97
|
|
|
@@ -107,6 +108,7 @@ def check(
|
|
|
107
108
|
graph_name=graph_name,
|
|
108
109
|
no_confidence_filter=no_confidence_filter,
|
|
109
110
|
no_dynamic_widening=no_dynamic_widening,
|
|
111
|
+
baseline_sha=baseline_sha,
|
|
110
112
|
)
|
|
111
113
|
|
|
112
114
|
is_fallback = res.is_fallback
|
|
@@ -175,6 +177,7 @@ def run(
|
|
|
175
177
|
graph_name: str = "guard",
|
|
176
178
|
strict: bool = False,
|
|
177
179
|
session_id: str | None = None,
|
|
180
|
+
baseline_sha: str | None = None,
|
|
178
181
|
) -> tuple[dict[str, Any], int]:
|
|
179
182
|
"""Runs Guard check and executes the selected tests.
|
|
180
183
|
|
|
@@ -192,6 +195,7 @@ def run(
|
|
|
192
195
|
repo_path=repo,
|
|
193
196
|
graph_name=graph_name,
|
|
194
197
|
strict=strict,
|
|
198
|
+
baseline_sha=baseline_sha,
|
|
195
199
|
)
|
|
196
200
|
except Exception as exc:
|
|
197
201
|
if strict:
|
|
@@ -271,16 +275,18 @@ def run(
|
|
|
271
275
|
py_bin = sys.executable
|
|
272
276
|
if repo:
|
|
273
277
|
try:
|
|
274
|
-
from
|
|
278
|
+
from guard_cli.init_cmd import find_test_environment
|
|
275
279
|
py_bin = str(find_test_environment(repo))
|
|
276
280
|
except Exception:
|
|
277
281
|
pass
|
|
278
282
|
|
|
279
|
-
# 3. Execute pytest
|
|
283
|
+
# 3. Execute pytest with configurable timeout
|
|
284
|
+
from guard_cli.hook import get_hook_timeout
|
|
285
|
+
timeout = get_hook_timeout(repo)
|
|
280
286
|
cmd = [py_bin, "-m", "pytest", "-q", "--tb=short"] + tests_to_run
|
|
281
287
|
start = time.time()
|
|
282
288
|
try:
|
|
283
|
-
proc = subprocess.run(cmd, cwd=str(repo), capture_output=True, text=True, timeout=
|
|
289
|
+
proc = subprocess.run(cmd, cwd=str(repo), capture_output=True, text=True, timeout=timeout)
|
|
284
290
|
duration_s = round(time.time() - start, 3)
|
|
285
291
|
passed = (proc.returncode == 0)
|
|
286
292
|
failures = _extract_failures(proc.stdout, proc.stderr)
|
|
@@ -289,9 +295,20 @@ def run(
|
|
|
289
295
|
except subprocess.TimeoutExpired:
|
|
290
296
|
duration_s = round(time.time() - start, 3)
|
|
291
297
|
passed = False
|
|
292
|
-
failures = ["
|
|
293
|
-
summary_line = f"Test execution timed out after {duration_s:.1f}s."
|
|
298
|
+
failures = [f"test_suite_timeout_expired_{timeout}s"]
|
|
299
|
+
summary_line = f"Test execution timed out after {duration_s:.1f}s (timeout: {timeout}s)."
|
|
294
300
|
proc_exit_code = 124
|
|
301
|
+
log_event(
|
|
302
|
+
event="timeout",
|
|
303
|
+
session_id=session_id,
|
|
304
|
+
repo_path=repo,
|
|
305
|
+
details={
|
|
306
|
+
"timeout_s": timeout,
|
|
307
|
+
"duration_s": duration_s,
|
|
308
|
+
"tests_run": tests_to_run,
|
|
309
|
+
"policy": check_dict.get("policy", "subset"),
|
|
310
|
+
},
|
|
311
|
+
)
|
|
295
312
|
|
|
296
313
|
report = {
|
|
297
314
|
"version": SCHEMA_VERSION,
|
|
@@ -338,23 +338,64 @@ class DockerBackend(GraphBackend):
|
|
|
338
338
|
self._client = None
|
|
339
339
|
|
|
340
340
|
|
|
341
|
+
def check_embedded_platform_support() -> tuple[bool, str]:
|
|
342
|
+
"""Checks whether the current operating system and C library satisfy FalkorDBLite requirements.
|
|
343
|
+
|
|
344
|
+
Requirements:
|
|
345
|
+
- Python 3.12+
|
|
346
|
+
- macOS 15+ (Sequoia or newer)
|
|
347
|
+
- Linux with glibc >= 2.39
|
|
348
|
+
"""
|
|
349
|
+
if sys.platform not in ("darwin", "linux") and not sys.platform.startswith("linux"):
|
|
350
|
+
return False, (
|
|
351
|
+
f"Embedded backend (FalkorDBLite) is not supported on platform '{sys.platform}'. "
|
|
352
|
+
f"no embedded build for this platform, use Docker or a server"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if sys.version_info < (3, 12):
|
|
356
|
+
return False, (
|
|
357
|
+
f"Embedded backend (FalkorDBLite) requires Python 3.12 or newer. "
|
|
358
|
+
f"Current interpreter is Python {sys.version.split()[0]}. "
|
|
359
|
+
f"no embedded build for this platform, use Docker or a server"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if sys.platform == "darwin":
|
|
363
|
+
import platform
|
|
364
|
+
mac_ver = platform.mac_ver()[0]
|
|
365
|
+
if mac_ver:
|
|
366
|
+
try:
|
|
367
|
+
major = int(mac_ver.split(".")[0])
|
|
368
|
+
if major < 15:
|
|
369
|
+
return False, (
|
|
370
|
+
f"Embedded backend (FalkorDBLite) requires macOS 15 or newer (current: macOS {mac_ver}). "
|
|
371
|
+
f"no embedded build for this platform, use Docker or a server"
|
|
372
|
+
)
|
|
373
|
+
except (ValueError, IndexError):
|
|
374
|
+
pass
|
|
375
|
+
elif sys.platform.startswith("linux"):
|
|
376
|
+
import platform
|
|
377
|
+
libc_name, libc_ver = platform.libc_ver()
|
|
378
|
+
if libc_name == "glibc" and libc_ver:
|
|
379
|
+
try:
|
|
380
|
+
parts = [int(p) for p in libc_ver.split(".")[:2]]
|
|
381
|
+
if tuple(parts) < (2, 39):
|
|
382
|
+
return False, (
|
|
383
|
+
f"Embedded backend (FalkorDBLite) requires glibc 2.39 or newer (current: {libc_name} {libc_ver}). "
|
|
384
|
+
f"no embedded build for this platform, use Docker or a server"
|
|
385
|
+
)
|
|
386
|
+
except (ValueError, IndexError):
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
return True, ""
|
|
390
|
+
|
|
391
|
+
|
|
341
392
|
class EmbeddedBackend(GraphBackend):
|
|
342
|
-
"""Manages an embedded FalkorDBLite instance (Python 3.12+)."""
|
|
393
|
+
"""Manages an embedded FalkorDBLite instance (Python 3.12+ on supported platforms)."""
|
|
343
394
|
|
|
344
395
|
def __init__(self, data_path: str | Path | None = None):
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
f"FalkorDBLite only supports Linux and macOS. "
|
|
349
|
-
f"Please use Docker backend ('guard init --backend docker') or Server backend ('guard init --backend server')."
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
if sys.version_info < (3, 12):
|
|
353
|
-
raise RuntimeError(
|
|
354
|
-
f"Embedded backend (FalkorDBLite) requires Python 3.12 or newer. "
|
|
355
|
-
f"Current interpreter is Python {sys.version.split()[0]}. "
|
|
356
|
-
f"Try: uv tool install --python 3.12 guard-cli, or use Docker/Server backend."
|
|
357
|
-
)
|
|
396
|
+
ok, msg = check_embedded_platform_support()
|
|
397
|
+
if not ok:
|
|
398
|
+
raise RuntimeError(msg)
|
|
358
399
|
|
|
359
400
|
if data_path is None:
|
|
360
401
|
base_dir = Path.home() / ".guard" / "data"
|
|
@@ -372,18 +413,36 @@ class EmbeddedBackend(GraphBackend):
|
|
|
372
413
|
|
|
373
414
|
def connect(self) -> Any:
|
|
374
415
|
if self._client is None:
|
|
375
|
-
|
|
416
|
+
ok, msg = check_embedded_platform_support()
|
|
417
|
+
if not ok:
|
|
418
|
+
raise RuntimeError(msg)
|
|
376
419
|
try:
|
|
377
420
|
from redislite import FalkorDB
|
|
378
|
-
except ImportError:
|
|
421
|
+
except (ImportError, OSError) as err:
|
|
379
422
|
try:
|
|
380
423
|
from falkordblite import FalkorDB # type: ignore
|
|
381
|
-
except ImportError as
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
424
|
+
except (ImportError, OSError) as inner_err:
|
|
425
|
+
err_msg = f"{inner_err} {err}"
|
|
426
|
+
if any(term in err_msg for term in ("GLIBC", "version", "symbol", "dlopen", "image not found")):
|
|
427
|
+
raise RuntimeError(
|
|
428
|
+
f"Failed importing embedded FalkorDB: {inner_err}. "
|
|
429
|
+
f"no embedded build for this platform, use Docker or a server"
|
|
430
|
+
) from inner_err
|
|
431
|
+
raise RuntimeError(
|
|
432
|
+
f"falkordblite is not installed or not supported: {inner_err}. "
|
|
433
|
+
f"no embedded build for this platform, use Docker or a server"
|
|
434
|
+
) from inner_err
|
|
385
435
|
|
|
386
|
-
|
|
436
|
+
try:
|
|
437
|
+
self._client = FalkorDB(dbfilename=str(self.db_path))
|
|
438
|
+
except (ImportError, OSError, Exception) as run_err:
|
|
439
|
+
err_msg = str(run_err)
|
|
440
|
+
if any(term in err_msg for term in ("GLIBC", "version", "symbol", "dlopen", "image not found")):
|
|
441
|
+
raise RuntimeError(
|
|
442
|
+
f"Failed starting embedded FalkorDB: {run_err}. "
|
|
443
|
+
f"no embedded build for this platform, use Docker or a server"
|
|
444
|
+
) from run_err
|
|
445
|
+
raise
|
|
387
446
|
return self._client
|
|
388
447
|
|
|
389
448
|
def is_healthy(self) -> bool:
|
|
@@ -16,7 +16,7 @@ import argparse
|
|
|
16
16
|
import json
|
|
17
17
|
import sys
|
|
18
18
|
|
|
19
|
-
from
|
|
19
|
+
from guard_cli.ingest.static import analyze_repo
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def cmd_ingest_static(args) -> int:
|
|
@@ -26,7 +26,7 @@ def cmd_ingest_static(args) -> int:
|
|
|
26
26
|
print("parse error:", err, file=sys.stderr)
|
|
27
27
|
if args.dry_run:
|
|
28
28
|
return 0
|
|
29
|
-
from
|
|
29
|
+
from guard_cli import db
|
|
30
30
|
|
|
31
31
|
g = db.get_graph(args.graph)
|
|
32
32
|
db.reset_graph(g)
|
|
@@ -38,7 +38,7 @@ def cmd_ingest_static(args) -> int:
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def cmd_callers(args) -> int:
|
|
41
|
-
from
|
|
41
|
+
from guard_cli import api
|
|
42
42
|
|
|
43
43
|
hops = int(args.hops)
|
|
44
44
|
callers = api.callers(
|
|
@@ -54,7 +54,7 @@ def cmd_callers(args) -> int:
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def cmd_stats(args) -> int:
|
|
57
|
-
from
|
|
57
|
+
from guard_cli import db
|
|
58
58
|
|
|
59
59
|
g = db.get_graph(args.graph)
|
|
60
60
|
counts = db.get_schema_counts(g)
|
|
@@ -69,7 +69,7 @@ def cmd_stats(args) -> int:
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def cmd_covering_tests(args) -> int:
|
|
72
|
-
from
|
|
72
|
+
from guard_cli import api
|
|
73
73
|
|
|
74
74
|
tests = api.covering_tests(
|
|
75
75
|
args.function,
|
|
@@ -83,7 +83,7 @@ def cmd_covering_tests(args) -> int:
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
def cmd_ingest_coverage(args) -> int:
|
|
86
|
-
from
|
|
86
|
+
from guard_cli.ingest.coverage import ingest_coverage
|
|
87
87
|
|
|
88
88
|
res = ingest_coverage(
|
|
89
89
|
repo_path=args.repo,
|
|
@@ -96,7 +96,7 @@ def cmd_ingest_coverage(args) -> int:
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def cmd_check(args) -> int:
|
|
99
|
-
from
|
|
99
|
+
from guard_cli import api
|
|
100
100
|
|
|
101
101
|
res = api.check(
|
|
102
102
|
diff_file=args.diff,
|
|
@@ -120,7 +120,7 @@ def cmd_check(args) -> int:
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
def cmd_feedback(args) -> int:
|
|
123
|
-
from
|
|
123
|
+
from guard_cli import api
|
|
124
124
|
|
|
125
125
|
res = api.report_miss(
|
|
126
126
|
function_id=args.function,
|
|
@@ -138,7 +138,7 @@ def cmd_feedback(args) -> int:
|
|
|
138
138
|
|
|
139
139
|
|
|
140
140
|
def cmd_run(args) -> int:
|
|
141
|
-
from
|
|
141
|
+
from guard_cli import api
|
|
142
142
|
|
|
143
143
|
report, exit_code = api.run(
|
|
144
144
|
diff_file=args.diff,
|
|
@@ -164,7 +164,7 @@ def cmd_run(args) -> int:
|
|
|
164
164
|
|
|
165
165
|
|
|
166
166
|
def cmd_hook_stop(args) -> int:
|
|
167
|
-
from
|
|
167
|
+
from guard_cli.hook import run_stop_hook
|
|
168
168
|
return run_stop_hook(
|
|
169
169
|
repo_override=args.repo,
|
|
170
170
|
strict=getattr(args, "strict", False),
|
|
@@ -172,41 +172,41 @@ def cmd_hook_stop(args) -> int:
|
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
def cmd_hook_install(args) -> int:
|
|
175
|
-
from
|
|
175
|
+
from guard_cli.integrations import claude
|
|
176
176
|
ok, msg = claude.install(repo_path=args.repo, shared=args.shared)
|
|
177
177
|
print(msg)
|
|
178
178
|
return 0 if ok else 1
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
def cmd_hook_uninstall(args) -> int:
|
|
182
|
-
from
|
|
182
|
+
from guard_cli.integrations import claude
|
|
183
183
|
ok, msg = claude.uninstall(repo_path=args.repo, shared=args.shared)
|
|
184
184
|
print(msg)
|
|
185
185
|
return 0 if ok else 1
|
|
186
186
|
|
|
187
187
|
|
|
188
188
|
def cmd_hook_status(args) -> int:
|
|
189
|
-
from
|
|
189
|
+
from guard_cli.integrations import claude
|
|
190
190
|
stat = claude.status(repo_path=args.repo, shared=args.shared)
|
|
191
191
|
print(json.dumps(stat, indent=2))
|
|
192
192
|
return 0
|
|
193
193
|
|
|
194
194
|
|
|
195
195
|
def cmd_init(args) -> int:
|
|
196
|
-
from
|
|
196
|
+
from guard_cli.init_cmd import run_init
|
|
197
197
|
|
|
198
198
|
return run_init(
|
|
199
199
|
repo_path=getattr(args, "repo", None),
|
|
200
200
|
backend_preference=getattr(args, "backend", None),
|
|
201
201
|
skip_coverage=getattr(args, "skip_coverage", False),
|
|
202
202
|
auto_yes=getattr(args, "yes", False),
|
|
203
|
-
agents=getattr(args, "agents", "claude,cursor,codex"),
|
|
203
|
+
agents=getattr(args, "agents", "claude,cursor,codex,antigravity"),
|
|
204
204
|
runs=getattr(args, "runs", None),
|
|
205
205
|
)
|
|
206
206
|
|
|
207
207
|
|
|
208
208
|
def cmd_doctor(args) -> int:
|
|
209
|
-
from
|
|
209
|
+
from guard_cli.doctor_cmd import run_doctor
|
|
210
210
|
|
|
211
211
|
return run_doctor(
|
|
212
212
|
repo_path=getattr(args, "repo", None),
|
|
@@ -216,10 +216,10 @@ def cmd_doctor(args) -> int:
|
|
|
216
216
|
|
|
217
217
|
|
|
218
218
|
def main(argv=None) -> int:
|
|
219
|
-
import
|
|
219
|
+
import guard_cli
|
|
220
220
|
|
|
221
221
|
p = argparse.ArgumentParser(prog="guard")
|
|
222
|
-
p.add_argument("-v", "--version", action="version", version=f"guard {
|
|
222
|
+
p.add_argument("-v", "--version", action="version", version=f"guard {guard_cli.__version__}")
|
|
223
223
|
p.add_argument("--graph", default="guard")
|
|
224
224
|
sub = p.add_subparsers(dest="cmd", required=True)
|
|
225
225
|
|
|
@@ -228,7 +228,7 @@ def main(argv=None) -> int:
|
|
|
228
228
|
ini.add_argument("--backend", choices=["server", "docker", "embedded"], default=None, help="database backend")
|
|
229
229
|
ini.add_argument("--skip-coverage", action="store_true", help="skip per-test coverage ingestion")
|
|
230
230
|
ini.add_argument("-y", "--yes", action="store_true", help="non-interactive mode accepting all defaults")
|
|
231
|
-
ini.add_argument("--agents", default="claude,cursor,codex", help="comma-separated agents to configure")
|
|
231
|
+
ini.add_argument("--agents", default="claude,cursor,codex,antigravity", help="comma-separated agents to configure")
|
|
232
232
|
ini.add_argument("--runs", type=int, default=None, help="number of baseline test runs (1 or 3)")
|
|
233
233
|
ini.set_defaults(fn=cmd_init)
|
|
234
234
|
|
|
@@ -11,6 +11,7 @@ INDEXES = [
|
|
|
11
11
|
"CREATE INDEX FOR (t:Test) ON (t.id)",
|
|
12
12
|
"CREATE INDEX FOR (x:File) ON (x.path)",
|
|
13
13
|
"CREATE INDEX FOR (r:Run) ON (r.id)",
|
|
14
|
+
"CREATE INDEX FOR (m:GuardMeta) ON (m.id)",
|
|
14
15
|
]
|
|
15
16
|
|
|
16
17
|
|
|
@@ -28,7 +29,7 @@ def get_backend(repo_path: Any = None) -> Any:
|
|
|
28
29
|
global _active_backend
|
|
29
30
|
if _active_backend is not None:
|
|
30
31
|
return _active_backend
|
|
31
|
-
from
|
|
32
|
+
from guard_cli.backend import resolve_backend, ServerBackend
|
|
32
33
|
|
|
33
34
|
if "FALKORDB_HOST" in os.environ or "FALKORDB_PORT" in os.environ:
|
|
34
35
|
host = os.getenv("FALKORDB_HOST", "localhost")
|
|
@@ -132,6 +133,7 @@ def write_static(g, result, commit_sha: str | None = None) -> None:
|
|
|
132
133
|
e.last_verified_sha = $sha""",
|
|
133
134
|
{"rows": b, "sha": commit_sha},
|
|
134
135
|
)
|
|
136
|
+
update_graph_meta(g)
|
|
135
137
|
|
|
136
138
|
|
|
137
139
|
def write_coverage(
|
|
@@ -170,9 +172,45 @@ def write_coverage(
|
|
|
170
172
|
e.last_verified_sha = $sha""",
|
|
171
173
|
{"rows": b, "conf": confidence, "sha": commit_sha},
|
|
172
174
|
)
|
|
175
|
+
update_graph_meta(g)
|
|
173
176
|
return len(pairs)
|
|
174
177
|
|
|
175
178
|
|
|
179
|
+
def update_graph_meta(g) -> None:
|
|
180
|
+
"""Updates Guard metadata generation and timestamp in the graph."""
|
|
181
|
+
import time
|
|
182
|
+
if g is None:
|
|
183
|
+
return
|
|
184
|
+
try:
|
|
185
|
+
ts = time.time()
|
|
186
|
+
g.query(
|
|
187
|
+
"MERGE (m:GuardMeta {id: 'meta'}) "
|
|
188
|
+
"SET m.updated_at = $ts, m.generation = coalesce(m.generation, 0) + 1",
|
|
189
|
+
{"ts": ts},
|
|
190
|
+
)
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def get_graph_state(g) -> dict[str, Any]:
|
|
196
|
+
"""Returns a dictionary summarizing graph state for cache invalidation."""
|
|
197
|
+
if g is None:
|
|
198
|
+
return {"status": "none"}
|
|
199
|
+
try:
|
|
200
|
+
counts = get_schema_counts(g)
|
|
201
|
+
meta = {}
|
|
202
|
+
try:
|
|
203
|
+
res = g.query("MATCH (m:GuardMeta {id: 'meta'}) RETURN m.updated_at, m.generation").result_set
|
|
204
|
+
if res and res[0]:
|
|
205
|
+
meta = {"updated_at": res[0][0], "generation": res[0][1]}
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
return {"counts": counts, "meta": meta}
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
return {"error": str(exc)}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
176
214
|
def write_run(
|
|
177
215
|
g,
|
|
178
216
|
run_data: dict[str, Any],
|
|
@@ -26,15 +26,15 @@ from typing import Any
|
|
|
26
26
|
from rich.console import Console
|
|
27
27
|
from rich.table import Table
|
|
28
28
|
|
|
29
|
-
import
|
|
30
|
-
from
|
|
31
|
-
from
|
|
29
|
+
import guard_cli
|
|
30
|
+
from guard_cli import db
|
|
31
|
+
from guard_cli.backend import (
|
|
32
32
|
GraphBackend,
|
|
33
33
|
get_repo_graph_name,
|
|
34
34
|
resolve_backend,
|
|
35
35
|
DockerBackend,
|
|
36
36
|
)
|
|
37
|
-
from
|
|
37
|
+
from guard_cli.init_cmd import (
|
|
38
38
|
find_repo_root,
|
|
39
39
|
find_test_environment,
|
|
40
40
|
write_integrations,
|
|
@@ -68,14 +68,14 @@ def check_environment() -> CheckResult:
|
|
|
68
68
|
return CheckResult(
|
|
69
69
|
name="environment",
|
|
70
70
|
status="warn",
|
|
71
|
-
message=f"Python {py_ver}, guard v{
|
|
71
|
+
message=f"Python {py_ver}, guard v{guard_cli.__version__}. 'guard' is not found on PATH for subshells and agent hooks.",
|
|
72
72
|
fix="Add your Python/uv tool bin directory to PATH (e.g. run 'uv tool update-shell' or add ~/.local/bin to PATH).",
|
|
73
73
|
)
|
|
74
74
|
|
|
75
75
|
return CheckResult(
|
|
76
76
|
name="environment",
|
|
77
77
|
status="pass",
|
|
78
|
-
message=f"Python {py_ver}, guard v{
|
|
78
|
+
message=f"Python {py_ver}, guard v{guard_cli.__version__} (binary: {guard_bin}).",
|
|
79
79
|
)
|
|
80
80
|
|
|
81
81
|
|
|
@@ -310,11 +310,12 @@ def check_integrations(repo_path: Path) -> CheckResult:
|
|
|
310
310
|
if not gi.exists() or (".guard/" not in gi.read_text(encoding="utf-8") and ".guard" not in gi.read_text(encoding="utf-8")):
|
|
311
311
|
missing.append(".guard/ in .gitignore")
|
|
312
312
|
|
|
313
|
-
# 2. Check
|
|
314
|
-
from
|
|
313
|
+
# 2. Check Stop hook in Claude Code or Antigravity CLI
|
|
314
|
+
from guard_cli.integrations import claude, antigravity
|
|
315
315
|
claude_info = claude.check_all_settings(repo_path)
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
agy_info = antigravity.status(repo_path)
|
|
317
|
+
if not claude_info["installed"] and not agy_info["installed"]:
|
|
318
|
+
missing.append("hook in .agents/hooks.json or .claude/settings.json")
|
|
318
319
|
|
|
319
320
|
# 3. Check AGENTS.md or CLAUDE.md marked block
|
|
320
321
|
agents_doc = repo_path / "AGENTS.md"
|
|
@@ -335,10 +336,59 @@ def check_integrations(repo_path: Path) -> CheckResult:
|
|
|
335
336
|
fix="Run 'guard doctor --fix' or 'guard init' to write integration files.",
|
|
336
337
|
)
|
|
337
338
|
|
|
339
|
+
active_agents = []
|
|
340
|
+
if claude_info["installed"]:
|
|
341
|
+
active_agents.append("claude")
|
|
342
|
+
if agy_info["installed"]:
|
|
343
|
+
active_agents.append("antigravity")
|
|
344
|
+
|
|
338
345
|
return CheckResult(
|
|
339
346
|
name="integrations",
|
|
340
347
|
status="pass",
|
|
341
|
-
message="Integrations verified
|
|
348
|
+
message=f"Integrations verified for {', '.join(active_agents) if active_agents else 'agents'} (AGENTS.md and .gitignore present).",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def check_hook_timeout(repo_path: Path) -> CheckResult:
|
|
353
|
+
"""Verify hook timeout configuration and warn if baseline test duration is near the limit."""
|
|
354
|
+
from guard_cli.hook import get_hook_timeout
|
|
355
|
+
timeout = get_hook_timeout(repo_path)
|
|
356
|
+
|
|
357
|
+
# Read baseline duration from state.json if present
|
|
358
|
+
state_file = repo_path / ".guard" / "state.json"
|
|
359
|
+
baseline_s = None
|
|
360
|
+
if state_file.exists():
|
|
361
|
+
try:
|
|
362
|
+
data = json.loads(state_file.read_text(encoding="utf-8"))
|
|
363
|
+
baseline_s = data.get("baseline_duration_s") or data.get("baseline_s")
|
|
364
|
+
except Exception:
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
if baseline_s is not None:
|
|
368
|
+
try:
|
|
369
|
+
baseline_f = float(baseline_s)
|
|
370
|
+
if baseline_f >= (0.7 * timeout):
|
|
371
|
+
return CheckResult(
|
|
372
|
+
name="hook_timeout",
|
|
373
|
+
status="warn",
|
|
374
|
+
message=(
|
|
375
|
+
f"Baseline test run took {baseline_f:.1f}s, which is near the hook timeout limit ({timeout}s). "
|
|
376
|
+
f"Slow test execution may cause agent hook timeout."
|
|
377
|
+
),
|
|
378
|
+
fix=f"Increase timeout in .guard/config.toml ([hook] timeout = {int(baseline_f * 2)}) or optimize slow tests.",
|
|
379
|
+
)
|
|
380
|
+
return CheckResult(
|
|
381
|
+
name="hook_timeout",
|
|
382
|
+
status="pass",
|
|
383
|
+
message=f"Baseline test run took {baseline_f:.1f}s (comfortably within {timeout}s timeout limit).",
|
|
384
|
+
)
|
|
385
|
+
except (ValueError, TypeError):
|
|
386
|
+
pass
|
|
387
|
+
|
|
388
|
+
return CheckResult(
|
|
389
|
+
name="hook_timeout",
|
|
390
|
+
status="pass",
|
|
391
|
+
message=f"Hook timeout is configured to {timeout}s.",
|
|
342
392
|
)
|
|
343
393
|
|
|
344
394
|
|
|
@@ -353,8 +403,8 @@ def apply_fixes(repo_path: Path, backend: GraphBackend | None, checks: list[Chec
|
|
|
353
403
|
applied.append(f"Started docker container '{backend.container_name}'.")
|
|
354
404
|
|
|
355
405
|
elif c.name == "integrations":
|
|
356
|
-
write_integrations(repo_path, agents=["claude", "cursor", "codex"])
|
|
357
|
-
applied.append("Wrote integrations to .gitignore, .claude/settings.json, AGENTS.md, and CLAUDE.md.")
|
|
406
|
+
write_integrations(repo_path, agents=["claude", "cursor", "codex", "antigravity"])
|
|
407
|
+
applied.append("Wrote integrations to .gitignore, .agents/hooks.json, .claude/settings.json, AGENTS.md, and CLAUDE.md.")
|
|
358
408
|
|
|
359
409
|
return applied
|
|
360
410
|
|
|
@@ -385,6 +435,7 @@ def run_doctor(
|
|
|
385
435
|
check_f_res = check_freshness(root)
|
|
386
436
|
check_t_res = check_target_repo(root)
|
|
387
437
|
check_i_res = check_integrations(root)
|
|
438
|
+
check_to_res = check_hook_timeout(root)
|
|
388
439
|
|
|
389
440
|
checks = [
|
|
390
441
|
check_env_res,
|
|
@@ -393,6 +444,7 @@ def run_doctor(
|
|
|
393
444
|
check_f_res,
|
|
394
445
|
check_t_res,
|
|
395
446
|
check_i_res,
|
|
447
|
+
check_to_res,
|
|
396
448
|
]
|
|
397
449
|
|
|
398
450
|
applied_fixes: list[str] = []
|