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,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()
|