gator-command 1.0.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.
- gator_command/__init__.py +2 -0
- gator_command/cli.py +137 -0
- gator_command/scripts/crawler.py +633 -0
- gator_command/scripts/dashboard/dashboard.css +982 -0
- gator_command/scripts/dashboard/dashboard.html +84 -0
- gator_command/scripts/dashboard/dashboard.js +419 -0
- gator_command/scripts/dashboard/views/audit.js +270 -0
- gator_command/scripts/dashboard/views/fleet.js +307 -0
- gator_command/scripts/dashboard/views/repo.js +599 -0
- gator_command/scripts/dashboard/views/settings.js +173 -0
- gator_command/scripts/dashboard/views/updates.js +308 -0
- gator_command/scripts/enforcer-prompt.md +22 -0
- gator_command/scripts/extract-claude-sessions.py +489 -0
- gator_command/scripts/extract-codex-sessions.py +477 -0
- gator_command/scripts/extract-gemini-sessions.py +410 -0
- gator_command/scripts/gator-audit.py +956 -0
- gator_command/scripts/gator-charter-draft.py +919 -0
- gator_command/scripts/gator-charter-lint.py +427 -0
- gator_command/scripts/gator-charter-verify.py +606 -0
- gator_command/scripts/gator-dashboard.py +1271 -0
- gator_command/scripts/gator-deploy.py +916 -0
- gator_command/scripts/gator-drift.py +569 -0
- gator_command/scripts/gator-enforce.py +82 -0
- gator_command/scripts/gator-fleet-intel.py +460 -0
- gator_command/scripts/gator-fleet-report.py +615 -0
- gator_command/scripts/gator-init-command-post.py +315 -0
- gator_command/scripts/gator-init.py +434 -0
- gator_command/scripts/gator-machine-id.py +153 -0
- gator_command/scripts/gator-policy-status.py +631 -0
- gator_command/scripts/gator-pulse.py +459 -0
- gator_command/scripts/gator-repo-status.py +649 -0
- gator_command/scripts/gator-session-common.py +372 -0
- gator_command/scripts/gator-session-sink.py +831 -0
- gator_command/scripts/gator-sessions.py +1244 -0
- gator_command/scripts/gator-update.py +615 -0
- gator_command/scripts/gator-version.py +38 -0
- gator_command/scripts/gator_core.py +489 -0
- gator_command/scripts/gator_remote.py +381 -0
- gator_command/scripts/gator_runtime.py +142 -0
- gator_command/scripts/gatorize-actions.sh +989 -0
- gator_command/scripts/gatorize-lib.sh +166 -0
- gator_command/scripts/gatorize-post.sh +394 -0
- gator_command/scripts/gatorize.py +1163 -0
- gator_command/scripts/gatorize.sh +185 -0
- gator_command/scripts/generate_markdown.py +212 -0
- gator_command/scripts/generate_wiki.py +424 -0
- gator_command/scripts/graph_health.py +780 -0
- gator_command/scripts/memex-lint.py +286 -0
- gator_command/scripts/memex-lint.sh +205 -0
- gator_command/scripts/memex.py +1472 -0
- gator_command/scripts/memex_formatters.py +191 -0
- gator_command/scripts/memex_state.py +236 -0
- gator_command/scripts/spawn.py +650 -0
- gator_command/templates/gator-starter/blueprints/README.md +32 -0
- gator_command/templates/gator-starter/charterignore +53 -0
- gator_command/templates/gator-starter/charters/README.md +178 -0
- gator_command/templates/gator-starter/charters/_template.md +31 -0
- gator_command/templates/gator-starter/commands/commit.md +33 -0
- gator_command/templates/gator-starter/commands/init.md +11 -0
- gator_command/templates/gator-starter/commands/update.md +5 -0
- gator_command/templates/gator-starter/constitution.md +165 -0
- gator_command/templates/gator-starter/field-guides/README.md +25 -0
- gator_command/templates/gator-starter/gator-start-up.md +119 -0
- gator_command/templates/gator-starter/procedures/charter-alignment.md +83 -0
- gator_command/templates/gator-starter/procedures/enforcer-review.md +317 -0
- gator_command/templates/gator-starter/procedures/field-guide-generation.md +176 -0
- gator_command/templates/gator-starter/procedures/knowledge-capture.md +57 -0
- gator_command/templates/gator-starter/procedures/significance-check.md +69 -0
- gator_command/templates/gator-starter/reference-notes/concierge-responses.md +535 -0
- gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +91 -0
- gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +22 -0
- gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +232 -0
- gator_command/templates/gator-starter/reference-notes/example-project.md +289 -0
- gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +72 -0
- gator_command/templates/gator-starter/reference-notes/git-workflow.md +60 -0
- gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +37 -0
- gator_command/templates/gator-starter/reference-notes/refactor-approach.md +155 -0
- gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +108 -0
- gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +99 -0
- gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +155 -0
- gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/enforcer-prompt.md +55 -0
- gator_command/templates/gator-starter/scripts/enforcer-review.py +1551 -0
- gator_command/templates/gator-starter/scripts/gator-approve.py +139 -0
- gator_command/templates/gator-starter/scripts/gator-enforce.py +82 -0
- gator_command/templates/gator-starter/scripts/gator-init.py +434 -0
- gator_command/templates/gator-starter/scripts/gator-pre-commit.py +2670 -0
- gator_command/templates/gator-starter/scripts/gator-pulse.py +459 -0
- gator_command/templates/gator-starter/scripts/gator-update.py +615 -0
- gator_command/templates/gator-starter/scripts/gator-version.py +38 -0
- gator_command/templates/gator-starter/scripts/gator_core.py +487 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/commit-msg +5 -0
- gator_command/templates/gator-starter/scripts/hooks/post-commit +7 -0
- gator_command/templates/gator-starter/scripts/hooks/pre-commit +5 -0
- gator_command/templates/gator-starter/sessions/.gitignore +7 -0
- gator_command/templates/gator-starter/vault/.gitkeep +0 -0
- gator_command/templates/gator-starter/whiteboard.md +5 -0
- gator_command-1.0.0.dist-info/METADATA +122 -0
- gator_command-1.0.0.dist-info/RECORD +110 -0
- gator_command-1.0.0.dist-info/WHEEL +5 -0
- gator_command-1.0.0.dist-info/entry_points.txt +2 -0
- gator_command-1.0.0.dist-info/licenses/LICENSE +21 -0
- gator_command-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator drift — Policy drift detection across the fleet.
|
|
4
|
+
|
|
5
|
+
Compares each registered repo's governance state against the command post's
|
|
6
|
+
current policy version, template generation, and structural expectations.
|
|
7
|
+
Reports which repos are stale, missing hooks, or diverging from org standards.
|
|
8
|
+
|
|
9
|
+
This is the first chargeable feature — the free engine produces the data,
|
|
10
|
+
drift detection produces the judgment.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
python gator-command/scripts/gator-drift.py
|
|
14
|
+
python gator-command/scripts/gator-drift.py --json
|
|
15
|
+
python gator-command/scripts/gator-drift.py --repo dangerous-golf
|
|
16
|
+
|
|
17
|
+
@reads: gator-command/registry.md, gator-command/org-policy.md,
|
|
18
|
+
.gator/ in each registered repo, git history
|
|
19
|
+
@writes: nothing (read-only, display only)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from gator_core import (
|
|
30
|
+
get_version, find_command_post, normalize_path, parse_registry,
|
|
31
|
+
git, ensure_utf8_stdout, CURRENT_GENERATION,
|
|
32
|
+
)
|
|
33
|
+
from gator_remote import (
|
|
34
|
+
ensure_cache, read_gator_state_remote, git_show, CACHE_DIR,
|
|
35
|
+
_resolve_ref, _cache_key,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
VERSION = get_version()
|
|
39
|
+
|
|
40
|
+
# Policy status integration — graceful degradation if module unavailable
|
|
41
|
+
try:
|
|
42
|
+
from gator_core import import_sibling
|
|
43
|
+
_ps = import_sibling("gator-policy-status")
|
|
44
|
+
if _ps:
|
|
45
|
+
_compute_sync_state = _ps.compute_sync_state
|
|
46
|
+
_get_governance_source = _ps.get_governance_source
|
|
47
|
+
_load_policy_link = _ps.load_policy_link
|
|
48
|
+
_HAS_POLICY_STATUS = True
|
|
49
|
+
else:
|
|
50
|
+
_HAS_POLICY_STATUS = False
|
|
51
|
+
except Exception:
|
|
52
|
+
_HAS_POLICY_STATUS = False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Command post state (the source of truth)
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def read_command_post_policy(command_post):
|
|
60
|
+
"""Read the current org policy version from the command post.
|
|
61
|
+
|
|
62
|
+
The policy version is the date in org-policy.md or the last
|
|
63
|
+
modification date of that file.
|
|
64
|
+
"""
|
|
65
|
+
gc_dir = command_post / "gator-command"
|
|
66
|
+
|
|
67
|
+
# Read policy version from org-policy.md git history
|
|
68
|
+
policy_date, ok = git(
|
|
69
|
+
"log", "-1", "--format=%ci", "--", "gator-command/org-policy.md",
|
|
70
|
+
cwd=command_post,
|
|
71
|
+
)
|
|
72
|
+
git_failed = not ok
|
|
73
|
+
|
|
74
|
+
if ok and policy_date:
|
|
75
|
+
policy_date = policy_date[:10] # YYYY-MM-DD
|
|
76
|
+
else:
|
|
77
|
+
policy_date = None
|
|
78
|
+
|
|
79
|
+
# Read the version field from org-policy if it has one
|
|
80
|
+
org_policy = gc_dir / "org-policy.md"
|
|
81
|
+
version_field = None
|
|
82
|
+
if org_policy.exists():
|
|
83
|
+
for line in org_policy.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
84
|
+
if line.startswith("version:"):
|
|
85
|
+
version_field = line.split(":", 1)[1].strip()
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"policy_date": policy_date,
|
|
90
|
+
"policy_version": version_field,
|
|
91
|
+
"generation": CURRENT_GENERATION,
|
|
92
|
+
"git_failed": git_failed,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Remote drift check (thin-fetch fallback)
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def check_repo_drift_remote(repo_entry, command_post_state):
|
|
101
|
+
"""Check a remote repo for drift via bare cache. Subset of local checks."""
|
|
102
|
+
name = repo_entry["name"]
|
|
103
|
+
remote = repo_entry["remote"]
|
|
104
|
+
|
|
105
|
+
result = {
|
|
106
|
+
"name": name,
|
|
107
|
+
"accessible": False,
|
|
108
|
+
"scan_mode": "remote",
|
|
109
|
+
"findings": [],
|
|
110
|
+
"severity": "ok",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
cache_path = CACHE_DIR / _cache_key(name, remote)
|
|
114
|
+
if not cache_path.is_dir():
|
|
115
|
+
cache_path = ensure_cache(name, remote)
|
|
116
|
+
if not cache_path:
|
|
117
|
+
result["findings"].append({
|
|
118
|
+
"check": "remote-fetch",
|
|
119
|
+
"severity": "drift",
|
|
120
|
+
"message": f"Cannot fetch remote: {remote}",
|
|
121
|
+
})
|
|
122
|
+
result["severity"] = "drift"
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
result["accessible"] = True
|
|
126
|
+
ref = _resolve_ref(cache_path)
|
|
127
|
+
state = read_gator_state_remote(cache_path, ref)
|
|
128
|
+
|
|
129
|
+
if not state["gatorized"]:
|
|
130
|
+
result["findings"].append({
|
|
131
|
+
"check": "gatorized",
|
|
132
|
+
"severity": "drift",
|
|
133
|
+
"message": "Repo is registered but not gatorized (no .gator/ on remote)",
|
|
134
|
+
})
|
|
135
|
+
result["severity"] = "drift"
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
# Generation check
|
|
139
|
+
if state["generation"] < command_post_state["generation"]:
|
|
140
|
+
result["findings"].append({
|
|
141
|
+
"check": "generation",
|
|
142
|
+
"severity": "drift",
|
|
143
|
+
"message": f"Generation {state['generation']}, command post is {command_post_state['generation']}. Run gator update.",
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
# Policy version check
|
|
147
|
+
if not command_post_state.get("git_failed") and command_post_state["policy_date"]:
|
|
148
|
+
if state["policy_version"] and state["policy_version"] < command_post_state["policy_date"]:
|
|
149
|
+
result["findings"].append({
|
|
150
|
+
"check": "policy-version",
|
|
151
|
+
"severity": "drift",
|
|
152
|
+
"message": f"Policy version {state['policy_version']}, command post updated {command_post_state['policy_date']}. Run gator update.",
|
|
153
|
+
})
|
|
154
|
+
elif not state["policy_version"]:
|
|
155
|
+
result["findings"].append({
|
|
156
|
+
"check": "policy-version",
|
|
157
|
+
"severity": "warn",
|
|
158
|
+
"message": "No policy version in command-post.md.",
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
# Hook sources check (can't check installed remotely)
|
|
162
|
+
if not state["hooks_sources"]:
|
|
163
|
+
result["findings"].append({
|
|
164
|
+
"check": "hooks",
|
|
165
|
+
"severity": "warn",
|
|
166
|
+
"message": "No hook sources in .gator/scripts/hooks/ (cannot verify installed hooks remotely).",
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
# Charter presence
|
|
170
|
+
if state["charters"] == 0:
|
|
171
|
+
result["findings"].append({
|
|
172
|
+
"check": "charters",
|
|
173
|
+
"severity": "warn",
|
|
174
|
+
"message": "No charters. The governance loop requires at least one charter.",
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
# Constitution check
|
|
178
|
+
constitution = git_show(cache_path, ref, ".gator/constitution.md")
|
|
179
|
+
if not constitution:
|
|
180
|
+
result["findings"].append({
|
|
181
|
+
"check": "constitution",
|
|
182
|
+
"severity": "drift",
|
|
183
|
+
"message": "No constitution.md found on remote.",
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
# Policy link check (remote — informational only)
|
|
187
|
+
policy_link_content = git_show(cache_path, ref, ".gator/policy-link.json")
|
|
188
|
+
if policy_link_content:
|
|
189
|
+
result["findings"].append({
|
|
190
|
+
"check": "policy-cached",
|
|
191
|
+
"severity": "info",
|
|
192
|
+
"message": "Policy cache present on remote (cannot verify freshness remotely).",
|
|
193
|
+
})
|
|
194
|
+
else:
|
|
195
|
+
result["findings"].append({
|
|
196
|
+
"check": "policy-unknown",
|
|
197
|
+
"severity": "info",
|
|
198
|
+
"message": "No policy-link.json on remote — policy sync state unknown.",
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
# Set overall severity
|
|
202
|
+
severities = [f["severity"] for f in result["findings"]]
|
|
203
|
+
if "drift" in severities:
|
|
204
|
+
result["severity"] = "drift"
|
|
205
|
+
elif "warn" in severities:
|
|
206
|
+
result["severity"] = "warn"
|
|
207
|
+
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Per-repo drift checks (local)
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
def check_repo_drift(repo_entry, command_post_state):
|
|
216
|
+
"""Check a single repo for drift against command post state.
|
|
217
|
+
|
|
218
|
+
Returns a dict with drift findings.
|
|
219
|
+
"""
|
|
220
|
+
repo_path = Path(normalize_path(repo_entry["path"]))
|
|
221
|
+
gator_dir = repo_path / ".gator"
|
|
222
|
+
|
|
223
|
+
result = {
|
|
224
|
+
"name": repo_entry["name"],
|
|
225
|
+
"accessible": repo_path.is_dir(),
|
|
226
|
+
"findings": [],
|
|
227
|
+
"severity": "ok", # ok, warn, drift
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if not result["accessible"]:
|
|
231
|
+
# Try remote fallback
|
|
232
|
+
remote = repo_entry.get("remote", "—")
|
|
233
|
+
if remote and remote != "—":
|
|
234
|
+
return check_repo_drift_remote(repo_entry, command_post_state)
|
|
235
|
+
result["findings"].append({
|
|
236
|
+
"check": "accessible",
|
|
237
|
+
"severity": "drift",
|
|
238
|
+
"message": f"Repo not accessible at {repo_path}",
|
|
239
|
+
})
|
|
240
|
+
result["severity"] = "drift"
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
if not gator_dir.is_dir():
|
|
244
|
+
result["findings"].append({
|
|
245
|
+
"check": "gatorized",
|
|
246
|
+
"severity": "drift",
|
|
247
|
+
"message": "Repo is registered but not gatorized (no .gator/ directory)",
|
|
248
|
+
})
|
|
249
|
+
result["severity"] = "drift"
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
# --- Generation check ---
|
|
253
|
+
repo_gen = 0
|
|
254
|
+
version_file = gator_dir / ".gator-version"
|
|
255
|
+
if version_file.exists():
|
|
256
|
+
for line in version_file.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
257
|
+
if line.startswith("generation:"):
|
|
258
|
+
try:
|
|
259
|
+
repo_gen = int(line.split(":", 1)[1].strip())
|
|
260
|
+
except ValueError:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
if repo_gen < command_post_state["generation"]:
|
|
264
|
+
result["findings"].append({
|
|
265
|
+
"check": "generation",
|
|
266
|
+
"severity": "drift",
|
|
267
|
+
"message": f"Generation {repo_gen}, command post is {command_post_state['generation']}. Run gator update.",
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
# --- Policy sync check (new system) ---
|
|
271
|
+
_policy_authoritative = False
|
|
272
|
+
if _HAS_POLICY_STATUS:
|
|
273
|
+
try:
|
|
274
|
+
source, _ = _get_governance_source(gator_dir)
|
|
275
|
+
if source:
|
|
276
|
+
status = _compute_sync_state(gator_dir, source)
|
|
277
|
+
state = status.get("state", "unknown")
|
|
278
|
+
source_type = status.get("source_type")
|
|
279
|
+
_policy_authoritative = state in ("synced", "behind", "diverged") or (
|
|
280
|
+
state == "no-cache" and source_type == "local"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if state == "behind":
|
|
284
|
+
result["findings"].append({
|
|
285
|
+
"check": "policy-behind",
|
|
286
|
+
"severity": "drift",
|
|
287
|
+
"message": "Policy cache is behind source. Run gator policy-status --sync.",
|
|
288
|
+
})
|
|
289
|
+
elif state == "diverged":
|
|
290
|
+
result["findings"].append({
|
|
291
|
+
"check": "policy-diverged",
|
|
292
|
+
"severity": "drift",
|
|
293
|
+
"message": "Policy cache was edited locally — diverged from source. Run gator policy-status --sync to reset.",
|
|
294
|
+
})
|
|
295
|
+
elif state == "no-cache" and _policy_authoritative:
|
|
296
|
+
result["findings"].append({
|
|
297
|
+
"check": "policy-no-cache",
|
|
298
|
+
"severity": "warn",
|
|
299
|
+
"message": "Policy not yet cached. Run gator policy-status --sync to materialize.",
|
|
300
|
+
})
|
|
301
|
+
# synced → no finding (good state)
|
|
302
|
+
# remote-only no-cache → not authoritative, old check handles it
|
|
303
|
+
except Exception:
|
|
304
|
+
pass # Fall through to old check
|
|
305
|
+
|
|
306
|
+
# --- Policy version check (old system, suppressed when new system is authoritative) ---
|
|
307
|
+
if not _policy_authoritative:
|
|
308
|
+
repo_policy = None
|
|
309
|
+
cp_file = gator_dir / "command-post.md"
|
|
310
|
+
if cp_file.exists():
|
|
311
|
+
for line in cp_file.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
312
|
+
if line.startswith("version:"):
|
|
313
|
+
repo_policy = line.split(":", 1)[1].strip()
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
if command_post_state.get("git_failed"):
|
|
317
|
+
result["findings"].append({
|
|
318
|
+
"check": "policy-version",
|
|
319
|
+
"severity": "warn",
|
|
320
|
+
"message": "Could not read command post policy date (git failed). Policy comparison skipped.",
|
|
321
|
+
})
|
|
322
|
+
elif command_post_state["policy_date"] and repo_policy:
|
|
323
|
+
if repo_policy < command_post_state["policy_date"]:
|
|
324
|
+
result["findings"].append({
|
|
325
|
+
"check": "policy-version",
|
|
326
|
+
"severity": "drift",
|
|
327
|
+
"message": f"Policy version {repo_policy}, command post updated {command_post_state['policy_date']}. Run gator update.",
|
|
328
|
+
})
|
|
329
|
+
elif not repo_policy:
|
|
330
|
+
result["findings"].append({
|
|
331
|
+
"check": "policy-version",
|
|
332
|
+
"severity": "warn",
|
|
333
|
+
"message": "No policy version in command-post.md.",
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
# --- Hook presence ---
|
|
337
|
+
hooks_dir = gator_dir / "scripts" / "hooks"
|
|
338
|
+
has_hooks = hooks_dir.is_dir() and any(hooks_dir.iterdir())
|
|
339
|
+
git_hooks_dir = repo_path / ".git" / "hooks"
|
|
340
|
+
|
|
341
|
+
pre_commit_installed = (git_hooks_dir / "pre-commit").exists() if git_hooks_dir.is_dir() else False
|
|
342
|
+
commit_msg_installed = (git_hooks_dir / "commit-msg").exists() if git_hooks_dir.is_dir() else False
|
|
343
|
+
|
|
344
|
+
if not has_hooks and not pre_commit_installed and not commit_msg_installed:
|
|
345
|
+
# No hooks anywhere — full drift
|
|
346
|
+
result["findings"].append({
|
|
347
|
+
"check": "hooks",
|
|
348
|
+
"severity": "drift",
|
|
349
|
+
"message": "No governance hooks (sources or installed). Run gator update.",
|
|
350
|
+
})
|
|
351
|
+
else:
|
|
352
|
+
if not has_hooks:
|
|
353
|
+
# Installed but no sources — functional but fragile
|
|
354
|
+
result["findings"].append({
|
|
355
|
+
"check": "hook-sources",
|
|
356
|
+
"severity": "warn",
|
|
357
|
+
"message": "Hooks installed in .git/hooks/ but no sources in .gator/scripts/hooks/. Run gator update to add sources.",
|
|
358
|
+
})
|
|
359
|
+
if not pre_commit_installed or not commit_msg_installed:
|
|
360
|
+
missing = []
|
|
361
|
+
if not pre_commit_installed:
|
|
362
|
+
missing.append("pre-commit")
|
|
363
|
+
if not commit_msg_installed:
|
|
364
|
+
missing.append("commit-msg")
|
|
365
|
+
result["findings"].append({
|
|
366
|
+
"check": "hook-installed",
|
|
367
|
+
"severity": "drift",
|
|
368
|
+
"message": f"Git hooks not installed: {', '.join(missing)}. Run gator update or gatorize.sh.",
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
# --- Charter presence ---
|
|
372
|
+
charters_dir = gator_dir / "charters"
|
|
373
|
+
skip = {"_template.md", "README.md", "INDEX.md", ".gitkeep"}
|
|
374
|
+
charter_count = 0
|
|
375
|
+
if charters_dir.is_dir():
|
|
376
|
+
charter_count = len([
|
|
377
|
+
f for f in charters_dir.iterdir()
|
|
378
|
+
if f.suffix == ".md" and f.name not in skip
|
|
379
|
+
])
|
|
380
|
+
|
|
381
|
+
if charter_count == 0:
|
|
382
|
+
result["findings"].append({
|
|
383
|
+
"check": "charters",
|
|
384
|
+
"severity": "warn",
|
|
385
|
+
"message": "No charters. The governance loop requires at least one charter to be meaningful.",
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
# --- Commit_draft format check ---
|
|
389
|
+
draft_file = gator_dir / "commit_draft.md"
|
|
390
|
+
if draft_file.exists():
|
|
391
|
+
text = draft_file.read_text(encoding="utf-8", errors="replace")
|
|
392
|
+
if not text.startswith("---"):
|
|
393
|
+
result["findings"].append({
|
|
394
|
+
"check": "commit-draft-format",
|
|
395
|
+
"severity": "warn",
|
|
396
|
+
"message": "commit_draft.md lacks YAML frontmatter. Run gator update to refresh template.",
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
# --- Constitution freshness ---
|
|
400
|
+
# Compare repo constitution to template
|
|
401
|
+
repo_constitution = gator_dir / "constitution.md"
|
|
402
|
+
if not repo_constitution.exists():
|
|
403
|
+
result["findings"].append({
|
|
404
|
+
"check": "constitution",
|
|
405
|
+
"severity": "drift",
|
|
406
|
+
"message": "No constitution.md found.",
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
# --- Branch check ---
|
|
410
|
+
branch_out, branch_ok = git("branch", "--show-current", cwd=repo_path)
|
|
411
|
+
if not branch_ok:
|
|
412
|
+
result["findings"].append({
|
|
413
|
+
"check": "branch",
|
|
414
|
+
"severity": "warn",
|
|
415
|
+
"message": "Could not read branch (git failed).",
|
|
416
|
+
})
|
|
417
|
+
else:
|
|
418
|
+
branch = branch_out or "detached"
|
|
419
|
+
if branch not in ("dev", "main", "master"):
|
|
420
|
+
result["findings"].append({
|
|
421
|
+
"check": "branch",
|
|
422
|
+
"severity": "warn",
|
|
423
|
+
"message": f"On branch '{branch}', expected 'dev'. May be mid-install or mid-feature.",
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
# --- Trailer presence (informational) ---
|
|
427
|
+
raw, trailer_ok = git("log", "-1", "--format=%(trailers)", "dev", cwd=repo_path)
|
|
428
|
+
if not trailer_ok:
|
|
429
|
+
result["findings"].append({
|
|
430
|
+
"check": "trailers",
|
|
431
|
+
"severity": "warn",
|
|
432
|
+
"message": "Could not read trailers (git failed).",
|
|
433
|
+
})
|
|
434
|
+
elif not raw or "Gator-" not in raw:
|
|
435
|
+
result["findings"].append({
|
|
436
|
+
"check": "trailers",
|
|
437
|
+
"severity": "warn",
|
|
438
|
+
"message": "No Gator-* trailers in latest dev commit. Commits made before hook installation.",
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
# --- Compute overall severity ---
|
|
442
|
+
severities = [f["severity"] for f in result["findings"]]
|
|
443
|
+
if "drift" in severities:
|
|
444
|
+
result["severity"] = "drift"
|
|
445
|
+
elif "warn" in severities:
|
|
446
|
+
result["severity"] = "warn"
|
|
447
|
+
else:
|
|
448
|
+
result["severity"] = "ok"
|
|
449
|
+
|
|
450
|
+
return result
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ---------------------------------------------------------------------------
|
|
454
|
+
# Display
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
def print_drift_report(results, command_post_state):
|
|
458
|
+
"""Print formatted drift report."""
|
|
459
|
+
print()
|
|
460
|
+
print(" gator drift")
|
|
461
|
+
policy_display = command_post_state['policy_date'] or ('error: git failed' if command_post_state.get('git_failed') else 'unknown')
|
|
462
|
+
print(f" command post: gen {command_post_state['generation']}, policy {policy_display}")
|
|
463
|
+
print(f" {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
|
464
|
+
print()
|
|
465
|
+
|
|
466
|
+
drift_count = sum(1 for r in results if r["severity"] == "drift")
|
|
467
|
+
warn_count = sum(1 for r in results if r["severity"] == "warn")
|
|
468
|
+
ok_count = sum(1 for r in results if r["severity"] == "ok")
|
|
469
|
+
|
|
470
|
+
for r in results:
|
|
471
|
+
name = r["name"]
|
|
472
|
+
sev = r["severity"]
|
|
473
|
+
|
|
474
|
+
if sev == "ok":
|
|
475
|
+
print(f" ✓ {name} — current")
|
|
476
|
+
elif sev == "warn":
|
|
477
|
+
print(f" ⚠ {name} — warnings")
|
|
478
|
+
else:
|
|
479
|
+
print(f" ✗ {name} — DRIFT")
|
|
480
|
+
|
|
481
|
+
for f in r["findings"]:
|
|
482
|
+
if f["severity"] == "drift":
|
|
483
|
+
marker = "✗"
|
|
484
|
+
elif f["severity"] == "info":
|
|
485
|
+
marker = "ℹ"
|
|
486
|
+
else:
|
|
487
|
+
marker = "⚠"
|
|
488
|
+
print(f" {marker} {f['check']}: {f['message']}")
|
|
489
|
+
|
|
490
|
+
if not r["findings"]:
|
|
491
|
+
print(f" No findings.")
|
|
492
|
+
|
|
493
|
+
print()
|
|
494
|
+
|
|
495
|
+
# Summary
|
|
496
|
+
print(f" summary: {ok_count} current, {warn_count} warnings, {drift_count} drifted")
|
|
497
|
+
if drift_count > 0:
|
|
498
|
+
print(f" action: run 'gator update' in drifted repos to align with command post")
|
|
499
|
+
print()
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def print_json_report(results, command_post_state):
|
|
503
|
+
"""Output drift report as JSON."""
|
|
504
|
+
output = {
|
|
505
|
+
"version": VERSION,
|
|
506
|
+
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
507
|
+
"command_post": command_post_state,
|
|
508
|
+
"repos": results,
|
|
509
|
+
"summary": {
|
|
510
|
+
"total": len(results),
|
|
511
|
+
"ok": sum(1 for r in results if r["severity"] == "ok"),
|
|
512
|
+
"warn": sum(1 for r in results if r["severity"] == "warn"),
|
|
513
|
+
"drift": sum(1 for r in results if r["severity"] == "drift"),
|
|
514
|
+
},
|
|
515
|
+
}
|
|
516
|
+
print(json.dumps(output, indent=2))
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
# ---------------------------------------------------------------------------
|
|
520
|
+
# Entry point
|
|
521
|
+
# ---------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
def main():
|
|
524
|
+
ensure_utf8_stdout()
|
|
525
|
+
|
|
526
|
+
parser = argparse.ArgumentParser(
|
|
527
|
+
description="Gator drift — policy drift detection across the fleet."
|
|
528
|
+
)
|
|
529
|
+
parser.add_argument(
|
|
530
|
+
"--json", "-j",
|
|
531
|
+
action="store_true",
|
|
532
|
+
help="Output as JSON",
|
|
533
|
+
)
|
|
534
|
+
parser.add_argument(
|
|
535
|
+
"--repo", "-r",
|
|
536
|
+
help="Check a single repo by name",
|
|
537
|
+
)
|
|
538
|
+
args = parser.parse_args()
|
|
539
|
+
|
|
540
|
+
command_post = find_command_post()
|
|
541
|
+
if not command_post:
|
|
542
|
+
print(" Error: not in a gator-command repo.", file=sys.stderr)
|
|
543
|
+
sys.exit(1)
|
|
544
|
+
|
|
545
|
+
repos = parse_registry(command_post)
|
|
546
|
+
if not repos:
|
|
547
|
+
print(" No repos in registry.", file=sys.stderr)
|
|
548
|
+
sys.exit(1)
|
|
549
|
+
|
|
550
|
+
if args.repo:
|
|
551
|
+
repos = [r for r in repos if r["name"] == args.repo]
|
|
552
|
+
if not repos:
|
|
553
|
+
print(f" Error: repo '{args.repo}' not found in registry.", file=sys.stderr)
|
|
554
|
+
sys.exit(1)
|
|
555
|
+
|
|
556
|
+
# Read command post state
|
|
557
|
+
cp_state = read_command_post_policy(command_post)
|
|
558
|
+
|
|
559
|
+
# Check each repo
|
|
560
|
+
results = [check_repo_drift(r, cp_state) for r in repos]
|
|
561
|
+
|
|
562
|
+
if args.json:
|
|
563
|
+
print_json_report(results, cp_state)
|
|
564
|
+
else:
|
|
565
|
+
print_drift_report(results, cp_state)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
if __name__ == "__main__":
|
|
569
|
+
main()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator-enforce.py — Set the enforcement level for a Gator-governed repo.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python gator-enforce.py --level strict|warn|off [--repo /path/to/repo]
|
|
7
|
+
|
|
8
|
+
Levels:
|
|
9
|
+
strict — CRITICAL/HIGH findings block commits (default)
|
|
10
|
+
warn — all findings reported but nothing blocks
|
|
11
|
+
off — governance checks skipped, trailers still written
|
|
12
|
+
|
|
13
|
+
Writes to .gator/config.json in the target repo.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
VALID_LEVELS = {"strict", "warn", "off"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def find_gator_dir(repo_path=None):
|
|
25
|
+
"""Find .gator/ directory from the given path or cwd."""
|
|
26
|
+
start = Path(repo_path) if repo_path else Path.cwd()
|
|
27
|
+
start = start.resolve()
|
|
28
|
+
|
|
29
|
+
if (start / ".gator").is_dir():
|
|
30
|
+
return start / ".gator"
|
|
31
|
+
for parent in start.parents:
|
|
32
|
+
if (parent / ".gator").is_dir():
|
|
33
|
+
return parent / ".gator"
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
parser = argparse.ArgumentParser(description="Set Gator enforcement level")
|
|
39
|
+
parser.add_argument("--level", required=True, choices=sorted(VALID_LEVELS),
|
|
40
|
+
help="Enforcement level: strict, warn, or off")
|
|
41
|
+
parser.add_argument("--repo", default=None,
|
|
42
|
+
help="Path to the repo (default: current directory)")
|
|
43
|
+
args = parser.parse_args()
|
|
44
|
+
|
|
45
|
+
gator_dir = find_gator_dir(args.repo)
|
|
46
|
+
if not gator_dir:
|
|
47
|
+
print("Error: No .gator/ directory found.", file=sys.stderr)
|
|
48
|
+
print("Are you in a Gator-governed repo?", file=sys.stderr)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
config_path = gator_dir / "config.json"
|
|
52
|
+
|
|
53
|
+
# Read existing config or start fresh
|
|
54
|
+
config = {}
|
|
55
|
+
if config_path.exists():
|
|
56
|
+
try:
|
|
57
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
58
|
+
except (json.JSONDecodeError, OSError):
|
|
59
|
+
config = {}
|
|
60
|
+
|
|
61
|
+
old_level = config.get("enforcement_level", "strict")
|
|
62
|
+
config["enforcement_level"] = args.level
|
|
63
|
+
|
|
64
|
+
config_path.write_text(
|
|
65
|
+
json.dumps(config, indent=2) + "\n", encoding="utf-8"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if old_level == args.level:
|
|
69
|
+
print(f" Enforcement level unchanged: {args.level}")
|
|
70
|
+
else:
|
|
71
|
+
print(f" Enforcement level: {old_level} -> {args.level}")
|
|
72
|
+
|
|
73
|
+
if args.level == "strict":
|
|
74
|
+
print(" CRITICAL/HIGH findings will block commits.")
|
|
75
|
+
elif args.level == "warn":
|
|
76
|
+
print(" Findings will be reported but commits will not be blocked.")
|
|
77
|
+
else:
|
|
78
|
+
print(" Governance checks disabled. Trailers still written.")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
main()
|