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.
Files changed (110) hide show
  1. gator_command/__init__.py +2 -0
  2. gator_command/cli.py +137 -0
  3. gator_command/scripts/crawler.py +633 -0
  4. gator_command/scripts/dashboard/dashboard.css +982 -0
  5. gator_command/scripts/dashboard/dashboard.html +84 -0
  6. gator_command/scripts/dashboard/dashboard.js +419 -0
  7. gator_command/scripts/dashboard/views/audit.js +270 -0
  8. gator_command/scripts/dashboard/views/fleet.js +307 -0
  9. gator_command/scripts/dashboard/views/repo.js +599 -0
  10. gator_command/scripts/dashboard/views/settings.js +173 -0
  11. gator_command/scripts/dashboard/views/updates.js +308 -0
  12. gator_command/scripts/enforcer-prompt.md +22 -0
  13. gator_command/scripts/extract-claude-sessions.py +489 -0
  14. gator_command/scripts/extract-codex-sessions.py +477 -0
  15. gator_command/scripts/extract-gemini-sessions.py +410 -0
  16. gator_command/scripts/gator-audit.py +956 -0
  17. gator_command/scripts/gator-charter-draft.py +919 -0
  18. gator_command/scripts/gator-charter-lint.py +427 -0
  19. gator_command/scripts/gator-charter-verify.py +606 -0
  20. gator_command/scripts/gator-dashboard.py +1271 -0
  21. gator_command/scripts/gator-deploy.py +916 -0
  22. gator_command/scripts/gator-drift.py +569 -0
  23. gator_command/scripts/gator-enforce.py +82 -0
  24. gator_command/scripts/gator-fleet-intel.py +460 -0
  25. gator_command/scripts/gator-fleet-report.py +615 -0
  26. gator_command/scripts/gator-init-command-post.py +315 -0
  27. gator_command/scripts/gator-init.py +434 -0
  28. gator_command/scripts/gator-machine-id.py +153 -0
  29. gator_command/scripts/gator-policy-status.py +631 -0
  30. gator_command/scripts/gator-pulse.py +459 -0
  31. gator_command/scripts/gator-repo-status.py +649 -0
  32. gator_command/scripts/gator-session-common.py +372 -0
  33. gator_command/scripts/gator-session-sink.py +831 -0
  34. gator_command/scripts/gator-sessions.py +1244 -0
  35. gator_command/scripts/gator-update.py +615 -0
  36. gator_command/scripts/gator-version.py +38 -0
  37. gator_command/scripts/gator_core.py +489 -0
  38. gator_command/scripts/gator_remote.py +381 -0
  39. gator_command/scripts/gator_runtime.py +142 -0
  40. gator_command/scripts/gatorize-actions.sh +989 -0
  41. gator_command/scripts/gatorize-lib.sh +166 -0
  42. gator_command/scripts/gatorize-post.sh +394 -0
  43. gator_command/scripts/gatorize.py +1163 -0
  44. gator_command/scripts/gatorize.sh +185 -0
  45. gator_command/scripts/generate_markdown.py +212 -0
  46. gator_command/scripts/generate_wiki.py +424 -0
  47. gator_command/scripts/graph_health.py +780 -0
  48. gator_command/scripts/memex-lint.py +286 -0
  49. gator_command/scripts/memex-lint.sh +205 -0
  50. gator_command/scripts/memex.py +1472 -0
  51. gator_command/scripts/memex_formatters.py +191 -0
  52. gator_command/scripts/memex_state.py +236 -0
  53. gator_command/scripts/spawn.py +650 -0
  54. gator_command/templates/gator-starter/blueprints/README.md +32 -0
  55. gator_command/templates/gator-starter/charterignore +53 -0
  56. gator_command/templates/gator-starter/charters/README.md +178 -0
  57. gator_command/templates/gator-starter/charters/_template.md +31 -0
  58. gator_command/templates/gator-starter/commands/commit.md +33 -0
  59. gator_command/templates/gator-starter/commands/init.md +11 -0
  60. gator_command/templates/gator-starter/commands/update.md +5 -0
  61. gator_command/templates/gator-starter/constitution.md +165 -0
  62. gator_command/templates/gator-starter/field-guides/README.md +25 -0
  63. gator_command/templates/gator-starter/gator-start-up.md +119 -0
  64. gator_command/templates/gator-starter/procedures/charter-alignment.md +83 -0
  65. gator_command/templates/gator-starter/procedures/enforcer-review.md +317 -0
  66. gator_command/templates/gator-starter/procedures/field-guide-generation.md +176 -0
  67. gator_command/templates/gator-starter/procedures/knowledge-capture.md +57 -0
  68. gator_command/templates/gator-starter/procedures/significance-check.md +69 -0
  69. gator_command/templates/gator-starter/reference-notes/concierge-responses.md +535 -0
  70. gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +91 -0
  71. gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +22 -0
  72. gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +232 -0
  73. gator_command/templates/gator-starter/reference-notes/example-project.md +289 -0
  74. gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +72 -0
  75. gator_command/templates/gator-starter/reference-notes/git-workflow.md +60 -0
  76. gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +37 -0
  77. gator_command/templates/gator-starter/reference-notes/refactor-approach.md +155 -0
  78. gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +108 -0
  79. gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +99 -0
  80. gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +155 -0
  81. gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
  82. gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
  83. gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
  84. gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
  85. gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
  86. gator_command/templates/gator-starter/scripts/enforcer-prompt.md +55 -0
  87. gator_command/templates/gator-starter/scripts/enforcer-review.py +1551 -0
  88. gator_command/templates/gator-starter/scripts/gator-approve.py +139 -0
  89. gator_command/templates/gator-starter/scripts/gator-enforce.py +82 -0
  90. gator_command/templates/gator-starter/scripts/gator-init.py +434 -0
  91. gator_command/templates/gator-starter/scripts/gator-pre-commit.py +2670 -0
  92. gator_command/templates/gator-starter/scripts/gator-pulse.py +459 -0
  93. gator_command/templates/gator-starter/scripts/gator-update.py +615 -0
  94. gator_command/templates/gator-starter/scripts/gator-version.py +38 -0
  95. gator_command/templates/gator-starter/scripts/gator_core.py +487 -0
  96. gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
  97. gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
  98. gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
  99. gator_command/templates/gator-starter/scripts/hooks/commit-msg +5 -0
  100. gator_command/templates/gator-starter/scripts/hooks/post-commit +7 -0
  101. gator_command/templates/gator-starter/scripts/hooks/pre-commit +5 -0
  102. gator_command/templates/gator-starter/sessions/.gitignore +7 -0
  103. gator_command/templates/gator-starter/vault/.gitkeep +0 -0
  104. gator_command/templates/gator-starter/whiteboard.md +5 -0
  105. gator_command-1.0.0.dist-info/METADATA +122 -0
  106. gator_command-1.0.0.dist-info/RECORD +110 -0
  107. gator_command-1.0.0.dist-info/WHEEL +5 -0
  108. gator_command-1.0.0.dist-info/entry_points.txt +2 -0
  109. gator_command-1.0.0.dist-info/licenses/LICENSE +21 -0
  110. 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()