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,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator-approve.py — Architect-only override approval for blocked commits.
|
|
4
|
+
|
|
5
|
+
When the pre-commit hook blocks a commit, it writes an override request
|
|
6
|
+
to .gator/override-request.json with a unique block ID. The Architect reviews
|
|
7
|
+
the findings, then runs this script to approve the override.
|
|
8
|
+
|
|
9
|
+
The agent must NOT run this script. The constitution forbids it.
|
|
10
|
+
Unauthorized self-approval is an auditable governance violation.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
python .gator/scripts/gator-approve.py
|
|
14
|
+
python .gator/scripts/gator-approve.py --reason "trivial whitespace change"
|
|
15
|
+
python .gator/scripts/gator-approve.py --reason "bug fix" --name "Architect"
|
|
16
|
+
|
|
17
|
+
The script reads the pending override request, confirms with the Architect,
|
|
18
|
+
and writes .gator/override-approved.json. The next git commit attempt
|
|
19
|
+
will accept the override if the block IDs match and the approval is
|
|
20
|
+
newer than the request.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import io
|
|
24
|
+
import json
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ensure_utf8_stdout():
|
|
31
|
+
"""Ensure stdout uses UTF-8 encoding (needed on Windows)."""
|
|
32
|
+
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
|
|
33
|
+
sys.stdout = io.TextIOWrapper(
|
|
34
|
+
sys.stdout.buffer, encoding="utf-8", errors="replace"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_gator_dir():
|
|
39
|
+
"""Walk up from cwd to find .gator/."""
|
|
40
|
+
d = Path.cwd().resolve()
|
|
41
|
+
for _ in range(10):
|
|
42
|
+
if (d / ".gator").is_dir():
|
|
43
|
+
return d / ".gator"
|
|
44
|
+
d = d.parent
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
_ensure_utf8_stdout()
|
|
50
|
+
|
|
51
|
+
gator_dir = find_gator_dir()
|
|
52
|
+
if not gator_dir:
|
|
53
|
+
print(" Error: no .gator/ found.", file=sys.stderr)
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
request_file = gator_dir / "override-request.json"
|
|
57
|
+
if not request_file.exists():
|
|
58
|
+
print(" No pending override request.")
|
|
59
|
+
print(" Override requests are created when the pre-commit hook blocks a commit.")
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
|
|
62
|
+
request = json.loads(request_file.read_text(encoding="utf-8"))
|
|
63
|
+
block_id = request.get("block_id", "unknown")
|
|
64
|
+
failure_type = request.get("failure_type", "unknown")
|
|
65
|
+
files = request.get("files", [])
|
|
66
|
+
override_type = request.get("override_type", "charter-skip")
|
|
67
|
+
timestamp = request.get("timestamp", "unknown")
|
|
68
|
+
|
|
69
|
+
print()
|
|
70
|
+
print(" gator override approval")
|
|
71
|
+
print()
|
|
72
|
+
print(f" Block ID: {block_id}")
|
|
73
|
+
print(f" Failure: {failure_type}")
|
|
74
|
+
print(f" Override: {override_type}")
|
|
75
|
+
print(f" Blocked at: {timestamp}")
|
|
76
|
+
if files:
|
|
77
|
+
print(f" Files: {', '.join(files[:5])}")
|
|
78
|
+
print()
|
|
79
|
+
|
|
80
|
+
# Parse --reason and --name from args
|
|
81
|
+
reason = ""
|
|
82
|
+
pi_name = ""
|
|
83
|
+
args = sys.argv[1:]
|
|
84
|
+
for i, arg in enumerate(args):
|
|
85
|
+
if arg == "--reason" and i + 1 < len(args):
|
|
86
|
+
reason = args[i + 1]
|
|
87
|
+
elif arg == "--name" and i + 1 < len(args):
|
|
88
|
+
pi_name = args[i + 1]
|
|
89
|
+
|
|
90
|
+
# Prompt for missing values
|
|
91
|
+
if not reason:
|
|
92
|
+
try:
|
|
93
|
+
reason = input(" Reason for override (required): ").strip()
|
|
94
|
+
except (EOFError, KeyboardInterrupt):
|
|
95
|
+
print("\n Cancelled.")
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
if not reason:
|
|
99
|
+
print(" Error: reason is required for override approval.")
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
if not pi_name:
|
|
103
|
+
try:
|
|
104
|
+
pi_name = input(" Your name (Architect): ").strip()
|
|
105
|
+
except (EOFError, KeyboardInterrupt):
|
|
106
|
+
print("\n Cancelled.")
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
|
|
109
|
+
if not pi_name:
|
|
110
|
+
print(" Error: Architect name is required.")
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
|
|
113
|
+
# Write approval
|
|
114
|
+
approval = {
|
|
115
|
+
"block_id": block_id,
|
|
116
|
+
"override_type": override_type,
|
|
117
|
+
"approved_by": pi_name,
|
|
118
|
+
"reason": reason,
|
|
119
|
+
"approved_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
120
|
+
"request_timestamp": timestamp,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
approved_file = gator_dir / "override-approved.json"
|
|
124
|
+
approved_file.write_text(
|
|
125
|
+
json.dumps(approval, indent=2) + "\n",
|
|
126
|
+
encoding="utf-8",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
print()
|
|
130
|
+
print(f" \u2713 Override approved by {pi_name}")
|
|
131
|
+
print(f" Block ID: {block_id}")
|
|
132
|
+
print(f" Reason: {reason}")
|
|
133
|
+
print()
|
|
134
|
+
print(" Now retry: git commit")
|
|
135
|
+
print()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
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()
|
|
@@ -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()
|