guard-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- guard/__init__.py +2 -0
- guard/agent_integration.py +122 -0
- guard/api.py +397 -0
- guard/backend.py +475 -0
- guard/cli.py +320 -0
- guard/db.py +271 -0
- guard/decision.py +316 -0
- guard/doctor_cmd.py +475 -0
- guard/events.py +77 -0
- guard/feedback.py +86 -0
- guard/hook.py +281 -0
- guard/ingest/__init__.py +1 -0
- guard/ingest/coverage.py +403 -0
- guard/ingest/static.py +290 -0
- guard/init_cmd.py +437 -0
- guard/integrations/__init__.py +1 -0
- guard/integrations/claude.py +195 -0
- guard/state.py +104 -0
- guard/thin_slice.py +278 -0
- guard_cli/__init__.py +37 -0
- guard_cli/api.py +2 -0
- guard_cli/backend.py +2 -0
- guard_cli/cli.py +3 -0
- guard_cli/db.py +2 -0
- guard_cli/decision.py +2 -0
- guard_cli/doctor_cmd.py +2 -0
- guard_cli/events.py +2 -0
- guard_cli/feedback.py +2 -0
- guard_cli/hook.py +2 -0
- guard_cli/ingest/__init__.py +1 -0
- guard_cli/ingest/coverage.py +1 -0
- guard_cli/ingest/static.py +1 -0
- guard_cli/init_cmd.py +2 -0
- guard_cli/integrations/__init__.py +1 -0
- guard_cli/integrations/claude.py +1 -0
- guard_cli/state.py +2 -0
- guard_cli/thin_slice.py +2 -0
- guard_cli-0.1.0.dist-info/METADATA +241 -0
- guard_cli-0.1.0.dist-info/RECORD +43 -0
- guard_cli-0.1.0.dist-info/WHEEL +5 -0
- guard_cli-0.1.0.dist-info/entry_points.txt +3 -0
- guard_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- guard_cli-0.1.0.dist-info/top_level.txt +2 -0
guard/state.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Session state tracking for Guard Loop Protection.
|
|
2
|
+
|
|
3
|
+
Maintains block counts per session in `.guard/state.json` to prevent
|
|
4
|
+
infinite agent stop-hook loops (capping consecutive blocks at 3).
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
from guard.events import get_guard_dir
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _state_file(repo_path: str | Path | None = None) -> Path:
|
|
15
|
+
return get_guard_dir(repo_path) / "session_state.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_state(repo_path: str | Path | None = None) -> dict[str, Any]:
|
|
19
|
+
"""Loads current state dictionary from .guard/session_state.json."""
|
|
20
|
+
sf = _state_file(repo_path)
|
|
21
|
+
if not sf.exists():
|
|
22
|
+
return {"sessions": {}}
|
|
23
|
+
try:
|
|
24
|
+
with open(sf, "r", encoding="utf-8") as f:
|
|
25
|
+
data = json.load(f)
|
|
26
|
+
if not isinstance(data, dict):
|
|
27
|
+
return {"sessions": {}}
|
|
28
|
+
data.setdefault("sessions", {})
|
|
29
|
+
return data
|
|
30
|
+
except Exception:
|
|
31
|
+
return {"sessions": {}}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_state(state: dict[str, Any], repo_path: str | Path | None = None) -> None:
|
|
35
|
+
"""Saves state dictionary to .guard/session_state.json safely."""
|
|
36
|
+
try:
|
|
37
|
+
sf = _state_file(repo_path)
|
|
38
|
+
tmp_file = sf.with_suffix(".tmp")
|
|
39
|
+
with open(tmp_file, "w", encoding="utf-8") as f:
|
|
40
|
+
json.dump(state, f, indent=2)
|
|
41
|
+
tmp_file.replace(sf)
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_session_blocks(session_id: str, repo_path: str | Path | None = None) -> int:
|
|
47
|
+
"""Returns the number of consecutive blocks for a session."""
|
|
48
|
+
state = load_state(repo_path)
|
|
49
|
+
session_data = state.get("sessions", {}).get(session_id, {})
|
|
50
|
+
return int(session_data.get("consecutive_blocks", 0))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def record_session_block(session_id: str, repo_path: str | Path | None = None) -> int:
|
|
54
|
+
"""Increments and returns the consecutive block count for a session."""
|
|
55
|
+
state = load_state(repo_path)
|
|
56
|
+
sessions = state.setdefault("sessions", {})
|
|
57
|
+
session_data = sessions.setdefault(session_id, {})
|
|
58
|
+
|
|
59
|
+
current = int(session_data.get("consecutive_blocks", 0)) + 1
|
|
60
|
+
session_data["consecutive_blocks"] = current
|
|
61
|
+
session_data["last_blocked_at"] = time.time()
|
|
62
|
+
|
|
63
|
+
save_state(state, repo_path)
|
|
64
|
+
return current
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def reset_session_blocks(session_id: str, repo_path: str | Path | None = None) -> None:
|
|
68
|
+
"""Resets consecutive blocks for a session back to 0."""
|
|
69
|
+
state = load_state(repo_path)
|
|
70
|
+
sessions = state.setdefault("sessions", {})
|
|
71
|
+
if session_id in sessions:
|
|
72
|
+
sessions[session_id]["consecutive_blocks"] = 0
|
|
73
|
+
sessions[session_id]["last_passed_at"] = time.time()
|
|
74
|
+
save_state(state, repo_path)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_last_passed_fingerprint(session_id: str, repo_path: str | Path | None = None) -> str | None:
|
|
78
|
+
"""Returns the working-tree fingerprint that last passed for this session."""
|
|
79
|
+
state = load_state(repo_path)
|
|
80
|
+
session_data = state.get("sessions", {}).get(session_id, {})
|
|
81
|
+
return session_data.get("last_passed_fingerprint")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def set_last_passed_fingerprint(
|
|
85
|
+
session_id: str,
|
|
86
|
+
fingerprint: str,
|
|
87
|
+
repo_path: str | Path | None = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Records working-tree fingerprint that passed tests for this session."""
|
|
90
|
+
state = load_state(repo_path)
|
|
91
|
+
sessions = state.setdefault("sessions", {})
|
|
92
|
+
session_data = sessions.setdefault(session_id, {})
|
|
93
|
+
session_data["last_passed_fingerprint"] = fingerprint
|
|
94
|
+
session_data["fingerprint_passed_at"] = time.time()
|
|
95
|
+
save_state(state, repo_path)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def clear_last_passed_fingerprint(session_id: str, repo_path: str | Path | None = None) -> None:
|
|
99
|
+
"""Clears cached passed fingerprint when a failure occurs."""
|
|
100
|
+
state = load_state(repo_path)
|
|
101
|
+
sessions = state.setdefault("sessions", {})
|
|
102
|
+
if session_id in sessions and "last_passed_fingerprint" in sessions[session_id]:
|
|
103
|
+
sessions[session_id].pop("last_passed_fingerprint", None)
|
|
104
|
+
save_state(state, repo_path)
|
guard/thin_slice.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Thin slice end-to-end decision engine (Task T5a).
|
|
2
|
+
|
|
3
|
+
Parses a git diff, maps changed line numbers to FalkorDB Function nodes,
|
|
4
|
+
queries covering tests from COVERED_BY edges, and applies crude decision logic:
|
|
5
|
+
- If ANY changed function has no_evidence=true (or 0 covering tests) -> run all.
|
|
6
|
+
- Otherwise -> select the covering tests.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from guard import db
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ChangedFunction:
|
|
20
|
+
id: str
|
|
21
|
+
file: str
|
|
22
|
+
name: str
|
|
23
|
+
start: int
|
|
24
|
+
end: int
|
|
25
|
+
no_evidence: bool
|
|
26
|
+
has_dynamic_dispatch: bool = False
|
|
27
|
+
covering_tests: list[str] = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CheckResult:
|
|
32
|
+
status: str # "select" | "run-all"
|
|
33
|
+
is_fallback: bool
|
|
34
|
+
fallback_reason: str | None
|
|
35
|
+
changed_functions: list[str]
|
|
36
|
+
selected_tests: list[str]
|
|
37
|
+
execution_time_ms: float = 0.0
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict[str, Any]:
|
|
40
|
+
return {
|
|
41
|
+
"status": self.status,
|
|
42
|
+
"is_fallback": self.is_fallback,
|
|
43
|
+
"fallback_reason": self.fallback_reason,
|
|
44
|
+
"changed_functions": self.changed_functions,
|
|
45
|
+
"selected_tests": self.selected_tests,
|
|
46
|
+
"selected_tests_count": len(self.selected_tests),
|
|
47
|
+
"execution_time_ms": self.execution_time_ms,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_unified_diff(diff_text: str) -> dict[str, set[int]]:
|
|
52
|
+
"""Parses a unified diff into a map of {relative_file_path: set(modified_lines)}."""
|
|
53
|
+
file_lines: dict[str, set[int]] = {}
|
|
54
|
+
current_file = None
|
|
55
|
+
current_line = 0
|
|
56
|
+
|
|
57
|
+
hunk_regex = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
|
|
58
|
+
|
|
59
|
+
for line in diff_text.splitlines():
|
|
60
|
+
if line.startswith("+++ b/"):
|
|
61
|
+
raw_path = line[6:].strip()
|
|
62
|
+
# Normalize path: strip repo prefixes if present
|
|
63
|
+
current_file = raw_path
|
|
64
|
+
if current_file not in file_lines:
|
|
65
|
+
file_lines[current_file] = set()
|
|
66
|
+
current_line = 0
|
|
67
|
+
continue
|
|
68
|
+
elif line.startswith("+++ "):
|
|
69
|
+
raw_path = line[4:].strip()
|
|
70
|
+
if raw_path != "/dev/null":
|
|
71
|
+
current_file = raw_path.lstrip("b/")
|
|
72
|
+
if current_file not in file_lines:
|
|
73
|
+
file_lines[current_file] = set()
|
|
74
|
+
current_line = 0
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
if not current_file:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
hunk_match = hunk_regex.match(line)
|
|
81
|
+
if hunk_match:
|
|
82
|
+
current_line = int(hunk_match.group(1))
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
86
|
+
file_lines[current_file].add(current_line)
|
|
87
|
+
current_line += 1
|
|
88
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
89
|
+
# Deletion occurs at current_line
|
|
90
|
+
file_lines[current_file].add(current_line)
|
|
91
|
+
elif line.startswith(" "):
|
|
92
|
+
current_line += 1
|
|
93
|
+
|
|
94
|
+
return file_lines
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def resolve_changed_functions(g, file_lines: dict[str, set[int]]) -> list[ChangedFunction]:
|
|
98
|
+
"""Queries FalkorDB to find Function nodes spanning the modified lines."""
|
|
99
|
+
changed_funcs: dict[str, ChangedFunction] = {}
|
|
100
|
+
|
|
101
|
+
for file_path, lines in file_lines.items():
|
|
102
|
+
if not lines:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# In FalkorDB, File nodes use paths like 'src/flask/config.py'
|
|
106
|
+
# Query functions defined in this file
|
|
107
|
+
q = (
|
|
108
|
+
"MATCH (f:Function) "
|
|
109
|
+
"WHERE f.file = $file OR f.file ENDS WITH $file OR $file ENDS WITH f.file "
|
|
110
|
+
"RETURN f.id, f.file, f.name, f.start, f.end, coalesce(f.no_evidence, false), coalesce(f.has_dynamic_dispatch, false)"
|
|
111
|
+
)
|
|
112
|
+
res = g.query(q, {"file": file_path}).result_set
|
|
113
|
+
|
|
114
|
+
for fid, ffile, fname, start, end, no_evidence, dyn in res:
|
|
115
|
+
start_i = int(start)
|
|
116
|
+
end_i = int(end)
|
|
117
|
+
# Check if any modified line falls within [start, end]
|
|
118
|
+
if any(start_i <= lineno <= end_i for lineno in lines):
|
|
119
|
+
if fid not in changed_funcs:
|
|
120
|
+
changed_funcs[fid] = ChangedFunction(
|
|
121
|
+
id=fid,
|
|
122
|
+
file=ffile,
|
|
123
|
+
name=fname,
|
|
124
|
+
start=start_i,
|
|
125
|
+
end=end_i,
|
|
126
|
+
no_evidence=bool(no_evidence),
|
|
127
|
+
has_dynamic_dispatch=bool(dyn),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return list(changed_funcs.values())
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def thin_slice_decision(
|
|
134
|
+
g,
|
|
135
|
+
changed_functions: list[ChangedFunction],
|
|
136
|
+
test_files_touched: bool = False,
|
|
137
|
+
no_confidence_filter: bool = False,
|
|
138
|
+
no_dynamic_widening: bool = False,
|
|
139
|
+
) -> CheckResult:
|
|
140
|
+
"""Decision logic:
|
|
141
|
+
- If test files were touched or no functions matched -> run all.
|
|
142
|
+
- If ANY changed function has no_evidence=true (and not no_confidence_filter) -> run all.
|
|
143
|
+
- Otherwise -> collect covering tests via COVERED_BY edges (with confidence filter unless ablated).
|
|
144
|
+
- If dynamic dispatch is present and widening enabled -> include module tests.
|
|
145
|
+
"""
|
|
146
|
+
if test_files_touched:
|
|
147
|
+
return CheckResult(
|
|
148
|
+
status="run-all",
|
|
149
|
+
is_fallback=True,
|
|
150
|
+
fallback_reason="Test files were modified in diff",
|
|
151
|
+
changed_functions=[f.id for f in changed_functions],
|
|
152
|
+
selected_tests=["tests"],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if not changed_functions:
|
|
156
|
+
return CheckResult(
|
|
157
|
+
status="run-all",
|
|
158
|
+
is_fallback=True,
|
|
159
|
+
fallback_reason="No parsed functions matched diff line changes",
|
|
160
|
+
changed_functions=[],
|
|
161
|
+
selected_tests=["tests"],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Hard override: If any function has no evidence -> fallback to run all (unless confidence filter ablated)
|
|
165
|
+
if not no_confidence_filter:
|
|
166
|
+
for fn in changed_functions:
|
|
167
|
+
if fn.no_evidence:
|
|
168
|
+
return CheckResult(
|
|
169
|
+
status="run-all",
|
|
170
|
+
is_fallback=True,
|
|
171
|
+
fallback_reason=f"Uncovered function has no_evidence=true: {fn.id}",
|
|
172
|
+
changed_functions=[f.id for f in changed_functions],
|
|
173
|
+
selected_tests=["tests"],
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Collect covering tests from graph
|
|
177
|
+
all_tests = set()
|
|
178
|
+
min_conf = 0.0 if no_confidence_filter else 0.8
|
|
179
|
+
for fn in changed_functions:
|
|
180
|
+
covering = db.get_covering_tests(g, fn.id, min_confidence=min_conf) if g is not None else []
|
|
181
|
+
fn_tests = [c["test_id"] for c in covering]
|
|
182
|
+
if not fn_tests and not no_confidence_filter:
|
|
183
|
+
# No covering tests found
|
|
184
|
+
return CheckResult(
|
|
185
|
+
status="run-all",
|
|
186
|
+
is_fallback=True,
|
|
187
|
+
fallback_reason=f"No covering tests found with confidence >= 0.8 for: {fn.id}",
|
|
188
|
+
changed_functions=[f.id for f in changed_functions],
|
|
189
|
+
selected_tests=["tests"],
|
|
190
|
+
)
|
|
191
|
+
fn.covering_tests = fn_tests
|
|
192
|
+
all_tests.update(fn_tests)
|
|
193
|
+
|
|
194
|
+
# Dynamic dispatch widening
|
|
195
|
+
if not no_dynamic_widening and g is not None:
|
|
196
|
+
dynamic_funcs = [fn for fn in changed_functions if fn.has_dynamic_dispatch]
|
|
197
|
+
if dynamic_funcs:
|
|
198
|
+
for fn in dynamic_funcs:
|
|
199
|
+
stem = Path(fn.file).stem
|
|
200
|
+
mod_q = "MATCH (t:Test) WHERE t.file CONTAINS $mod_stem RETURN t.id"
|
|
201
|
+
try:
|
|
202
|
+
mod_res = g.query(mod_q, {"mod_stem": f"test_{stem}"}).result_set
|
|
203
|
+
for r in mod_res:
|
|
204
|
+
all_tests.add(r[0])
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
if not all_tests:
|
|
209
|
+
return CheckResult(
|
|
210
|
+
status="run-all",
|
|
211
|
+
is_fallback=True,
|
|
212
|
+
fallback_reason="No covering tests found",
|
|
213
|
+
changed_functions=[f.id for f in changed_functions],
|
|
214
|
+
selected_tests=["tests"],
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
sorted_tests = sorted(all_tests)
|
|
218
|
+
return CheckResult(
|
|
219
|
+
status="select",
|
|
220
|
+
is_fallback=False,
|
|
221
|
+
fallback_reason=None,
|
|
222
|
+
changed_functions=[f.id for f in changed_functions],
|
|
223
|
+
selected_tests=sorted_tests,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def guard_check(
|
|
228
|
+
diff_text: str | None = None,
|
|
229
|
+
diff_file: Path | str | None = None,
|
|
230
|
+
repo_path: Path | str | None = None,
|
|
231
|
+
graph_name: str = "guard",
|
|
232
|
+
no_confidence_filter: bool = False,
|
|
233
|
+
no_dynamic_widening: bool = False,
|
|
234
|
+
) -> CheckResult:
|
|
235
|
+
"""Evaluates a diff and returns the test selection decision."""
|
|
236
|
+
import time
|
|
237
|
+
start_time = time.time()
|
|
238
|
+
|
|
239
|
+
if diff_text is None:
|
|
240
|
+
if diff_file:
|
|
241
|
+
diff_text = Path(diff_file).read_text(encoding="utf-8")
|
|
242
|
+
else:
|
|
243
|
+
# Run git diff HEAD in repo_path
|
|
244
|
+
cwd = str(repo_path) if repo_path else None
|
|
245
|
+
proc = subprocess.run(["git", "diff", "HEAD"], cwd=cwd, capture_output=True, text=True)
|
|
246
|
+
diff_text = proc.stdout
|
|
247
|
+
|
|
248
|
+
file_lines = parse_unified_diff(diff_text)
|
|
249
|
+
|
|
250
|
+
# Check if test files were touched
|
|
251
|
+
test_files_touched = any(
|
|
252
|
+
"test" in p.lower() or p.startswith("tests/")
|
|
253
|
+
for p in file_lines.keys()
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Resolve unique repo graph name if available
|
|
257
|
+
if repo_path and (graph_name == "guard" or graph_name is None):
|
|
258
|
+
try:
|
|
259
|
+
from guard.backend import get_repo_graph_name
|
|
260
|
+
repo_graph = get_repo_graph_name(repo_path)
|
|
261
|
+
cand = db.get_graph(repo_graph, repo_path=repo_path)
|
|
262
|
+
counts = db.get_schema_counts(cand)
|
|
263
|
+
if sum(counts.values()) > 0:
|
|
264
|
+
graph_name = repo_graph
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
g = db.get_graph(graph_name, repo_path=repo_path)
|
|
269
|
+
changed_funcs = resolve_changed_functions(g, file_lines)
|
|
270
|
+
result = thin_slice_decision(
|
|
271
|
+
g,
|
|
272
|
+
changed_funcs,
|
|
273
|
+
test_files_touched=test_files_touched,
|
|
274
|
+
no_confidence_filter=no_confidence_filter,
|
|
275
|
+
no_dynamic_widening=no_dynamic_widening,
|
|
276
|
+
)
|
|
277
|
+
result.execution_time_ms = round((time.time() - start_time) * 1000.0, 2)
|
|
278
|
+
return result
|
guard_cli/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""guard_cli: Graph-powered safety layer for AI coding agents."""
|
|
2
|
+
import guard
|
|
3
|
+
from guard import __version__
|
|
4
|
+
from guard import (
|
|
5
|
+
api,
|
|
6
|
+
backend,
|
|
7
|
+
cli,
|
|
8
|
+
db,
|
|
9
|
+
decision,
|
|
10
|
+
doctor_cmd,
|
|
11
|
+
events,
|
|
12
|
+
feedback,
|
|
13
|
+
hook,
|
|
14
|
+
init_cmd,
|
|
15
|
+
state,
|
|
16
|
+
thin_slice,
|
|
17
|
+
ingest,
|
|
18
|
+
integrations,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"__version__",
|
|
23
|
+
"api",
|
|
24
|
+
"backend",
|
|
25
|
+
"cli",
|
|
26
|
+
"db",
|
|
27
|
+
"decision",
|
|
28
|
+
"doctor_cmd",
|
|
29
|
+
"events",
|
|
30
|
+
"feedback",
|
|
31
|
+
"hook",
|
|
32
|
+
"init_cmd",
|
|
33
|
+
"state",
|
|
34
|
+
"thin_slice",
|
|
35
|
+
"ingest",
|
|
36
|
+
"integrations",
|
|
37
|
+
]
|
guard_cli/api.py
ADDED
guard_cli/backend.py
ADDED
guard_cli/cli.py
ADDED
guard_cli/db.py
ADDED
guard_cli/decision.py
ADDED
guard_cli/doctor_cmd.py
ADDED
guard_cli/events.py
ADDED
guard_cli/feedback.py
ADDED
guard_cli/hook.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from guard.ingest import *
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from guard.ingest.coverage import *
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from guard.ingest.static import *
|
guard_cli/init_cmd.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from guard.integrations import *
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from guard.integrations.claude import *
|
guard_cli/state.py
ADDED
guard_cli/thin_slice.py
ADDED