gator-command 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. gator_command/__init__.py +2 -0
  2. gator_command/cli.py +137 -0
  3. gator_command/scripts/crawler.py +633 -0
  4. gator_command/scripts/dashboard/dashboard.css +982 -0
  5. gator_command/scripts/dashboard/dashboard.html +84 -0
  6. gator_command/scripts/dashboard/dashboard.js +419 -0
  7. gator_command/scripts/dashboard/views/audit.js +270 -0
  8. gator_command/scripts/dashboard/views/fleet.js +307 -0
  9. gator_command/scripts/dashboard/views/repo.js +599 -0
  10. gator_command/scripts/dashboard/views/settings.js +173 -0
  11. gator_command/scripts/dashboard/views/updates.js +308 -0
  12. gator_command/scripts/enforcer-prompt.md +22 -0
  13. gator_command/scripts/extract-claude-sessions.py +489 -0
  14. gator_command/scripts/extract-codex-sessions.py +477 -0
  15. gator_command/scripts/extract-gemini-sessions.py +410 -0
  16. gator_command/scripts/gator-audit.py +956 -0
  17. gator_command/scripts/gator-charter-draft.py +919 -0
  18. gator_command/scripts/gator-charter-lint.py +427 -0
  19. gator_command/scripts/gator-charter-verify.py +606 -0
  20. gator_command/scripts/gator-dashboard.py +1271 -0
  21. gator_command/scripts/gator-deploy.py +916 -0
  22. gator_command/scripts/gator-drift.py +569 -0
  23. gator_command/scripts/gator-enforce.py +82 -0
  24. gator_command/scripts/gator-fleet-intel.py +460 -0
  25. gator_command/scripts/gator-fleet-report.py +615 -0
  26. gator_command/scripts/gator-init-command-post.py +315 -0
  27. gator_command/scripts/gator-init.py +434 -0
  28. gator_command/scripts/gator-machine-id.py +153 -0
  29. gator_command/scripts/gator-policy-status.py +631 -0
  30. gator_command/scripts/gator-pulse.py +459 -0
  31. gator_command/scripts/gator-repo-status.py +649 -0
  32. gator_command/scripts/gator-session-common.py +372 -0
  33. gator_command/scripts/gator-session-sink.py +831 -0
  34. gator_command/scripts/gator-sessions.py +1244 -0
  35. gator_command/scripts/gator-update.py +615 -0
  36. gator_command/scripts/gator-version.py +38 -0
  37. gator_command/scripts/gator_core.py +489 -0
  38. gator_command/scripts/gator_remote.py +381 -0
  39. gator_command/scripts/gator_runtime.py +142 -0
  40. gator_command/scripts/gatorize-actions.sh +989 -0
  41. gator_command/scripts/gatorize-lib.sh +166 -0
  42. gator_command/scripts/gatorize-post.sh +394 -0
  43. gator_command/scripts/gatorize.py +1163 -0
  44. gator_command/scripts/gatorize.sh +185 -0
  45. gator_command/scripts/generate_markdown.py +212 -0
  46. gator_command/scripts/generate_wiki.py +424 -0
  47. gator_command/scripts/graph_health.py +780 -0
  48. gator_command/scripts/memex-lint.py +286 -0
  49. gator_command/scripts/memex-lint.sh +205 -0
  50. gator_command/scripts/memex.py +1472 -0
  51. gator_command/scripts/memex_formatters.py +191 -0
  52. gator_command/scripts/memex_state.py +236 -0
  53. gator_command/scripts/spawn.py +650 -0
  54. gator_command/templates/gator-starter/blueprints/README.md +32 -0
  55. gator_command/templates/gator-starter/charterignore +53 -0
  56. gator_command/templates/gator-starter/charters/README.md +178 -0
  57. gator_command/templates/gator-starter/charters/_template.md +31 -0
  58. gator_command/templates/gator-starter/commands/commit.md +33 -0
  59. gator_command/templates/gator-starter/commands/init.md +11 -0
  60. gator_command/templates/gator-starter/commands/update.md +5 -0
  61. gator_command/templates/gator-starter/constitution.md +165 -0
  62. gator_command/templates/gator-starter/field-guides/README.md +25 -0
  63. gator_command/templates/gator-starter/gator-start-up.md +119 -0
  64. gator_command/templates/gator-starter/procedures/charter-alignment.md +83 -0
  65. gator_command/templates/gator-starter/procedures/enforcer-review.md +317 -0
  66. gator_command/templates/gator-starter/procedures/field-guide-generation.md +176 -0
  67. gator_command/templates/gator-starter/procedures/knowledge-capture.md +57 -0
  68. gator_command/templates/gator-starter/procedures/significance-check.md +69 -0
  69. gator_command/templates/gator-starter/reference-notes/concierge-responses.md +535 -0
  70. gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +91 -0
  71. gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +22 -0
  72. gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +232 -0
  73. gator_command/templates/gator-starter/reference-notes/example-project.md +289 -0
  74. gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +72 -0
  75. gator_command/templates/gator-starter/reference-notes/git-workflow.md +60 -0
  76. gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +37 -0
  77. gator_command/templates/gator-starter/reference-notes/refactor-approach.md +155 -0
  78. gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +108 -0
  79. gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +99 -0
  80. gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +155 -0
  81. gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
  82. gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
  83. gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
  84. gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
  85. gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
  86. gator_command/templates/gator-starter/scripts/enforcer-prompt.md +55 -0
  87. gator_command/templates/gator-starter/scripts/enforcer-review.py +1551 -0
  88. gator_command/templates/gator-starter/scripts/gator-approve.py +139 -0
  89. gator_command/templates/gator-starter/scripts/gator-enforce.py +82 -0
  90. gator_command/templates/gator-starter/scripts/gator-init.py +434 -0
  91. gator_command/templates/gator-starter/scripts/gator-pre-commit.py +2670 -0
  92. gator_command/templates/gator-starter/scripts/gator-pulse.py +459 -0
  93. gator_command/templates/gator-starter/scripts/gator-update.py +615 -0
  94. gator_command/templates/gator-starter/scripts/gator-version.py +38 -0
  95. gator_command/templates/gator-starter/scripts/gator_core.py +487 -0
  96. gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
  97. gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
  98. gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
  99. gator_command/templates/gator-starter/scripts/hooks/commit-msg +5 -0
  100. gator_command/templates/gator-starter/scripts/hooks/post-commit +7 -0
  101. gator_command/templates/gator-starter/scripts/hooks/pre-commit +5 -0
  102. gator_command/templates/gator-starter/sessions/.gitignore +7 -0
  103. gator_command/templates/gator-starter/vault/.gitkeep +0 -0
  104. gator_command/templates/gator-starter/whiteboard.md +5 -0
  105. gator_command-1.0.0.dist-info/METADATA +122 -0
  106. gator_command-1.0.0.dist-info/RECORD +110 -0
  107. gator_command-1.0.0.dist-info/WHEEL +5 -0
  108. gator_command-1.0.0.dist-info/entry_points.txt +2 -0
  109. gator_command-1.0.0.dist-info/licenses/LICENSE +21 -0
  110. gator_command-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1163 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ gatorize.py — Install, upgrade, or morph Gator into a project repo.
