@snipcodeit/mgw 0.1.0

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.
@@ -0,0 +1,489 @@
1
+ ---
2
+ name: mgw:roadmap
3
+ description: Render project milestones as a roadmap — completion table, due dates, and optional Discussion post
4
+ argument-hint: "[--set-dates] [--post-discussion] [--json]"
5
+ allowed-tools:
6
+ - Bash
7
+ - Read
8
+ - Write
9
+ - Edit
10
+ ---
11
+
12
+ <objective>
13
+ Render the current MGW project's milestones as a roadmap view. Three capabilities:
14
+
15
+ - **Table** (always) — Prints a markdown table of milestone name, issue count, completion
16
+ percentage, and board URL for each milestone in project.json.
17
+ - **Due dates** (`--set-dates`) — Interactively prompts for a due date per milestone and
18
+ sets it on the corresponding GitHub milestone via the REST API. Enables the Roadmap
19
+ layout timeline in GitHub Projects v2.
20
+ - **Discussion post** (`--post-discussion`) — Posts the roadmap table as a new Discussion
21
+ in the repo's roadmap category (creates the category if it doesn't exist). Intended as
22
+ a persistent, pinnable roadmap artifact.
23
+
24
+ Reads `.mgw/project.json` and calls GitHub API only. Never reads application source code.
25
+ Follows delegation boundary: no Task() spawns needed — all operations are metadata-only.
26
+ </objective>
27
+
28
+ <execution_context>
29
+ @~/.claude/commands/mgw/workflows/state.md
30
+ @~/.claude/commands/mgw/workflows/github.md
31
+ </execution_context>
32
+
33
+ <context>
34
+ Flags: $ARGUMENTS
35
+
36
+ Repo detected via: gh repo view --json nameWithOwner -q .nameWithOwner
37
+ State: .mgw/project.json
38
+ </context>
39
+
40
+ <process>
41
+
42
+ <step name="parse_arguments">
43
+ **Parse $ARGUMENTS for flags:**
44
+
45
+ ```bash
46
+ SET_DATES=false
47
+ POST_DISCUSSION=false
48
+ JSON_OUTPUT=false
49
+
50
+ for ARG in $ARGUMENTS; do
51
+ case "$ARG" in
52
+ --set-dates) SET_DATES=true ;;
53
+ --post-discussion) POST_DISCUSSION=true ;;
54
+ --json) JSON_OUTPUT=true ;;
55
+ esac
56
+ done
57
+ ```
58
+ </step>
59
+
60
+ <step name="validate_environment">
61
+ **Validate git repo and GitHub remote:**
62
+
63
+ ```bash
64
+ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
65
+ if [ -z "$REPO_ROOT" ]; then
66
+ echo "Not a git repository. Run from a repo root."
67
+ exit 1
68
+ fi
69
+
70
+ MGW_DIR="${REPO_ROOT}/.mgw"
71
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null)
72
+ if [ -z "$REPO" ]; then
73
+ echo "No GitHub remote found. MGW requires a GitHub repo."
74
+ exit 1
75
+ fi
76
+
77
+ if [ ! -f "${MGW_DIR}/project.json" ]; then
78
+ echo "No project initialized. Run /mgw:project first."
79
+ exit 1
80
+ fi
81
+
82
+ OWNER=$(echo "$REPO" | cut -d'/' -f1)
83
+ REPO_NAME=$(echo "$REPO" | cut -d'/' -f2)
84
+ ```
85
+ </step>
86
+
87
+ <step name="load_project">
88
+ **Load project.json and extract milestone data:**
89
+
90
+ ```bash
91
+ PROJECT_JSON=$(cat "${MGW_DIR}/project.json")
92
+
93
+ PROJECT_NAME=$(echo "$PROJECT_JSON" | python3 -c "
94
+ import json, sys
95
+ p = json.load(sys.stdin)
96
+ print(p.get('project', {}).get('name', 'unknown'))
97
+ ")
98
+
99
+ # Extract board URL (top-level board_url or nested project_board.url)
100
+ BOARD_URL=$(echo "$PROJECT_JSON" | python3 -c "
101
+ import json, sys
102
+ p = json.load(sys.stdin)
103
+ url = (p.get('project', {}).get('project_board', {}).get('url', '')
104
+ or p.get('board_url', ''))
105
+ print(url or '')
106
+ " 2>/dev/null || echo "")
107
+
108
+ # Extract all milestones with computed stats
109
+ MILESTONES_DATA=$(echo "$PROJECT_JSON" | python3 -c "
110
+ import json, sys
111
+
112
+ p = json.load(sys.stdin)
113
+ milestones = p.get('milestones', [])
114
+ out = []
115
+ for m in milestones:
116
+ issues = m.get('issues', [])
117
+ total = len(issues)
118
+ done = sum(1 for i in issues if i.get('pipeline_stage') in ('done', 'pr-created'))
119
+ pct = int((done / total) * 100) if total > 0 else 0
120
+ out.append({
121
+ 'github_number': m.get('github_number'),
122
+ 'name': m.get('name', 'Unnamed'),
123
+ 'gsd_state': m.get('gsd_state'),
124
+ 'total': total,
125
+ 'done': done,
126
+ 'pct': pct,
127
+ 'github_url': m.get('github_url', ''),
128
+ })
129
+ print(json.dumps(out))
130
+ ")
131
+
132
+ TOTAL_MILESTONES=$(echo "$MILESTONES_DATA" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
133
+ ```
134
+ </step>
135
+
136
+ <step name="fetch_github_milestone_due_dates">
137
+ **Fetch current due dates from GitHub for each milestone:**
138
+
139
+ ```bash
140
+ # Fetch all GitHub milestones for this repo once (avoids N+1 API calls)
141
+ GH_MILESTONES=$(gh api "repos/${REPO}/milestones?state=all&per_page=100" 2>/dev/null || echo "[]")
142
+
143
+ # Build map: github_number -> due_on
144
+ DUE_DATE_MAP=$(echo "$GH_MILESTONES" | python3 -c "
145
+ import json, sys
146
+ milestones = json.load(sys.stdin)
147
+ result = {}
148
+ for m in milestones:
149
+ due = m.get('due_on', '') or ''
150
+ # Trim to date only (GitHub returns ISO datetime)
151
+ if due:
152
+ due = due[:10]
153
+ result[str(m['number'])] = due
154
+ print(json.dumps(result))
155
+ ")
156
+ ```
157
+ </step>
158
+
159
+ <step name="build_roadmap_table">
160
+ **Compute per-milestone rows for the roadmap table:**
161
+
162
+ ```bash
163
+ TABLE_DATA=$(echo "$MILESTONES_DATA" | python3 -c "
164
+ import json, sys, os
165
+
166
+ milestones = json.load(sys.stdin)
167
+ due_map = json.loads(os.environ.get('DUE_DATE_MAP', '{}'))
168
+ board_url = os.environ.get('BOARD_URL', '')
169
+
170
+ rows = []
171
+ for m in milestones:
172
+ num = str(m.get('github_number', ''))
173
+ due = due_map.get(num, '')
174
+ bar_filled = int(m['pct'] / 100 * 8)
175
+ bar = chr(9608) * bar_filled + chr(9617) * (8 - bar_filled)
176
+
177
+ # Status indicator
178
+ if m.get('gsd_state') == 'completed':
179
+ status = 'done'
180
+ elif m.get('gsd_state') == 'active':
181
+ status = 'active'
182
+ else:
183
+ status = 'planned'
184
+
185
+ rows.append({
186
+ 'number': num,
187
+ 'name': m['name'],
188
+ 'status': status,
189
+ 'done': m['done'],
190
+ 'total': m['total'],
191
+ 'pct': m['pct'],
192
+ 'bar': bar,
193
+ 'due': due or 'not set',
194
+ 'github_url': m.get('github_url', ''),
195
+ })
196
+
197
+ print(json.dumps(rows))
198
+ ")
199
+
200
+ # Export for use in due-date and discussion steps
201
+ export DUE_DATE_MAP BOARD_URL TABLE_DATA
202
+ ```
203
+ </step>
204
+
205
+ <step name="display_roadmap_table">
206
+ **Print the roadmap table to the terminal:**
207
+
208
+ ```bash
209
+ echo "$TABLE_DATA" | python3 -c "
210
+ import json, sys
211
+
212
+ rows = json.load(sys.stdin)
213
+
214
+ print()
215
+ print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
216
+ print(' MGW > ROADMAP: ${PROJECT_NAME}')
217
+ if '${BOARD_URL}':
218
+ print(' Board: ${BOARD_URL}')
219
+ print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
220
+ print()
221
+
222
+ # Header
223
+ print(f'{\"Milestone\":<48} {\"Status\":<8} {\"Issues\":<10} {\"Progress\":<14} {\"Due Date\":<12}')
224
+ print('-' * 98)
225
+
226
+ for r in rows:
227
+ name_cell = r['name'][:47]
228
+ issues_cell = f\"{r['done']}/{r['total']}\"
229
+ progress_cell = f\"{r['bar']} {r['pct']}%\"
230
+ print(f\"{name_cell:<48} {r['status']:<8} {issues_cell:<10} {progress_cell:<14} {r['due']:<12}\")
231
+
232
+ print()
233
+ total_issues = sum(r['total'] for r in rows)
234
+ done_issues = sum(r['done'] for r in rows)
235
+ overall_pct = int((done_issues / total_issues) * 100) if total_issues > 0 else 0
236
+ print(f'Overall: {done_issues}/{total_issues} issues done ({overall_pct}%)')
237
+ print()
238
+ "
239
+ ```
240
+
241
+ **If `--json` flag:** emit JSON instead of formatted table and exit:
242
+
243
+ ```bash
244
+ if [ "$JSON_OUTPUT" = true ]; then
245
+ echo "$TABLE_DATA" | python3 -c "
246
+ import json, sys
247
+ rows = json.load(sys.stdin)
248
+ result = {
249
+ 'repo': '${REPO}',
250
+ 'project_name': '${PROJECT_NAME}',
251
+ 'board_url': '${BOARD_URL}',
252
+ 'milestones': rows
253
+ }
254
+ print(json.dumps(result, indent=2))
255
+ "
256
+ exit 0
257
+ fi
258
+ ```
259
+ </step>
260
+
261
+ <step name="set_due_dates">
262
+ **If `--set-dates`: interactively set GitHub milestone due dates:**
263
+
264
+ This step runs only when `SET_DATES=true`. It prompts for a date per milestone and
265
+ calls the GitHub REST API to set `due_on`. Setting due dates enables the Roadmap layout
266
+ timeline in GitHub Projects v2.
267
+
268
+ ```bash
269
+ if [ "$SET_DATES" = true ]; then
270
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
271
+ echo " SET DUE DATES"
272
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
273
+ echo ""
274
+ echo "Enter due dates for each milestone (YYYY-MM-DD format, or press Enter to skip)."
275
+ echo ""
276
+
277
+ # Iterate each milestone and prompt
278
+ echo "$TABLE_DATA" | python3 -c "import json,sys; [print(r['number'], r['name']) for r in json.load(sys.stdin) if r['number']]" | \
279
+ while read -r MILESTONE_NUM MILESTONE_NAME; do
280
+ CURRENT_DUE=$(echo "$DUE_DATE_MAP" | python3 -c "
281
+ import json,sys
282
+ m = json.load(sys.stdin)
283
+ print(m.get('${MILESTONE_NUM}', 'not set'))
284
+ ")
285
+ echo -n " ${MILESTONE_NAME} (current: ${CURRENT_DUE}): "
286
+ read -r INPUT_DATE
287
+
288
+ if [ -z "$INPUT_DATE" ]; then
289
+ echo " → skipped"
290
+ continue
291
+ fi
292
+
293
+ # Validate date format
294
+ if ! echo "$INPUT_DATE" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then
295
+ echo " → invalid format (expected YYYY-MM-DD), skipped"
296
+ continue
297
+ fi
298
+
299
+ # GitHub requires ISO 8601 with time: append T07:00:00Z (noon UTC)
300
+ DUE_ISO="${INPUT_DATE}T07:00:00Z"
301
+
302
+ RESULT=$(gh api "repos/${REPO}/milestones/${MILESTONE_NUM}" \
303
+ --method PATCH \
304
+ -f due_on="$DUE_ISO" \
305
+ --jq '.due_on' 2>&1)
306
+
307
+ if echo "$RESULT" | grep -q "^[0-9]"; then
308
+ echo " → set to ${INPUT_DATE}"
309
+ else
310
+ echo " → error setting date: ${RESULT}"
311
+ fi
312
+ done
313
+
314
+ echo ""
315
+ echo "Due dates updated. Roadmap timeline is now enabled in GitHub Projects v2."
316
+ echo "Open the board and switch to Roadmap view to see the timeline."
317
+ echo ""
318
+ fi
319
+ ```
320
+
321
+ Note: `due_on` is set via the GitHub REST API at `PATCH /repos/{owner}/{repo}/milestones/{milestone_number}`.
322
+ The milestone number here is the GitHub milestone number (integer), not the project.json index.
323
+ </step>
324
+
325
+ <step name="post_discussion">
326
+ **If `--post-discussion`: post the roadmap table as a GitHub Discussion:**
327
+
328
+ This step runs only when `POST_DISCUSSION=true`. It creates (or finds) a "Roadmap"
329
+ Discussion category and posts the markdown table as a new Discussion titled
330
+ "Project Roadmap — {project_name}".
331
+
332
+ ```bash
333
+ if [ "$POST_DISCUSSION" = true ]; then
334
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
335
+ echo " POST DISCUSSION"
336
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
337
+ echo ""
338
+
339
+ # Get repo node ID (needed for GraphQL Discussion mutation)
340
+ REPO_NODE_ID=$(gh api graphql -f query='
341
+ query($owner: String!, $name: String!) {
342
+ repository(owner: $owner, name: $name) { id }
343
+ }
344
+ ' -f owner="$OWNER" -f name="$REPO_NAME" --jq '.data.repository.id')
345
+
346
+ # Find or create the Roadmap discussion category
347
+ CATEGORY_ID=$(gh api graphql -f query='
348
+ query($owner: String!, $name: String!) {
349
+ repository(owner: $owner, name: $name) {
350
+ discussionCategories(first: 25) {
351
+ nodes { id name }
352
+ }
353
+ }
354
+ }
355
+ ' -f owner="$OWNER" -f name="$REPO_NAME" \
356
+ --jq '.data.repository.discussionCategories.nodes[] | select(.name == "Roadmap") | .id' 2>/dev/null)
357
+
358
+ if [ -z "$CATEGORY_ID" ]; then
359
+ echo "No 'Roadmap' discussion category found."
360
+ echo "Please create a 'Roadmap' category in GitHub Discussions settings, then re-run."
361
+ echo "Path: https://github.com/${REPO}/settings -> Discussions -> Categories"
362
+ echo ""
363
+ echo "Alternatively, posting to the 'Announcements' category (first available)..."
364
+
365
+ # Fallback: use the first available category
366
+ CATEGORY_ID=$(gh api graphql -f query='
367
+ query($owner: String!, $name: String!) {
368
+ repository(owner: $owner, name: $name) {
369
+ discussionCategories(first: 1) {
370
+ nodes { id name }
371
+ }
372
+ }
373
+ }
374
+ ' -f owner="$OWNER" -f name="$REPO_NAME" \
375
+ --jq '.data.repository.discussionCategories.nodes[0].id' 2>/dev/null)
376
+
377
+ CATEGORY_NAME=$(gh api graphql -f query='
378
+ query($owner: String!, $name: String!) {
379
+ repository(owner: $owner, name: $name) {
380
+ discussionCategories(first: 1) {
381
+ nodes { id name }
382
+ }
383
+ }
384
+ }
385
+ ' -f owner="$OWNER" -f name="$REPO_NAME" \
386
+ --jq '.data.repository.discussionCategories.nodes[0].name' 2>/dev/null)
387
+
388
+ echo "Using category: ${CATEGORY_NAME}"
389
+ echo ""
390
+ fi
391
+
392
+ if [ -z "$CATEGORY_ID" ]; then
393
+ echo "Could not find a discussion category. Discussions may not be enabled."
394
+ echo "Enable Discussions at: https://github.com/${REPO}/settings"
395
+ else
396
+ # Build discussion body (markdown table)
397
+ DISCUSSION_BODY=$(echo "$TABLE_DATA" | python3 -c "
398
+ import json, sys, os
399
+
400
+ rows = json.load(sys.stdin)
401
+ board_url = os.environ.get('BOARD_URL', '')
402
+ project_name = os.environ.get('PROJECT_NAME', 'MGW')
403
+
404
+ lines = []
405
+ lines.append('## Project Roadmap')
406
+ lines.append('')
407
+ if board_url:
408
+ lines.append(f'> Board: {board_url}')
409
+ lines.append('')
410
+ lines.append('| Milestone | Status | Issues | Progress | Due Date |')
411
+ lines.append('|-----------|--------|--------|----------|----------|')
412
+
413
+ for r in rows:
414
+ name_link = f\"[{r['name']}]({r['github_url']})\" if r.get('github_url') else r['name']
415
+ issues_cell = f\"{r['done']}/{r['total']}\"
416
+ progress_cell = f\"{r['bar']} {r['pct']}%\"
417
+ lines.append(f\"| {name_link} | {r['status']} | {issues_cell} | {progress_cell} | {r['due']} |\")
418
+
419
+ lines.append('')
420
+ total_issues = sum(r['total'] for r in rows)
421
+ done_issues = sum(r['done'] for r in rows)
422
+ overall_pct = int((done_issues / total_issues) * 100) if total_issues > 0 else 0
423
+ lines.append(f'**Overall:** {done_issues}/{total_issues} issues done ({overall_pct}%)')
424
+ lines.append('')
425
+ lines.append('---')
426
+ lines.append('*Auto-generated by [MGW](https://github.com/snipcodeit/mgw) — `/mgw:roadmap --post-discussion`*')
427
+
428
+ print('\n'.join(lines))
429
+ ")
430
+
431
+ DISCUSSION_TITLE="Project Roadmap — ${PROJECT_NAME}"
432
+
433
+ # Create the Discussion via GraphQL
434
+ DISCUSSION_RESULT=$(gh api graphql -f query='
435
+ mutation($repoId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
436
+ createDiscussion(input: {
437
+ repositoryId: $repoId,
438
+ categoryId: $categoryId,
439
+ title: $title,
440
+ body: $body
441
+ }) {
442
+ discussion { url number }
443
+ }
444
+ }
445
+ ' \
446
+ -f repoId="$REPO_NODE_ID" \
447
+ -f categoryId="$CATEGORY_ID" \
448
+ -f title="$DISCUSSION_TITLE" \
449
+ -f body="$DISCUSSION_BODY" 2>&1)
450
+
451
+ DISCUSSION_URL=$(echo "$DISCUSSION_RESULT" | python3 -c "
452
+ import json, sys
453
+ try:
454
+ d = json.load(sys.stdin)
455
+ print(d['data']['createDiscussion']['discussion']['url'])
456
+ except Exception:
457
+ print('')
458
+ " 2>/dev/null)
459
+
460
+ if [ -n "$DISCUSSION_URL" ]; then
461
+ echo "Discussion posted: ${DISCUSSION_URL}"
462
+ echo ""
463
+ echo "To pin this roadmap: open the discussion and click 'Pin discussion'."
464
+ else
465
+ echo "Failed to post discussion. Response:"
466
+ echo "$DISCUSSION_RESULT" | python3 -m json.tool 2>/dev/null || echo "$DISCUSSION_RESULT"
467
+ fi
468
+ fi
469
+ fi
470
+ ```
471
+ </step>
472
+
473
+ </process>
474
+
475
+ <success_criteria>
476
+ - [ ] project.json loaded; graceful error when missing
477
+ - [ ] Milestone table printed with name, status, done/total issues, progress bar + percentage, due date
478
+ - [ ] Overall summary line (total done/total across all milestones)
479
+ - [ ] --json flag outputs machine-readable JSON and exits 0
480
+ - [ ] --set-dates: prompts for date per milestone, sets GitHub milestone due_on via REST API
481
+ - [ ] --set-dates: skips milestones where Enter is pressed with no input
482
+ - [ ] --set-dates: validates YYYY-MM-DD format before API call; reports invalid format without exiting
483
+ - [ ] --post-discussion: finds or falls back from Roadmap category; creates Discussion with markdown table
484
+ - [ ] --post-discussion: outputs Discussion URL on success
485
+ - [ ] --post-discussion: error message when Discussions not enabled, no exit failure
486
+ - [ ] Board URL shown in header when present in project.json
487
+ - [ ] Read-only by default: no GitHub writes unless --set-dates or --post-discussion passed
488
+ - [ ] Delegation boundary respected: no application source reads, no Task() spawns needed
489
+ </success_criteria>