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,434 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator init — Branded boot sequence for Gator-governed repos.
|
|
4
|
+
|
|
5
|
+
Detects .gator/ in the current directory (or a specified path), reads
|
|
6
|
+
the knowledge layer, and prints a formatted status display. Designed
|
|
7
|
+
to run at session open in any AI CLI (Claude Code, Codex, Gemini).
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python .gator/scripts/gator-init.py
|
|
11
|
+
python .gator/scripts/gator-init.py --path /some/repo
|
|
12
|
+
python .gator/scripts/gator-init.py --json
|
|
13
|
+
|
|
14
|
+
@reads: .gator/ directory structure, constitution.md, charters/, threads/, command-post.md
|
|
15
|
+
@writes: .git/hooks/ (self-heal only when hooks are missing or stale)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from gator_core import (
|
|
25
|
+
get_version, find_gator_root, ensure_utf8_stdout, GATOR_MARK_LINES,
|
|
26
|
+
import_sibling,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
VERSION = get_version()
|
|
30
|
+
|
|
31
|
+
TAGLINE = "oriented. the terrain is mapped."
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# --- Detection and counting ---
|
|
35
|
+
|
|
36
|
+
def count_constitution_rules(gator_dir):
|
|
37
|
+
"""Count enforceable rules in the constitution.
|
|
38
|
+
|
|
39
|
+
Counts numbered list items, table rows with content, and bold-prefixed
|
|
40
|
+
rules. This is a heuristic — not a parser.
|
|
41
|
+
|
|
42
|
+
@reads: .gator/constitution.md
|
|
43
|
+
"""
|
|
44
|
+
constitution = gator_dir / "constitution.md"
|
|
45
|
+
if not constitution.exists():
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
text = constitution.read_text(encoding="utf-8", errors="replace")
|
|
49
|
+
count = 0
|
|
50
|
+
|
|
51
|
+
for line in text.splitlines():
|
|
52
|
+
stripped = line.strip()
|
|
53
|
+
# Numbered steps (1. **Before changing code**: ...)
|
|
54
|
+
if re.match(r'^\d+\.?\s+\*\*', stripped):
|
|
55
|
+
count += 1
|
|
56
|
+
# Bold-prefixed rules (- **BRANCHING**: ...)
|
|
57
|
+
elif re.match(r'^-\s+\*\*[A-Z]', stripped):
|
|
58
|
+
count += 1
|
|
59
|
+
# Standalone bold imperatives (**The agent always asks before committing**)
|
|
60
|
+
elif re.match(r'^\*\*The (agent|Architect|PI|enforcer)', stripped):
|
|
61
|
+
count += 1
|
|
62
|
+
|
|
63
|
+
return count
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def count_charters(gator_dir):
|
|
67
|
+
"""Count charter files and estimate mapping coverage.
|
|
68
|
+
|
|
69
|
+
@reads: .gator/charters/
|
|
70
|
+
"""
|
|
71
|
+
charters_dir = gator_dir / "charters"
|
|
72
|
+
if not charters_dir.is_dir():
|
|
73
|
+
return 0, 0, 0.0
|
|
74
|
+
|
|
75
|
+
skip = {"_template.md", "README.md", "INDEX.md"}
|
|
76
|
+
charter_files = [
|
|
77
|
+
f for f in charters_dir.iterdir()
|
|
78
|
+
if f.suffix == ".md" and f.name not in skip
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# Count function entries across all charters (lines starting with ### )
|
|
82
|
+
function_count = 0
|
|
83
|
+
for cf in charter_files:
|
|
84
|
+
text = cf.read_text(encoding="utf-8", errors="replace")
|
|
85
|
+
for line in text.splitlines():
|
|
86
|
+
if line.strip().startswith("### ") and "(" in line:
|
|
87
|
+
function_count += 1
|
|
88
|
+
|
|
89
|
+
# Estimate total functions in the repo (count def/func/function declarations)
|
|
90
|
+
repo_root = gator_dir.parent
|
|
91
|
+
total_functions = 0
|
|
92
|
+
for ext in ("*.py", "*.gd", "*.js", "*.ts", "*.go", "*.rs", "*.java"):
|
|
93
|
+
for code_file in repo_root.rglob(ext):
|
|
94
|
+
# Skip .gator/ itself
|
|
95
|
+
if ".gator" in str(code_file):
|
|
96
|
+
continue
|
|
97
|
+
try:
|
|
98
|
+
text = code_file.read_text(encoding="utf-8", errors="replace")
|
|
99
|
+
for line in text.splitlines():
|
|
100
|
+
stripped = line.strip()
|
|
101
|
+
if (stripped.startswith("def ") or
|
|
102
|
+
stripped.startswith("func ") or
|
|
103
|
+
stripped.startswith("function ") or
|
|
104
|
+
re.match(r'^(pub\s+)?(fn|func)\s+', stripped) or
|
|
105
|
+
re.match(r'^(export\s+)?(async\s+)?function\s+', stripped)):
|
|
106
|
+
total_functions += 1
|
|
107
|
+
except (OSError, UnicodeDecodeError):
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
coverage = (function_count / total_functions * 100) if total_functions > 0 else 0.0
|
|
111
|
+
|
|
112
|
+
return len(charter_files), function_count, coverage
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def count_working_set(gator_dir):
|
|
116
|
+
"""Count threads and their total line count.
|
|
117
|
+
|
|
118
|
+
@reads: .gator/threads/, .gator/active-threads/ (if present)
|
|
119
|
+
"""
|
|
120
|
+
total_threads = 0
|
|
121
|
+
total_lines = 0
|
|
122
|
+
|
|
123
|
+
for subdir_name in ("active-threads", "threads"):
|
|
124
|
+
subdir = gator_dir / subdir_name
|
|
125
|
+
if not subdir.is_dir():
|
|
126
|
+
continue
|
|
127
|
+
for f in subdir.iterdir():
|
|
128
|
+
if f.suffix == ".md" and f.name != ".gitkeep":
|
|
129
|
+
total_threads += 1
|
|
130
|
+
try:
|
|
131
|
+
total_lines += len(f.read_text(encoding="utf-8", errors="replace").splitlines())
|
|
132
|
+
except (OSError, UnicodeDecodeError):
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
return total_threads, total_lines
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def detect_enforcer(gator_dir):
|
|
139
|
+
"""Detect enforcer configuration.
|
|
140
|
+
|
|
141
|
+
@reads: .gator/scripts/enforcer-prompt.md, .gator/scripts/enforcer-review.py
|
|
142
|
+
"""
|
|
143
|
+
has_prompt = (gator_dir / "scripts" / "enforcer-prompt.md").exists()
|
|
144
|
+
has_script = (gator_dir / "scripts" / "enforcer-review.py").exists()
|
|
145
|
+
|
|
146
|
+
if has_prompt and has_script:
|
|
147
|
+
return "ready"
|
|
148
|
+
elif has_prompt or has_script:
|
|
149
|
+
return "partial"
|
|
150
|
+
else:
|
|
151
|
+
return "not configured"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def count_field_guides(gator_dir):
|
|
155
|
+
"""Count field guide languages from pattern files.
|
|
156
|
+
|
|
157
|
+
Field guides use two files per language: {lang}-patterns.md and
|
|
158
|
+
{lang}-tutorial.md. Count languages from pattern files.
|
|
159
|
+
|
|
160
|
+
@reads: .gator/field-guides/
|
|
161
|
+
"""
|
|
162
|
+
guides_dir = gator_dir / "field-guides"
|
|
163
|
+
if not guides_dir.is_dir():
|
|
164
|
+
return 0, []
|
|
165
|
+
|
|
166
|
+
guides = [
|
|
167
|
+
f.name.removesuffix("-patterns.md")
|
|
168
|
+
for f in guides_dir.iterdir()
|
|
169
|
+
if f.name.endswith("-patterns.md")
|
|
170
|
+
]
|
|
171
|
+
return len(guides), sorted(guides)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def detect_command_post(gator_dir):
|
|
175
|
+
"""Check for command-post thin link.
|
|
176
|
+
|
|
177
|
+
@reads: .gator/command-post.md
|
|
178
|
+
"""
|
|
179
|
+
cp_file = gator_dir / "command-post.md"
|
|
180
|
+
if not cp_file.exists():
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
text = cp_file.read_text(encoding="utf-8", errors="replace")
|
|
184
|
+
for line in text.splitlines():
|
|
185
|
+
if line.startswith("command-post:"):
|
|
186
|
+
return line.split(":", 1)[1].strip()
|
|
187
|
+
return "linked"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def read_version(gator_dir):
|
|
191
|
+
"""Read generation from .gator-version.
|
|
192
|
+
|
|
193
|
+
@reads: .gator/.gator-version
|
|
194
|
+
"""
|
|
195
|
+
version_file = gator_dir / ".gator-version"
|
|
196
|
+
if not version_file.exists():
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
text = version_file.read_text(encoding="utf-8", errors="replace")
|
|
200
|
+
info = {}
|
|
201
|
+
for line in text.splitlines():
|
|
202
|
+
if ":" in line:
|
|
203
|
+
key, val = line.split(":", 1)
|
|
204
|
+
info[key.strip()] = val.strip()
|
|
205
|
+
return info
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def ensure_git_hooks(repo_root, gator_dir):
|
|
209
|
+
"""Self-heal git hooks at session start.
|
|
210
|
+
|
|
211
|
+
Uses gator-update's hook planning and installation logic so there is
|
|
212
|
+
exactly one definition of what "correctly installed hooks" means.
|
|
213
|
+
"""
|
|
214
|
+
try:
|
|
215
|
+
gator_update = import_sibling("gator-update")
|
|
216
|
+
except ImportError as e:
|
|
217
|
+
return {
|
|
218
|
+
"status": "error",
|
|
219
|
+
"detail": f"hook repair unavailable: {e}",
|
|
220
|
+
"adds": 0,
|
|
221
|
+
"updates": 0,
|
|
222
|
+
}
|
|
223
|
+
if gator_update is None:
|
|
224
|
+
return {
|
|
225
|
+
"status": "error",
|
|
226
|
+
"detail": "gator-update.py not found",
|
|
227
|
+
"adds": 0,
|
|
228
|
+
"updates": 0,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if not (repo_root / ".git").exists():
|
|
232
|
+
return {
|
|
233
|
+
"status": "unavailable",
|
|
234
|
+
"detail": "no .git directory",
|
|
235
|
+
"adds": 0,
|
|
236
|
+
"updates": 0,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Check that the hook target script exists — without it, hooks can't work
|
|
240
|
+
# even if they're installed, and plan_hook_updates returns [] (masking the problem).
|
|
241
|
+
gator_script = gator_dir / "scripts" / "gator-pre-commit.py"
|
|
242
|
+
if not gator_script.exists():
|
|
243
|
+
return {
|
|
244
|
+
"status": "degraded",
|
|
245
|
+
"detail": "gator-pre-commit.py missing",
|
|
246
|
+
"adds": 0,
|
|
247
|
+
"updates": 0,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
plan = gator_update.plan_hook_updates(gator_dir, repo_root)
|
|
251
|
+
adds = sum(1 for _, action in plan if action == "add")
|
|
252
|
+
updates = sum(1 for _, action in plan if action == "update")
|
|
253
|
+
|
|
254
|
+
if adds or updates:
|
|
255
|
+
installed = gator_update.install_git_hooks(gator_dir, repo_root)
|
|
256
|
+
if updates:
|
|
257
|
+
detail = f"refreshed ({installed} hooks)"
|
|
258
|
+
status = "refreshed"
|
|
259
|
+
else:
|
|
260
|
+
detail = f"installed ({installed} hooks)"
|
|
261
|
+
status = "installed"
|
|
262
|
+
else:
|
|
263
|
+
detail = "ok"
|
|
264
|
+
status = "ok"
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
"status": status,
|
|
268
|
+
"detail": detail,
|
|
269
|
+
"adds": adds,
|
|
270
|
+
"updates": updates,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# --- Output formatting ---
|
|
275
|
+
|
|
276
|
+
def format_check(label, value, width=16):
|
|
277
|
+
"""Format a single status line."""
|
|
278
|
+
return f" \u2713 {label:<{width}}{value}"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def print_boot_sequence(repo_root, gator_dir, hook_status):
|
|
282
|
+
"""Print the branded boot sequence.
|
|
283
|
+
|
|
284
|
+
@reads: all detection functions above
|
|
285
|
+
@writes: stdout
|
|
286
|
+
"""
|
|
287
|
+
repo_name = repo_root.name
|
|
288
|
+
|
|
289
|
+
# Header
|
|
290
|
+
print()
|
|
291
|
+
for line in GATOR_MARK_LINES:
|
|
292
|
+
print(f" {line}")
|
|
293
|
+
print()
|
|
294
|
+
print(f" {VERSION} · navigation coding, governed")
|
|
295
|
+
print()
|
|
296
|
+
print(f" $ gator init")
|
|
297
|
+
print()
|
|
298
|
+
|
|
299
|
+
# Detection
|
|
300
|
+
print(f" found .gator/")
|
|
301
|
+
print()
|
|
302
|
+
|
|
303
|
+
# Constitution
|
|
304
|
+
rule_count = count_constitution_rules(gator_dir)
|
|
305
|
+
print(format_check("constitution", f"{rule_count} rules in force"))
|
|
306
|
+
|
|
307
|
+
# Charters
|
|
308
|
+
charter_count, func_count, coverage = count_charters(gator_dir)
|
|
309
|
+
if charter_count > 0:
|
|
310
|
+
detail = f"{charter_count} modules \u00b7 {func_count} functions documented"
|
|
311
|
+
print(format_check("charters", detail))
|
|
312
|
+
else:
|
|
313
|
+
print(format_check("charters", "empty \u00b7 ready for bootstrap"))
|
|
314
|
+
|
|
315
|
+
# Working set
|
|
316
|
+
thread_count, line_count = count_working_set(gator_dir)
|
|
317
|
+
if thread_count > 0:
|
|
318
|
+
print(format_check("working set", f"{thread_count} threads \u00b7 {line_count:,} lines"))
|
|
319
|
+
else:
|
|
320
|
+
print(format_check("working set", "empty \u00b7 threads emerge from work"))
|
|
321
|
+
|
|
322
|
+
# Field guides (only show when present)
|
|
323
|
+
guide_count, guide_langs = count_field_guides(gator_dir)
|
|
324
|
+
if guide_count > 0:
|
|
325
|
+
lang_list = ", ".join(guide_langs)
|
|
326
|
+
print(format_check("field guides", f"{guide_count} languages ({lang_list})"))
|
|
327
|
+
|
|
328
|
+
# Enforcer
|
|
329
|
+
enforcer_status = detect_enforcer(gator_dir)
|
|
330
|
+
print(format_check("enforcer", enforcer_status))
|
|
331
|
+
|
|
332
|
+
# Git hooks
|
|
333
|
+
print(format_check("hooks", hook_status["detail"]))
|
|
334
|
+
|
|
335
|
+
# Command post
|
|
336
|
+
cp = detect_command_post(gator_dir)
|
|
337
|
+
if cp:
|
|
338
|
+
print(format_check("command post", "linked"))
|
|
339
|
+
|
|
340
|
+
# Tagline
|
|
341
|
+
print()
|
|
342
|
+
print(f" {TAGLINE}")
|
|
343
|
+
print(f" \u25b8")
|
|
344
|
+
print()
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def print_json(repo_root, gator_dir, hook_status):
|
|
348
|
+
"""Print status as JSON for programmatic consumption."""
|
|
349
|
+
rule_count = count_constitution_rules(gator_dir)
|
|
350
|
+
charter_count, func_count, coverage = count_charters(gator_dir)
|
|
351
|
+
thread_count, line_count = count_working_set(gator_dir)
|
|
352
|
+
guide_count, guide_langs = count_field_guides(gator_dir)
|
|
353
|
+
enforcer_status = detect_enforcer(gator_dir)
|
|
354
|
+
cp = detect_command_post(gator_dir)
|
|
355
|
+
version_info = read_version(gator_dir)
|
|
356
|
+
|
|
357
|
+
data = {
|
|
358
|
+
"version": VERSION,
|
|
359
|
+
"repo": repo_root.name,
|
|
360
|
+
"repo_path": str(repo_root),
|
|
361
|
+
"constitution_rules": rule_count,
|
|
362
|
+
"charters": {
|
|
363
|
+
"modules": charter_count,
|
|
364
|
+
"functions_mapped": func_count,
|
|
365
|
+
"coverage_pct": round(coverage, 1),
|
|
366
|
+
},
|
|
367
|
+
"working_set": {
|
|
368
|
+
"threads": thread_count,
|
|
369
|
+
"lines": line_count,
|
|
370
|
+
},
|
|
371
|
+
"field_guides": {
|
|
372
|
+
"count": guide_count,
|
|
373
|
+
"languages": guide_langs,
|
|
374
|
+
},
|
|
375
|
+
"enforcer": enforcer_status,
|
|
376
|
+
"hooks": hook_status,
|
|
377
|
+
"command_post": cp,
|
|
378
|
+
"gator_version": version_info,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
print(json.dumps(data, indent=2))
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def print_not_found():
|
|
385
|
+
"""Print message when .gator/ is not found."""
|
|
386
|
+
print()
|
|
387
|
+
for line in GATOR_MARK_LINES:
|
|
388
|
+
print(f" {line}")
|
|
389
|
+
print()
|
|
390
|
+
print(f" {VERSION} · navigation coding, governed")
|
|
391
|
+
print()
|
|
392
|
+
print(" no .gator/ found.")
|
|
393
|
+
print()
|
|
394
|
+
print(" to gatorize this repo:")
|
|
395
|
+
print(" bash gator-command/scripts/gatorize.sh .")
|
|
396
|
+
print()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# --- Entry point ---
|
|
400
|
+
|
|
401
|
+
def main():
|
|
402
|
+
ensure_utf8_stdout()
|
|
403
|
+
|
|
404
|
+
parser = argparse.ArgumentParser(
|
|
405
|
+
description="Gator init — branded boot sequence for governed repos."
|
|
406
|
+
)
|
|
407
|
+
parser.add_argument(
|
|
408
|
+
"--path", "-p",
|
|
409
|
+
help="Path to search for .gator/ (default: current directory)",
|
|
410
|
+
)
|
|
411
|
+
parser.add_argument(
|
|
412
|
+
"--json", "-j",
|
|
413
|
+
action="store_true",
|
|
414
|
+
help="Output as JSON instead of formatted display",
|
|
415
|
+
)
|
|
416
|
+
args = parser.parse_args()
|
|
417
|
+
|
|
418
|
+
repo_root = find_gator_root(args.path)
|
|
419
|
+
|
|
420
|
+
if not repo_root:
|
|
421
|
+
print_not_found()
|
|
422
|
+
sys.exit(1)
|
|
423
|
+
|
|
424
|
+
gator_dir = repo_root / ".gator"
|
|
425
|
+
hook_status = ensure_git_hooks(repo_root, gator_dir)
|
|
426
|
+
|
|
427
|
+
if args.json:
|
|
428
|
+
print_json(repo_root, gator_dir, hook_status)
|
|
429
|
+
else:
|
|
430
|
+
print_boot_sequence(repo_root, gator_dir, hook_status)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
if __name__ == "__main__":
|
|
434
|
+
main()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator-machine-id.py — Stable machine identity for session audit trails.
|
|
4
|
+
|
|
5
|
+
Generates and stores a persistent machine identifier at ~/.gator/machine-id.
|
|
6
|
+
The ID is a UUID generated once, persists across sessions and repos,
|
|
7
|
+
and is used in session summaries as the "where" pointer for audit trails.
|
|
8
|
+
|
|
9
|
+
Why not hostname?
|
|
10
|
+
- Hostnames change (reimages, domain joins, laptop swaps)
|
|
11
|
+
- Hostnames can be meaningless in enterprise (DESKTOP-NKSU8RO)
|
|
12
|
+
- Hostnames can collide across machines
|
|
13
|
+
|
|
14
|
+
The machine-id file also stores a human-readable label that the PI
|
|
15
|
+
can set to make the audit trail legible:
|
|
16
|
+
|
|
17
|
+
~/.gator/machine-id:
|
|
18
|
+
id: a1b2c3d4-5678-90ab-cdef-1234567890ab
|
|
19
|
+
hostname: DESKTOP-NKSU8RO
|
|
20
|
+
label: alan-home-desktop
|
|
21
|
+
created: 2026-05-30
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
python gator-command/scripts/gator-machine-id.py # show or create
|
|
25
|
+
python gator-command/scripts/gator-machine-id.py --label "alan-home-desktop"
|
|
26
|
+
python gator-command/scripts/gator-machine-id.py --json
|
|
27
|
+
|
|
28
|
+
As a module:
|
|
29
|
+
from gator_machine_id import get_machine_id
|
|
30
|
+
mid = get_machine_id() # {"id": "...", "hostname": "...", "label": "..."}
|
|
31
|
+
|
|
32
|
+
@reads: ~/.gator/machine-id
|
|
33
|
+
@writes: ~/.gator/machine-id (on first run only, or --label update)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import json
|
|
38
|
+
import platform
|
|
39
|
+
import sys
|
|
40
|
+
import uuid
|
|
41
|
+
from datetime import date
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
GATOR_USER_DIR = Path.home() / ".gator"
|
|
46
|
+
MACHINE_ID_FILE = GATOR_USER_DIR / "machine-id"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_machine_id():
|
|
50
|
+
"""Get or create the stable machine identity.
|
|
51
|
+
|
|
52
|
+
Returns dict with id, hostname, label, created.
|
|
53
|
+
Creates ~/.gator/machine-id on first call.
|
|
54
|
+
"""
|
|
55
|
+
if MACHINE_ID_FILE.exists():
|
|
56
|
+
return _read_machine_id()
|
|
57
|
+
|
|
58
|
+
# First run — generate and store
|
|
59
|
+
return _create_machine_id()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _read_machine_id():
|
|
63
|
+
"""Read existing machine-id file."""
|
|
64
|
+
data = {}
|
|
65
|
+
try:
|
|
66
|
+
for line in MACHINE_ID_FILE.read_text(encoding="utf-8").splitlines():
|
|
67
|
+
line = line.strip()
|
|
68
|
+
if ":" in line and not line.startswith("#"):
|
|
69
|
+
key, _, value = line.partition(":")
|
|
70
|
+
data[key.strip()] = value.strip()
|
|
71
|
+
except OSError:
|
|
72
|
+
return _create_machine_id()
|
|
73
|
+
|
|
74
|
+
if "id" not in data:
|
|
75
|
+
return _create_machine_id()
|
|
76
|
+
|
|
77
|
+
return data
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _create_machine_id():
|
|
81
|
+
"""Generate a new machine-id and write to file."""
|
|
82
|
+
GATOR_USER_DIR.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
data = {
|
|
85
|
+
"id": str(uuid.uuid4()),
|
|
86
|
+
"hostname": platform.node(),
|
|
87
|
+
"label": platform.node(), # default label = hostname, PI can change
|
|
88
|
+
"created": str(date.today()),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
lines = [
|
|
92
|
+
f"id: {data['id']}",
|
|
93
|
+
f"hostname: {data['hostname']}",
|
|
94
|
+
f"label: {data['label']}",
|
|
95
|
+
f"created: {data['created']}",
|
|
96
|
+
]
|
|
97
|
+
MACHINE_ID_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
98
|
+
|
|
99
|
+
return data
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def set_label(new_label):
|
|
103
|
+
"""Update the human-readable label."""
|
|
104
|
+
data = get_machine_id()
|
|
105
|
+
data["label"] = new_label
|
|
106
|
+
|
|
107
|
+
lines = [
|
|
108
|
+
f"id: {data['id']}",
|
|
109
|
+
f"hostname: {data.get('hostname', platform.node())}",
|
|
110
|
+
f"label: {data['label']}",
|
|
111
|
+
f"created: {data.get('created', str(date.today()))}",
|
|
112
|
+
]
|
|
113
|
+
MACHINE_ID_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
114
|
+
|
|
115
|
+
return data
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def main():
|
|
119
|
+
parser = argparse.ArgumentParser(
|
|
120
|
+
description="Gator machine identity for session audit trails."
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
"--label", "-l",
|
|
124
|
+
help="Set a human-readable label for this machine",
|
|
125
|
+
)
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"--json", "-j",
|
|
128
|
+
action="store_true",
|
|
129
|
+
help="Output as JSON",
|
|
130
|
+
)
|
|
131
|
+
args = parser.parse_args()
|
|
132
|
+
|
|
133
|
+
if args.label:
|
|
134
|
+
data = set_label(args.label)
|
|
135
|
+
if args.json:
|
|
136
|
+
print(json.dumps(data, indent=2))
|
|
137
|
+
else:
|
|
138
|
+
print(f" Machine label set to: {data['label']}")
|
|
139
|
+
print(f" ID: {data['id']}")
|
|
140
|
+
else:
|
|
141
|
+
data = get_machine_id()
|
|
142
|
+
if args.json:
|
|
143
|
+
print(json.dumps(data, indent=2))
|
|
144
|
+
else:
|
|
145
|
+
print(f" Machine ID: {data['id']}")
|
|
146
|
+
print(f" Hostname: {data.get('hostname', '?')}")
|
|
147
|
+
print(f" Label: {data.get('label', '?')}")
|
|
148
|
+
print(f" Created: {data.get('created', '?')}")
|
|
149
|
+
print(f" Stored at: {MACHINE_ID_FILE}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
main()
|