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,460 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator fleet-intel — generate per-repo intelligence summaries for the command post.
|
|
4
|
+
|
|
5
|
+
Reads fleet state (git history, .gator/ files, committed summaries, trailers)
|
|
6
|
+
and produces structured intelligence profiles in gator-command/threads/.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python gator-fleet-intel.py # update all repo profiles
|
|
10
|
+
python gator-fleet-intel.py --repo NAME # update one repo
|
|
11
|
+
python gator-fleet-intel.py --json # JSON output instead of files
|
|
12
|
+
python gator-fleet-intel.py --dry-run # show what would be written
|
|
13
|
+
|
|
14
|
+
This script synthesizes, it does not invent. Every field is derived from
|
|
15
|
+
observable git/file state — no LLM inference, no fabrication.
|
|
16
|
+
|
|
17
|
+
**Limitation**: currently requires local repo checkouts. Remote-only repos
|
|
18
|
+
(accessible via thin-fetch / bare cache) are skipped. Future: add remote
|
|
19
|
+
profiling via gator_remote.py to match fleet-report's remote capability.
|
|
20
|
+
|
|
21
|
+
@reads: gator-command/registry.md, .gator/ in each repo, git history,
|
|
22
|
+
.gator/sessions/*.md (committed summaries)
|
|
23
|
+
@writes: gator-command/threads/repo-*.md
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import re
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
from datetime import datetime, timezone, timedelta
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
35
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
36
|
+
|
|
37
|
+
from gator_core import (
|
|
38
|
+
get_version, find_command_post, normalize_path, parse_registry,
|
|
39
|
+
git, ensure_utf8_stdout,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
VERSION = get_version()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Data collection
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def get_recent_commits(repo_path, days=30, limit=20):
|
|
50
|
+
"""Get recent commit messages with dates and trailers."""
|
|
51
|
+
since = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
|
|
52
|
+
log, ok = git(
|
|
53
|
+
"log", "--format=%H|%s|%ci|%(trailers:key=Gator-Change-Type,valueonly)",
|
|
54
|
+
f"--since={since}", f"-{limit}", "dev",
|
|
55
|
+
cwd=repo_path,
|
|
56
|
+
)
|
|
57
|
+
if not ok:
|
|
58
|
+
log, ok = git(
|
|
59
|
+
"log", "--format=%H|%s|%ci|%(trailers:key=Gator-Change-Type,valueonly)",
|
|
60
|
+
f"--since={since}", f"-{limit}",
|
|
61
|
+
cwd=repo_path,
|
|
62
|
+
)
|
|
63
|
+
if not ok:
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
commits = []
|
|
67
|
+
for line in log.strip().splitlines():
|
|
68
|
+
if not line.strip():
|
|
69
|
+
continue
|
|
70
|
+
parts = line.split("|", 3)
|
|
71
|
+
if len(parts) >= 3:
|
|
72
|
+
commits.append({
|
|
73
|
+
"hash": parts[0][:8],
|
|
74
|
+
"message": parts[1].strip(),
|
|
75
|
+
"date": parts[2].strip()[:10],
|
|
76
|
+
"change_type": parts[3].strip() if len(parts) > 3 else "",
|
|
77
|
+
})
|
|
78
|
+
return commits
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_commit_frequency(repo_path):
|
|
82
|
+
"""Compute commit frequency stats."""
|
|
83
|
+
counts = {}
|
|
84
|
+
for days_label, days in [("7d", 7), ("30d", 30)]:
|
|
85
|
+
since = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
|
|
86
|
+
out, ok = git("rev-list", "--count", f"--since={since}", "HEAD", cwd=repo_path)
|
|
87
|
+
counts[days_label] = int(out.strip()) if ok and out.strip().isdigit() else 0
|
|
88
|
+
return counts
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_change_type_distribution(commits):
|
|
92
|
+
"""Count change types from trailer data."""
|
|
93
|
+
types = {}
|
|
94
|
+
for c in commits:
|
|
95
|
+
ct = c.get("change_type", "").strip()
|
|
96
|
+
if ct:
|
|
97
|
+
types[ct] = types.get(ct, 0) + 1
|
|
98
|
+
return types
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def read_mission(repo_path):
|
|
102
|
+
"""Read mission summary from .gator/mission.md."""
|
|
103
|
+
for candidate in [
|
|
104
|
+
repo_path / ".gator" / "mission.md",
|
|
105
|
+
repo_path / "gator-command" / "mission.md",
|
|
106
|
+
]:
|
|
107
|
+
if candidate.exists():
|
|
108
|
+
text = candidate.read_text(encoding="utf-8", errors="replace")
|
|
109
|
+
lines = []
|
|
110
|
+
for line in text.splitlines():
|
|
111
|
+
stripped = line.strip()
|
|
112
|
+
if stripped and not stripped.startswith("#") and not stripped.startswith("["):
|
|
113
|
+
lines.append(stripped)
|
|
114
|
+
if len(lines) >= 3:
|
|
115
|
+
break
|
|
116
|
+
return " ".join(lines)[:300] if lines else ""
|
|
117
|
+
return ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def read_charter_names(repo_path):
|
|
121
|
+
"""Read charter file names (minus templates/index)."""
|
|
122
|
+
charters_dir = repo_path / ".gator" / "charters"
|
|
123
|
+
if not charters_dir.is_dir():
|
|
124
|
+
return []
|
|
125
|
+
skip = {"_template.md", "README.md", "INDEX.md"}
|
|
126
|
+
return sorted(f.stem for f in charters_dir.glob("*.md") if f.name not in skip)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def read_active_threads(repo_path):
|
|
130
|
+
"""Read active thread names from .gator/active-threads/."""
|
|
131
|
+
threads_dir = repo_path / ".gator" / "active-threads"
|
|
132
|
+
if not threads_dir.is_dir():
|
|
133
|
+
return []
|
|
134
|
+
return sorted(f.stem for f in threads_dir.glob("*.md"))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def read_issues_count(repo_path):
|
|
138
|
+
"""Count open issues from .gator/issues.md."""
|
|
139
|
+
issues_file = repo_path / ".gator" / "issues.md"
|
|
140
|
+
if not issues_file.exists():
|
|
141
|
+
return 0
|
|
142
|
+
text = issues_file.read_text(encoding="utf-8", errors="replace")
|
|
143
|
+
return text.lower().count("**status**: open")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def read_committed_decisions(repo_path, days=30):
|
|
147
|
+
"""Read decisions from committed session summaries."""
|
|
148
|
+
sessions_dir = repo_path / ".gator" / "sessions"
|
|
149
|
+
if not sessions_dir.is_dir():
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
|
|
153
|
+
decisions = []
|
|
154
|
+
|
|
155
|
+
for f in sorted(sessions_dir.glob("*.md"), reverse=True):
|
|
156
|
+
if f.name.startswith("_"):
|
|
157
|
+
continue
|
|
158
|
+
if len(f.name) >= 10 and f.name[:10] < cutoff:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
text = f.read_text(encoding="utf-8", errors="replace")
|
|
163
|
+
except OSError:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
in_decisions = False
|
|
167
|
+
for line in text.splitlines():
|
|
168
|
+
stripped = line.strip()
|
|
169
|
+
if stripped == "## Decisions":
|
|
170
|
+
in_decisions = True
|
|
171
|
+
continue
|
|
172
|
+
if stripped.startswith("## ") and in_decisions:
|
|
173
|
+
break
|
|
174
|
+
if in_decisions and stripped.startswith("- "):
|
|
175
|
+
# Strip timestamp prefix
|
|
176
|
+
m = re.match(r'^- \[([^\]]*)\]\s*(.*)', stripped)
|
|
177
|
+
text_part = m.group(2) if m else stripped[2:]
|
|
178
|
+
if text_part and text_part != "*No decisions extracted*":
|
|
179
|
+
decisions.append(text_part)
|
|
180
|
+
|
|
181
|
+
return decisions[:20] # cap at 20 most recent
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def read_outbox(repo_path):
|
|
185
|
+
"""Read outbox items (cross-repo observations)."""
|
|
186
|
+
outbox = repo_path / ".gator" / "outbox.md"
|
|
187
|
+
if not outbox.exists():
|
|
188
|
+
return []
|
|
189
|
+
text = outbox.read_text(encoding="utf-8", errors="replace")
|
|
190
|
+
items = []
|
|
191
|
+
for line in text.splitlines():
|
|
192
|
+
stripped = line.strip()
|
|
193
|
+
if stripped.startswith("- ") and stripped != "- *(empty)*":
|
|
194
|
+
items.append(stripped[2:])
|
|
195
|
+
return items
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# Profile generation
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def build_profile(name, repo_path):
|
|
203
|
+
"""Build a complete intelligence profile for a repo."""
|
|
204
|
+
path = Path(normalize_path(str(repo_path)))
|
|
205
|
+
|
|
206
|
+
if not path.is_dir():
|
|
207
|
+
return {
|
|
208
|
+
"name": name,
|
|
209
|
+
"accessible": False,
|
|
210
|
+
"path": str(repo_path),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
commits = get_recent_commits(path)
|
|
214
|
+
freq = get_commit_frequency(path)
|
|
215
|
+
change_types = get_change_type_distribution(commits)
|
|
216
|
+
mission = read_mission(path)
|
|
217
|
+
charters = read_charter_names(path)
|
|
218
|
+
threads = read_active_threads(path)
|
|
219
|
+
open_issues = read_issues_count(path)
|
|
220
|
+
decisions = read_committed_decisions(path)
|
|
221
|
+
outbox = read_outbox(path)
|
|
222
|
+
|
|
223
|
+
# Derive activity level
|
|
224
|
+
c7 = freq.get("7d", 0)
|
|
225
|
+
if c7 >= 10:
|
|
226
|
+
activity = "high"
|
|
227
|
+
elif c7 >= 3:
|
|
228
|
+
activity = "moderate"
|
|
229
|
+
elif c7 >= 1:
|
|
230
|
+
activity = "low"
|
|
231
|
+
else:
|
|
232
|
+
activity = "dormant"
|
|
233
|
+
|
|
234
|
+
# Recent commit themes — group by message prefix patterns
|
|
235
|
+
themes = {}
|
|
236
|
+
for c in commits[:15]:
|
|
237
|
+
msg = c["message"]
|
|
238
|
+
# Extract first word/prefix as rough theme
|
|
239
|
+
prefix = msg.split(":")[0].split("(")[0].strip() if ":" in msg or "(" in msg else msg.split()[0] if msg else ""
|
|
240
|
+
prefix = prefix.lower()
|
|
241
|
+
if prefix and len(prefix) < 30:
|
|
242
|
+
themes[prefix] = themes.get(prefix, 0) + 1
|
|
243
|
+
|
|
244
|
+
# Top themes by frequency
|
|
245
|
+
top_themes = sorted(themes.items(), key=lambda x: -x[1])[:5]
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
"name": name,
|
|
249
|
+
"accessible": True,
|
|
250
|
+
"path": str(path),
|
|
251
|
+
"mission": mission,
|
|
252
|
+
"activity": activity,
|
|
253
|
+
"commits_7d": freq.get("7d", 0),
|
|
254
|
+
"commits_30d": freq.get("30d", 0),
|
|
255
|
+
"charters": charters,
|
|
256
|
+
"threads": threads,
|
|
257
|
+
"open_issues": open_issues,
|
|
258
|
+
"change_types": change_types,
|
|
259
|
+
"commit_themes": top_themes,
|
|
260
|
+
"recent_commits": [
|
|
261
|
+
{"hash": c["hash"], "message": c["message"], "date": c["date"]}
|
|
262
|
+
for c in commits[:10]
|
|
263
|
+
],
|
|
264
|
+
"decisions": decisions[:10],
|
|
265
|
+
"outbox": outbox,
|
|
266
|
+
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
# Output: markdown thread
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
def render_thread(profile):
|
|
275
|
+
"""Render a per-repo intelligence profile as markdown thread."""
|
|
276
|
+
name = profile["name"]
|
|
277
|
+
lines = []
|
|
278
|
+
|
|
279
|
+
lines.append("---")
|
|
280
|
+
lines.append(f"title: \"{name}\"")
|
|
281
|
+
lines.append("category: repo-profile")
|
|
282
|
+
lines.append(f"generated: {profile.get('generated_at', '')[:10]}")
|
|
283
|
+
lines.append(f"repo-path: {profile.get('path', '')}")
|
|
284
|
+
lines.append("---")
|
|
285
|
+
lines.append("")
|
|
286
|
+
lines.append(f"# {name}")
|
|
287
|
+
lines.append("")
|
|
288
|
+
|
|
289
|
+
# What it is
|
|
290
|
+
lines.append("## What It Is")
|
|
291
|
+
lines.append("")
|
|
292
|
+
mission = profile.get("mission", "")
|
|
293
|
+
lines.append(mission if mission else "*No mission.md found*")
|
|
294
|
+
lines.append("")
|
|
295
|
+
|
|
296
|
+
# Current state
|
|
297
|
+
lines.append("## Current State")
|
|
298
|
+
lines.append("")
|
|
299
|
+
activity = profile.get("activity", "unknown")
|
|
300
|
+
c7 = profile.get("commits_7d", 0)
|
|
301
|
+
c30 = profile.get("commits_30d", 0)
|
|
302
|
+
lines.append(f"**Activity**: {activity} ({c7} commits in 7d, {c30} in 30d)")
|
|
303
|
+
lines.append("")
|
|
304
|
+
|
|
305
|
+
if profile.get("open_issues"):
|
|
306
|
+
lines.append(f"**Open issues**: {profile['open_issues']}")
|
|
307
|
+
lines.append("")
|
|
308
|
+
|
|
309
|
+
# Charter coverage
|
|
310
|
+
charters = profile.get("charters", [])
|
|
311
|
+
if charters:
|
|
312
|
+
lines.append(f"**Charters**: {', '.join(charters)}")
|
|
313
|
+
lines.append("")
|
|
314
|
+
|
|
315
|
+
threads = profile.get("threads", [])
|
|
316
|
+
if threads:
|
|
317
|
+
lines.append(f"**Active threads**: {', '.join(threads)}")
|
|
318
|
+
lines.append("")
|
|
319
|
+
|
|
320
|
+
# What changed recently
|
|
321
|
+
lines.append("## Recent Activity")
|
|
322
|
+
lines.append("")
|
|
323
|
+
|
|
324
|
+
change_types = profile.get("change_types", {})
|
|
325
|
+
if change_types:
|
|
326
|
+
ct_parts = [f"{k} ({v})" for k, v in sorted(change_types.items(), key=lambda x: -x[1])]
|
|
327
|
+
lines.append(f"**Change types**: {', '.join(ct_parts)}")
|
|
328
|
+
lines.append("")
|
|
329
|
+
|
|
330
|
+
commit_themes = profile.get("commit_themes", [])
|
|
331
|
+
if commit_themes:
|
|
332
|
+
theme_parts = [f"{t[0]} ({t[1]})" for t in commit_themes]
|
|
333
|
+
lines.append(f"**Commit themes**: {', '.join(theme_parts)}")
|
|
334
|
+
lines.append("")
|
|
335
|
+
|
|
336
|
+
recent = profile.get("recent_commits", [])
|
|
337
|
+
if recent:
|
|
338
|
+
for c in recent[:7]:
|
|
339
|
+
lines.append(f"- `{c['hash']}` {c['date']} — {c['message']}")
|
|
340
|
+
if len(recent) > 7:
|
|
341
|
+
lines.append(f"- ... and {len(recent) - 7} more")
|
|
342
|
+
lines.append("")
|
|
343
|
+
|
|
344
|
+
# Decisions
|
|
345
|
+
decisions = profile.get("decisions", [])
|
|
346
|
+
if decisions:
|
|
347
|
+
lines.append("## Recent Decisions")
|
|
348
|
+
lines.append("")
|
|
349
|
+
for d in decisions[:7]:
|
|
350
|
+
lines.append(f"- {d}")
|
|
351
|
+
if len(decisions) > 7:
|
|
352
|
+
lines.append(f"- ... and {len(decisions) - 7} more")
|
|
353
|
+
lines.append("")
|
|
354
|
+
|
|
355
|
+
# Outbox
|
|
356
|
+
outbox = profile.get("outbox", [])
|
|
357
|
+
if outbox:
|
|
358
|
+
lines.append("## Outbox Items")
|
|
359
|
+
lines.append("")
|
|
360
|
+
for item in outbox:
|
|
361
|
+
lines.append(f"- {item}")
|
|
362
|
+
lines.append("")
|
|
363
|
+
|
|
364
|
+
# Footer
|
|
365
|
+
lines.append("---")
|
|
366
|
+
lines.append("")
|
|
367
|
+
lines.append(f"*Generated by gator fleet-intel {VERSION} on {profile.get('generated_at', '')[:10]}. "
|
|
368
|
+
f"Do not edit manually — regenerated on each run.*")
|
|
369
|
+
lines.append("")
|
|
370
|
+
|
|
371
|
+
return "\n".join(lines)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
# Main
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
def main():
|
|
379
|
+
ensure_utf8_stdout()
|
|
380
|
+
|
|
381
|
+
parser = argparse.ArgumentParser(
|
|
382
|
+
description="Generate per-repo intelligence summaries for the command post."
|
|
383
|
+
)
|
|
384
|
+
parser.add_argument("--repo", metavar="NAME", help="Update one repo only")
|
|
385
|
+
parser.add_argument("--json", "-j", action="store_true", help="JSON output to stdout")
|
|
386
|
+
parser.add_argument("--dry-run", action="store_true", help="Show what would be written")
|
|
387
|
+
|
|
388
|
+
args = parser.parse_args()
|
|
389
|
+
|
|
390
|
+
# Find command post
|
|
391
|
+
cp = find_command_post()
|
|
392
|
+
if not cp:
|
|
393
|
+
print(" ! Could not find gator-command/ directory")
|
|
394
|
+
sys.exit(1)
|
|
395
|
+
|
|
396
|
+
gc_dir = cp / "gator-command"
|
|
397
|
+
registry = parse_registry(cp)
|
|
398
|
+
|
|
399
|
+
if not registry:
|
|
400
|
+
print(" No repos in registry.")
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
# Filter to single repo if requested
|
|
404
|
+
if args.repo:
|
|
405
|
+
registry = [r for r in registry if r["name"] == args.repo]
|
|
406
|
+
if not registry:
|
|
407
|
+
print(f" ! Repo '{args.repo}' not found in registry")
|
|
408
|
+
sys.exit(1)
|
|
409
|
+
|
|
410
|
+
# In JSON mode, status goes to stderr
|
|
411
|
+
log = (lambda msg: print(msg, file=sys.stderr)) if args.json else print
|
|
412
|
+
|
|
413
|
+
log("")
|
|
414
|
+
log(f" fleet-intel: profiling {len(registry)} repos")
|
|
415
|
+
log("")
|
|
416
|
+
|
|
417
|
+
profiles = []
|
|
418
|
+
for entry in registry:
|
|
419
|
+
name = entry["name"]
|
|
420
|
+
repo_path = entry["path"]
|
|
421
|
+
log(f" scanning {name} ...")
|
|
422
|
+
profile = build_profile(name, repo_path)
|
|
423
|
+
profiles.append(profile)
|
|
424
|
+
|
|
425
|
+
log("")
|
|
426
|
+
|
|
427
|
+
if args.json:
|
|
428
|
+
print(json.dumps(profiles, indent=2, default=str))
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
# Write thread files
|
|
432
|
+
threads_dir = gc_dir / "threads"
|
|
433
|
+
threads_dir.mkdir(exist_ok=True)
|
|
434
|
+
|
|
435
|
+
written = 0
|
|
436
|
+
for profile in profiles:
|
|
437
|
+
if not profile.get("accessible"):
|
|
438
|
+
print(f" ! {profile['name']}: inaccessible, skipped")
|
|
439
|
+
continue
|
|
440
|
+
|
|
441
|
+
filename = f"repo-{profile['name']}.md"
|
|
442
|
+
filepath = threads_dir / filename
|
|
443
|
+
content = render_thread(profile)
|
|
444
|
+
|
|
445
|
+
if args.dry_run:
|
|
446
|
+
print(f" would write: {filepath}")
|
|
447
|
+
print(f" {len(content)} chars, {content.count(chr(10))} lines")
|
|
448
|
+
else:
|
|
449
|
+
filepath.write_text(content, encoding="utf-8")
|
|
450
|
+
print(f" ✓ {filename}")
|
|
451
|
+
written += 1
|
|
452
|
+
|
|
453
|
+
print()
|
|
454
|
+
if not args.dry_run:
|
|
455
|
+
print(f" Written: {written} repo profiles to {threads_dir}")
|
|
456
|
+
print()
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
if __name__ == "__main__":
|
|
460
|
+
main()
|