gator-command 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gator_command/__init__.py +2 -0
- gator_command/cli.py +137 -0
- gator_command/scripts/crawler.py +633 -0
- gator_command/scripts/dashboard/dashboard.css +982 -0
- gator_command/scripts/dashboard/dashboard.html +84 -0
- gator_command/scripts/dashboard/dashboard.js +419 -0
- gator_command/scripts/dashboard/views/audit.js +270 -0
- gator_command/scripts/dashboard/views/fleet.js +307 -0
- gator_command/scripts/dashboard/views/repo.js +599 -0
- gator_command/scripts/dashboard/views/settings.js +173 -0
- gator_command/scripts/dashboard/views/updates.js +308 -0
- gator_command/scripts/enforcer-prompt.md +22 -0
- gator_command/scripts/extract-claude-sessions.py +489 -0
- gator_command/scripts/extract-codex-sessions.py +477 -0
- gator_command/scripts/extract-gemini-sessions.py +410 -0
- gator_command/scripts/gator-audit.py +956 -0
- gator_command/scripts/gator-charter-draft.py +919 -0
- gator_command/scripts/gator-charter-lint.py +427 -0
- gator_command/scripts/gator-charter-verify.py +606 -0
- gator_command/scripts/gator-dashboard.py +1271 -0
- gator_command/scripts/gator-deploy.py +916 -0
- gator_command/scripts/gator-drift.py +569 -0
- gator_command/scripts/gator-enforce.py +82 -0
- gator_command/scripts/gator-fleet-intel.py +460 -0
- gator_command/scripts/gator-fleet-report.py +615 -0
- gator_command/scripts/gator-init-command-post.py +315 -0
- gator_command/scripts/gator-init.py +434 -0
- gator_command/scripts/gator-machine-id.py +153 -0
- gator_command/scripts/gator-policy-status.py +631 -0
- gator_command/scripts/gator-pulse.py +459 -0
- gator_command/scripts/gator-repo-status.py +649 -0
- gator_command/scripts/gator-session-common.py +372 -0
- gator_command/scripts/gator-session-sink.py +831 -0
- gator_command/scripts/gator-sessions.py +1244 -0
- gator_command/scripts/gator-update.py +615 -0
- gator_command/scripts/gator-version.py +38 -0
- gator_command/scripts/gator_core.py +489 -0
- gator_command/scripts/gator_remote.py +381 -0
- gator_command/scripts/gator_runtime.py +142 -0
- gator_command/scripts/gatorize-actions.sh +989 -0
- gator_command/scripts/gatorize-lib.sh +166 -0
- gator_command/scripts/gatorize-post.sh +394 -0
- gator_command/scripts/gatorize.py +1163 -0
- gator_command/scripts/gatorize.sh +185 -0
- gator_command/scripts/generate_markdown.py +212 -0
- gator_command/scripts/generate_wiki.py +424 -0
- gator_command/scripts/graph_health.py +780 -0
- gator_command/scripts/memex-lint.py +286 -0
- gator_command/scripts/memex-lint.sh +205 -0
- gator_command/scripts/memex.py +1472 -0
- gator_command/scripts/memex_formatters.py +191 -0
- gator_command/scripts/memex_state.py +236 -0
- gator_command/scripts/spawn.py +650 -0
- gator_command/templates/gator-starter/blueprints/README.md +32 -0
- gator_command/templates/gator-starter/charterignore +53 -0
- gator_command/templates/gator-starter/charters/README.md +178 -0
- gator_command/templates/gator-starter/charters/_template.md +31 -0
- gator_command/templates/gator-starter/commands/commit.md +33 -0
- gator_command/templates/gator-starter/commands/init.md +11 -0
- gator_command/templates/gator-starter/commands/update.md +5 -0
- gator_command/templates/gator-starter/constitution.md +165 -0
- gator_command/templates/gator-starter/field-guides/README.md +25 -0
- gator_command/templates/gator-starter/gator-start-up.md +119 -0
- gator_command/templates/gator-starter/procedures/charter-alignment.md +83 -0
- gator_command/templates/gator-starter/procedures/enforcer-review.md +317 -0
- gator_command/templates/gator-starter/procedures/field-guide-generation.md +176 -0
- gator_command/templates/gator-starter/procedures/knowledge-capture.md +57 -0
- gator_command/templates/gator-starter/procedures/significance-check.md +69 -0
- gator_command/templates/gator-starter/reference-notes/concierge-responses.md +535 -0
- gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +91 -0
- gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +22 -0
- gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +232 -0
- gator_command/templates/gator-starter/reference-notes/example-project.md +289 -0
- gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +72 -0
- gator_command/templates/gator-starter/reference-notes/git-workflow.md +60 -0
- gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +37 -0
- gator_command/templates/gator-starter/reference-notes/refactor-approach.md +155 -0
- gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +108 -0
- gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +99 -0
- gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +155 -0
- gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/enforcer-prompt.md +55 -0
- gator_command/templates/gator-starter/scripts/enforcer-review.py +1551 -0
- gator_command/templates/gator-starter/scripts/gator-approve.py +139 -0
- gator_command/templates/gator-starter/scripts/gator-enforce.py +82 -0
- gator_command/templates/gator-starter/scripts/gator-init.py +434 -0
- gator_command/templates/gator-starter/scripts/gator-pre-commit.py +2670 -0
- gator_command/templates/gator-starter/scripts/gator-pulse.py +459 -0
- gator_command/templates/gator-starter/scripts/gator-update.py +615 -0
- gator_command/templates/gator-starter/scripts/gator-version.py +38 -0
- gator_command/templates/gator-starter/scripts/gator_core.py +487 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/commit-msg +5 -0
- gator_command/templates/gator-starter/scripts/hooks/post-commit +7 -0
- gator_command/templates/gator-starter/scripts/hooks/pre-commit +5 -0
- gator_command/templates/gator-starter/sessions/.gitignore +7 -0
- gator_command/templates/gator-starter/vault/.gitkeep +0 -0
- gator_command/templates/gator-starter/whiteboard.md +5 -0
- gator_command-1.0.0.dist-info/METADATA +122 -0
- gator_command-1.0.0.dist-info/RECORD +110 -0
- gator_command-1.0.0.dist-info/WHEEL +5 -0
- gator_command-1.0.0.dist-info/entry_points.txt +2 -0
- gator_command-1.0.0.dist-info/licenses/LICENSE +21 -0
- gator_command-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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()
|