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.
Files changed (69) hide show
  1. {guard_cli-0.1.0 → guard_cli-0.1.1}/PKG-INFO +18 -2
  2. {guard_cli-0.1.0 → guard_cli-0.1.1}/README.md +16 -0
  3. guard_cli-0.1.1/guard_cli/__init__.py +7 -0
  4. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/agent_integration.py +1 -1
  5. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/api.py +27 -10
  6. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/backend.py +80 -21
  7. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/cli.py +19 -19
  8. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/db.py +39 -1
  9. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/doctor_cmd.py +65 -13
  10. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/feedback.py +2 -1
  11. guard_cli-0.1.1/guard_cli/hook.py +491 -0
  12. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/ingest/coverage.py +1 -1
  13. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/init_cmd.py +22 -10
  14. guard_cli-0.1.1/guard_cli/integrations/antigravity.py +154 -0
  15. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/integrations/claude.py +4 -1
  16. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/state.py +45 -1
  17. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/thin_slice.py +14 -5
  18. {guard_cli-0.1.0 → guard_cli-0.1.1}/guard_cli.egg-info/PKG-INFO +18 -2
  19. {guard_cli-0.1.0 → guard_cli-0.1.1}/guard_cli.egg-info/SOURCES.txt +8 -19
  20. guard_cli-0.1.1/guard_cli.egg-info/entry_points.txt +3 -0
  21. {guard_cli-0.1.0 → guard_cli-0.1.1}/guard_cli.egg-info/top_level.txt +0 -1
  22. {guard_cli-0.1.0 → guard_cli-0.1.1}/pyproject.toml +6 -5
  23. guard_cli-0.1.1/tests/test_antigravity_integration.py +143 -0
  24. {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_api_and_hook.py +22 -24
  25. guard_cli-0.1.1/tests/test_commit_bypass.py +72 -0
  26. {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_doctor_cmd.py +1 -1
  27. guard_cli-0.1.1/tests/test_embedded_platform.py +63 -0
  28. guard_cli-0.1.1/tests/test_event_reasons.py +123 -0
  29. guard_cli-0.1.1/tests/test_fingerprint_invalidation.py +121 -0
  30. guard_cli-0.1.1/tests/test_hook_timeout.py +91 -0
  31. guard_cli-0.1.0/guard/__init__.py +0 -2
  32. guard_cli-0.1.0/guard/hook.py +0 -281
  33. guard_cli-0.1.0/guard_cli/__init__.py +0 -37
  34. guard_cli-0.1.0/guard_cli/api.py +0 -2
  35. guard_cli-0.1.0/guard_cli/backend.py +0 -2
  36. guard_cli-0.1.0/guard_cli/cli.py +0 -3
  37. guard_cli-0.1.0/guard_cli/db.py +0 -2
  38. guard_cli-0.1.0/guard_cli/decision.py +0 -2
  39. guard_cli-0.1.0/guard_cli/doctor_cmd.py +0 -2
  40. guard_cli-0.1.0/guard_cli/events.py +0 -2
  41. guard_cli-0.1.0/guard_cli/feedback.py +0 -2
  42. guard_cli-0.1.0/guard_cli/hook.py +0 -2
  43. guard_cli-0.1.0/guard_cli/ingest/__init__.py +0 -1
  44. guard_cli-0.1.0/guard_cli/ingest/coverage.py +0 -1
  45. guard_cli-0.1.0/guard_cli/ingest/static.py +0 -1
  46. guard_cli-0.1.0/guard_cli/init_cmd.py +0 -2
  47. guard_cli-0.1.0/guard_cli/integrations/__init__.py +0 -1
  48. guard_cli-0.1.0/guard_cli/integrations/claude.py +0 -1
  49. guard_cli-0.1.0/guard_cli/state.py +0 -2
  50. guard_cli-0.1.0/guard_cli/thin_slice.py +0 -2
  51. guard_cli-0.1.0/guard_cli.egg-info/entry_points.txt +0 -3
  52. {guard_cli-0.1.0 → guard_cli-0.1.1}/LICENSE +0 -0
  53. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/decision.py +0 -0
  54. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/events.py +0 -0
  55. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/ingest/__init__.py +0 -0
  56. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/ingest/static.py +0 -0
  57. {guard_cli-0.1.0/guard → guard_cli-0.1.1/guard_cli}/integrations/__init__.py +0 -0
  58. {guard_cli-0.1.0 → guard_cli-0.1.1}/guard_cli.egg-info/dependency_links.txt +0 -0
  59. {guard_cli-0.1.0 → guard_cli-0.1.1}/guard_cli.egg-info/requires.txt +0 -0
  60. {guard_cli-0.1.0 → guard_cli-0.1.1}/setup.cfg +0 -0
  61. {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_backend.py +0 -0
  62. {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_coverage.py +0 -0
  63. {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_db.py +0 -0
  64. {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_decision.py +0 -0
  65. {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_feedback.py +0 -0
  66. {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_init_cmd.py +0 -0
  67. {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_integration_init_doctor.py +0 -0
  68. {guard_cli-0.1.0 → guard_cli-0.1.1}/tests/test_static.py +0 -0
  69. {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.0
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
@@ -0,0 +1,7 @@
1
+ """Guard: Graph-powered safety layer for AI coding agents."""
2
+ from importlib.metadata import version, PackageNotFoundError
3
+
4
+ try:
5
+ __version__ = version("guard-cli")
6
+ except PackageNotFoundError:
7
+ __version__ = "0.1.1"
@@ -14,7 +14,7 @@ import subprocess
14
14
  from pathlib import Path
15
15
  from typing import Any
16
16
 
17
- from guard.thin_slice import guard_check
17
+ from guard_cli.thin_slice import guard_check
18
18
 
19
19
  REPO_ROOT = Path("target_repos/flask").resolve()
20
20
 
@@ -19,17 +19,17 @@ import subprocess
19
19
  from pathlib import Path
20
20
  from typing import Any
21
21
 
22
- from guard import db
23
- from guard import feedback
24
- from guard.events import log_event
25
- from guard.thin_slice import (
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 guard.agent_integration import (
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 guard.init_cmd import find_test_environment
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 (180s timeout for worst-case run-all test suites)
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=180)
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 = ["test_suite_timeout_expired_180s"]
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
- if sys.platform not in ("darwin", "linux") and not sys.platform.startswith("linux"):
346
- raise RuntimeError(
347
- f"Embedded backend (FalkorDBLite) is not supported on platform '{sys.platform}'. "
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
- # Check import paths (redislite vs falkordblite)
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 err:
382
- raise ImportError(
383
- "falkordblite is not installed. Install with: pip install 'guard-cli[embedded]'"
384
- ) from err
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
- self._client = FalkorDB(dbfilename=str(self.db_path))
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 guard.ingest.static import analyze_repo
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 guard import db
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 guard import api
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 guard import db
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 guard import api
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 guard.ingest.coverage import ingest_coverage
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 guard import api
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 guard import api
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 guard import api
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 guard.hook import run_stop_hook
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 guard.integrations import claude
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 guard.integrations import claude
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 guard.integrations import claude
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 guard.init_cmd import run_init
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 guard.doctor_cmd import run_doctor
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 guard
219
+ import guard_cli
220
220
 
221
221
  p = argparse.ArgumentParser(prog="guard")
222
- p.add_argument("-v", "--version", action="version", version=f"guard {guard.__version__}")
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 guard.backend import resolve_backend, ServerBackend
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 guard
30
- from guard import db
31
- from guard.backend import (
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 guard.init_cmd import (
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{guard.__version__}. 'guard' is not found on PATH for subshells and agent hooks.",
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{guard.__version__} (binary: {guard_bin}).",
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 Claude Code Stop hook in settings.local.json or settings.json
314
- from guard.integrations import claude
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
- if not claude_info["installed"]:
317
- missing.append("hook in .claude/settings.local.json or .claude/settings.json")
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 in .claude/settings.json, AGENTS.md, and .gitignore.",
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] = []