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,916 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ gator-deploy.py - Deploy the public Gator 1.0 repo as a blank command post.
4
+
5
+ Produces a working command post with:
6
+ gator-engine/ - product code (scripts, templates, docs, tests)
7
+ gator-command/ - blank knowledge layer (stubs, empty dirs)
8
+ .gator/ - self-governance (hooks, constitution, stub charters)
9
+ README.md - short, links to docs/
10
+
11
+ The structural separation means users can update product code via
12
+ `git fetch upstream && git checkout upstream/main -- gator-engine/`
13
+ without touching their content in gator-command/.
14
+
15
+ Usage:
16
+ python src/gator_command/scripts/gator-deploy.py ../gator
17
+ python src/gator_command/scripts/gator-deploy.py ../gator --dry-run
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import io
24
+ import re
25
+ import shutil
26
+ import subprocess
27
+ import sys
28
+ from pathlib import Path
29
+
30
+
31
+ # Preserved across deploys — never deleted by remove_target_contents().
32
+ # gator-command/ is user-owned knowledge; .gator/ has user charters and content.
33
+ PRESERVE = {".git", "LICENSE", "gator-command", ".gator"}
34
+
35
+ PUBLIC_PROCEDURES = [
36
+ "branching-standard.md",
37
+ "charter-lookup.md",
38
+ "coding-standard.md",
39
+ "knowledge-capture.md",
40
+ "gatorize-permutations.md",
41
+ "session-schema.md",
42
+ "thin-link-spec.md",
43
+ ]
44
+
45
+ DOC_STUBS = {} # Real docs now live in docs/ at repo root
46
+
47
+ COMMAND_POST_STUBS = {
48
+ "mission.md": (
49
+ "# Mission\n\n## What We're Building\n\n"
50
+ "[What is this project? One paragraph.]\n\n"
51
+ "## Why It Exists\n\n[What problem does it solve?]\n"
52
+ ),
53
+ "roadmap.md": (
54
+ "# Roadmap\n\n[Priority-ordered. Updated as items complete or priorities shift.]\n\n"
55
+ "**Status key**: Done · Building · Designed · Considering · Deferred\n"
56
+ ),
57
+ "inbox.md": (
58
+ "# Inbox\n\nDrop anything here. No formatting needed. "
59
+ "The chat agent triages at session open.\n\n---\n\n"
60
+ ),
61
+ "identity.md": (
62
+ "---\noperating-mode: designer\n---\n\n# Identity\n\n## Basics\n\n"
63
+ "- **Name**: [Your name]\n"
64
+ "- **Location**: [Your timezone]\n"
65
+ "- **Role**: [Your role]\n\n"
66
+ "## Background\n\n[Brief background]\n"
67
+ ),
68
+ "issues.md": (
69
+ "# Issues\n\nActive bugs, blockers, and known fragilities. "
70
+ "Checked at session open.\n\n**Status key**: Open · Working · Resolved\n\n---\n\n"
71
+ ),
72
+ "registry.md": (
73
+ "# Registry\n\nRepos governed by this Gator Command post. "
74
+ "Updated by `gatorize.sh` and maintained by the Architect.\n\n"
75
+ "| Repo | Local path | Remote | Registered | Status |\n"
76
+ "|------|-----------|--------|------------|--------|\n"
77
+ ),
78
+ "commit_draft.md": (
79
+ "---\nmessage: \"\"\nchange-type:\nsignificance:\ndecision-tags: []\n"
80
+ "agent:\narchitect:\n---\n\n# Session Change Log\n\n"
81
+ ),
82
+ }
83
+
84
+ COMMAND_POST_DIRS = [
85
+ "active-threads",
86
+ "threads",
87
+ "artifacts",
88
+ "vault",
89
+ "patterns",
90
+ "policies",
91
+ ]
92
+
93
+ SELF_GOVERNANCE_STUBS = {
94
+ "commit_draft.md": (
95
+ "---\nmessage: \"\"\nchange-type:\nsignificance:\ndecision-tags: []\n"
96
+ "agent:\narchitect:\n---\n\n# Session Change Log\n\n"
97
+ ),
98
+ "whiteboard.md": "# Whiteboard\n\nNo findings.\n",
99
+ "issues.md": "# Issues\n\n---\n\n",
100
+ "mission.md": (
101
+ "# Mission\n\n"
102
+ "This Gator Command post governs AI-assisted development across "
103
+ "a fleet of project repos.\n\n"
104
+ "See `gator-command/mission.md` for your project-specific mission "
105
+ "once populated during the concierge bootstrap.\n"
106
+ ),
107
+ "command-post.md": (
108
+ "command-post: ..\nversion: 2026-05-30\n\n# Gator Command Post\n\n"
109
+ "This repo is the command post.\n"
110
+ ),
111
+ }
112
+
113
+ SELF_GOVERNANCE_DIRS = ["threads", "artifacts", "sessions", "blueprints"]
114
+
115
+ ENTRY_POINTS = [
116
+ ("CLAUDE.md", "# Claude Code Entry Point"),
117
+ ("AGENTS.md", "# Codex Entry Point"),
118
+ ("GEMINI.md", "# Gemini Entry Point"),
119
+ ]
120
+
121
+ ENTRY_BODY = (
122
+ "\nThis is a **Gator Command post**.\n\n"
123
+ "Read [`constitution.md`](constitution.md) before your first response.\n\n"
124
+ "**Fresh instance detection**: If `gator-command/identity.md` still has bracket "
125
+ "placeholders (e.g., `[Your name]`), this is a first session. Greet the Architect and "
126
+ "ask what they're working on.\n\n"
127
+ "**Returning sessions**: Follow the session-opening procedure in the constitution.\n\n"
128
+ '**"gator init"**: run `python gator-engine/scripts/gator-init-command-post.py` '
129
+ "and display the output exactly as printed. This is the command post boot sequence.\n\n"
130
+ '**"gator dashboard"**: run `python gator-engine/scripts/gator-dashboard.py` '
131
+ "to open the governance dashboard in your browser.\n"
132
+ )
133
+
134
+ ROOT_GITIGNORE = (
135
+ "# Vault — sensitive material and large files that must not be committed\n"
136
+ "gator-command/vault/\n"
137
+ ".gator/vault/\n"
138
+ "\n"
139
+ "__pycache__/\n"
140
+ "*.pyc\n"
141
+ ".pytest_tmp/\n"
142
+ ".pytest_cache/\n"
143
+ ".vscode/\n"
144
+ ".claude/settings.local.json\n"
145
+ "\n"
146
+ "# Ephemeral commit artifacts — reset by hooks\n"
147
+ ".gator/commit_draft.md\n"
148
+ ".gator/commit_issues.md\n"
149
+ "gator-command/commit_draft.md\n"
150
+ "\n"
151
+ "# Override request/approval artifacts — ephemeral\n"
152
+ ".gator/override-request.json\n"
153
+ ".gator/override-approved.json\n"
154
+ "\n"
155
+ "# Rolling active session logs\n"
156
+ ".gator/sessions/_active/\n"
157
+ "\n"
158
+ "# Machine-local state (not portable)\n"
159
+ ".gator/product-source.json\n"
160
+ )
161
+
162
+
163
+ def find_command_post():
164
+ """Find the source command post root."""
165
+ script_dir = Path(__file__).resolve().parent
166
+ # Scripts are now at src/gator_command/scripts/ — repo root is 3 levels up
167
+ candidate = script_dir.parent.parent.parent
168
+ if (candidate / "gator-command" / "mission.md").exists():
169
+ return candidate
170
+ # Fallback: try 2 levels up (old layout or deployed gator-engine/)
171
+ candidate = script_dir.parent.parent
172
+ if (candidate / "gator-command" / "mission.md").exists():
173
+ return candidate
174
+ return None
175
+
176
+
177
+ def get_source_version(source):
178
+ """Get version string from source repo via git describe.
179
+
180
+ Returns a string like 'v1.0.0-5-gabcdef1' between tags, or 'v1.0.0'
181
+ on an exact tag. Falls back to VERSION file content, then 'dev'.
182
+ """
183
+ try:
184
+ result = subprocess.run(
185
+ ["git", "describe", "--tags", "--always"],
186
+ capture_output=True, encoding="utf-8", errors="replace", cwd=source, timeout=5,
187
+ )
188
+ if result.returncode == 0 and result.stdout.strip():
189
+ version = result.stdout.strip()
190
+ if version.startswith("v"):
191
+ return version
192
+ except (OSError, subprocess.TimeoutExpired):
193
+ pass
194
+
195
+ version_file = source / "VERSION"
196
+ if version_file.exists():
197
+ text = version_file.read_text(encoding="utf-8").strip()
198
+ if text:
199
+ return f"v{text}" if not text.startswith("v") else text
200
+
201
+ return "dev"
202
+
203
+
204
+ def get_source_short_hash(source):
205
+ """Get the short commit hash of the source repo HEAD."""
206
+ try:
207
+ result = subprocess.run(
208
+ ["git", "rev-parse", "--short", "HEAD"],
209
+ capture_output=True, encoding="utf-8", errors="replace", cwd=source, timeout=5,
210
+ )
211
+ if result.returncode == 0 and result.stdout.strip():
212
+ return result.stdout.strip()
213
+ except (OSError, subprocess.TimeoutExpired):
214
+ pass
215
+ return None
216
+
217
+
218
+ def stamp_readme_version(target, version, source_hash=None):
219
+ """Replace the static version in README.md with the deploy version and source hash."""
220
+ readme = target / "README.md"
221
+ if not readme.exists():
222
+ return
223
+ text = readme.read_text(encoding="utf-8")
224
+
225
+ # Build the replacement: "v1.0.0 (b8b1542) · navigation..." or just version
226
+ version_stamp = version
227
+ if source_hash:
228
+ version_stamp = f"{version} ({source_hash})"
229
+
230
+ # Match the version line in the ASCII banner: " v1.0.0 · ..." or " v1.0.0 (hash) · ..."
231
+ text = re.sub(
232
+ r"^( )v[\d]+\.[\d]+\.[\d]+\S*(?:\s+\([a-f0-9]+\))?( · .*)$",
233
+ rf"\g<1>{version_stamp}\g<2>",
234
+ text,
235
+ count=1,
236
+ flags=re.MULTILINE,
237
+ )
238
+ readme.write_text(text, encoding="utf-8")
239
+
240
+
241
+ def remove_target_contents(target, dry_run=False):
242
+ """Remove everything except files explicitly preserved."""
243
+ removed = []
244
+ for item in sorted(target.iterdir()):
245
+ if item.name in PRESERVE:
246
+ continue
247
+ if item.is_dir():
248
+ if not dry_run:
249
+ shutil.rmtree(item)
250
+ removed.append(f"{item.name}/")
251
+ else:
252
+ if not dry_run:
253
+ item.unlink()
254
+ removed.append(item.name)
255
+ return removed
256
+
257
+
258
+ def write_file(path, content, dry_run=False):
259
+ """Write a file, creating parent dirs."""
260
+ if dry_run:
261
+ return
262
+ path.parent.mkdir(parents=True, exist_ok=True)
263
+ path.write_text(content, encoding="utf-8")
264
+
265
+
266
+ def copy_file(src, dest, dry_run=False):
267
+ """Copy a file, creating parent dirs."""
268
+ if dry_run:
269
+ return
270
+ dest.parent.mkdir(parents=True, exist_ok=True)
271
+ shutil.copy2(src, dest)
272
+
273
+
274
+ def copy_tree_mirror(src, dest, dry_run=False):
275
+ """Mirror a directory tree — exact copy, prune stale destination files.
276
+
277
+ Used for product-owned directories (gator-engine/, docs/, .github/)
278
+ where the deploy output should exactly match the source.
279
+ """
280
+ if dry_run:
281
+ return
282
+ dest.parent.mkdir(parents=True, exist_ok=True)
283
+ if dest.exists():
284
+ shutil.rmtree(dest)
285
+ shutil.copytree(
286
+ src, dest,
287
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
288
+ )
289
+
290
+
291
+ def copy_tree_overlay(src, dest, dry_run=False):
292
+ """Overlay a directory tree — update same-named files, preserve extras.
293
+
294
+ Used for user-owned directories (gator-command/procedures/,
295
+ reference-notes/) where user-authored content must survive deploys.
296
+ """
297
+ if dry_run:
298
+ return
299
+ dest.mkdir(parents=True, exist_ok=True)
300
+ for item in src.iterdir():
301
+ if item.name in ("__pycache__",) or item.suffix == ".pyc":
302
+ continue
303
+ target = dest / item.name
304
+ if item.is_dir():
305
+ copy_tree_overlay(item, target, dry_run)
306
+ else:
307
+ shutil.copy2(item, target)
308
+
309
+
310
+ def write_stub_files(base_dir, files, dry_run=False):
311
+ """Write a mapping of relative paths to file contents.
312
+
313
+ Only creates files that don't already exist — user-owned content
314
+ is never overwritten. This is the deploy equivalent of gatorize.sh's
315
+ overlay-not-replace principle.
316
+ """
317
+ for relative_path, content in files.items():
318
+ dest = base_dir / relative_path
319
+ if dest.exists():
320
+ continue
321
+ write_file(dest, content, dry_run)
322
+
323
+
324
+ def ensure_gitkeep_dirs(base_dir, dir_names, dry_run=False):
325
+ """Create empty directories tracked with .gitkeep."""
326
+ for dirname in dir_names:
327
+ path = base_dir / dirname
328
+ if dry_run:
329
+ continue
330
+ path.mkdir(parents=True, exist_ok=True)
331
+ (path / ".gitkeep").touch()
332
+
333
+
334
+ def copy_matching_files(src_dir, dest_dir, suffixes, results, result_prefix, dry_run=False):
335
+ """Copy files from a directory when their suffix matches."""
336
+ if not dry_run:
337
+ dest_dir.mkdir(parents=True, exist_ok=True)
338
+
339
+ for src in sorted(src_dir.iterdir()):
340
+ if src.is_file() and src.suffix in suffixes:
341
+ copy_file(src, dest_dir / src.name, dry_run)
342
+ results["created"].append(f"{result_prefix}/{src.name}")
343
+
344
+
345
+ def copy_optional_tree(src_dir, dest_dir, results, result_label, dry_run=False,
346
+ overlay=False):
347
+ """Copy a directory if it exists and record a concise result label.
348
+
349
+ overlay=False (default): mirror mode — exact copy, prune stale files.
350
+ Use for product-owned directories.
351
+ overlay=True: overlay mode — update same-named, preserve extras.
352
+ Use for user-owned directories.
353
+ """
354
+ if not src_dir.is_dir():
355
+ return
356
+ if overlay:
357
+ copy_tree_overlay(src_dir, dest_dir, dry_run)
358
+ else:
359
+ copy_tree_mirror(src_dir, dest_dir, dry_run)
360
+ count = sum(1 for item in src_dir.rglob("*") if item.is_file())
361
+ results["created"].append(f"{result_label} ({count} files)")
362
+
363
+
364
+ def copy_optional_file(src, dest, results, result_label, dry_run=False):
365
+ """Copy a file if it exists and record the destination label."""
366
+ if not src.exists():
367
+ return
368
+ copy_file(src, dest, dry_run)
369
+ results["created"].append(result_label)
370
+
371
+
372
+ def _fixup_test_paths(tests_dir, dry_run=False):
373
+ """Rewrite path references in test files for the deployed layout.
374
+
375
+ In the command-post layout, tests/ is a sibling of gator-command/scripts/.
376
+ In the deployed layout, tests live at gator-engine/tests/ and scripts at
377
+ gator-engine/scripts/. This function patches conftest.py and
378
+ test_pre_commit.py so imports resolve correctly after deploy.
379
+ """
380
+ if dry_run:
381
+ return
382
+
383
+ # conftest.py: SCRIPTS_DIR should resolve to gator-engine/scripts/
384
+ # (parent.parent / "scripts" from gator-engine/tests/)
385
+ conftest = tests_dir / "conftest.py"
386
+ if conftest.is_file():
387
+ text = conftest.read_text(encoding="utf-8")
388
+ # Replace any gator-command/scripts path with the deployed relative path
389
+ text = text.replace(
390
+ 'Path(__file__).parent.parent / "src" / "gator_command" / "scripts"',
391
+ 'Path(__file__).resolve().parent.parent / "scripts"',
392
+ )
393
+ conftest.write_text(text, encoding="utf-8")
394
+
395
+ # test_pre_commit.py: pre-commit hook lives at repo-root/.gator/scripts/
396
+ # (parent.parent.parent from gator-engine/tests/)
397
+ test_pc = tests_dir / "test_pre_commit.py"
398
+ if test_pc.is_file():
399
+ text = test_pc.read_text(encoding="utf-8")
400
+ text = text.replace(
401
+ 'Path(__file__).parent.parent / ".gator" / "scripts"',
402
+ 'Path(__file__).resolve().parent.parent.parent / ".gator" / "scripts"',
403
+ )
404
+ test_pc.write_text(text, encoding="utf-8")
405
+
406
+
407
+ def build_engine(source, target, results, dry_run=False):
408
+ """Build gator-engine/, the shipped product surface."""
409
+ pkg = source / "src" / "gator_command"
410
+ engine = target / "gator-engine"
411
+
412
+ copy_matching_files(
413
+ pkg / "scripts",
414
+ engine / "scripts",
415
+ {".py", ".sh"},
416
+ results,
417
+ "gator-engine/scripts",
418
+ dry_run,
419
+ )
420
+
421
+ copy_optional_tree(
422
+ pkg / "scripts" / "dashboard",
423
+ engine / "scripts" / "dashboard",
424
+ results,
425
+ "gator-engine/scripts/dashboard/",
426
+ dry_run,
427
+ )
428
+ copy_optional_tree(
429
+ pkg / "templates" / "gator-starter",
430
+ engine / "templates" / "gator-starter",
431
+ results,
432
+ "gator-engine/templates/gator-starter/",
433
+ dry_run,
434
+ )
435
+ copy_optional_tree(
436
+ source / "tests",
437
+ engine / "tests",
438
+ results,
439
+ "gator-engine/tests/",
440
+ dry_run,
441
+ )
442
+ _fixup_test_paths(engine / "tests", dry_run)
443
+
444
+ docs_dest = engine / "docs"
445
+ gc = source / "gator-command" # knowledge layer (didn't move)
446
+ copy_optional_file(
447
+ gc / "charters" / "README.md",
448
+ docs_dest / "charter-philosophy.md",
449
+ results,
450
+ "gator-engine/docs/charter-philosophy.md",
451
+ dry_run,
452
+ )
453
+ copy_optional_file(
454
+ gc / "org-policy.md",
455
+ docs_dest / "org-policy-example.md",
456
+ results,
457
+ "gator-engine/docs/org-policy-example.md",
458
+ dry_run,
459
+ )
460
+
461
+ for name in PUBLIC_PROCEDURES:
462
+ copy_optional_file(
463
+ gc / "procedures" / name,
464
+ docs_dest / "procedures" / name,
465
+ results,
466
+ f"gator-engine/docs/procedures/{name}",
467
+ dry_run,
468
+ )
469
+
470
+ write_stub_files(docs_dest, DOC_STUBS, dry_run)
471
+ results["created"].extend(
472
+ f"gator-engine/docs/{name}" for name in DOC_STUBS
473
+ )
474
+
475
+ copy_optional_file(
476
+ source / "pytest.ini",
477
+ engine / "pytest.ini",
478
+ results,
479
+ "gator-engine/pytest.ini",
480
+ dry_run,
481
+ )
482
+
483
+
484
+ def build_command_post_layer(source, target, results, dry_run=False):
485
+ """Build gator-command/, the blank user-owned knowledge layer."""
486
+ gc = source / "gator-command"
487
+ dest = target / "gator-command"
488
+
489
+ write_stub_files(dest, COMMAND_POST_STUBS, dry_run)
490
+ results["created"].append("gator-command/ (knowledge layer stubs)")
491
+
492
+ copy_optional_file(
493
+ gc / "org-policy.md",
494
+ dest / "org-policy.md",
495
+ results,
496
+ "gator-command/org-policy.md",
497
+ dry_run,
498
+ )
499
+
500
+ ensure_gitkeep_dirs(dest, COMMAND_POST_DIRS, dry_run)
501
+
502
+ for subdir in ("procedures", "reference-notes"):
503
+ copy_optional_tree(
504
+ gc / subdir,
505
+ dest / subdir,
506
+ results,
507
+ f"gator-command/{subdir}/",
508
+ dry_run,
509
+ overlay=True, # user-owned: preserve extras
510
+ )
511
+
512
+ write_file(
513
+ dest / "scripts" / "README.md",
514
+ "# Scripts\n\nProduct scripts live in `gator-engine/scripts/`.\n\n"
515
+ "This directory is for command-post-specific scripts you create.\n",
516
+ dry_run,
517
+ )
518
+ write_file(
519
+ dest / "templates" / "README.md",
520
+ "# Templates\n\nThe gator-starter template lives in "
521
+ "`gator-engine/templates/gator-starter/`.\n",
522
+ dry_run,
523
+ )
524
+
525
+
526
+ def copy_template_scripts(starter_scripts, dest_scripts, dry_run=False):
527
+ """Copy template scripts, including nested hook directories."""
528
+ if not starter_scripts.is_dir():
529
+ return
530
+
531
+ for src in sorted(starter_scripts.iterdir()):
532
+ if src.name == "__pycache__":
533
+ continue
534
+ if src.is_file():
535
+ if src.suffix == ".pyc":
536
+ continue
537
+ copy_file(src, dest_scripts / src.name, dry_run)
538
+ continue
539
+ for nested in sorted(src.iterdir()):
540
+ if nested.is_file() and nested.suffix != ".pyc":
541
+ copy_file(nested, dest_scripts / src.name / nested.name, dry_run)
542
+
543
+
544
+ def copy_flat_directory(src_dir, dest_dir, dry_run=False):
545
+ """Copy only direct child files from one directory to another."""
546
+ if not src_dir.is_dir():
547
+ return
548
+ for src in sorted(src_dir.iterdir()):
549
+ if src.is_file():
550
+ copy_file(src, dest_dir / src.name, dry_run)
551
+
552
+
553
+ def build_self_governance(source, target, results, dry_run=False):
554
+ """Build .gator/, the repo's own governance scaffolding."""
555
+ starter = source / "src" / "gator_command" / "templates" / "gator-starter"
556
+ dest = target / ".gator"
557
+
558
+ copy_optional_file(
559
+ starter / "constitution.md",
560
+ dest / "constitution.md",
561
+ results,
562
+ ".gator/constitution.md",
563
+ dry_run,
564
+ )
565
+ copy_optional_file(
566
+ starter / "gator-start-up.md",
567
+ dest / "gator-start-up.md",
568
+ results,
569
+ ".gator/gator-start-up.md",
570
+ dry_run,
571
+ )
572
+
573
+ for name in ("README.md", "_template.md"):
574
+ copy_optional_file(
575
+ starter / "charters" / name,
576
+ dest / "charters" / name,
577
+ results,
578
+ f".gator/charters/{name}",
579
+ dry_run,
580
+ )
581
+ index_path = dest / "charters" / "INDEX.md"
582
+ if not index_path.exists():
583
+ write_file(
584
+ index_path,
585
+ "# Charter Index\n\n| If you're changing... | Read these charters |\n"
586
+ "|---|---|\n| | |\n",
587
+ dry_run,
588
+ )
589
+
590
+ copy_template_scripts(starter / "scripts", dest / "scripts", dry_run)
591
+
592
+ for subdir in ("reference-notes", "procedures"):
593
+ copy_flat_directory(starter / subdir, dest / subdir, dry_run)
594
+
595
+ write_stub_files(dest, SELF_GOVERNANCE_STUBS, dry_run)
596
+ ensure_gitkeep_dirs(dest, SELF_GOVERNANCE_DIRS, dry_run)
597
+ results["created"].append(".gator/ (self-governance)")
598
+
599
+
600
+ def build_root_files(source, target, results, dry_run=False):
601
+ """Build repo-root files for the deployed public command post."""
602
+ starter = source / "src" / "gator_command" / "templates" / "gator-starter"
603
+
604
+ copy_optional_file(
605
+ starter / "constitution.md",
606
+ target / "constitution.md",
607
+ results,
608
+ "constitution.md",
609
+ dry_run,
610
+ )
611
+
612
+ for filename, header in ENTRY_POINTS:
613
+ write_file(target / filename, f"{header}\n{ENTRY_BODY}", dry_run)
614
+ results["created"].append(filename)
615
+
616
+ copy_optional_file(
617
+ source / "gator-command" / "deploy" / "README.md",
618
+ target / "README.md",
619
+ results,
620
+ "README.md",
621
+ dry_run,
622
+ )
623
+
624
+ # User-facing docs (persona pages, feature docs)
625
+ copy_optional_tree(
626
+ source / "docs",
627
+ target / "docs",
628
+ results,
629
+ "docs/",
630
+ dry_run,
631
+ )
632
+
633
+ copy_optional_file(
634
+ source / "VERSION",
635
+ target / "VERSION",
636
+ results,
637
+ "VERSION",
638
+ dry_run,
639
+ )
640
+ copy_optional_file(
641
+ source / "mkdocs.yml",
642
+ target / "mkdocs.yml",
643
+ results,
644
+ "mkdocs.yml",
645
+ dry_run,
646
+ )
647
+ copy_optional_file(
648
+ source / "CHANGELOG.md",
649
+ target / "CHANGELOG.md",
650
+ results,
651
+ "CHANGELOG.md",
652
+ dry_run,
653
+ )
654
+
655
+ copy_optional_file(
656
+ source / "CONTRIBUTING.md",
657
+ target / "CONTRIBUTING.md",
658
+ results,
659
+ "CONTRIBUTING.md",
660
+ dry_run,
661
+ )
662
+ copy_optional_file(
663
+ source / "SECURITY.md",
664
+ target / "SECURITY.md",
665
+ results,
666
+ "SECURITY.md",
667
+ dry_run,
668
+ )
669
+ copy_optional_tree(
670
+ source / ".github",
671
+ target / ".github",
672
+ results,
673
+ ".github/",
674
+ dry_run,
675
+ )
676
+
677
+ write_file(target / ".gitignore", ROOT_GITIGNORE, dry_run)
678
+ results["created"].append(".gitignore")
679
+
680
+
681
+ def find_last_deploy_source_hash(target):
682
+ """Find the source repo hash from the most recent deploy commit in the target.
683
+
684
+ Looks for "From gator-command <hash>" in recent commit messages.
685
+ Returns the hash string, or None if not found.
686
+ """
687
+ try:
688
+ result = subprocess.run(
689
+ ["git", "log", "--format=%B", "-20"],
690
+ capture_output=True, encoding="utf-8", errors="replace", cwd=target, timeout=5,
691
+ )
692
+ if result.returncode != 0:
693
+ return None
694
+ for line in result.stdout.splitlines():
695
+ line = line.strip().lstrip("- ")
696
+ # Match both "From gator-command <hash>" and "Deploy from gator-command <hash> (...)"
697
+ if "from gator-command" in line.lower():
698
+ parts = line.split()
699
+ # Find the token after "gator-command"
700
+ for i, token in enumerate(parts):
701
+ if token == "gator-command" and i + 1 < len(parts):
702
+ return parts[i + 1].rstrip(":").split("(")[0].strip()
703
+ break
704
+ except (OSError, subprocess.TimeoutExpired):
705
+ pass
706
+ return None
707
+
708
+
709
+ def build_deploy_changelog(source, target):
710
+ """Build a changelog from source repo commits since the last deploy.
711
+
712
+ Returns a list of one-line commit summaries, newest first.
713
+ """
714
+ base_hash = find_last_deploy_source_hash(target)
715
+ if not base_hash:
716
+ # No previous deploy found — use last 10 commits as context
717
+ log_range = ["-10"]
718
+ else:
719
+ log_range = [f"{base_hash}..HEAD"]
720
+
721
+ try:
722
+ result = subprocess.run(
723
+ ["git", "log", "--oneline", "--no-decorate"] + log_range,
724
+ capture_output=True, encoding="utf-8", errors="replace", cwd=source, timeout=10,
725
+ )
726
+ if result.returncode == 0 and result.stdout.strip():
727
+ return result.stdout.strip().splitlines()
728
+ except (OSError, subprocess.TimeoutExpired):
729
+ pass
730
+ return []
731
+
732
+
733
+ def write_deploy_commit_draft(target, source, version, dry_run=False):
734
+ """Write the target repo's commit_draft.md with the deploy changelog.
735
+
736
+ Populates the frontmatter and body so the deploy commit carries
737
+ the full source changelog, not just "deploy from hash."
738
+ """
739
+ if dry_run:
740
+ return
741
+
742
+ changelog = build_deploy_changelog(source, target)
743
+
744
+ # Get the current source HEAD for the "From" reference
745
+ try:
746
+ result = subprocess.run(
747
+ ["git", "rev-parse", "--short", "HEAD"],
748
+ capture_output=True, encoding="utf-8", errors="replace", cwd=source, timeout=5,
749
+ )
750
+ source_hash = result.stdout.strip() if result.returncode == 0 else "unknown"
751
+ except (OSError, subprocess.TimeoutExpired):
752
+ source_hash = "unknown"
753
+
754
+ # Build the body
755
+ body_lines = [f"- Deploy from gator-command {source_hash} ({version})"]
756
+ for line in changelog:
757
+ # Each line is "hash summary" — prefix with bullet
758
+ parts = line.split(" ", 1)
759
+ if len(parts) == 2:
760
+ body_lines.append(f"- {parts[1]}")
761
+ else:
762
+ body_lines.append(f"- {line}")
763
+
764
+ body = "\n".join(body_lines)
765
+
766
+ # Summarize for the message field — use first changelog entry as description
767
+ if changelog:
768
+ # Each changelog line is "hash summary" — extract the summary part
769
+ first = changelog[0].split(" ", 1)
770
+ summary = first[1] if len(first) == 2 else changelog[0]
771
+ # Strip redundant "Deploy:" prefix to avoid "Deploy v1.0.0: Deploy: ..."
772
+ if summary.lower().startswith("deploy:"):
773
+ summary = summary[7:].strip()
774
+ elif summary.lower().startswith("deploy "):
775
+ summary = summary[7:].strip()
776
+ if len(changelog) == 1:
777
+ message = f"Deploy {version}: {summary}"
778
+ else:
779
+ message = f"Deploy {version}: {summary} (+{len(changelog) - 1} more)"
780
+ else:
781
+ message = f"Deploy {version} from gator-command {source_hash}"
782
+
783
+ draft = (
784
+ f"---\n"
785
+ f'message: "{message}"\n'
786
+ f"change-type: deploy\n"
787
+ f"significance: routine\n"
788
+ f"decision-tags: [deploy]\n"
789
+ f"agent:\n"
790
+ f"architect:\n"
791
+ f"---\n\n"
792
+ f"# Session Change Log\n\n"
793
+ f"{body}\n"
794
+ )
795
+
796
+ draft_path = target / ".gator" / "commit_draft.md"
797
+ draft_path.parent.mkdir(parents=True, exist_ok=True)
798
+ draft_path.write_text(draft, encoding="utf-8")
799
+
800
+
801
+ def deploy(source, target, dry_run=False):
802
+ """Build the complete public repo structure."""
803
+ results = {"nuked": remove_target_contents(target, dry_run), "created": []}
804
+ build_engine(source, target, results, dry_run)
805
+ build_command_post_layer(source, target, results, dry_run)
806
+ build_self_governance(source, target, results, dry_run)
807
+ build_root_files(source, target, results, dry_run)
808
+
809
+ # Stamp the README with the source repo's git describe version + commit hash
810
+ version = get_source_version(source)
811
+ source_hash = get_source_short_hash(source)
812
+ if not dry_run:
813
+ stamp_readme_version(target, version, source_hash)
814
+
815
+ # Write the deploy commit_draft in the target with source changelog
816
+ write_deploy_commit_draft(target, source, version, dry_run)
817
+
818
+ # Pre-approve charter-skip override — deploy targets typically have blank
819
+ # charters, so the pre-commit hook would always block on code changes.
820
+ if not dry_run:
821
+ override_file = target / ".gator" / ".override"
822
+ override_file.write_text("charter-skip", encoding="utf-8")
823
+
824
+ return results
825
+
826
+
827
+ def target_looks_initialized(target):
828
+ """Detect whether the target appears to contain real command-post content."""
829
+ identity = target / "gator-command" / "identity.md"
830
+ threads_dir = target / "gator-command" / "active-threads"
831
+ if not identity.exists():
832
+ return False
833
+
834
+ text = identity.read_text(encoding="utf-8", errors="replace")
835
+ if "[Your name]" in text:
836
+ return False
837
+
838
+ if not threads_dir.is_dir():
839
+ return True
840
+
841
+ for entry in threads_dir.iterdir():
842
+ if entry.name != ".gitkeep":
843
+ return True
844
+ return False
845
+
846
+
847
+ def print_report(results, dry_run=False):
848
+ """Render a concise deployment summary."""
849
+ if sys.stdout.encoding != "utf-8":
850
+ sys.stdout = io.TextIOWrapper(
851
+ sys.stdout.buffer,
852
+ encoding="utf-8",
853
+ errors="replace",
854
+ )
855
+
856
+ mode = " (dry run)" if dry_run else ""
857
+ print(f"\n gator deploy{mode}\n")
858
+
859
+ if results["nuked"]:
860
+ print(f" Cleaned ({len(results['nuked'])}):")
861
+ for path in results["nuked"]:
862
+ print(f" x {path}")
863
+ print()
864
+
865
+ print(f" Created ({len(results['created'])}):")
866
+ for path in results["created"]:
867
+ print(f" + {path}")
868
+ print()
869
+
870
+
871
+ def main():
872
+ parser = argparse.ArgumentParser(
873
+ description="Deploy Gator 1.0 as a blank command post."
874
+ )
875
+ parser.add_argument("target", help="Path to the public gator repo")
876
+ parser.add_argument("--dry-run", "-n", action="store_true")
877
+ parser.add_argument(
878
+ "--force",
879
+ action="store_true",
880
+ help="Allow deploying into a target that appears to contain real command-post content.",
881
+ )
882
+ args = parser.parse_args()
883
+
884
+ source = find_command_post()
885
+ if not source:
886
+ print(" Error: not in a gator-command repo.", file=sys.stderr)
887
+ sys.exit(1)
888
+
889
+ target = Path(args.target).resolve()
890
+ if not target.is_dir():
891
+ print(f" Error: target does not exist: {target}", file=sys.stderr)
892
+ sys.exit(1)
893
+
894
+ if target_looks_initialized(target) and not args.force:
895
+ print(" Error: target appears to be an active command post with real content.", file=sys.stderr)
896
+ print(" Use --force to override.", file=sys.stderr)
897
+ sys.exit(1)
898
+
899
+ print(f" Source: {source}")
900
+ print(f" Target: {target}")
901
+
902
+ results = deploy(source, target, dry_run=args.dry_run)
903
+ print_report(results, dry_run=args.dry_run)
904
+
905
+ if not args.dry_run:
906
+ print(" Structure:")
907
+ print(" gator-engine/ <- product code (refreshed each deploy)")
908
+ print(" gator-command/ <- your knowledge layer (preserved, stubs only if missing)")
909
+ print(" .gator/ <- self-governance (scripts refreshed, content preserved)")
910
+ print()
911
+ print(" Next: cd target && git add -A && git commit")
912
+ print()
913
+
914
+
915
+ if __name__ == "__main__":
916
+ main()