4
+
5
+ Cross-platform Python implementation of gatorize.sh. Works on Windows
6
+ CMD/PowerShell, Git Bash, macOS Terminal, and Linux.
7
+
8
+ Usage:
9
+ python gatorize.py <target-directory>
10
+ python gatorize.py .
11
+
12
+ Handles five scenarios:
13
+ 1. Fresh directory (no git) → git init + install
14
+ 2. Git repo, clean → install
15
+ 3. Git repo, has .gator/ → run update
16
+ 4. Git repo, has memex structure → morph memex → gator
17
+ 5. Git repo, has both → warn, let user choose
18
+
19
+ @reads: templates in gator-starter/
20
+ @writes: target .gator/, entry points, hooks, registry
21
+ """
22
+
23
+ import argparse
24
+ import os
25
+ import shutil
26
+ import stat
27
+ import subprocess
28
+ import sys
29
+ from datetime import datetime
30
+ from pathlib import Path
31
+
32
+ SCRIPTS_DIR = Path(__file__).resolve().parent
33
+ # Scripts are at src/gator_command/scripts/ — repo root is 3 levels up
34
+ COMMAND_POST = SCRIPTS_DIR.parent.parent.parent
35
+
36
+ sys.path.insert(0, str(SCRIPTS_DIR))
37
+ from gator_core import find_command_post, ensure_utf8_stdout, import_sibling # noqa: E402
38
+
39
+ # ── constants ──────────────────────────────────────────────────────────────
40
+
41
+ GATOR_BRANCH = "gator-install"
42
+ TODAY = datetime.now().strftime("%Y-%m-%d")
43
+ NOW = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
44
+ GATOR_MARKER = "# --- Gator Navigation Coding ---"
45
+ COMMAND_POST_MARKER = "# --- Gator Command Post ---"
46
+
47
+ # Detect template directory
48
+ TEMPLATES = None
49
+ for tpl_rel in ("templates/gator-starter", "../templates/gator-starter"):
50
+ candidate = (SCRIPTS_DIR.parent / tpl_rel).resolve()
51
+ if candidate.is_dir():
52
+ TEMPLATES = candidate
53
+ break
54
+ if not TEMPLATES:
55
+ # Try gator-engine layout
56
+ for tpl_rel in ("gator-engine/templates/gator-starter",
57
+ "gator-command/templates/gator-starter"):
58
+ candidate = (COMMAND_POST / tpl_rel).resolve()
59
+ if candidate.is_dir():
60
+ TEMPLATES = candidate
61
+ break
62
+
63
+ # Read generation from gator_core.py
64
+ GATOR_GEN = 2
65
+ try:
66
+ from gator_core import CURRENT_GENERATION
67
+ GATOR_GEN = CURRENT_GENERATION
68
+ except ImportError:
69
+ pass
70
+
71
+
72
+ # ── helpers ────────────────────────────────────────────────────────────────
73
+
74
+ def git(*args, cwd=None):
75
+ """Run a git command. Returns (stdout, success)."""
76
+ try:
77
+ result = subprocess.run(
78
+ ["git"] + list(args),
79
+ capture_output=True, text=True, timeout=30,
80
+ cwd=str(cwd) if cwd else None,
81
+ )
82
+ return result.stdout.strip(), result.returncode == 0
83
+ except Exception as e:
84
+ return str(e), False
85
+
86
+
87
+ def log_step(msg):
88
+ print(f" {msg}")
89
+
90
+
91
+ def prompt(question, options="", default=""):
92
+ """Interactive prompt. Returns user input."""
93
+ suffix = f" [{options}]: " if options else ": "
94
+ try:
95
+ return input(f" {question}{suffix}").strip()
96
+ except (EOFError, KeyboardInterrupt):
97
+ return default
98
+
99
+
100
+ def confirm(question, default="Y"):
101
+ """Y/N confirmation."""
102
+ choice = prompt(question, "Y/n" if default == "Y" else "y/N")
103
+ if not choice:
104
+ return default == "Y"
105
+ return choice.lower().startswith("y")
106
+
107
+
108
+ def copy_tree_overlay(src, dest):
109
+ """Copy files from src to dest, preserving extras in dest."""
110
+ dest.mkdir(parents=True, exist_ok=True)
111
+ for item in sorted(src.iterdir()):
112
+ if item.name in ("__pycache__",) or item.suffix == ".pyc":
113
+ continue
114
+ target = dest / item.name
115
+ if item.is_dir():
116
+ copy_tree_overlay(item, target)
117
+ else:
118
+ shutil.copy2(item, target)
119
+
120
+
121
+ # ── detection ──────────────────────────────────────────────────────────────
122
+
123
+ def detect_scenario(target):
124
+ """Returns scenario number 1-5."""
125
+ has_git = (target / ".git").is_dir()
126
+ has_gator = (target / ".gator").is_dir()
127
+ has_memex = (target / "memex").is_dir() or (target / ".memex").is_dir()
128
+
129
+ if not has_git and not has_memex and not has_gator:
130
+ return 1
131
+ if has_git and not has_memex and not has_gator:
132
+ return 2
133
+ if has_git and not has_memex and has_gator:
134
+ return 3
135
+ if has_git and has_memex and not has_gator:
136
+ return 4
137
+ if has_git and has_memex and has_gator:
138
+ return 5
139
+ # Edge: memex without git
140
+ print(" Error: memex structure found but no git. Initialize git first.")
141
+ sys.exit(1)
142
+
143
+
144
+ def detect_generation(target):
145
+ """Returns generation number of existing .gator/."""
146
+ version_file = target / ".gator" / ".gator-version"
147
+ if version_file.exists():
148
+ for line in version_file.read_text(encoding="utf-8", errors="replace").splitlines():
149
+ if line.startswith("generation:"):
150
+ try:
151
+ return int(line.split(":", 1)[1].strip())
152
+ except ValueError:
153
+ pass
154
+ return 2
155
+ if (target / ".gator" / "command-post.md").exists():
156
+ return 1
157
+ return 0
158
+
159
+
160
+ # ── git operations ─────────────────────────────────────────────────────────
161
+
162
+ def action_git_init(target):
163
+ """Scenario 1: initialize git in a fresh directory."""
164
+ print()
165
+ log_step(f"Initializing git in {target}...")
166
+ git("init", cwd=target)
167
+ # Ensure main branch
168
+ branch, _ = git("branch", "--show-current", cwd=target)
169
+ if branch != "main":
170
+ git("branch", "-M", "main", cwd=target)
171
+ # Initial commit
172
+ files = [f for f in target.iterdir() if f.name != ".git"]
173
+ if files:
174
+ git("add", "-A", cwd=target)
175
+ git("commit", "-m", "Initial commit (pre-Gator)", cwd=target)
176
+ else:
177
+ git("commit", "--allow-empty", "-m", "Initial commit (pre-Gator)", cwd=target)
178
+ git("branch", "dev", cwd=target)
179
+ git("checkout", "-b", GATOR_BRANCH, cwd=target)
180
+ log_step(f"On '{GATOR_BRANCH}' (git initialized).")
181
+
182
+
183
+ def action_feature_branch(target):
184
+ """Create or switch to gator-install branch."""
185
+ # Check for uncommitted changes
186
+ status, _ = git("status", "--porcelain", cwd=target)
187
+ if status:
188
+ print()
189
+ print(f" Warning: uncommitted changes detected in {target}.")
190
+ print(" Strongly recommend committing or stashing before gatorizing.")
191
+ print()
192
+ choice = prompt("Continue anyway or abort?", "c/a")
193
+ if choice.lower() == "a":
194
+ print(" Aborted.")
195
+ sys.exit(0)
196
+
197
+ # Handle existing gator-install branch
198
+ _, has_branch = git("rev-parse", "--verify", GATOR_BRANCH, cwd=target)
199
+ if has_branch:
200
+ print()
201
+ print(f" Branch '{GATOR_BRANCH}' already exists (prior install attempt?).")
202
+ print()
203
+ print(" [1] Delete and start fresh (recommended)")
204
+ print(" [2] Continue on existing branch")
205
+ print(" [x] Cancel installation")
206
+ print()
207
+ choice = prompt("Choice", "1/2/x")
208
+ if choice.lower() == "x":
209
+ print(" Installation cancelled.")
210
+ sys.exit(0)
211
+ elif choice == "1":
212
+ git("checkout", "dev", cwd=target)
213
+ git("checkout", "main", cwd=target) # fallback
214
+ git("branch", "-D", GATOR_BRANCH, cwd=target)
215
+ log_step(f"Deleted old '{GATOR_BRANCH}' branch.")
216
+
217
+ # Get branches (excluding gator-install)
218
+ branch_out, _ = git("branch", cwd=target)
219
+ branches = []
220
+ current = ""
221
+ for line in (branch_out or "").splitlines():
222
+ is_current = line.startswith("*")
223
+ name = line.lstrip("* ").strip()
224
+ if name == GATOR_BRANCH:
225
+ continue
226
+ branches.append(name)
227
+ if is_current:
228
+ current = name
229
+
230
+ if not branches:
231
+ log_step("No branches found. Installing on current state.")
232
+ git("checkout", "-b", GATOR_BRANCH, cwd=target)
233
+ return
234
+
235
+ print()
236
+ print(f" Gator installs on a feature branch ('{GATOR_BRANCH}') for safety.")
237
+ print(" This keeps the install reversible — delete the branch if anything goes wrong,")
238
+ print(" merge to dev when you're happy.")
239
+ print()
240
+
241
+ source_branch = current
242
+ if len(branches) == 1:
243
+ source_branch = branches[0]
244
+ print(f" Only one branch: {source_branch}")
245
+ else:
246
+ print(" Available branches:")
247
+ print()
248
+ for i, b in enumerate(branches):
249
+ marker = "* " if b == current else " "
250
+ print(f" [{i + 1}] {marker}{b}")
251
+ print()
252
+ choice = prompt(f"Which branch should '{GATOR_BRANCH}' branch from?", f"1-{len(branches)}")
253
+ try:
254
+ idx = int(choice) - 1
255
+ if 0 <= idx < len(branches):
256
+ source_branch = branches[idx]
257
+ except (ValueError, IndexError):
258
+ pass
259
+
260
+ git("checkout", source_branch, cwd=target)
261
+ # Ensure dev exists — track origin/dev if available, else create from current
262
+ _, has_dev = git("rev-parse", "--verify", "dev", cwd=target)
263
+ if not has_dev:
264
+ _, has_remote_dev = git("rev-parse", "--verify", "origin/dev", cwd=target)
265
+ if has_remote_dev:
266
+ git("branch", "dev", "origin/dev", cwd=target)
267
+ log_step("Created 'dev' branch tracking origin/dev.")
268
+ else:
269
+ git("branch", "dev", cwd=target)
270
+ log_step("Created 'dev' branch.")
271
+
272
+ # Create or switch to gator-install
273
+ _, has_gi = git("rev-parse", "--verify", GATOR_BRANCH, cwd=target)
274
+ if has_gi:
275
+ git("checkout", GATOR_BRANCH, cwd=target)
276
+ else:
277
+ git("checkout", "-b", GATOR_BRANCH, cwd=target)
278
+ log_step(f"On '{GATOR_BRANCH}' (from '{source_branch}').")
279
+
280
+
281
+ # ── install ────────────────────────────────────────────────────────────────
282
+
283
+ def action_install_gator(target):
284
+ """Fresh install of .gator/ from templates."""
285
+ log_step(f"Installing .gator/ into {target}...")
286
+
287
+ gator_dir = target / ".gator"
288
+
289
+ # Create directory structure
290
+ for d in ["charters", "blueprints", "reference-notes", "procedures",
291
+ "scripts", "threads", "artifacts", "vault", "policies", "sessions"]:
292
+ (gator_dir / d).mkdir(parents=True, exist_ok=True)
293
+
294
+ # Copy templates
295
+ for name in ("constitution.md", "gator-start-up.md"):
296
+ src = TEMPLATES / name
297
+ if src.exists():
298
+ shutil.copy2(src, gator_dir / name)
299
+
300
+ # Charter scaffolding
301
+ for name in ("README.md", "_template.md"):
302
+ src = TEMPLATES / "charters" / name
303
+ if src.exists():
304
+ shutil.copy2(src, gator_dir / "charters" / name)
305
+
306
+ # Blueprint scaffolding
307
+ src = TEMPLATES / "blueprints" / "README.md"
308
+ if src.exists():
309
+ shutil.copy2(src, gator_dir / "blueprints" / "README.md")
310
+
311
+ # Overlay directories
312
+ for subdir in ("reference-notes", "procedures"):
313
+ src_dir = TEMPLATES / subdir
314
+ if src_dir.is_dir():
315
+ copy_tree_overlay(src_dir, gator_dir / subdir)
316
+
317
+ # Scripts (including subdirectories like hooks/)
318
+ scripts_src = TEMPLATES / "scripts"
319
+ if scripts_src.is_dir():
320
+ copy_tree_overlay(scripts_src, gator_dir / "scripts")
321
+
322
+ # Slash commands
323
+ commands_src = TEMPLATES / "commands"
324
+ if commands_src.is_dir():
325
+ claude_commands = target / ".claude" / "commands"
326
+ claude_commands.mkdir(parents=True, exist_ok=True)
327
+ copy_tree_overlay(commands_src, claude_commands)
328
+
329
+ # Install hooks
330
+ install_hooks(target)
331
+
332
+ # Write stubs
333
+ write_stubs(gator_dir)
334
+
335
+ # Write version
336
+ write_gator_version(gator_dir, "install")
337
+
338
+ log_step(".gator/ installed.")
339
+
340
+
341
+ def install_hooks(target):
342
+ """Install git hooks."""
343
+ hooks_dir = target / ".git" / "hooks"
344
+ hooks_dir.mkdir(parents=True, exist_ok=True)
345
+ hook_src = TEMPLATES / "scripts" / "hooks"
346
+
347
+ # Use the shared hook builder from gator-update.py — one definition of
348
+ # "correctly installed hooks" across gatorize, gator-update, and gator-init.
349
+ gator_update = import_sibling("gator-update")
350
+ hook_wrappers = gator_update.build_git_hook_wrappers()
351
+
352
+ for hook_name, hook_content in hook_wrappers.items():
353
+ dest = hooks_dir / hook_name
354
+
355
+ # Backup existing non-Gator hook
356
+ if dest.exists():
357
+ content = dest.read_text(encoding="utf-8", errors="replace")
358
+ if "Gator" not in content and "gator" not in content:
359
+ backup = dest.with_suffix(".pre-gator")
360
+ shutil.copy2(dest, backup)
361
+
362
+ dest.write_text(hook_content, encoding="utf-8")
363
+ # Make executable on Unix
364
+ if os.name != "nt":
365
+ dest.chmod(dest.stat().st_mode | stat.S_IEXEC)
366
+
367
+ log_step("Git hooks installed.")
368
+
369
+
370
+ def write_stubs(gator_dir):
371
+ """Write stub content files (only if missing)."""
372
+ stubs = {
373
+ "mission.md": "# Mission\n\n[What are we building and why?]\n",
374
+ "roadmap.md": "# Roadmap\n\n[Priority-ordered. Updated as items complete.]\n\n"
375
+ "**Status key**: Done · Building · Designed · Considering · Deferred\n",
376
+ "inbox.md": "# Inbox\n\nDrop anything here. No formatting needed.\n\n---\n\n",
377
+ "identity.md": "---\noperating-mode: designer\n---\n\n# Identity\n\n"
378
+ "## Basics\n\n- **Name**: [Your name]\n- **Location**: [Your timezone]\n"
379
+ "- **Role**: [Your role]\n",
380
+ "issues.md": "# Issues\n\nActive bugs, blockers, and known fragilities.\n\n"
381
+ "**Status key**: Open · Working · Resolved\n\n---\n\n",
382
+ "commit_draft.md": "---\nmessage: \"\"\nchange-type:\nsignificance:\n"
383
+ "decision-tags: []\nagent:\narchitect:\n---\n\n"
384
+ "# Session Change Log\n\n",
385
+ "patterns.md": "# Patterns\n\nRecurring rhythms, obligations, schedules.\n\n---\n\n",
386
+ "whiteboard.md": "# Whiteboard\n\nNo findings.\n",
387
+ }
388
+
389
+ for name, content in stubs.items():
390
+ path = gator_dir / name
391
+ if not path.exists():
392
+ path.write_text(content, encoding="utf-8")
393
+
394
+ # JSON stubs
395
+ lint_allow = gator_dir / "lint-allow.json"
396
+ if not lint_allow.exists():
397
+ lint_allow.write_text("[]", encoding="utf-8")
398
+
399
+ commit_issues = gator_dir / "commit_issues.md"
400
+ if not commit_issues.exists():
401
+ commit_issues.write_text("# Commit Issues\n\nNo findings.\n", encoding="utf-8")
402
+
403
+ config = gator_dir / "config.json"
404
+ if not config.exists():
405
+ config.write_text('{\n "enforcement_level": "strict"\n}\n', encoding="utf-8")
406
+
407
+ # Charter INDEX
408
+ index = gator_dir / "charters" / "INDEX.md"
409
+ if not index.exists():
410
+ index.write_text(
411
+ "# Charter Index\n\n**Always read first:** [Cross-Cutting](cross-cutting.md) (when it exists)\n\n"
412
+ "| If you're changing... | Read these charters |\n|---|---|\n| | |\n",
413
+ encoding="utf-8",
414
+ )
415
+
416
+ # Sessions .gitignore
417
+ sessions_gi = gator_dir / "sessions" / ".gitignore"
418
+ sessions_gi.parent.mkdir(exist_ok=True)
419
+ if not sessions_gi.exists():
420
+ sessions_gi.write_text("_active/\n", encoding="utf-8")
421
+
422
+ # Vault — gitignored directory for sensitive material and large files
423
+ vault_dir = gator_dir / "vault"
424
+ vault_dir.mkdir(exist_ok=True)
425
+ vault_gitkeep = vault_dir / ".gitkeep"
426
+ if not vault_gitkeep.exists():
427
+ vault_gitkeep.write_text("", encoding="utf-8")
428
+ ensure_repo_gitignore(gator_dir.parent)
429
+
430
+
431
+ def ensure_repo_gitignore(repo_root):
432
+ """Ensure standard gitignore rules exist in the repo's .gitignore.
433
+
434
+ Called on both fresh install and upgrade paths so all governed repos
435
+ converge on the same set of ignore rules.
436
+ """
437
+ repo_gi = repo_root / ".gitignore"
438
+ gi_rules = {
439
+ ".gator/vault/": "# Vault — sensitive material and large files",
440
+ ".vscode/": "# IDE settings",
441
+ "__pycache__/": "# Python cache",
442
+ }
443
+ if repo_gi.exists():
444
+ gi_text = repo_gi.read_text(encoding="utf-8", errors="replace")
445
+ else:
446
+ gi_text = ""
447
+ additions = []
448
+ for rule, comment in gi_rules.items():
449
+ if rule not in gi_text:
450
+ additions.append(f"{comment}\n{rule}")
451
+ if additions:
452
+ with open(repo_gi, "a" if gi_text else "w", encoding="utf-8") as f:
453
+ f.write("\n" + "\n".join(additions) + "\n")
454
+
455
+
456
+ def write_gator_version(gator_dir, action):
457
+ """Write .gator-version with generation and timestamps."""
458
+ version_file = gator_dir / ".gator-version"
459
+
460
+ # Preserve install date on upgrade
461
+ installed = TODAY
462
+ if version_file.exists():
463
+ for line in version_file.read_text(encoding="utf-8", errors="replace").splitlines():
464
+ if line.startswith("installed:"):
465
+ prev = line.split(":", 1)[1].strip()
466
+ if prev:
467
+ installed = prev
468
+
469
+ version_file.write_text(
470
+ f"generation: {GATOR_GEN}\n"
471
+ f"installed: {installed}\n"
472
+ f"updated: {NOW}\n"
473
+ f"action: {action}\n"
474
+ f"installer: gatorize.py\n",
475
+ encoding="utf-8",
476
+ )
477
+
478
+
479
+ # ── entry points ───────────────────────────────────────────────────────────
480
+
481
+ GATOR_BEGIN = "<!-- GATOR:BEGIN -->"
482
+ GATOR_END = "<!-- GATOR:END -->"
483
+
484
+
485
+ def render_entry_content(has_command_post, agent_type="claude"):
486
+ """Render the canonical Gator-managed block for an entry point."""
487
+ gator_init_block = (
488
+ '**"gator init" means**: run `python .gator/scripts/gator-init.py` '
489
+ "and display the output exactly as printed. It is NOT a repo bootstrap "
490
+ "or git init — it is the standardized Gator boot sequence. Run the script, "
491
+ "show the output, then proceed with session opening.\n\n"
492
+ '**"gator pulse"**: run `python .gator/scripts/gator-pulse.py` to generate '
493
+ "`.gator/pulse.md` — a strategic operations brief with next steps, project "
494
+ "assessment, roadmap check, priorities, and recent activity.\n\n"
495
+ "**Project assessment**: when the Architect asks for a project assessment, "
496
+ "write a 2-paragraph expert evaluation to "
497
+ "`.gator/artifacts/YYYY-MM-DD-project-assessment.md` with YAML frontmatter "
498
+ "(`date`, `type: project-assessment`, `model: <your model name>`). "
499
+ "Write as an expert consultant advising an engineering manager. "
500
+ "The next `gator pulse` will include it automatically."
501
+ )
502
+
503
+ constitution_line = "Read [`.gator/constitution.md`](.gator/constitution.md) before your first response. It governs how you work here."
504
+ bootstrap_line = "If this is a **fresh project** (charters/ is empty or contains only templates), follow the bootstrap procedure in [`.gator/gator-start-up.md`](.gator/gator-start-up.md)."
505
+
506
+ cp_section = ""
507
+ if has_command_post:
508
+ cp_section = (
509
+ f"\n\n{COMMAND_POST_MARKER}\n"
510
+ "This repo is governed by a Gator Command post. "
511
+ "Read [`.gator/command-post.md`](.gator/command-post.md) for the command post location. "
512
+ "Read org standards from there at session open. "
513
+ "**Never write to the command post from this session.** "
514
+ "Cross-repo discoveries go in [`outbox.md`](outbox.md)."
515
+ )
516
+
517
+ content = f"{gator_init_block}\n\n{constitution_line}\n\n{bootstrap_line}"
518
+
519
+ if agent_type == "agents":
520
+ content += (
521
+ "\n\nIf the PI asks for an **enforcer review**, do not repurpose yourself as the enforcer. "
522
+ "Use the dedicated enforcer prompt at [`.gator/scripts/enforcer-prompt.md`](.gator/scripts/enforcer-prompt.md)."
523
+ )
524
+
525
+ content += cp_section
526
+ return content
527
+
528
+
529
+ def action_install_entry_points(target, has_command_post):
530
+ """Install or refresh CLAUDE.md, AGENTS.md, GEMINI.md.
531
+
532
+ Three cases:
533
+ - File missing: create with sentinels
534
+ - Non-Gator file (no markers): interactive prompt (backup/append/overwrite)
535
+ - Gator-managed file (has markers): refresh managed block in place
536
+ """
537
+ entries = [
538
+ ("CLAUDE.md", "# Claude Code Entry Point", "claude", "CLAUDE_ROLLBACK.md"),
539
+ ("AGENTS.md", "# Codex Entry Point", "agents", "AGENTS_ROLLBACK.md"),
540
+ ("GEMINI.md", "# Gemini Entry Point", "gemini", "GEMINI_ROLLBACK.md"),
541
+ ]
542
+
543
+ idx = 0
544
+ while idx < len(entries):
545
+ filename, header, agent_type, rollback_name = entries[idx]
546
+ filepath = target / filename
547
+ content = render_entry_content(has_command_post, agent_type)
548
+ managed_block = f"{GATOR_BEGIN}\n{content}\n{GATOR_END}"
549
+
550
+ if filepath.exists():
551
+ existing = filepath.read_text(encoding="utf-8", errors="replace")
552
+
553
+ # Case: Gator-managed file — refresh the managed block
554
+ if GATOR_BEGIN in existing and GATOR_END in existing:
555
+ # Replace between sentinels
556
+ before = existing[:existing.index(GATOR_BEGIN)]
557
+ after = existing[existing.index(GATOR_END) + len(GATOR_END):]
558
+ filepath.write_text(
559
+ f"{before}{managed_block}{after}",
560
+ encoding="utf-8",
561
+ )
562
+ log_step(f"{filename} — Gator block refreshed")
563
+ idx += 1
564
+ continue
565
+
566
+ # Case: has old Gator marker OR recognizable Gator content but no sentinels
567
+ # Detect Gator ownership: markers, or known Gator references
568
+ is_gator_owned = (
569
+ GATOR_MARKER in existing
570
+ or COMMAND_POST_MARKER in existing
571
+ or "gator-init.py" in existing
572
+ or ".gator/constitution.md" in existing
573
+ )
574
+
575
+ if is_gator_owned:
576
+ # Find the split point — where Gator content starts
577
+ marker_pos = None
578
+ for marker in (GATOR_MARKER, COMMAND_POST_MARKER):
579
+ if marker in existing:
580
+ marker_pos = existing.index(marker)
581
+ break
582
+
583
+ if marker_pos is not None:
584
+ pre_gator = existing[:marker_pos].rstrip()
585
+ # Preserve post-Gator content (e.g., ## Pre-Gator Instructions)
586
+ post_gator = ""
587
+ pre_gator_section = "## Pre-Gator Instructions"
588
+ if pre_gator_section in existing:
589
+ post_gator = "\n\n" + existing[existing.index(pre_gator_section):]
590
+ else:
591
+ # No marker — entire file is Gator-managed (standalone old format)
592
+ pre_gator = ""
593
+ # Check for rollback reference
594
+ post_gator = ""
595
+ pre_gator_section = "## Pre-Gator Instructions"
596
+ if pre_gator_section in existing:
597
+ post_gator = "\n\n" + existing[existing.index(pre_gator_section):]
598
+
599
+ if pre_gator:
600
+ filepath.write_text(
601
+ f"{pre_gator}\n\n{managed_block}{post_gator}\n",
602
+ encoding="utf-8",
603
+ )
604
+ else:
605
+ filepath.write_text(
606
+ f"{header}\n\nYou are the primary agent for this project.\n\n{managed_block}{post_gator}\n",
607
+ encoding="utf-8",
608
+ )
609
+ log_step(f"{filename} — upgraded to sentinel format + refreshed")
610
+ idx += 1
611
+ continue
612
+
613
+ # Case: non-Gator file — interactive prompt
614
+ line_count = len(existing.splitlines())
615
+ print()
616
+ print(f" {filename} already exists ({line_count} lines).")
617
+ print()
618
+ print(" Options:")
619
+ print()
620
+ print(f" [1] Backup & replace (recommended) — save as {rollback_name},")
621
+ print(f" create new {filename} with Gator governance + reference to original.")
622
+ print()
623
+ print(f" [2] Append — add Gator lines to the end of the existing file.")
624
+ print()
625
+ print(f" [3] Overwrite — replace entirely (original content lost).")
626
+ if idx > 0:
627
+ print(f" [b] Go back — return to previous entry point.")
628
+ print(f" [x] Cancel installation.")
629
+ print()
630
+ choice = prompt("Choice", f"1/2/3{'/b' if idx > 0 else ''}/x")
631
+
632
+ if choice == "1":
633
+ shutil.copy2(filepath, target / rollback_name)
634
+ log_step(f"Saved original as {rollback_name}")
635
+ filepath.write_text(
636
+ f"{header}\n\nYou are the primary agent for this project.\n\n"
637
+ f"{managed_block}\n\n"
638
+ f"## Pre-Gator Instructions\n\n"
639
+ f"This project had an existing {filename} before Gator was installed.\n"
640
+ f"Those instructions have been preserved in [`{rollback_name}`]({rollback_name}).\n"
641
+ f"**Read that file now** — it may contain skills, custom instructions, personas,\n"
642
+ f"or project-specific rules that should still be followed alongside the\n"
643
+ f"Gator constitution.\n\n"
644
+ f"If any instructions in {rollback_name} conflict with the Gator constitution,\n"
645
+ f"the constitution takes precedence for code governance (charters, commit loop,\n"
646
+ f"enforcer rules). For everything else (skills, personas, custom workflows),\n"
647
+ f"follow {rollback_name}.\n",
648
+ encoding="utf-8",
649
+ )
650
+ log_step(f"Created new {filename} with Gator governance + reference to {rollback_name}")
651
+ elif choice == "2":
652
+ with open(filepath, "a", encoding="utf-8") as f:
653
+ f.write(f"\n{managed_block}\n")
654
+ log_step(f"Appended Gator entry point to {filename}")
655
+ elif choice == "3":
656
+ filepath.write_text(
657
+ f"{header}\n\nYou are the primary agent for this project.\n\n{managed_block}\n",
658
+ encoding="utf-8",
659
+ )
660
+ log_step(f"Replaced {filename} (original content lost)")
661
+ elif choice.lower() == "b" and idx > 0:
662
+ idx -= 1
663
+ continue
664
+ elif choice.lower() == "x":
665
+ print()
666
+ print(" Installation cancelled.")
667
+ print(" To clean up: git checkout dev && git branch -D gator-install")
668
+ sys.exit(0)
669
+ else:
670
+ # New file — create with sentinels
671
+ filepath.write_text(
672
+ f"{header}\n\nYou are the primary agent for this project.\n\n{managed_block}\n",
673
+ encoding="utf-8",
674
+ )
675
+ log_step(f"Created {filename}")
676
+
677
+ idx += 1
678
+
679
+
680
+ # ── post-install ───────────────────────────────────────────────────────────
681
+
682
+ def action_install_outbox(target):
683
+ """Create outbox.md stub."""
684
+ outbox = target / "outbox.md"
685
+ if outbox.exists():
686
+ # Check if it has content
687
+ text = outbox.read_text(encoding="utf-8", errors="replace")
688
+ lines = [l for l in text.splitlines() if l.strip() and not l.startswith("#") and not l.startswith("---")]
689
+ if lines:
690
+ log_step("outbox.md already exists with content — preserved")
691
+ return
692
+ outbox.write_text(
693
+ "# Outbox\n\n"
694
+ "Messages for Gator Command. The Architect triages these during command post sessions.\n"
695
+ "Do not clear this file from the project repo — the command post session clears\n"
696
+ "entries after processing.\n\n---\n\n",
697
+ encoding="utf-8",
698
+ )
699
+ log_step("Created outbox.md")
700
+
701
+
702
+ def action_install_thin_link(target):
703
+ """Create .gator/command-post.md with paths to command post."""
704
+ cp_path = str(COMMAND_POST)
705
+ try:
706
+ rel_path = os.path.relpath(str(COMMAND_POST), str(target))
707
+ except ValueError:
708
+ rel_path = cp_path
709
+
710
+ (target / ".gator" / "command-post.md").write_text(
711
+ f"---\ncommand-post: {rel_path}\n"
712
+ f"command-post-absolute: {cp_path} # machine-local\n"
713
+ f"remote: # optional\nversion: {TODAY}\n---\n\n"
714
+ f"# Gator Command Post\n\nThis repo is governed by [Gator Command]({rel_path}).\n"
715
+ f"The agent should read org standards from the command post at session open.\n"
716
+ f"**Do not modify the command post from this repo.**\n",
717
+ encoding="utf-8",
718
+ )
719
+ log_step("Installed thin link (.gator/command-post.md)")
720
+
721
+
722
+ def action_install_product_source(target):
723
+ """Write .gator/product-source.json."""
724
+ gator_root = str(COMMAND_POST)
725
+ template_dir = ""
726
+ if (COMMAND_POST / "gator-engine" / "templates" / "gator-starter").is_dir():
727
+ template_dir = "gator-engine/templates/gator-starter"
728
+ elif (COMMAND_POST / "gator-command" / "templates" / "gator-starter").is_dir():
729
+ template_dir = "gator-command/templates/gator-starter"
730
+
731
+ import json
732
+ (target / ".gator" / "product-source.json").write_text(
733
+ json.dumps({
734
+ "gator_root": gator_root,
735
+ "template_dir": template_dir,
736
+ "installed": TODAY,
737
+ "updated": TODAY,
738
+ }, indent=2) + "\n",
739
+ encoding="utf-8",
740
+ )
741
+ log_step(f"Wrote product-source.json (source: {gator_root})")
742
+
743
+
744
+ def action_register(target):
745
+ """Add repo to registry.md."""
746
+ registry = COMMAND_POST / "gator-command" / "registry.md"
747
+ repo_name = target.name
748
+
749
+ if not registry.parent.is_dir():
750
+ return
751
+
752
+ if not registry.exists():
753
+ registry.write_text(
754
+ "# Registry\n\n"
755
+ "Repos governed by this Gator Command post. Updated by `gatorize.sh` and maintained by the Architect.\n\n"
756
+ "| Repo | Local path | Remote | Registered | Status |\n"
757
+ "|------|-----------|--------|------------|--------|\n",
758
+ encoding="utf-8",
759
+ )
760
+
761
+ text = registry.read_text(encoding="utf-8", errors="replace")
762
+ if str(target) in text:
763
+ log_step(f"{repo_name} already registered.")
764
+ return
765
+
766
+ remote, _ = git("remote", "get-url", "origin", cwd=target)
767
+ if not remote:
768
+ remote = "—"
769
+
770
+ with open(registry, "a", encoding="utf-8") as f:
771
+ f.write(f"| {repo_name} | {target} | {remote} | {TODAY} | current |\n")
772
+ print()
773
+ log_step(f"Registered {repo_name} in registry.")
774
+
775
+
776
+ def print_summary(target, scenario, has_command_post):
777
+ """Print SUCCESS banner."""
778
+ repo_name = target.name
779
+ print()
780
+ print(" ================================================================")
781
+ print(f" Gatorized: {repo_name}")
782
+ print(" ================================================================")
783
+ print()
784
+
785
+ # Git instructions
786
+ branch, _ = git("branch", "--show-current", cwd=target)
787
+ if branch == GATOR_BRANCH:
788
+ print(f" You are on the '{GATOR_BRANCH}' branch. To finalize:")
789
+ print()
790
+ print(f" cd {target}")
791
+ print(f" git add -A && git commit -m 'Gatorize: install .gator/ governance layer'")
792
+ print(f" git checkout dev && git merge {GATOR_BRANCH}")
793
+ print(f" git branch -d {GATOR_BRANCH}")
794
+
795
+ print()
796
+ print(" ================================================================")
797
+ print(" SUCCESS")
798
+ print(" ================================================================")
799
+ print()
800
+ print(" What to do now:")
801
+ print()
802
+ print(f" 1. cd {target}")
803
+ print(f" 2. Launch your AI coding tool (Claude Code, Codex, Gemini, etc.)")
804
+ print(f" 3. Type: gator init")
805
+ print(f" 4. The agent will read the constitution and orient to the project.")
806
+ print(f" Your first git commit fires the governance hooks.")
807
+ print()
808
+ print(f" Enforcement: strict (commit gate active)")
809
+ if has_command_post:
810
+ print(f" Command post: linked")
811
+ else:
812
+ print(f" Command post: standalone")
813
+ print()
814
+
815
+
816
+
817
+ # ── memex morph ────────────────────────────────────────────────────────────
818
+
819
+ def detect_legacy_memex(target):
820
+ """Detect what legacy memex structure exists."""
821
+ return {
822
+ "memex": (target / "memex").is_dir(),
823
+ "dot_memex": (target / ".memex").is_dir(),
824
+ "root_constitution": (target / "constitution.md").is_file(),
825
+ "root_constitution_core": (target / "constitution-core.md").is_file(),
826
+ }
827
+
828
+
829
+ def action_morph_memex(target):
830
+ """Morph legacy memex structure into .gator/."""
831
+ legacy = detect_legacy_memex(target)
832
+
833
+ print(" Legacy memex structure detected:")
834
+ if legacy["memex"]:
835
+ print(" memex/ (content layer)")
836
+ if legacy["dot_memex"]:
837
+ print(" .memex/ (system layer)")
838
+ if legacy["root_constitution"]:
839
+ print(" constitution.md (root)")
840
+ if legacy["root_constitution_core"]:
841
+ print(" constitution-core.md (root)")
842
+ print()
843
+ print(" Gatorize will morph this into .gator/ structure:")
844
+ print(" 1. Rename legacy dirs to *.pre-gator/")
845
+ print(" 2. Install fresh .gator/ from templates")
846
+ print(" 3. Copy your content into .gator/")
847
+ print(" 4. Archive root constitution files")
848
+ print()
849
+ if not confirm("Proceed with morph?"):
850
+ print(" Cancelled.")
851
+ sys.exit(0)
852
+
853
+ # Step 1: Rename legacy dirs
854
+ if legacy["memex"]:
855
+ pre = target / "memex.pre-gator"
856
+ if pre.exists():
857
+ log_step("memex.pre-gator/ already exists — skipping rename")
858
+ else:
859
+ _, ok = git("mv", "memex", "memex.pre-gator", cwd=target)
860
+ if not ok:
861
+ shutil.move(str(target / "memex"), str(pre))
862
+ log_step("Renamed memex/ -> memex.pre-gator/")
863
+
864
+ if legacy["dot_memex"]:
865
+ pre = target / ".memex.pre-gator"
866
+ if pre.exists():
867
+ log_step(".memex.pre-gator/ already exists — skipping rename")
868
+ else:
869
+ _, ok = git("mv", ".memex", ".memex.pre-gator", cwd=target)
870
+ if not ok:
871
+ shutil.move(str(target / ".memex"), str(pre))
872
+ log_step("Renamed .memex/ -> .memex.pre-gator/")
873
+
874
+ # Step 2: Install .gator/ (or upgrade if it exists)
875
+ if (target / ".gator").is_dir():
876
+ log_step("Existing .gator/ found — upgrading templates.")
877
+ subprocess.run(
878
+ [sys.executable, str(SCRIPTS_DIR / "gator-update.py"),
879
+ "--path", str(target), "--source", str(COMMAND_POST), "--no-policy"],
880
+ cwd=str(target),
881
+ )
882
+ else:
883
+ action_install_gator(target)
884
+
885
+ # Step 3: Migrate content from memex.pre-gator/
886
+ src = target / "memex.pre-gator"
887
+ gator_dir = target / ".gator"
888
+
889
+ if src.is_dir():
890
+ log_step("Migrating content from memex.pre-gator/ into .gator/...")
891
+
892
+ # Direct-copy content files
893
+ for name in ("mission.md", "roadmap.md", "identity.md", "inbox.md",
894
+ "issues.md", "commit_draft.md", "whiteboard.md",
895
+ "friction.md", "audit-tracker.md"):
896
+ f = src / name
897
+ if f.is_file():
898
+ shutil.copy2(f, gator_dir / name)
899
+ log_step(f" Migrated {name}")
900
+
901
+ # Patterns
902
+ if (src / "patterns").is_dir():
903
+ (gator_dir / "patterns").mkdir(exist_ok=True)
904
+ copy_tree_overlay(src / "patterns", gator_dir / "patterns")
905
+ log_step(" Migrated patterns/")
906
+ elif (src / "patterns.md").is_file():
907
+ shutil.copy2(src / "patterns.md", gator_dir / "patterns.md")
908
+ log_step(" Migrated patterns.md")
909
+
910
+ # Active threads -> threads/ (demoted)
911
+ _migrate_md_dir(src / "active-threads", gator_dir / "threads", "active threads -> threads/ (demoted)")
912
+
913
+ # Threads -> threads/ (merge)
914
+ _migrate_md_dir(src / "threads", gator_dir / "threads", "threads")
915
+
916
+ # Artifacts
917
+ _migrate_md_dir(src / "artifacts", gator_dir / "artifacts", "artifacts")
918
+
919
+ # Charters (skip templates)
920
+ if (src / "charters").is_dir():
921
+ count = 0
922
+ for f in sorted((src / "charters").glob("*.md")):
923
+ if f.name in ("_template.md", "README.md"):
924
+ continue
925
+ shutil.copy2(f, gator_dir / "charters" / f.name)
926
+ count += 1
927
+ if count:
928
+ log_step(f" Migrated {count} charters")
929
+
930
+ # Vault
931
+ if (src / "vault").is_dir() and any((src / "vault").iterdir()):
932
+ (gator_dir / "vault").mkdir(exist_ok=True)
933
+ copy_tree_overlay(src / "vault", gator_dir / "vault")
934
+ log_step(" Migrated vault/")
935
+
936
+ # Reference notes (conflict-aware)
937
+ if (src / "reference-notes").is_dir():
938
+ rn_count = 0
939
+ rn_conflict = 0
940
+ for f in sorted((src / "reference-notes").glob("*.md")):
941
+ dest = gator_dir / "reference-notes" / f.name
942
+ if dest.exists():
943
+ # Check if different
944
+ if f.read_bytes() != dest.read_bytes():
945
+ conflict_name = f.stem + "-project.md"
946
+ shutil.copy2(f, gator_dir / "reference-notes" / conflict_name)
947
+ rn_conflict += 1
948
+ else:
949
+ shutil.copy2(f, dest)
950
+ rn_count += 1
951
+ if rn_count:
952
+ log_step(f" Migrated {rn_count} reference notes")
953
+ if rn_conflict:
954
+ log_step(f" Preserved {rn_conflict} conflicting notes with -project suffix")
955
+
956
+ # Migrate from .memex.pre-gator/
957
+ dotsrc = target / ".memex.pre-gator"
958
+ if dotsrc.is_dir():
959
+ log_step("Migrating system layer from .memex.pre-gator/...")
960
+ if (dotsrc / "roles.yaml").is_file():
961
+ shutil.copy2(dotsrc / "roles.yaml", gator_dir / "roles.yaml")
962
+ log_step(" Migrated roles.yaml")
963
+ if (dotsrc / "policies").is_dir():
964
+ (gator_dir / "policies").mkdir(exist_ok=True)
965
+ copy_tree_overlay(dotsrc / "policies", gator_dir / "policies")
966
+ log_step(" Migrated policies/")
967
+
968
+ # Step 4: Archive root constitution files
969
+ if legacy["root_constitution"] and (target / "constitution.md").is_file():
970
+ _, ok = git("mv", "constitution.md", ".gator/legacy-constitution.md", cwd=target)
971
+ if not ok:
972
+ shutil.move(str(target / "constitution.md"),
973
+ str(gator_dir / "legacy-constitution.md"))
974
+ log_step("Archived constitution.md -> .gator/legacy-constitution.md")
975
+
976
+ if legacy["root_constitution_core"] and (target / "constitution-core.md").is_file():
977
+ _, ok = git("mv", "constitution-core.md", ".gator/legacy-constitution-core.md", cwd=target)
978
+ if not ok:
979
+ shutil.move(str(target / "constitution-core.md"),
980
+ str(gator_dir / "legacy-constitution-core.md"))
981
+ log_step("Archived constitution-core.md -> .gator/legacy-constitution-core.md")
982
+
983
+ # Clean up .gitkeep in dirs that now have content
984
+ for d in ("threads", "artifacts"):
985
+ gitkeep = gator_dir / d / ".gitkeep"
986
+ if gitkeep.exists() and any((gator_dir / d).glob("*.md")):
987
+ gitkeep.unlink()
988
+
989
+ write_gator_version(gator_dir, "morph")
990
+
991
+ print()
992
+ log_step("Morph complete. Pre-gator directories preserved for review:")
993
+ if legacy["memex"]:
994
+ log_step(" memex.pre-gator/ — your original content")
995
+ if legacy["dot_memex"]:
996
+ log_step(" .memex.pre-gator/ — your original system layer")
997
+ log_step("Review .gator/, then delete pre-gator dirs when satisfied.")
998
+
999
+
1000
+ def _migrate_md_dir(src_dir, dest_dir, label):
1001
+ """Migrate .md files from src to dest, handling name collisions."""
1002
+ if not src_dir.is_dir():
1003
+ return
1004
+ count = 0
1005
+ dest_dir.mkdir(exist_ok=True)
1006
+ for f in sorted(src_dir.glob("*.md")):
1007
+ if f.name in ("_TEMPLATE.md", "_template.md"):
1008
+ continue
1009
+ dest = dest_dir / f.name
1010
+ if dest.exists():
1011
+ # Collision — add -legacy suffix
1012
+ dest = dest_dir / (f.stem + "-legacy.md")
1013
+ shutil.copy2(f, dest)
1014
+ count += 1
1015
+ if count:
1016
+ log_step(f" Migrated {count} {label}")
1017
+
1018
+
1019
+ # ── main ───────────────────────────────────────────────────────────────────
1020
+
1021
+ def main():
1022
+ ensure_utf8_stdout()
1023
+
1024
+ parser = argparse.ArgumentParser(
1025
+ description="Install Gator governance into a project repo.",
1026
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1027
+ )
1028
+ parser.add_argument("target", help="Target directory to gatorize")
1029
+ args = parser.parse_args()
1030
+
1031
+ target = Path(args.target).resolve()
1032
+
1033
+ # Create directory if it doesn't exist
1034
+ if not target.exists():
1035
+ print()
1036
+ print(f" Directory does not exist: {target}")
1037
+ if confirm("Create it and gatorize?"):
1038
+ target.mkdir(parents=True)
1039
+ else:
1040
+ print(" Aborted.")
1041
+ sys.exit(0)
1042
+
1043
+ # Self-gatorize guard
1044
+ if target.resolve() == COMMAND_POST.resolve():
1045
+ print(" Error: Target is the command post itself. Gatorize a project repo, not gator-command.")
1046
+ sys.exit(1)
1047
+
1048
+ # Check templates
1049
+ if not TEMPLATES or not TEMPLATES.is_dir():
1050
+ print(f" Error: Templates not found. Are you running this from the gator repo?")
1051
+ sys.exit(1)
1052
+
1053
+ # Detect scenario
1054
+ scenario = detect_scenario(target)
1055
+
1056
+ print()
1057
+ print(" ================================================================")
1058
+ print(f" gatorize.py — {target.name}")
1059
+ print(" ================================================================")
1060
+
1061
+ # Detect command post — a bare gator clone is NOT a command post.
1062
+ # Only treat as command post if identity.md has been populated
1063
+ # (bracket placeholders mean it's still a bare clone).
1064
+ cp = find_command_post(str(SCRIPTS_DIR))
1065
+ has_command_post = False
1066
+ if cp:
1067
+ identity = Path(str(cp)) / "gator-command" / "identity.md"
1068
+ if identity.exists():
1069
+ text = identity.read_text(encoding="utf-8", errors="replace")
1070
+ # Populated = no bracket placeholders
1071
+ has_command_post = "[Your name]" not in text and "[Your role]" not in text
1072
+ else:
1073
+ has_command_post = False
1074
+
1075
+ # Dispatch
1076
+ if scenario == 1:
1077
+ print(" Scenario 1: Fresh directory (no git)")
1078
+ print()
1079
+ action_git_init(target)
1080
+ action_install_gator(target)
1081
+ elif scenario == 2:
1082
+ print(" Scenario 2: Git repo, clean")
1083
+ print()
1084
+ action_feature_branch(target)
1085
+ action_install_gator(target)
1086
+ elif scenario == 3:
1087
+ gen = detect_generation(target)
1088
+ print(f" Gator is already installed (generation {gen}). Running update...")
1089
+ print()
1090
+ result = subprocess.run(
1091
+ [sys.executable, str(SCRIPTS_DIR / "gator-update.py"),
1092
+ "--path", str(target), "--source", str(COMMAND_POST), "--no-policy"],
1093
+ cwd=str(target),
1094
+ )
1095
+ if result.returncode != 0:
1096
+ print()
1097
+ print(" ================================================================")
1098
+ print(" UPDATE FAILED (exit code {})".format(result.returncode))
1099
+ print(" ================================================================")
1100
+ print()
1101
+ sys.exit(result.returncode)
1102
+ # Fall through to common tail — refreshes entry points, outbox, product-source
1103
+ elif scenario == 4:
1104
+ print(" Scenario 4: Legacy memex structure detected")
1105
+ print()
1106
+ action_feature_branch(target)
1107
+ action_morph_memex(target)
1108
+ elif scenario == 5:
1109
+ print(" Scenario 5: Both memex and gator found")
1110
+ print()
1111
+ legacy = detect_legacy_memex(target)
1112
+ print(" Legacy memex:")
1113
+ if legacy["memex"]:
1114
+ print(" memex/")
1115
+ if legacy["dot_memex"]:
1116
+ print(" .memex/")
1117
+ if legacy["root_constitution"]:
1118
+ print(" constitution.md (root)")
1119
+ print(" Gator:")
1120
+ print(" .gator/")
1121
+ print()
1122
+ print(" Options:")
1123
+ print(" [m] Morph — fold memex content into existing .gator/ (recommended)")
1124
+ print(" [u] Upgrade — refresh .gator/ templates, ignore memex dirs")
1125
+ print(" [x] Cancel")
1126
+ print()
1127
+ choice = prompt("Choice", "m/u/x")
1128
+ if choice.lower() == "m":
1129
+ action_feature_branch(target)
1130
+ action_morph_memex(target)
1131
+ elif choice.lower() == "u":
1132
+ gen = detect_generation(target)
1133
+ print(f" Running update (generation {gen})...")
1134
+ result = subprocess.run(
1135
+ [sys.executable, str(SCRIPTS_DIR / "gator-update.py"),
1136
+ "--path", str(target), "--source", str(COMMAND_POST), "--no-policy"],
1137
+ cwd=str(target),
1138
+ )
1139
+ if result.returncode != 0:
1140
+ print()
1141
+ print(" ================================================================")
1142
+ print(" UPDATE FAILED (exit code {})".format(result.returncode))
1143
+ print(" ================================================================")
1144
+ print()
1145
+ sys.exit(result.returncode)
1146
+ # Fall through to common tail
1147
+ else:
1148
+ print(" Cancelled.")
1149
+ sys.exit(0)
1150
+
1151
+ # Common tail (all scenarios except cancel/failure)
1152
+ ensure_repo_gitignore(target)
1153
+ if has_command_post:
1154
+ action_install_thin_link(target)
1155
+ action_install_entry_points(target, has_command_post)
1156
+ action_install_outbox(target)
1157
+ action_install_product_source(target)
1158
+ action_register(target)
1159
+ print_summary(target, scenario, has_command_post)
1160
+
1161
+
1162
+ if __name__ == "__main__":
1163
+ main()