@automagik/genie 4.260407.6 → 4.260408.1
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.genie/agents/metrics-updater/runs.jsonl +1 -0
- package/.genie/agents/metrics-updater/state.json +1 -1
- package/.genie/agents/metrics-updater/tools/parse-metrics.py +121 -34
- package/.genie/agents/metrics-updater/tools/update-readme.py +30 -16
- package/README.md +10 -8
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "genie",
|
|
13
|
-
"version": "4.
|
|
13
|
+
"version": "4.260408.1",
|
|
14
14
|
"source": "./plugins/genie",
|
|
15
15
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
|
|
16
16
|
}
|
|
@@ -20,3 +20,4 @@
|
|
|
20
20
|
{"timestamp":"2026-04-05T12:24:47Z","duration_ms":32000,"api_calls":0,"tools_generated":0,"errors":["gh CLI not available","GitHub MCP tools not present in session"],"status":"success","fallback":true,"metrics":{"releases_24h":2,"merged_prs_7d":35,"avg_merge_time_h":0.3,"ship_rate_pct":85}}
|
|
21
21
|
{"timestamp":"2026-04-06T12:29:47.000Z","duration_ms":12500,"api_calls":0,"tools_generated":1,"errors":["gh_cli_unavailable: fallback to last_metrics from state.json"],"metrics":{"releases_24h":2,"merged_prs_7d":35,"avg_merge_time_h":0.3,"ship_rate_pct":85}}
|
|
22
22
|
{"timestamp":"2026-04-07T12:04:30Z","duration_ms":27000,"api_calls":0,"tools_generated":0,"errors":["gh CLI not available","no GitHub token or MCP GitHub tools present in session"],"status":"failed","fallback":false,"metrics":null}
|
|
23
|
+
{"timestamp":"2026-04-08T00:00:00.000Z","duration_ms":18000,"api_calls":0,"tools_generated":0,"errors":["gh CLI not available","GitHub MCP tools not present in session","fallback data older than current README — README update skipped"],"status":"failed","fallback":false,"metrics":null}
|
|
@@ -69,12 +69,20 @@ def calc_ship_rate(prs_json: list, days: int = 7) -> float:
|
|
|
69
69
|
return round((shipped_first / total) * 100, 0)
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
def calc_loc_changed(repo_root: str) -> int:
|
|
73
|
-
"""Calculate total lines changed (additions + deletions) in last
|
|
72
|
+
def calc_loc_changed(repo_root: str, branches: list[str] | None = None, hours: int = 24) -> int:
|
|
73
|
+
"""Calculate total lines changed (additions + deletions) in last N hours.
|
|
74
|
+
|
|
75
|
+
When branches is provided, counts changes across all listed branches
|
|
76
|
+
(e.g. ['main', 'dev']) to capture work that hasn't been promoted yet.
|
|
77
|
+
"""
|
|
74
78
|
try:
|
|
79
|
+
cmd = ['git', 'log', f'--since={hours} hours ago', '--stat', '--format=']
|
|
80
|
+
if branches:
|
|
81
|
+
cmd.extend(branches)
|
|
82
|
+
else:
|
|
83
|
+
cmd.append('--all')
|
|
75
84
|
result = subprocess.run(
|
|
76
|
-
|
|
77
|
-
capture_output=True, text=True, cwd=repo_root
|
|
85
|
+
cmd, capture_output=True, text=True, cwd=repo_root
|
|
78
86
|
)
|
|
79
87
|
total = 0
|
|
80
88
|
for line in result.stdout.splitlines():
|
|
@@ -91,12 +99,20 @@ def calc_loc_changed(repo_root: str) -> int:
|
|
|
91
99
|
return 0
|
|
92
100
|
|
|
93
101
|
|
|
94
|
-
def calc_commits_24h(repo_root: str) -> int:
|
|
95
|
-
"""Count commits in the last
|
|
102
|
+
def calc_commits_24h(repo_root: str, branches: list[str] | None = None, hours: int = 24) -> int:
|
|
103
|
+
"""Count commits in the last N hours.
|
|
104
|
+
|
|
105
|
+
When branches is provided, counts across all listed branches
|
|
106
|
+
(e.g. ['main', 'dev']) to capture work not yet promoted to main.
|
|
107
|
+
"""
|
|
96
108
|
try:
|
|
109
|
+
cmd = ['git', 'log', f'--since={hours} hours ago', '--oneline']
|
|
110
|
+
if branches:
|
|
111
|
+
cmd.extend(branches)
|
|
112
|
+
else:
|
|
113
|
+
cmd.append('--all')
|
|
97
114
|
result = subprocess.run(
|
|
98
|
-
|
|
99
|
-
capture_output=True, text=True, cwd=repo_root
|
|
115
|
+
cmd, capture_output=True, text=True, cwd=repo_root
|
|
100
116
|
)
|
|
101
117
|
lines = [l for l in result.stdout.strip().splitlines() if l.strip()]
|
|
102
118
|
return len(lines)
|
|
@@ -104,32 +120,98 @@ def calc_commits_24h(repo_root: str) -> int:
|
|
|
104
120
|
return 0
|
|
105
121
|
|
|
106
122
|
|
|
107
|
-
def
|
|
108
|
-
"""Count
|
|
123
|
+
def calc_files_changed(repo_root: str, hours: int = 24, branches: list[str] | None = None) -> int:
|
|
124
|
+
"""Count files changed in the last N hours."""
|
|
125
|
+
try:
|
|
126
|
+
cmd = ['git', 'log', f'--since={hours} hours ago', '--stat', '--format=']
|
|
127
|
+
if branches:
|
|
128
|
+
cmd.extend(branches)
|
|
129
|
+
else:
|
|
130
|
+
cmd.append('--all')
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
cmd, capture_output=True, text=True, cwd=repo_root
|
|
133
|
+
)
|
|
134
|
+
total = 0
|
|
135
|
+
for line in result.stdout.splitlines():
|
|
136
|
+
if 'file' in line and 'changed' in line:
|
|
137
|
+
total += int(line.strip().split()[0])
|
|
138
|
+
return total
|
|
139
|
+
except Exception:
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def calc_npm_releases(package: str, hours: int = 24) -> int:
|
|
144
|
+
"""Count npm releases in the last N hours."""
|
|
109
145
|
try:
|
|
110
|
-
since = (datetime.now(timezone.utc) - timedelta(hours=24)).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
111
146
|
result = subprocess.run(
|
|
112
|
-
['
|
|
113
|
-
'--jq', '.total_count'],
|
|
147
|
+
['npm', 'view', package, 'time', '--json'],
|
|
114
148
|
capture_output=True, text=True
|
|
115
149
|
)
|
|
116
|
-
|
|
150
|
+
if not result.stdout.strip():
|
|
151
|
+
return 0
|
|
152
|
+
data = json.loads(result.stdout)
|
|
153
|
+
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
154
|
+
count = 0
|
|
155
|
+
for ver, ts in data.items():
|
|
156
|
+
if ver in ('created', 'modified'):
|
|
157
|
+
continue
|
|
158
|
+
dt = datetime.fromisoformat(ts.replace('Z', '+00:00'))
|
|
159
|
+
if dt >= cutoff:
|
|
160
|
+
count += 1
|
|
161
|
+
return count
|
|
117
162
|
except Exception:
|
|
118
163
|
return 0
|
|
119
164
|
|
|
120
165
|
|
|
121
|
-
def
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
166
|
+
def calc_merged_prs(prs_json: list, hours: int = 24) -> int:
|
|
167
|
+
"""Count merged PRs within the last N hours."""
|
|
168
|
+
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
169
|
+
count = 0
|
|
170
|
+
for pr in prs_json:
|
|
171
|
+
if not pr.get('merged_at'):
|
|
172
|
+
continue
|
|
173
|
+
merged = datetime.fromisoformat(pr['merged_at'].replace('Z', '+00:00'))
|
|
174
|
+
if merged >= cutoff:
|
|
175
|
+
count += 1
|
|
176
|
+
return count
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def calc_avg_merge_time_window(prs_json: list, hours: int = 24) -> float:
|
|
180
|
+
"""Calculate average merge time in hours for PRs merged in the last N hours."""
|
|
181
|
+
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
182
|
+
durations = []
|
|
183
|
+
for pr in prs_json:
|
|
184
|
+
if not pr.get('merged_at'):
|
|
185
|
+
continue
|
|
186
|
+
merged = datetime.fromisoformat(pr['merged_at'].replace('Z', '+00:00'))
|
|
187
|
+
if merged < cutoff:
|
|
188
|
+
continue
|
|
189
|
+
created = datetime.fromisoformat(pr['created_at'].replace('Z', '+00:00'))
|
|
190
|
+
durations.append((merged - created).total_seconds() / 3600)
|
|
191
|
+
if not durations:
|
|
192
|
+
return 0.0
|
|
193
|
+
return round(sum(durations) / len(durations), 1)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def format_metrics(*, commits_7d: int = 0, loc_7d: int = 0, files_7d: int = 0,
|
|
197
|
+
prs_7d: int = 0, releases_7d: int = 0, avg_merge_7d: float = 0,
|
|
198
|
+
commits_24h: int = 0, loc_24h: int = 0, files_24h: int = 0,
|
|
199
|
+
prs_24h: int = 0, releases_24h: int = 0, avg_merge_24h: float = 0,
|
|
200
|
+
parallel_agents: int = 0) -> dict:
|
|
125
201
|
"""Format metrics into the standard output structure."""
|
|
126
202
|
return {
|
|
127
|
-
'
|
|
128
|
-
'
|
|
129
|
-
'
|
|
130
|
-
'
|
|
203
|
+
'commits_7d': commits_7d,
|
|
204
|
+
'loc_changed_7d': loc_7d,
|
|
205
|
+
'files_changed_7d': files_7d,
|
|
206
|
+
'prs_7d': prs_7d,
|
|
207
|
+
'releases_7d': releases_7d,
|
|
208
|
+
'avg_merge_time_7d': avg_merge_7d,
|
|
131
209
|
'commits_24h': commits_24h,
|
|
210
|
+
'loc_changed_24h': loc_24h,
|
|
211
|
+
'files_changed_24h': files_24h,
|
|
132
212
|
'prs_24h': prs_24h,
|
|
213
|
+
'releases_24h': releases_24h,
|
|
214
|
+
'avg_merge_time_24h': avg_merge_24h,
|
|
133
215
|
'parallel_agents': parallel_agents,
|
|
134
216
|
'updated': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
135
217
|
}
|
|
@@ -142,6 +224,8 @@ def main():
|
|
|
142
224
|
parser.add_argument('--parallel-agents', type=int, default=0,
|
|
143
225
|
help='Number of parallel agents active')
|
|
144
226
|
parser.add_argument('--repo-root', default='.', help='Path to repo root for git log commands')
|
|
227
|
+
parser.add_argument('--branches', nargs='+', default=['main', 'dev'],
|
|
228
|
+
help='Branches to include in git metrics (default: main dev)')
|
|
145
229
|
parser.add_argument('--owner', default='automagik-dev', help='GitHub owner')
|
|
146
230
|
parser.add_argument('--repo', default='genie', help='GitHub repo name')
|
|
147
231
|
parser.add_argument('--from-state', help='Fallback: read last_metrics from state.json')
|
|
@@ -166,21 +250,24 @@ def main():
|
|
|
166
250
|
with open(args.prs_json) as f:
|
|
167
251
|
prs = json.load(f)
|
|
168
252
|
|
|
169
|
-
|
|
170
|
-
avg_merge = calc_avg_merge_time_hours(prs)
|
|
171
|
-
ship_rate = calc_ship_rate(prs)
|
|
172
|
-
loc_changed = calc_loc_changed(args.repo_root)
|
|
173
|
-
commits = calc_commits_24h(args.repo_root)
|
|
174
|
-
prs_count = calc_prs_24h(args.owner, args.repo)
|
|
253
|
+
pkg = f'@{args.owner}/{args.repo}'
|
|
175
254
|
|
|
176
255
|
metrics = format_metrics(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
256
|
+
# 7-day metrics
|
|
257
|
+
commits_7d=calc_commits_24h(args.repo_root, args.branches, hours=168),
|
|
258
|
+
loc_7d=calc_loc_changed(args.repo_root, args.branches, hours=168),
|
|
259
|
+
files_7d=calc_files_changed(args.repo_root, hours=168, branches=args.branches),
|
|
260
|
+
prs_7d=calc_merged_prs(prs, hours=168),
|
|
261
|
+
releases_7d=calc_npm_releases(pkg, hours=168),
|
|
262
|
+
avg_merge_7d=calc_avg_merge_time_window(prs, hours=168),
|
|
263
|
+
# 24h metrics
|
|
264
|
+
commits_24h=calc_commits_24h(args.repo_root, args.branches),
|
|
265
|
+
loc_24h=calc_loc_changed(args.repo_root, args.branches),
|
|
266
|
+
files_24h=calc_files_changed(args.repo_root, branches=args.branches),
|
|
267
|
+
prs_24h=calc_merged_prs(prs),
|
|
268
|
+
releases_24h=calc_npm_releases(pkg),
|
|
269
|
+
avg_merge_24h=calc_avg_merge_time_window(prs),
|
|
180
270
|
parallel_agents=args.parallel_agents,
|
|
181
|
-
loc_changed_24h=loc_changed,
|
|
182
|
-
commits_24h=commits,
|
|
183
|
-
prs_24h=prs_count,
|
|
184
271
|
)
|
|
185
272
|
|
|
186
273
|
output = json.dumps(metrics, indent=2)
|
|
@@ -23,27 +23,41 @@ END_MARKER = '<!-- METRICS:END \u2014 \U0001f9de automagik/genie -->'
|
|
|
23
23
|
def build_metrics_table(metrics: dict) -> str:
|
|
24
24
|
"""Build the markdown metrics table with signature markers."""
|
|
25
25
|
timestamp = metrics.get('updated', datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'))
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
date_str = timestamp[:10] if 'T' in timestamp else timestamp
|
|
27
|
+
|
|
28
|
+
# 7-day metrics
|
|
29
|
+
commits_7d = metrics.get('commits_7d', 0)
|
|
30
|
+
loc_7d = metrics.get('loc_changed_7d', 0)
|
|
31
|
+
files_7d = metrics.get('files_changed_7d', 0)
|
|
32
|
+
prs_7d = metrics.get('prs_7d', 0)
|
|
33
|
+
releases_7d = metrics.get('releases_7d', 0)
|
|
34
|
+
avg_merge_7d = metrics.get('avg_merge_time_7d', 0)
|
|
35
|
+
|
|
36
|
+
# 24h metrics
|
|
37
|
+
commits_24h = metrics.get('commits_24h', 0)
|
|
38
|
+
loc_24h = metrics.get('loc_changed_24h', 0)
|
|
39
|
+
files_24h = metrics.get('files_changed_24h', 0)
|
|
40
|
+
prs_24h = metrics.get('prs_24h', 0)
|
|
41
|
+
releases_24h = metrics.get('releases_24h', metrics.get('releases_per_day', 0))
|
|
42
|
+
avg_merge_24h = metrics.get('avg_merge_time_24h', metrics.get('avg_bugfix_time_hours', 0))
|
|
33
43
|
|
|
34
44
|
start_marker = f'<!-- METRICS:START \u2014 Updated by Genie Metrics Agent at {timestamp} -->'
|
|
35
45
|
|
|
36
46
|
lines = [
|
|
37
47
|
start_marker,
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
f'|
|
|
43
|
-
f'| Lines changed
|
|
44
|
-
f'|
|
|
45
|
-
f'|
|
|
46
|
-
f'|
|
|
48
|
+
'<p align="center">',
|
|
49
|
+
'',
|
|
50
|
+
'| Metric | 7 days | 24h |',
|
|
51
|
+
'|--------|--------|-----|',
|
|
52
|
+
f'| Commits | {commits_7d:,} | {commits_24h} |',
|
|
53
|
+
f'| Lines changed | {loc_7d:,} | {loc_24h:,} |',
|
|
54
|
+
f'| Files touched | {files_7d:,} | {files_24h} |',
|
|
55
|
+
f'| Merged PRs | {prs_7d} | {prs_24h} |',
|
|
56
|
+
f'| npm releases | {releases_7d} | {releases_24h} |',
|
|
57
|
+
f'| Avg merge time | {avg_merge_7d}h | {avg_merge_24h}h |',
|
|
58
|
+
'',
|
|
59
|
+
f'*Last updated: {date_str}*',
|
|
60
|
+
'</p>',
|
|
47
61
|
END_MARKER,
|
|
48
62
|
]
|
|
49
63
|
return '\n'.join(lines)
|
package/README.md
CHANGED
|
@@ -27,14 +27,16 @@
|
|
|
27
27
|
<!-- METRICS:START -->
|
|
28
28
|
<p align="center">
|
|
29
29
|
|
|
30
|
-
| Metric |
|
|
31
|
-
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
| Metric | 7 days | 24h |
|
|
31
|
+
|--------|--------|-----|
|
|
32
|
+
| Commits | 445 | 38 |
|
|
33
|
+
| Lines changed | 57,462 | 3,917 |
|
|
34
|
+
| Files touched | 1,062 | 80 |
|
|
35
|
+
| Merged PRs | 87 | 10 |
|
|
36
|
+
| npm releases | 68 | 7 |
|
|
37
|
+
| Avg merge time | 3.3h | 5.4h |
|
|
38
|
+
|
|
39
|
+
*Last updated: 2026-04-07*
|
|
38
40
|
</p>
|
|
39
41
|
<!-- METRICS:END -->
|
|
40
42
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.260408.1",
|
|
4
4
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Namastex Labs"
|