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,631 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ gator policy-status — Policy sync status for governed repos.
4
+
5
+ Reports whether a repo's cached org policy matches its governance source.
6
+ Works with both local command posts and remote Git URLs.
7
+
8
+ Phase 1: no custom refs. Source truth is the last commit hash of
9
+ org-policy.md in the command post (local or remote).
10
+
11
+ Usage:
12
+ python gator-command/scripts/gator-policy-status.py --path <repo>
13
+ python gator-command/scripts/gator-policy-status.py --path <repo> --json
14
+ python gator-command/scripts/gator-policy-status.py --path <repo> --sync
15
+ python gator-command/scripts/gator-policy-status.py --path <repo> --init
16
+ python gator-command/scripts/gator-policy-status.py --path <repo> --reinit
17
+
18
+ Modes:
19
+ (default) Report policy sync state
20
+ --sync Materialize/refresh the policy cache from source
21
+ --init Generate governance-source.json from existing thin link
22
+ --reinit Rebuild governance-source.json from thin link (repairs implicit-remote bug)
23
+ --json JSON output (combinable with other modes)
24
+
25
+ @reads: .gator/governance-source.json, .gator/policy-link.json,
26
+ .gator/policy-cache/org-policy.md, .gator/command-post.md (fallback),
27
+ command post org-policy.md (local or remote)
28
+ @writes: .gator/governance-source.json (--init, --reinit), .gator/policy-link.json (--sync),
29
+ .gator/policy-cache/org-policy.md (--sync)
30
+ """
31
+
32
+ import argparse
33
+ import hashlib
34
+ import json
35
+ import subprocess
36
+ import sys
37
+ from datetime import datetime, timezone
38
+ from pathlib import Path
39
+
40
+ from gator_core import (
41
+ get_version, find_gator_root, ensure_utf8_stdout,
42
+ git, normalize_path, resolve_thin_link,
43
+ )
44
+
45
+ VERSION = get_version()
46
+
47
+ # The policy file path relative to the command post root
48
+ POLICY_REL_PATH = "gator-command/org-policy.md"
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Governance source resolution
53
+ # ---------------------------------------------------------------------------
54
+
55
+ def load_governance_source(gator_dir):
56
+ """Load governance-source.json if it exists.
57
+
58
+ Normalizes _local_path_hint into local_path for internal use.
59
+ Returns dict or None.
60
+ @reads: .gator/governance-source.json
61
+ """
62
+ src_file = gator_dir / "governance-source.json"
63
+ if not src_file.exists():
64
+ return None
65
+ try:
66
+ data = json.loads(src_file.read_text(encoding="utf-8"))
67
+ # Normalize _local_path_hint → local_path for internal use
68
+ if "_local_path_hint" in data and "local_path" not in data:
69
+ hint = data["_local_path_hint"]
70
+ if Path(normalize_path(hint)).is_dir():
71
+ data["local_path"] = str(Path(normalize_path(hint)))
72
+ return data
73
+ except (OSError, json.JSONDecodeError):
74
+ return None
75
+
76
+
77
+ def derive_governance_source(gator_dir):
78
+ """Derive governance source from the existing thin link.
79
+
80
+ Fallback for repos that don't have governance-source.json yet.
81
+ Reads command-post.md to find the local path and any remote URL.
82
+
83
+ @reads: .gator/command-post.md
84
+ """
85
+ cp_file = gator_dir / "command-post.md"
86
+ if not cp_file.exists():
87
+ return None
88
+
89
+ try:
90
+ text = cp_file.read_text(encoding="utf-8", errors="replace")
91
+ except OSError:
92
+ return None
93
+
94
+ local_path = None
95
+ remote_url = None
96
+
97
+ for line in text.splitlines():
98
+ if line.startswith("command-post-absolute:"):
99
+ raw = line.split(":", 1)[1].strip().split("#")[0].strip()
100
+ if raw:
101
+ resolved = Path(normalize_path(raw))
102
+ if resolved.is_dir():
103
+ local_path = str(resolved)
104
+
105
+ elif line.startswith("command-post:") and not line.startswith("command-post-absolute:"):
106
+ raw = line.split(":", 1)[1].strip()
107
+ if raw and not local_path:
108
+ resolved = (gator_dir.parent / raw).resolve()
109
+ if resolved.is_dir():
110
+ local_path = str(resolved)
111
+
112
+ elif line.startswith("remote:"):
113
+ raw = line.split(":", 1)[1].strip().split("#")[0].strip()
114
+ if raw:
115
+ remote_url = raw
116
+
117
+ if not local_path and not remote_url:
118
+ return None
119
+
120
+ source = {"policy_file": POLICY_REL_PATH}
121
+ if local_path:
122
+ source["local_path"] = local_path
123
+ if remote_url:
124
+ source["remote_url"] = remote_url
125
+
126
+ return source
127
+
128
+
129
+ def get_governance_source(gator_dir):
130
+ """Get governance source, trying governance-source.json first, then thin link.
131
+
132
+ Returns (source_dict, is_derived) tuple.
133
+ """
134
+ source = load_governance_source(gator_dir)
135
+ if source:
136
+ return source, False
137
+
138
+ derived = derive_governance_source(gator_dir)
139
+ if derived:
140
+ return derived, True
141
+
142
+ return None, False
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Policy source queries
147
+ # ---------------------------------------------------------------------------
148
+
149
+ def query_local_policy(source):
150
+ """Get the current policy commit and content from a local command post.
151
+
152
+ Returns (commit_hash, content, error) tuple.
153
+ @reads: local command post git log + file content
154
+ """
155
+ local_path = source.get("local_path")
156
+ if not local_path or not Path(local_path).is_dir():
157
+ return None, None, "local path not accessible"
158
+
159
+ policy_file = source.get("policy_file", POLICY_REL_PATH)
160
+
161
+ # Get the last commit hash for org-policy.md specifically
162
+ commit_hash, ok = git(
163
+ "log", "-1", "--format=%H", "--", policy_file,
164
+ cwd=local_path,
165
+ )
166
+ if not ok or not commit_hash:
167
+ return None, None, "git log failed for policy file"
168
+
169
+ # Read the actual file content
170
+ full_path = Path(local_path) / policy_file
171
+ if not full_path.exists():
172
+ return commit_hash, None, "policy file not found"
173
+
174
+ try:
175
+ content = full_path.read_text(encoding="utf-8", errors="replace")
176
+ except OSError as e:
177
+ return commit_hash, None, str(e)
178
+
179
+ return commit_hash, content, None
180
+
181
+
182
+ def fetch_remote_policy(source, repo_root):
183
+ """Fetch the policy file from a remote command post.
184
+
185
+ Uses `git fetch <url> HEAD --depth=1` into the fleet repo, then
186
+ `git show FETCH_HEAD:<path>` to read the content. No named remote
187
+ is created — FETCH_HEAD is temporary.
188
+
189
+ Returns (content, error) tuple.
190
+ @reads: remote Git URL via fetch + show
191
+ """
192
+ remote_url = source.get("remote_url")
193
+ if not remote_url:
194
+ return None, "no remote URL configured"
195
+
196
+ policy_file = source.get("policy_file", POLICY_REL_PATH)
197
+
198
+ try:
199
+ # Shallow fetch HEAD into fleet repo's object store
200
+ result = subprocess.run(
201
+ ["git", "fetch", "--depth=1", remote_url, "HEAD"],
202
+ capture_output=True, text=True, timeout=30,
203
+ cwd=str(repo_root),
204
+ )
205
+ if result.returncode != 0:
206
+ return None, f"fetch failed: {result.stderr.strip()}"
207
+
208
+ # Read the policy file from FETCH_HEAD
209
+ result = subprocess.run(
210
+ ["git", "show", f"FETCH_HEAD:{policy_file}"],
211
+ capture_output=True, text=True, timeout=10,
212
+ cwd=str(repo_root),
213
+ )
214
+ if result.returncode != 0:
215
+ return None, f"policy file not found in remote: {policy_file}"
216
+
217
+ return result.stdout, None
218
+
219
+ except subprocess.TimeoutExpired:
220
+ return None, "remote fetch timed out"
221
+ except Exception as e:
222
+ return None, str(e)
223
+
224
+
225
+ # ---------------------------------------------------------------------------
226
+ # Policy link (provenance manifest)
227
+ # ---------------------------------------------------------------------------
228
+
229
+ def load_policy_link(gator_dir):
230
+ """Load the existing policy-link.json manifest.
231
+
232
+ @reads: .gator/policy-link.json
233
+ """
234
+ link_file = gator_dir / "policy-link.json"
235
+ if not link_file.exists():
236
+ return None
237
+ try:
238
+ return json.loads(link_file.read_text(encoding="utf-8"))
239
+ except (OSError, json.JSONDecodeError):
240
+ return None
241
+
242
+
243
+ def write_policy_link(gator_dir, link_data):
244
+ """Write the policy-link.json manifest.
245
+
246
+ @writes: .gator/policy-link.json
247
+ """
248
+ link_file = gator_dir / "policy-link.json"
249
+ link_file.write_text(
250
+ json.dumps(link_data, indent=2) + "\n",
251
+ encoding="utf-8",
252
+ )
253
+
254
+
255
+ def sha256_of(content):
256
+ """Compute SHA-256 hash of string content."""
257
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # Sync state computation
262
+ # ---------------------------------------------------------------------------
263
+
264
+ def compute_sync_state(gator_dir, source):
265
+ """Determine the policy sync state for a repo.
266
+
267
+ Returns a dict with state, details, and actionable info.
268
+ """
269
+ link = load_policy_link(gator_dir)
270
+ cache_path = gator_dir / "policy-cache" / "org-policy.md"
271
+ has_cache = cache_path.exists()
272
+
273
+ result = {
274
+ "state": "unknown",
275
+ "source_type": None,
276
+ "source_commit": None,
277
+ "cached_commit": None,
278
+ "cached_at": None,
279
+ "has_cache": has_cache,
280
+ "has_local_path": bool(source.get("local_path")),
281
+ "has_remote_url": bool(source.get("remote_url")),
282
+ "local_override": False,
283
+ "error": None,
284
+ }
285
+
286
+ if link:
287
+ result["cached_commit"] = link.get("source_commit")
288
+ result["cached_at"] = link.get("cached_at")
289
+ result["local_override"] = link.get("local_override", False)
290
+
291
+ # Check for local override (cache was edited directly)
292
+ if has_cache and link:
293
+ try:
294
+ cache_content = cache_path.read_text(encoding="utf-8", errors="replace")
295
+ cache_hash = sha256_of(cache_content)
296
+ if link.get("cache_sha256") and cache_hash != link["cache_sha256"]:
297
+ result["state"] = "diverged"
298
+ result["local_override"] = True
299
+ return result
300
+ except OSError:
301
+ pass
302
+
303
+ # Try local source first (fast, no network, per-file commit precision)
304
+ if source.get("local_path"):
305
+ commit, content, err = query_local_policy(source)
306
+ if not err:
307
+ result["source_type"] = "local"
308
+ result["source_commit"] = commit
309
+ if not link or not link.get("source_commit"):
310
+ result["state"] = "no-cache" if not has_cache else "unknown"
311
+ elif commit == link.get("source_commit"):
312
+ result["state"] = "synced"
313
+ else:
314
+ result["state"] = "behind"
315
+ return result
316
+ # Local path failed — don't pollute result with stale error if remote works
317
+
318
+ # Remote-only: status check cannot determine per-file freshness without
319
+ # custom refs (Phase 2). Report cached state honestly.
320
+ if source.get("remote_url"):
321
+ if has_cache and link:
322
+ result["source_type"] = "remote"
323
+ result["state"] = "cached" # Have cache, can't verify freshness without --sync
324
+ return result
325
+ elif not has_cache:
326
+ result["source_type"] = "remote"
327
+ result["state"] = "no-cache"
328
+ return result
329
+
330
+ # No source reachable
331
+ if has_cache:
332
+ result["state"] = "local-only"
333
+ else:
334
+ result["state"] = "no-source"
335
+ result["error"] = "no local path or remote URL configured"
336
+
337
+ return result
338
+
339
+
340
+ # ---------------------------------------------------------------------------
341
+ # Sync action
342
+ # ---------------------------------------------------------------------------
343
+
344
+ def sync_policy(gator_dir, source, repo_root):
345
+ """Materialize the policy cache from source.
346
+
347
+ Tries local source first (fast, per-file commit precision).
348
+ Falls back to remote source (fetches file via git fetch + show).
349
+
350
+ @reads: command post org-policy.md (local or remote)
351
+ @writes: .gator/policy-cache/org-policy.md, .gator/policy-link.json
352
+ """
353
+ content = None
354
+ commit = None
355
+ source_type = None
356
+
357
+ # Try local source first
358
+ if source.get("local_path"):
359
+ commit, content, err = query_local_policy(source)
360
+ if not err and content:
361
+ source_type = "local"
362
+
363
+ # Fall back to remote source
364
+ if content is None and source.get("remote_url"):
365
+ content, err = fetch_remote_policy(source, repo_root)
366
+ if err:
367
+ return {"status": "error", "error": err}
368
+ if content:
369
+ source_type = "remote"
370
+ # No per-file commit hash from remote — use content hash as identifier
371
+ commit = "remote:" + sha256_of(content)[:16]
372
+
373
+ if content is None:
374
+ return {"status": "error", "error": "no source reachable"}
375
+
376
+ # Write cache
377
+ cache_dir = gator_dir / "policy-cache"
378
+ cache_dir.mkdir(parents=True, exist_ok=True)
379
+ cache_path = cache_dir / "org-policy.md"
380
+ cache_path.write_text(content, encoding="utf-8")
381
+
382
+ # Write provenance manifest
383
+ now = datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
384
+ link_data = {
385
+ "source_commit": commit,
386
+ "source_type": source_type,
387
+ "policy_file": source.get("policy_file", POLICY_REL_PATH),
388
+ "cached_at": now,
389
+ "cache_path": ".gator/policy-cache/org-policy.md",
390
+ "cache_sha256": sha256_of(content),
391
+ "local_override": False,
392
+ }
393
+
394
+ if source.get("local_path"):
395
+ link_data["source_path"] = source["local_path"]
396
+ if source.get("remote_url"):
397
+ link_data["remote_url"] = source["remote_url"]
398
+
399
+ write_policy_link(gator_dir, link_data)
400
+
401
+ return {"status": "ok", "commit": commit, "source_type": source_type, "cached_at": now}
402
+
403
+
404
+ # ---------------------------------------------------------------------------
405
+ # Init action
406
+ # ---------------------------------------------------------------------------
407
+
408
+ def init_governance_source(gator_dir, source, force=False):
409
+ """Write governance-source.json from derived source info.
410
+
411
+ Stores remote_url as the portable source (committable, works across
412
+ machines) only if one was explicitly configured (e.g. in the registry
413
+ or command-post.md). Stores local_path as a machine-local hint.
414
+
415
+ Does NOT infer a remote URL from the command post's git origin —
416
+ that would silently promote an unrelated upstream (e.g. a public
417
+ GitHub repo) into fleet governance state.
418
+
419
+ If force=True, overwrites an existing file (repair path for repos
420
+ initialized before the implicit-remote bug was fixed).
421
+
422
+ @writes: .gator/governance-source.json
423
+ """
424
+ src_file = gator_dir / "governance-source.json"
425
+ if src_file.exists() and not force:
426
+ return {"status": "exists", "message": "governance-source.json already exists"}
427
+
428
+ # Structure: remote_url is the portable primary, local_path is a hint
429
+ output = {"policy_file": source.get("policy_file", POLICY_REL_PATH)}
430
+ if source.get("remote_url"):
431
+ output["remote_url"] = source["remote_url"]
432
+ if source.get("local_path"):
433
+ output["_local_path_hint"] = source["local_path"]
434
+ output["_local_path_note"] = "Machine-local — may not resolve on other machines"
435
+
436
+ src_file.write_text(
437
+ json.dumps(output, indent=2) + "\n",
438
+ encoding="utf-8",
439
+ )
440
+ return {"status": "ok" if not force else "repaired", "source": output}
441
+
442
+
443
+ # ---------------------------------------------------------------------------
444
+ # Output
445
+ # ---------------------------------------------------------------------------
446
+
447
+ _STATE_LABELS = {
448
+ "synced": ("Synced", "Policy cache matches source"),
449
+ "behind": ("Behind", "Source has newer policy"),
450
+ "cached": ("Cached", "Remote-only — run --sync to verify freshness"),
451
+ "diverged": ("Diverged", "Local cache was edited directly"),
452
+ "no-cache": ("No cache", "Policy not yet materialized — run --sync"),
453
+ "local-only": ("Local only", "No remote configured; cache present"),
454
+ "offline": ("Offline", "Remote unreachable; using cached policy"),
455
+ "unreachable": ("Unreachable", "No source reachable and no cache"),
456
+ "no-source": ("No source", "No governance source configured"),
457
+ "unknown": ("Unknown", "Could not determine sync state"),
458
+ }
459
+
460
+
461
+ def print_status(status, source, is_derived):
462
+ """Print human-readable policy status."""
463
+ state = status["state"]
464
+ label, description = _STATE_LABELS.get(state, (state, ""))
465
+
466
+ print()
467
+ print(f" gator policy-status")
468
+ print()
469
+ print(f" State: {label}")
470
+ print(f" {description}")
471
+ print()
472
+
473
+ if source:
474
+ if source.get("local_path"):
475
+ print(f" Source: {source['local_path']} (local)")
476
+ if source.get("remote_url"):
477
+ print(f" Remote: {source['remote_url']}")
478
+ if is_derived:
479
+ print(f" Note: derived from thin link (run --init to persist)")
480
+
481
+ print()
482
+
483
+ if status.get("source_commit"):
484
+ print(f" Source commit: {status['source_commit'][:12]}")
485
+ if status.get("cached_commit"):
486
+ print(f" Cached commit: {status['cached_commit'][:12]}")
487
+ if status.get("cached_at"):
488
+ print(f" Cached at: {status['cached_at']}")
489
+ if status.get("error"):
490
+ print(f" Error: {status['error']}")
491
+
492
+ print()
493
+
494
+
495
+ def print_json_status(status, source, is_derived):
496
+ """Print JSON policy status."""
497
+ output = {
498
+ "version": VERSION,
499
+ "governance_source": source,
500
+ "governance_source_derived": is_derived,
501
+ "policy_status": status,
502
+ }
503
+ print(json.dumps(output, indent=2))
504
+
505
+
506
+ # ---------------------------------------------------------------------------
507
+ # Main
508
+ # ---------------------------------------------------------------------------
509
+
510
+ def main():
511
+ ensure_utf8_stdout()
512
+
513
+ parser = argparse.ArgumentParser(
514
+ description="Gator policy-status — policy sync status for governed repos.",
515
+ formatter_class=argparse.RawDescriptionHelpFormatter,
516
+ )
517
+ parser.add_argument(
518
+ "--path", "-p",
519
+ help="Path to repo (default: current directory, walks up to find .gator/)",
520
+ )
521
+ parser.add_argument(
522
+ "--sync", "-s", action="store_true",
523
+ help="Materialize/refresh the policy cache from source",
524
+ )
525
+ parser.add_argument(
526
+ "--init", action="store_true",
527
+ help="Generate governance-source.json from existing thin link",
528
+ )
529
+ parser.add_argument(
530
+ "--reinit", action="store_true",
531
+ help="Rebuild governance-source.json from thin link (repairs implicit-remote bug)",
532
+ )
533
+ parser.add_argument(
534
+ "--json", "-j", action="store_true",
535
+ help="JSON output",
536
+ )
537
+ args = parser.parse_args()
538
+
539
+ # Find repo root
540
+ repo_root = find_gator_root(args.path)
541
+ if not repo_root:
542
+ print(" Error: no .gator/ found. Run from a gatorized repo.", file=sys.stderr)
543
+ sys.exit(1)
544
+
545
+ gator_dir = repo_root / ".gator"
546
+
547
+ # --reinit: rebuild governance-source.json from thin link (repair path)
548
+ # Must re-derive from command-post.md, not from the existing (possibly
549
+ # corrupted) governance-source.json.
550
+ if args.reinit:
551
+ derived = derive_governance_source(gator_dir)
552
+ if not derived:
553
+ msg = "no thin link (command-post.md) found — cannot reinit"
554
+ if args.json:
555
+ print(json.dumps({"error": msg}))
556
+ else:
557
+ print(f" Error: {msg}")
558
+ sys.exit(1)
559
+ result = init_governance_source(gator_dir, derived, force=True)
560
+ if args.json:
561
+ print(json.dumps(result, indent=2))
562
+ else:
563
+ written = result.get("source", {})
564
+ print(f" Rebuilt .gator/governance-source.json")
565
+ if written.get("remote_url"):
566
+ print(f" remote_url: {written['remote_url']}")
567
+ else:
568
+ print(f" remote_url: (none — local-path only)")
569
+ if written.get("_local_path_hint"):
570
+ print(f" local hint: {written['_local_path_hint']}")
571
+ return
572
+
573
+ # Get governance source
574
+ source, is_derived = get_governance_source(gator_dir)
575
+
576
+ if not source:
577
+ if args.json:
578
+ print(json.dumps({"version": VERSION, "error": "no governance source found"}))
579
+ else:
580
+ print()
581
+ print(" gator policy-status")
582
+ print()
583
+ print(" No governance source found.")
584
+ print(" This repo has no .gator/governance-source.json and no thin link.")
585
+ print()
586
+ sys.exit(1)
587
+
588
+ # --init: persist governance-source.json
589
+ if args.init:
590
+ result = init_governance_source(gator_dir, source)
591
+ if args.json:
592
+ print(json.dumps(result, indent=2))
593
+ else:
594
+ if result["status"] == "exists":
595
+ print(f" governance-source.json already exists.")
596
+ else:
597
+ written = result.get("source", {})
598
+ print(f" Created .gator/governance-source.json")
599
+ if written.get("remote_url"):
600
+ print(f" remote_url: {written['remote_url']}")
601
+ if written.get("_local_path_hint"):
602
+ print(f" local hint: {written['_local_path_hint']}")
603
+ return
604
+
605
+ # --sync: materialize policy cache
606
+ if args.sync:
607
+ result = sync_policy(gator_dir, source, repo_root)
608
+ if args.json:
609
+ print(json.dumps(result, indent=2))
610
+ else:
611
+ if result["status"] == "ok":
612
+ print()
613
+ print(f" Policy cache synced")
614
+ print(f" Commit: {result['commit'][:12]}")
615
+ print(f" Cached: {result['cached_at']}")
616
+ print()
617
+ else:
618
+ print(f" Error: {result['error']}")
619
+ return
620
+
621
+ # Default: report status
622
+ status = compute_sync_state(gator_dir, source)
623
+
624
+ if args.json:
625
+ print_json_status(status, source, is_derived)
626
+ else:
627
+ print_status(status, source, is_derived)
628
+
629
+
630
+ if __name__ == "__main__":
631
+ main()