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,459 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ gator-pulse.py — Generate a strategic operations brief for a governed repo.
4
+
5
+ Reads the .gator/ knowledge layer (roadmap, issues, inbox, mission,
6
+ sessions) and git history, writes a structured summary to .gator/pulse.md.
7
+
8
+ Sections: Top 5 Next Steps, Roadmap Check, Top 5 Priorities,
9
+ Issues & Blockers, Recent Activity.
10
+
11
+ Usage:
12
+ python gator-pulse.py [--path /repo] [--days 7] [--dry-run]
13
+
14
+ @reads: git log, .gator/roadmap.md, .gator/issues.md, .gator/inbox.md,
15
+ .gator/mission.md, .gator/sessions/
16
+ @writes: .gator/pulse.md
17
+ """
18
+
19
+ import argparse
20
+ import re
21
+ import subprocess
22
+ import sys
23
+ from datetime import datetime, timedelta
24
+ from pathlib import Path
25
+
26
+ SCRIPTS_DIR = Path(__file__).resolve().parent
27
+ sys.path.insert(0, str(SCRIPTS_DIR))
28
+
29
+ from gator_core import find_gator_root, ensure_utf8_stdout # noqa: E402
30
+
31
+
32
+ def git(*args, cwd=None):
33
+ """Run a git command, return (stdout, ok)."""
34
+ try:
35
+ result = subprocess.run(
36
+ ["git"] + list(args),
37
+ capture_output=True, text=True, timeout=30, cwd=cwd,
38
+ )
39
+ return result.stdout.strip(), result.returncode == 0
40
+ except Exception:
41
+ return "", False
42
+
43
+
44
+ # ── evidence gathering ─────────────────────────────────────────────────────
45
+
46
+ def read_file(path, max_lines=200):
47
+ """Read a file, return content or empty string."""
48
+ try:
49
+ text = path.read_text(encoding="utf-8", errors="replace")
50
+ lines = text.splitlines()
51
+ return "\n".join(lines[:max_lines])
52
+ except (OSError, UnicodeDecodeError):
53
+ return ""
54
+
55
+
56
+ def get_recent_commits(repo_path, days=7, limit=30):
57
+ """Get recent commits."""
58
+ since = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
59
+ out, ok = git(
60
+ "log", f"--since={since}", "--format=%h|%s|%ai",
61
+ f"-{limit}", cwd=str(repo_path),
62
+ )
63
+ if not ok or not out:
64
+ return []
65
+ commits = []
66
+ for line in out.splitlines():
67
+ parts = line.split("|", 2)
68
+ if len(parts) >= 3:
69
+ commits.append({
70
+ "hash": parts[0],
71
+ "message": parts[1],
72
+ "date": parts[2][:10],
73
+ })
74
+ return commits
75
+
76
+
77
+ def get_branch(repo_path):
78
+ """Get current branch."""
79
+ out, ok = git("branch", "--show-current", cwd=str(repo_path))
80
+ return out if ok else "unknown"
81
+
82
+
83
+ def get_latest_assessment(artifacts_dir):
84
+ """Find and read the most recent project-assessment artifact.
85
+
86
+ Looks for files matching *project-assessment*.md in .gator/artifacts/,
87
+ sorted by filename (date-prefixed) descending. Returns dict with
88
+ content, model, and date, or None if no assessment exists.
89
+ """
90
+ if not artifacts_dir.is_dir():
91
+ return None
92
+ assessments = sorted(
93
+ [f for f in artifacts_dir.glob("*project-assessment*.md") if f.is_file()],
94
+ reverse=True,
95
+ )
96
+ if not assessments:
97
+ return None
98
+ latest = assessments[0]
99
+ try:
100
+ text = latest.read_text(encoding="utf-8", errors="replace")
101
+ except OSError:
102
+ return None
103
+
104
+ # Parse frontmatter for model and date
105
+ model = ""
106
+ date = ""
107
+ body_lines = []
108
+ in_frontmatter = False
109
+ past_frontmatter = False
110
+ for line in text.splitlines():
111
+ if line.strip() == "---" and not past_frontmatter:
112
+ if in_frontmatter:
113
+ past_frontmatter = True
114
+ else:
115
+ in_frontmatter = True
116
+ continue
117
+ if in_frontmatter and not past_frontmatter:
118
+ if line.startswith("model:"):
119
+ model = line.split(":", 1)[1].strip()
120
+ elif line.startswith("date:"):
121
+ date = line.split(":", 1)[1].strip()
122
+ continue
123
+ if past_frontmatter:
124
+ body_lines.append(line)
125
+
126
+ # Extract just the assessment body (skip the ## heading if present)
127
+ content_lines = []
128
+ for line in body_lines:
129
+ if line.startswith("## Project Assessment"):
130
+ continue
131
+ if line.startswith("# "):
132
+ continue
133
+ content_lines.append(line)
134
+
135
+ content = "\n".join(content_lines).strip()
136
+ if not content:
137
+ return None
138
+
139
+ return {
140
+ "content": content,
141
+ "model": model,
142
+ "date": date,
143
+ "file": latest.name,
144
+ }
145
+
146
+
147
+ def extract_roadmap_table(roadmap_text):
148
+ """Extract the first markdown table from the roadmap, preserving all columns."""
149
+ table_lines = []
150
+ in_table = False
151
+ for line in roadmap_text.splitlines():
152
+ if line.startswith("|"):
153
+ in_table = True
154
+ table_lines.append(line)
155
+ elif in_table:
156
+ # End of table — stop at first non-table line after table started
157
+ break
158
+ return table_lines if len(table_lines) >= 3 else [] # header + separator + at least 1 row
159
+
160
+
161
+ def parse_roadmap_items(roadmap_text):
162
+ """Extract roadmap items with status from markdown tables."""
163
+ items = []
164
+ for line in roadmap_text.splitlines():
165
+ if not line.startswith("|"):
166
+ continue
167
+ if line.startswith("|---") or line.startswith("| #") or line.startswith("| Priority"):
168
+ continue
169
+ cells = [c.strip() for c in line.split("|")[1:-1]]
170
+ if len(cells) < 3:
171
+ continue
172
+ # Try to find status cell — look for Done/Building/Designed/etc
173
+ for i, cell in enumerate(cells):
174
+ if any(s in cell for s in ("Done", "Building", "Working", "Designed", "Considering", "Deferred", "Open")):
175
+ name = cells[i - 1] if i > 0 else cells[0]
176
+ # Clean up bold markers
177
+ name = re.sub(r'\*\*([^*]+)\*\*', r'\1', name).strip()
178
+ # Shorten long names
179
+ if len(name) > 80:
180
+ name = name[:80] + "..."
181
+ items.append({"name": name, "status": cell.strip()})
182
+ break
183
+ return items
184
+
185
+
186
+ def parse_issues(issues_text):
187
+ """Extract issues with status."""
188
+ issues = []
189
+ current = None
190
+ for line in issues_text.splitlines():
191
+ if line.startswith("### "):
192
+ if current:
193
+ issues.append(current)
194
+ current = {"title": line[4:].strip(), "status": "Open"}
195
+ elif current and line.startswith("**Status**:"):
196
+ current["status"] = line.split(":", 1)[1].strip()
197
+ if current:
198
+ issues.append(current)
199
+ return issues
200
+
201
+
202
+ def parse_inbox_items(inbox_text):
203
+ """Extract inbox bullet items."""
204
+ items = []
205
+ for line in inbox_text.splitlines():
206
+ line = line.strip()
207
+ if line.startswith("- ") and len(line) > 10:
208
+ text = line[2:].strip()
209
+ if len(text) > 100:
210
+ text = text[:100] + "..."
211
+ items.append(text)
212
+ return items
213
+
214
+
215
+ def get_session_decisions(sessions_dir, days=7):
216
+ """Extract recent decisions from committed session summaries."""
217
+ decisions = []
218
+ if not sessions_dir.is_dir():
219
+ return decisions
220
+ cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
221
+ for f in sorted(sessions_dir.glob("*.md"), reverse=True):
222
+ if f.name.startswith("_"):
223
+ continue
224
+ # Quick date check from filename
225
+ if f.name[:10] < cutoff:
226
+ continue
227
+ try:
228
+ text = f.read_text(encoding="utf-8", errors="replace")
229
+ in_decisions = False
230
+ for line in text.splitlines():
231
+ if line.startswith("## Decisions") or line.startswith("## decisions"):
232
+ in_decisions = True
233
+ continue
234
+ if in_decisions and line.startswith("## "):
235
+ break
236
+ if in_decisions and line.startswith("- "):
237
+ dec = line[2:].strip()
238
+ # Strip timestamp prefix if present
239
+ dec = re.sub(r'^\[\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}\]\s*', '', dec)
240
+ if len(dec) > 10:
241
+ decisions.append(dec)
242
+ except (OSError, UnicodeDecodeError):
243
+ continue
244
+ return decisions[:10]
245
+
246
+
247
+ # ── pulse generation ───────────────────────────────────────────────────────
248
+
249
+ def build_pulse(repo_path, days=7):
250
+ """Build the pulse.md content from repo evidence."""
251
+ today = datetime.now().strftime("%Y-%m-%d")
252
+ branch = get_branch(repo_path)
253
+ gator_dir = repo_path / ".gator"
254
+
255
+ # Gather evidence
256
+ roadmap_text = read_file(gator_dir / "roadmap.md")
257
+ issues_text = read_file(gator_dir / "issues.md")
258
+ inbox_text = read_file(gator_dir / "inbox.md")
259
+ mission_text = read_file(gator_dir / "mission.md", max_lines=10)
260
+ commits = get_recent_commits(repo_path, days)
261
+ roadmap_items = parse_roadmap_items(roadmap_text)
262
+ issues = parse_issues(issues_text)
263
+ inbox_items = parse_inbox_items(inbox_text)
264
+ decisions = get_session_decisions(gator_dir / "sessions", days)
265
+ assessment = get_latest_assessment(gator_dir / "artifacts")
266
+
267
+ # Classify roadmap items
268
+ done = [i for i in roadmap_items if "Done" in i["status"]]
269
+ building = [i for i in roadmap_items if any(s in i["status"] for s in ("Building", "Working"))]
270
+ designed = [i for i in roadmap_items if "Designed" in i["status"]]
271
+ other = [i for i in roadmap_items if i not in done and i not in building and i not in designed]
272
+
273
+ # Classify issues
274
+ open_issues = [i for i in issues if "Open" in i["status"] or "Working" in i["status"]]
275
+ resolved_issues = [i for i in issues if "Resolved" in i["status"]]
276
+
277
+ lines = []
278
+
279
+ # ── Header
280
+ lines.append("# Pulse")
281
+ lines.append("")
282
+ lines.append(f"*{today} | branch: {branch} | {len(commits)} commits in {days}d*")
283
+ lines.append("")
284
+
285
+ # ── Top 5 Next Steps
286
+ lines.append("## Top 5 Next Steps")
287
+ lines.append("")
288
+ next_steps = []
289
+ # Building items are the most actionable
290
+ for item in building[:3]:
291
+ next_steps.append(f"Continue: {item['name']}")
292
+ # Designed items are next in line
293
+ for item in designed:
294
+ if len(next_steps) >= 5:
295
+ break
296
+ next_steps.append(f"Build: {item['name']}")
297
+ # Open issues are actionable
298
+ for issue in open_issues:
299
+ if len(next_steps) >= 5:
300
+ break
301
+ next_steps.append(f"Fix: {issue['title']}")
302
+ # Inbox items might contain action items
303
+ for item in inbox_items:
304
+ if len(next_steps) >= 5:
305
+ break
306
+ if any(kw in item.upper() for kw in ("TODO", "NEED", "SHOULD", "MUST", "FIX", "BUG")):
307
+ next_steps.append(f"Triage: {item}")
308
+
309
+ if next_steps:
310
+ for i, step in enumerate(next_steps[:5], 1):
311
+ lines.append(f"{i}. {step}")
312
+ else:
313
+ lines.append("No actionable items identified from roadmap or issues.")
314
+ lines.append("")
315
+
316
+ # ── Project Assessment
317
+ if assessment:
318
+ lines.append("## Project Assessment")
319
+ lines.append("")
320
+ lines.append(assessment["content"])
321
+ lines.append("")
322
+ sig = assessment["model"] or "unknown model"
323
+ date = assessment["date"] or "undated"
324
+ lines.append(f"*-- {sig}, {date}*")
325
+ lines.append("")
326
+ else:
327
+ lines.append("## Project Assessment")
328
+ lines.append("")
329
+ lines.append("*No assessment yet. Ask the AI agent to generate one:*")
330
+ lines.append('*"Write a project assessment artifact"*')
331
+ lines.append("")
332
+
333
+ # ── Roadmap Check
334
+ lines.append("## Roadmap Check")
335
+ lines.append("")
336
+ if roadmap_items:
337
+ # Pass through the original roadmap table from the source file
338
+ roadmap_table = extract_roadmap_table(roadmap_text)
339
+ if roadmap_table:
340
+ for row in roadmap_table:
341
+ lines.append(row)
342
+ else:
343
+ # Fallback: build a simple table from parsed items
344
+ lines.append("| Item | Status |")
345
+ lines.append("|------|--------|")
346
+ for item in roadmap_items:
347
+ lines.append(f"| {item['name']} | {item['status']} |")
348
+ lines.append("")
349
+
350
+ # In progress detail below the table
351
+ if building:
352
+ lines.append("**In progress:**")
353
+ lines.append("")
354
+ for item in building:
355
+ lines.append(f"- {item['name']}")
356
+ lines.append("")
357
+ else:
358
+ lines.append("No roadmap items found.")
359
+ lines.append("")
360
+
361
+ # ── Top 5 Priorities
362
+ lines.append("## Top 5 Priorities")
363
+ lines.append("")
364
+ priorities = []
365
+ # Open issues are priorities
366
+ for issue in open_issues[:3]:
367
+ priorities.append(f"**Issue**: {issue['title']}")
368
+ # Inbox items with urgency signals
369
+ for item in inbox_items:
370
+ if len(priorities) >= 5:
371
+ break
372
+ priorities.append(f"**Inbox**: {item}")
373
+ # Decisions that need follow-up
374
+ for dec in decisions:
375
+ if len(priorities) >= 5:
376
+ break
377
+ if any(kw in dec.lower() for kw in ("need", "should", "todo", "defer", "block")):
378
+ priorities.append(f"**Decision follow-up**: {dec}")
379
+
380
+ if priorities:
381
+ for i, p in enumerate(priorities[:5], 1):
382
+ lines.append(f"{i}. {p}")
383
+ else:
384
+ lines.append("No urgent priorities identified.")
385
+ lines.append("")
386
+
387
+ # ── Issues & Blockers
388
+ lines.append("## Issues & Blockers")
389
+ lines.append("")
390
+ if open_issues:
391
+ for issue in open_issues:
392
+ status_tag = "Working" if "Working" in issue["status"] else "Open"
393
+ lines.append(f"- **{status_tag}**: {issue['title']}")
394
+ else:
395
+ lines.append("No open issues.")
396
+ if resolved_issues:
397
+ lines.append("")
398
+ lines.append(f"*{len(resolved_issues)} resolved issue{'s' if len(resolved_issues) > 1 else ''}*")
399
+ lines.append("")
400
+
401
+ # ── Recent Activity
402
+ lines.append("## Recent Activity")
403
+ lines.append("")
404
+ if commits:
405
+ lines.append(f"{len(commits)} commits in the last {days} days.")
406
+ lines.append("")
407
+ for c in commits[:10]:
408
+ lines.append(f"- `{c['hash']}` {c['message']} ({c['date']})")
409
+ if len(commits) > 10:
410
+ lines.append(f"- *...and {len(commits) - 10} more*")
411
+ else:
412
+ lines.append("No commits in this window.")
413
+
414
+ if decisions:
415
+ lines.append("")
416
+ lines.append("**Recent decisions:**")
417
+ for dec in decisions[:5]:
418
+ lines.append(f"- {dec}")
419
+ lines.append("")
420
+
421
+ # ── Footer
422
+ lines.append("---")
423
+ lines.append(f"*Generated by gator-pulse.py on {today}*")
424
+ lines.append("")
425
+
426
+ return "\n".join(lines)
427
+
428
+
429
+ def main():
430
+ ensure_utf8_stdout()
431
+
432
+ parser = argparse.ArgumentParser(
433
+ description="Generate repo pulse — strategic operations brief"
434
+ )
435
+ parser.add_argument("--path", "-p", help="Path to repo (default: cwd)")
436
+ parser.add_argument("--days", "-d", type=int, default=7,
437
+ help="Lookback window in days (default: 7)")
438
+ parser.add_argument("--dry-run", "-n", action="store_true",
439
+ help="Print to stdout instead of writing file")
440
+ args = parser.parse_args()
441
+
442
+ repo_root = find_gator_root(args.path)
443
+ if not repo_root:
444
+ print("Error: no .gator/ found.", file=sys.stderr)
445
+ sys.exit(1)
446
+
447
+ content = build_pulse(repo_root, args.days)
448
+
449
+ if args.dry_run:
450
+ print(content)
451
+ return
452
+
453
+ pulse_file = repo_root / ".gator" / "pulse.md"
454
+ pulse_file.write_text(content, encoding="utf-8")
455
+ print(f" Wrote {pulse_file}")
456
+
457
+
458
+ if __name__ == "__main__":
459
+ main()