@snipcodeit/mgw 0.2.2 → 0.4.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.
package/commands/board.md CHANGED
@@ -145,1552 +145,32 @@ BOARD_CONFIGURED=$([ -n "$BOARD_NODE_ID" ] && echo "true" || echo "false")
145
145
  ```
146
146
  </step>
147
147
 
148
- <step name="subcommand_create">
149
- **Execute 'create' subcommand:**
148
+ <step name="route_subcommand">
149
+ **Route to subcommand implementation:**
150
150
 
151
- Only run if `$SUBCOMMAND = "create"`.
152
-
153
- **Idempotency check:**
154
-
155
- ```bash
156
- if [ "$SUBCOMMAND" = "create" ]; then
157
- if [ "$BOARD_CONFIGURED" = "true" ]; then
158
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
159
- echo " MGW ► BOARD ALREADY CONFIGURED"
160
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
161
- echo ""
162
- echo "Board: #${BOARD_NUMBER} — ${BOARD_URL}"
163
- echo "Node ID: ${BOARD_NODE_ID}"
164
- echo ""
165
- echo "Custom fields:"
166
- echo "$FIELDS_JSON" | python3 -c "
167
- import json,sys
168
- fields = json.load(sys.stdin)
169
- for name, data in fields.items():
170
- print(f\" {name}: {data.get('field_id', 'unknown')} ({data.get('type','?')})\")
171
- " 2>/dev/null
172
- echo ""
173
- echo "To update field options: /mgw:board configure"
174
- echo "To see board items: /mgw:board show"
175
- exit 0
176
- fi
177
- ```
178
-
179
- **Board discovery: check GitHub for an existing board before creating a new one:**
180
-
181
- One lightweight GraphQL list call. Searches the first 20 user/org projects for a title
182
- containing the project name. If found, registers it in project.json and exits — no fields
183
- created, no board duplicated. Only runs when `BOARD_CONFIGURED = false`.
184
-
185
- ```bash
186
- echo "Checking GitHub for existing boards..."
187
- DISCOVERED=$(node -e "
188
- const { findExistingBoard, getProjectFields } = require('./lib/github.cjs');
189
- const board = findExistingBoard('${OWNER}', '${PROJECT_NAME}');
190
- if (!board) { process.stdout.write(''); process.exit(0); }
191
- const fields = getProjectFields('${OWNER}', board.number) || {};
192
- console.log(JSON.stringify({ ...board, fields }));
193
- " 2>/dev/null || echo "")
194
-
195
- if [ -n "$DISCOVERED" ]; then
196
- DISC_NUMBER=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['number'])")
197
- DISC_URL=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['url'])")
198
- DISC_NODE_ID=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['nodeId'])")
199
- DISC_TITLE=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['title'])")
200
- DISC_FIELDS=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin).get('fields', {})))")
201
-
202
- echo " Found existing board: #${DISC_NUMBER} \"${DISC_TITLE}\" — ${DISC_URL}"
203
-
204
- python3 << PYEOF
205
- import json
206
-
207
- with open('${MGW_DIR}/project.json') as f:
208
- project = json.load(f)
209
-
210
- fields = json.loads('''${DISC_FIELDS}''') if '${DISC_FIELDS}' not in ('', '{}') else {}
211
-
212
- project['project']['project_board'] = {
213
- 'number': int('${DISC_NUMBER}'),
214
- 'url': '${DISC_URL}',
215
- 'node_id': '${DISC_NODE_ID}',
216
- 'fields': fields
217
- }
218
-
219
- with open('${MGW_DIR}/project.json', 'w') as f:
220
- json.dump(project, f, indent=2)
221
-
222
- print('project.json updated')
223
- PYEOF
224
-
225
- echo ""
226
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
227
- echo " MGW ► EXISTING BOARD REGISTERED"
228
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
229
- echo ""
230
- echo "Board: #${DISC_NUMBER} — ${DISC_URL}"
231
- echo "Node ID: ${DISC_NODE_ID}"
232
- echo ""
233
- if [ "$DISC_FIELDS" != "{}" ] && [ -n "$DISC_FIELDS" ]; then
234
- echo "Fields registered:"
235
- echo "$DISC_FIELDS" | python3 -c "
236
- import json,sys
237
- fields = json.load(sys.stdin)
238
- for name, data in fields.items():
239
- ftype = data.get('type', '?')
240
- print(f' {name}: {data.get(\"field_id\",\"?\")} ({ftype})')
241
- " 2>/dev/null
242
- else
243
- echo " (no custom fields found — run /mgw:board configure to add them)"
244
- fi
245
- echo ""
246
- echo "To see board items: /mgw:board show"
247
- exit 0
248
- fi
249
-
250
- echo " No existing board found — creating new board..."
251
- echo ""
252
- ```
253
-
254
- **Get owner and repo node IDs (required for GraphQL mutations):**
255
-
256
- ```bash
257
- OWNER_ID=$(gh api graphql -f query='
258
- query($login: String!) {
259
- user(login: $login) { id }
260
- }
261
- ' -f login="$OWNER" --jq '.data.user.id' 2>/dev/null)
262
-
263
- # Fall back to org if user lookup fails
264
- if [ -z "$OWNER_ID" ]; then
265
- OWNER_ID=$(gh api graphql -f query='
266
- query($login: String!) {
267
- organization(login: $login) { id }
268
- }
269
- ' -f login="$OWNER" --jq '.data.organization.id' 2>/dev/null)
270
- fi
271
-
272
- if [ -z "$OWNER_ID" ]; then
273
- echo "ERROR: Cannot resolve owner ID for '${OWNER}'. Check your GitHub token permissions."
274
- exit 1
275
- fi
276
-
277
- REPO_NODE_ID=$(gh api graphql -f query='
278
- query($owner: String!, $name: String!) {
279
- repository(owner: $owner, name: $name) { id }
280
- }
281
- ' -f owner="$OWNER" -f name="$REPO_NAME" --jq '.data.repository.id' 2>/dev/null)
282
- ```
283
-
284
- **Create the project board:**
285
-
286
- ```bash
287
- BOARD_TITLE="${PROJECT_NAME} — MGW Pipeline Board"
288
- echo "Creating GitHub Projects v2 board: '${BOARD_TITLE}'..."
289
-
290
- CREATE_RESULT=$(gh api graphql -f query='
291
- mutation($ownerId: ID!, $title: String!) {
292
- createProjectV2(input: {
293
- ownerId: $ownerId
294
- title: $title
295
- }) {
296
- projectV2 {
297
- id
298
- number
299
- url
300
- }
301
- }
302
- }
303
- ' -f ownerId="$OWNER_ID" -f title="$BOARD_TITLE" 2>&1)
304
-
305
- NEW_PROJECT_ID=$(echo "$CREATE_RESULT" | python3 -c "
306
- import json,sys
307
- d = json.load(sys.stdin)
308
- print(d['data']['createProjectV2']['projectV2']['id'])
309
- " 2>/dev/null)
310
-
311
- NEW_PROJECT_NUMBER=$(echo "$CREATE_RESULT" | python3 -c "
312
- import json,sys
313
- d = json.load(sys.stdin)
314
- print(d['data']['createProjectV2']['projectV2']['number'])
315
- " 2>/dev/null)
316
-
317
- NEW_PROJECT_URL=$(echo "$CREATE_RESULT" | python3 -c "
318
- import json,sys
319
- d = json.load(sys.stdin)
320
- print(d['data']['createProjectV2']['projectV2']['url'])
321
- " 2>/dev/null)
322
-
323
- if [ -z "$NEW_PROJECT_ID" ]; then
324
- echo "ERROR: Failed to create project board."
325
- echo "GraphQL response: ${CREATE_RESULT}"
326
- exit 1
327
- fi
328
-
329
- echo " Created board: #${NEW_PROJECT_NUMBER} — ${NEW_PROJECT_URL}"
330
- echo " Board node ID: ${NEW_PROJECT_ID}"
331
- ```
332
-
333
- **Create custom fields (Status, AI Agent State, Milestone, Phase, GSD Route):**
334
-
335
- Field definitions follow docs/BOARD-SCHEMA.md from issue #71.
336
-
337
- ```bash
338
- echo ""
339
- echo "Creating custom fields..."
340
-
341
- # Field 1: Status (SINGLE_SELECT — maps to pipeline_stage)
342
- STATUS_RESULT=$(gh api graphql -f query='
343
- mutation($projectId: ID!) {
344
- createProjectV2Field(input: {
345
- projectId: $projectId
346
- dataType: SINGLE_SELECT
347
- name: "Status"
348
- singleSelectOptions: [
349
- { name: "New", color: GRAY, description: "Issue created, not yet triaged" }
350
- { name: "Triaged", color: BLUE, description: "Triage complete, ready for execution" }
351
- { name: "Needs Info", color: YELLOW, description: "Blocked at triage gate" }
352
- { name: "Needs Security Review", color: RED, description: "High security risk flagged" }
353
- { name: "Discussing", color: PURPLE, description: "Awaiting stakeholder scope approval" }
354
- { name: "Approved", color: GREEN, description: "Cleared for execution" }
355
- { name: "Planning", color: BLUE, description: "GSD planner agent active" }
356
- { name: "Executing", color: ORANGE, description: "GSD executor agent active" }
357
- { name: "Verifying", color: BLUE, description: "GSD verifier agent active" }
358
- { name: "PR Created", color: GREEN, description: "PR open, awaiting review" }
359
- { name: "Done", color: GREEN, description: "PR merged, issue closed" }
360
- { name: "Failed", color: RED, description: "Unrecoverable pipeline error" }
361
- { name: "Blocked", color: RED, description: "Blocking comment detected" }
362
- ]
363
- }) {
364
- projectV2Field {
365
- ... on ProjectV2SingleSelectField {
366
- id
367
- name
368
- options { id name }
369
- }
370
- }
371
- }
372
- }
373
- ' -f projectId="$NEW_PROJECT_ID" 2>&1)
374
-
375
- STATUS_FIELD_ID=$(echo "$STATUS_RESULT" | python3 -c "
376
- import json,sys
377
- d = json.load(sys.stdin)
378
- print(d['data']['createProjectV2Field']['projectV2Field']['id'])
379
- " 2>/dev/null)
380
-
381
- # Build option ID map from result
382
- STATUS_OPTIONS=$(echo "$STATUS_RESULT" | python3 -c "
383
- import json,sys
384
- d = json.load(sys.stdin)
385
- options = d['data']['createProjectV2Field']['projectV2Field']['options']
386
- # Map lowercase pipeline_stage keys to option IDs
387
- stage_map = {
388
- 'new': 'New', 'triaged': 'Triaged', 'needs-info': 'Needs Info',
389
- 'needs-security-review': 'Needs Security Review', 'discussing': 'Discussing',
390
- 'approved': 'Approved', 'planning': 'Planning', 'executing': 'Executing',
391
- 'verifying': 'Verifying', 'pr-created': 'PR Created', 'done': 'Done',
392
- 'failed': 'Failed', 'blocked': 'Blocked'
393
- }
394
- name_to_id = {o['name']: o['id'] for o in options}
395
- result = {stage: name_to_id.get(display, '') for stage, display in stage_map.items()}
396
- print(json.dumps(result))
397
- " 2>/dev/null || echo "{}")
398
-
399
- if [ -n "$STATUS_FIELD_ID" ]; then
400
- echo " Status field created: ${STATUS_FIELD_ID}"
401
- else
402
- echo " WARNING: Status field creation failed: ${STATUS_RESULT}"
403
- fi
404
-
405
- # Field 2: AI Agent State (TEXT)
406
- AI_STATE_RESULT=$(gh api graphql -f query='
407
- mutation($projectId: ID!) {
408
- createProjectV2Field(input: {
409
- projectId: $projectId
410
- dataType: TEXT
411
- name: "AI Agent State"
412
- }) {
413
- projectV2Field {
414
- ... on ProjectV2Field {
415
- id
416
- name
417
- }
418
- }
419
- }
420
- }
421
- ' -f projectId="$NEW_PROJECT_ID" 2>&1)
422
-
423
- AI_STATE_FIELD_ID=$(echo "$AI_STATE_RESULT" | python3 -c "
424
- import json,sys
425
- d = json.load(sys.stdin)
426
- print(d['data']['createProjectV2Field']['projectV2Field']['id'])
427
- " 2>/dev/null)
428
-
429
- if [ -n "$AI_STATE_FIELD_ID" ]; then
430
- echo " AI Agent State field created: ${AI_STATE_FIELD_ID}"
431
- else
432
- echo " WARNING: AI Agent State field creation failed"
433
- fi
434
-
435
- # Field 3: Milestone (TEXT)
436
- MILESTONE_FIELD_RESULT=$(gh api graphql -f query='
437
- mutation($projectId: ID!) {
438
- createProjectV2Field(input: {
439
- projectId: $projectId
440
- dataType: TEXT
441
- name: "Milestone"
442
- }) {
443
- projectV2Field {
444
- ... on ProjectV2Field {
445
- id
446
- name
447
- }
448
- }
449
- }
450
- }
451
- ' -f projectId="$NEW_PROJECT_ID" 2>&1)
452
-
453
- MILESTONE_FIELD_ID=$(echo "$MILESTONE_FIELD_RESULT" | python3 -c "
454
- import json,sys
455
- d = json.load(sys.stdin)
456
- print(d['data']['createProjectV2Field']['projectV2Field']['id'])
457
- " 2>/dev/null)
458
-
459
- if [ -n "$MILESTONE_FIELD_ID" ]; then
460
- echo " Milestone field created: ${MILESTONE_FIELD_ID}"
461
- else
462
- echo " WARNING: Milestone field creation failed"
463
- fi
464
-
465
- # Field 4: Phase (TEXT)
466
- PHASE_FIELD_RESULT=$(gh api graphql -f query='
467
- mutation($projectId: ID!) {
468
- createProjectV2Field(input: {
469
- projectId: $projectId
470
- dataType: TEXT
471
- name: "Phase"
472
- }) {
473
- projectV2Field {
474
- ... on ProjectV2Field {
475
- id
476
- name
477
- }
478
- }
479
- }
480
- }
481
- ' -f projectId="$NEW_PROJECT_ID" 2>&1)
482
-
483
- PHASE_FIELD_ID=$(echo "$PHASE_FIELD_RESULT" | python3 -c "
484
- import json,sys
485
- d = json.load(sys.stdin)
486
- print(d['data']['createProjectV2Field']['projectV2Field']['id'])
487
- " 2>/dev/null)
488
-
489
- if [ -n "$PHASE_FIELD_ID" ]; then
490
- echo " Phase field created: ${PHASE_FIELD_ID}"
491
- else
492
- echo " WARNING: Phase field creation failed"
493
- fi
494
-
495
- # Field 5: GSD Route (SINGLE_SELECT)
496
- GSD_ROUTE_RESULT=$(gh api graphql -f query='
497
- mutation($projectId: ID!) {
498
- createProjectV2Field(input: {
499
- projectId: $projectId
500
- dataType: SINGLE_SELECT
501
- name: "GSD Route"
502
- singleSelectOptions: [
503
- { name: "quick", color: BLUE, description: "Small/atomic task, direct execution" }
504
- { name: "quick --full", color: BLUE, description: "Small task with plan-checker and verifier" }
505
- { name: "plan-phase", color: PURPLE, description: "Medium task with phase planning" }
506
- { name: "new-milestone", color: ORANGE, description: "Large task with full milestone lifecycle" }
507
- ]
508
- }) {
509
- projectV2Field {
510
- ... on ProjectV2SingleSelectField {
511
- id
512
- name
513
- options { id name }
514
- }
515
- }
516
- }
517
- }
518
- ' -f projectId="$NEW_PROJECT_ID" 2>&1)
519
-
520
- GSD_ROUTE_FIELD_ID=$(echo "$GSD_ROUTE_RESULT" | python3 -c "
521
- import json,sys
522
- d = json.load(sys.stdin)
523
- print(d['data']['createProjectV2Field']['projectV2Field']['id'])
524
- " 2>/dev/null)
525
-
526
- GSD_ROUTE_OPTIONS=$(echo "$GSD_ROUTE_RESULT" | python3 -c "
527
- import json,sys
528
- d = json.load(sys.stdin)
529
- options = d['data']['createProjectV2Field']['projectV2Field']['options']
530
- route_map = {
531
- 'gsd:quick': 'quick', 'gsd:quick --full': 'quick --full',
532
- 'gsd:plan-phase': 'plan-phase', 'gsd:new-milestone': 'new-milestone'
533
- }
534
- name_to_id = {o['name']: o['id'] for o in options}
535
- result = {route: name_to_id.get(display, '') for route, display in route_map.items()}
536
- print(json.dumps(result))
537
- " 2>/dev/null || echo "{}")
538
-
539
- if [ -n "$GSD_ROUTE_FIELD_ID" ]; then
540
- echo " GSD Route field created: ${GSD_ROUTE_FIELD_ID}"
541
- else
542
- echo " WARNING: GSD Route field creation failed"
543
- fi
544
- ```
545
-
546
- **Update project.json with board metadata:**
547
-
548
- ```bash
549
- echo ""
550
- echo "Updating project.json with board metadata..."
551
-
552
- python3 << PYEOF
553
- import json
554
-
555
- with open('${MGW_DIR}/project.json') as f:
556
- project = json.load(f)
557
-
558
- # Build field schema
559
- status_options = json.loads('''${STATUS_OPTIONS}''') if '${STATUS_OPTIONS}' != '{}' else {}
560
- gsd_route_options = json.loads('''${GSD_ROUTE_OPTIONS}''') if '${GSD_ROUTE_OPTIONS}' != '{}' else {}
561
-
562
- fields = {}
563
-
564
- if '${STATUS_FIELD_ID}':
565
- fields['status'] = {
566
- 'field_id': '${STATUS_FIELD_ID}',
567
- 'field_name': 'Status',
568
- 'type': 'SINGLE_SELECT',
569
- 'options': status_options
570
- }
571
-
572
- if '${AI_STATE_FIELD_ID}':
573
- fields['ai_agent_state'] = {
574
- 'field_id': '${AI_STATE_FIELD_ID}',
575
- 'field_name': 'AI Agent State',
576
- 'type': 'TEXT'
577
- }
578
-
579
- if '${MILESTONE_FIELD_ID}':
580
- fields['milestone'] = {
581
- 'field_id': '${MILESTONE_FIELD_ID}',
582
- 'field_name': 'Milestone',
583
- 'type': 'TEXT'
584
- }
585
-
586
- if '${PHASE_FIELD_ID}':
587
- fields['phase'] = {
588
- 'field_id': '${PHASE_FIELD_ID}',
589
- 'field_name': 'Phase',
590
- 'type': 'TEXT'
591
- }
592
-
593
- if '${GSD_ROUTE_FIELD_ID}':
594
- fields['gsd_route'] = {
595
- 'field_id': '${GSD_ROUTE_FIELD_ID}',
596
- 'field_name': 'GSD Route',
597
- 'type': 'SINGLE_SELECT',
598
- 'options': gsd_route_options
599
- }
600
-
601
- # Update project_board section
602
- project['project']['project_board'] = {
603
- 'number': int('${NEW_PROJECT_NUMBER}') if '${NEW_PROJECT_NUMBER}'.isdigit() else None,
604
- 'url': '${NEW_PROJECT_URL}',
605
- 'node_id': '${NEW_PROJECT_ID}',
606
- 'fields': fields
607
- }
608
-
609
- with open('${MGW_DIR}/project.json', 'w') as f:
610
- json.dump(project, f, indent=2)
611
-
612
- print('project.json updated')
613
- PYEOF
614
-
615
- echo ""
616
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
617
- echo " MGW ► BOARD CREATED"
618
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
619
- echo ""
620
- echo "Board: #${NEW_PROJECT_NUMBER} — ${NEW_PROJECT_URL}"
621
- echo "Node ID: ${NEW_PROJECT_ID}"
622
- echo ""
623
- echo "Custom fields created:"
624
- echo " status ${STATUS_FIELD_ID:-FAILED} (SINGLE_SELECT, 13 options)"
625
- echo " ai_agent_state ${AI_STATE_FIELD_ID:-FAILED} (TEXT)"
626
- echo " milestone ${MILESTONE_FIELD_ID:-FAILED} (TEXT)"
627
- echo " phase ${PHASE_FIELD_ID:-FAILED} (TEXT)"
628
- echo " gsd_route ${GSD_ROUTE_FIELD_ID:-FAILED} (SINGLE_SELECT, 4 options)"
629
- echo ""
630
- echo "Field IDs stored in .mgw/project.json"
631
- echo ""
632
- echo "Next:"
633
- echo " /mgw:board show Display board state"
634
- echo " /mgw:run 73 Sync issues onto board items (#73)"
635
-
636
- fi # end create subcommand
637
- ```
638
- </step>
639
-
640
- <step name="subcommand_show">
641
- **Execute 'show' subcommand:**
642
-
643
- Only run if `$SUBCOMMAND = "show"`.
644
-
645
- ```bash
646
- if [ "$SUBCOMMAND" = "show" ]; then
647
- if [ "$BOARD_CONFIGURED" = "false" ]; then
648
- echo "No board configured. Run /mgw:board create first."
649
- exit 1
650
- fi
651
-
652
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
653
- echo " MGW ► BOARD STATE: ${PROJECT_NAME}"
654
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
655
- echo ""
656
- echo "Board: #${BOARD_NUMBER} — ${BOARD_URL}"
657
- echo "Node ID: ${BOARD_NODE_ID}"
658
- echo ""
659
- echo "Custom Fields:"
660
- echo "$FIELDS_JSON" | python3 -c "
661
- import json,sys
662
- fields = json.load(sys.stdin)
663
- for name, data in fields.items():
664
- fid = data.get('field_id', 'unknown')
665
- ftype = data.get('type', 'unknown')
666
- fname = data.get('field_name', name)
667
- if ftype == 'SINGLE_SELECT':
668
- opts = len(data.get('options', {}))
669
- print(f' {fname:<20} {fid} ({ftype}, {opts} options)')
670
- else:
671
- print(f' {fname:<20} {fid} ({ftype})')
672
- " 2>/dev/null
673
- echo ""
674
- ```
675
-
676
- **Fetch board items from GitHub to show current state:**
677
-
678
- ```bash
679
- echo "Fetching board items from GitHub..."
680
-
681
- ITEMS_RESULT=$(gh api graphql -f query='
682
- query($owner: String!, $number: Int!) {
683
- user(login: $owner) {
684
- projectV2(number: $number) {
685
- title
686
- items(first: 50) {
687
- totalCount
688
- nodes {
689
- id
690
- content {
691
- ... on Issue {
692
- number
693
- title
694
- state
695
- }
696
- ... on PullRequest {
697
- number
698
- title
699
- state
700
- }
701
- }
702
- fieldValues(first: 10) {
703
- nodes {
704
- ... on ProjectV2ItemFieldSingleSelectValue {
705
- name
706
- field { ... on ProjectV2SingleSelectField { name } }
707
- }
708
- ... on ProjectV2ItemFieldTextValue {
709
- text
710
- field { ... on ProjectV2Field { name } }
711
- }
712
- }
713
- }
714
- }
715
- }
716
- }
717
- }
718
- }
719
- ' -f owner="$OWNER" -F number="$BOARD_NUMBER" 2>/dev/null)
720
-
721
- # Fall back to org query if user query fails
722
- if echo "$ITEMS_RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); _ = d['data']['user']['projectV2']" 2>/dev/null; then
723
- ITEM_NODES=$(echo "$ITEMS_RESULT" | python3 -c "
724
- import json,sys
725
- d = json.load(sys.stdin)
726
- print(json.dumps(d['data']['user']['projectV2']['items']['nodes']))
727
- " 2>/dev/null || echo "[]")
728
- TOTAL_ITEMS=$(echo "$ITEMS_RESULT" | python3 -c "
729
- import json,sys
730
- d = json.load(sys.stdin)
731
- print(d['data']['user']['projectV2']['items']['totalCount'])
732
- " 2>/dev/null || echo "0")
733
- else
734
- # Try organization lookup
735
- ITEMS_RESULT=$(gh api graphql -f query='
736
- query($owner: String!, $number: Int!) {
737
- organization(login: $owner) {
738
- projectV2(number: $number) {
739
- title
740
- items(first: 50) {
741
- totalCount
742
- nodes {
743
- id
744
- content {
745
- ... on Issue { number title state }
746
- ... on PullRequest { number title state }
747
- }
748
- fieldValues(first: 10) {
749
- nodes {
750
- ... on ProjectV2ItemFieldSingleSelectValue {
751
- name
752
- field { ... on ProjectV2SingleSelectField { name } }
753
- }
754
- ... on ProjectV2ItemFieldTextValue {
755
- text
756
- field { ... on ProjectV2Field { name } }
757
- }
758
- }
759
- }
760
- }
761
- }
762
- }
763
- }
764
- }
765
- ' -f owner="$OWNER" -F number="$BOARD_NUMBER" 2>/dev/null)
766
-
767
- ITEM_NODES=$(echo "$ITEMS_RESULT" | python3 -c "
768
- import json,sys
769
- d = json.load(sys.stdin)
770
- org_data = d.get('data', {}).get('organization') or d.get('data', {}).get('user', {})
771
- proj = org_data.get('projectV2', {})
772
- print(json.dumps(proj.get('items', {}).get('nodes', [])))
773
- " 2>/dev/null || echo "[]")
774
- TOTAL_ITEMS=$(echo "$ITEMS_RESULT" | python3 -c "
775
- import json,sys
776
- d = json.load(sys.stdin)
777
- org_data = d.get('data', {}).get('organization') or d.get('data', {}).get('user', {})
778
- proj = org_data.get('projectV2', {})
779
- print(proj.get('items', {}).get('totalCount', 0))
780
- " 2>/dev/null || echo "0")
781
- fi
782
-
783
- echo "Board Items (${TOTAL_ITEMS} total):"
784
- echo ""
785
-
786
- echo "$ITEM_NODES" | python3 -c "
787
- import json,sys
788
- nodes = json.load(sys.stdin)
789
-
790
- if not nodes:
791
- print(' No items on board yet.')
792
- print(' Run /mgw:run 73 to sync issues as board items (#73).')
793
- sys.exit(0)
794
-
795
- # Group by Status field
796
- by_status = {}
797
- for node in nodes:
798
- content = node.get('content', {})
799
- num = content.get('number', '?')
800
- title = content.get('title', 'Unknown')[:45]
801
- status = 'No Status'
802
- for fv in node.get('fieldValues', {}).get('nodes', []):
803
- field = fv.get('field', {})
804
- if field.get('name') == 'Status':
805
- status = fv.get('name', 'No Status')
806
- break
807
- by_status.setdefault(status, []).append((num, title))
808
-
809
- order = ['Executing', 'Planning', 'Verifying', 'PR Created', 'Triaged', 'Approved',
810
- 'Discussing', 'New', 'Needs Info', 'Needs Security Review', 'Blocked', 'Failed', 'Done', 'No Status']
811
-
812
- for status in order:
813
- items = by_status.pop(status, [])
814
- if items:
815
- print(f' {status} ({len(items)}):')
816
- for num, title in items:
817
- print(f' #{num} {title}')
818
-
819
- for status, items in by_status.items():
820
- print(f' {status} ({len(items)}):')
821
- for num, title in items:
822
- print(f' #{num} {title}')
823
- " 2>/dev/null
824
-
825
- echo ""
826
-
827
- # Show configured views if any
828
- VIEWS_JSON=$(echo "$PROJECT_JSON" | python3 -c "
829
- import json,sys
830
- p = json.load(sys.stdin)
831
- board = p.get('project', {}).get('project_board', {})
832
- views = board.get('views', {})
833
- print(json.dumps(views))
834
- " 2>/dev/null || echo "{}")
835
-
836
- if [ "$VIEWS_JSON" != "{}" ] && [ -n "$VIEWS_JSON" ]; then
837
- echo "Configured Views:"
838
- echo "$VIEWS_JSON" | python3 -c "
839
- import json,sys
840
- views = json.load(sys.stdin)
841
- for key, v in views.items():
842
- print(f' {v[\"name\"]:<40} {v[\"layout\"]:<16} (ID: {v[\"view_id\"]})')
843
- " 2>/dev/null
844
- echo ""
845
- else
846
- echo "Views: none configured"
847
- echo " Run /mgw:board views kanban to create the kanban view"
848
- echo ""
849
- fi
850
-
851
- echo "Open board: ${BOARD_URL}"
852
-
853
- fi # end show subcommand
854
- ```
855
- </step>
856
-
857
- <step name="subcommand_configure">
858
- **Execute 'configure' subcommand:**
859
-
860
- Only run if `$SUBCOMMAND = "configure"`.
861
-
862
- Reads current field options from GitHub and compares to the canonical schema in
863
- docs/BOARD-SCHEMA.md / .mgw/board-schema.json. Adds any missing options.
151
+ Based on the parsed `$SUBCOMMAND`, execute the corresponding subcommand file.
152
+ Each subcommand file expects these variables from load_project:
153
+ `OWNER`, `REPO_NAME`, `MGW_DIR`, `PROJECT_JSON`, `PROJECT_NAME`,
154
+ `BOARD_NUMBER`, `BOARD_URL`, `BOARD_NODE_ID`, `FIELDS_JSON`, `BOARD_CONFIGURED`.
864
155
 
865
156
  ```bash
866
- if [ "$SUBCOMMAND" = "configure" ]; then
867
- if [ "$BOARD_CONFIGURED" = "false" ]; then
868
- echo "No board configured. Run /mgw:board create first."
869
- exit 1
870
- fi
871
-
872
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
873
- echo " MGW ► BOARD CONFIGURE: ${PROJECT_NAME}"
874
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
875
- echo ""
876
- echo "Board: #${BOARD_NUMBER} — ${BOARD_URL}"
877
- echo ""
878
- ```
879
-
880
- **Fetch current field state from GitHub:**
881
-
882
- ```bash
883
- FIELDS_STATE=$(gh api graphql -f query='
884
- query($owner: String!, $number: Int!) {
885
- user(login: $owner) {
886
- projectV2(number: $number) {
887
- fields(first: 20) {
888
- nodes {
889
- ... on ProjectV2SingleSelectField {
890
- id
891
- name
892
- options { id name color description }
893
- }
894
- ... on ProjectV2Field {
895
- id
896
- name
897
- dataType
898
- }
899
- }
900
- }
901
- }
902
- }
903
- }
904
- ' -f owner="$OWNER" -F number="$BOARD_NUMBER" 2>/dev/null)
905
-
906
- # Try org if user fails
907
- if ! echo "$FIELDS_STATE" | python3 -c "import json,sys; d=json.load(sys.stdin); _ = d['data']['user']['projectV2']" 2>/dev/null; then
908
- FIELDS_STATE=$(gh api graphql -f query='
909
- query($owner: String!, $number: Int!) {
910
- organization(login: $owner) {
911
- projectV2(number: $number) {
912
- fields(first: 20) {
913
- nodes {
914
- ... on ProjectV2SingleSelectField {
915
- id
916
- name
917
- options { id name color description }
918
- }
919
- ... on ProjectV2Field {
920
- id
921
- name
922
- dataType
923
- }
924
- }
925
- }
926
- }
927
- }
928
- }
929
- ' -f owner="$OWNER" -F number="$BOARD_NUMBER" 2>/dev/null)
930
- fi
931
-
932
- echo "Current fields on board:"
933
- echo "$FIELDS_STATE" | python3 -c "
934
- import json,sys
935
- d = json.load(sys.stdin)
936
- data = d.get('data', {})
937
- proj = (data.get('user') or data.get('organization', {})).get('projectV2', {})
938
- nodes = proj.get('fields', {}).get('nodes', [])
939
- for node in nodes:
940
- name = node.get('name', 'unknown')
941
- nid = node.get('id', 'unknown')
942
- opts = node.get('options')
943
- if opts is not None:
944
- print(f' {name} (SINGLE_SELECT, {len(opts)} options): {nid}')
945
- for opt in opts:
946
- print(f' - {opt[\"name\"]} ({opt[\"color\"]}) [{opt[\"id\"]}]')
947
- else:
948
- dtype = node.get('dataType', 'TEXT')
949
- print(f' {name} ({dtype}): {nid}')
950
- " 2>/dev/null || echo " (could not fetch field details)"
951
-
952
- echo ""
953
- ```
954
-
955
- **Compare with canonical schema and identify missing options:**
956
-
957
- ```bash
958
- # Canonical Status options from BOARD-SCHEMA.md
959
- CANONICAL_STATUS_OPTIONS='["New","Triaged","Needs Info","Needs Security Review","Discussing","Approved","Planning","Executing","Verifying","PR Created","Done","Failed","Blocked"]'
960
-
961
- # Get current Status option names
962
- CURRENT_STATUS_OPTIONS=$(echo "$FIELDS_STATE" | python3 -c "
963
- import json,sys
964
- d = json.load(sys.stdin)
965
- data = d.get('data', {})
966
- proj = (data.get('user') or data.get('organization', {})).get('projectV2', {})
967
- nodes = proj.get('fields', {}).get('nodes', [])
968
- for node in nodes:
969
- if node.get('name') == 'Status' and 'options' in node:
970
- print(json.dumps([o['name'] for o in node['options']]))
971
- sys.exit(0)
972
- print('[]')
973
- " 2>/dev/null || echo "[]")
974
-
975
- MISSING_STATUS=$(python3 -c "
976
- import json
977
- canonical = json.loads('${CANONICAL_STATUS_OPTIONS}')
978
- current = json.loads('''${CURRENT_STATUS_OPTIONS}''')
979
- missing = [o for o in canonical if o not in current]
980
- if missing:
981
- print('Missing Status options: ' + ', '.join(missing))
982
- else:
983
- print('Status field: all options present')
984
- " 2>/dev/null)
985
-
986
- echo "Schema comparison:"
987
- echo " ${MISSING_STATUS}"
988
-
989
- # Canonical GSD Route options
990
- CANONICAL_GSD_OPTIONS='["quick","quick --full","plan-phase","new-milestone"]'
991
-
992
- CURRENT_GSD_OPTIONS=$(echo "$FIELDS_STATE" | python3 -c "
993
- import json,sys
994
- d = json.load(sys.stdin)
995
- data = d.get('data', {})
996
- proj = (data.get('user') or data.get('organization', {})).get('projectV2', {})
997
- nodes = proj.get('fields', {}).get('nodes', [])
998
- for node in nodes:
999
- if node.get('name') == 'GSD Route' and 'options' in node:
1000
- print(json.dumps([o['name'] for o in node['options']]))
1001
- sys.exit(0)
1002
- print('[]')
1003
- " 2>/dev/null || echo "[]")
1004
-
1005
- MISSING_GSD=$(python3 -c "
1006
- import json
1007
- canonical = json.loads('${CANONICAL_GSD_OPTIONS}')
1008
- current = json.loads('''${CURRENT_GSD_OPTIONS}''')
1009
- missing = [o for o in canonical if o not in current]
1010
- if missing:
1011
- print('Missing GSD Route options: ' + ', '.join(missing))
1012
- else:
1013
- print('GSD Route field: all options present')
1014
- " 2>/dev/null)
1015
-
1016
- echo " ${MISSING_GSD}"
1017
- echo ""
1018
-
1019
- # Check for missing text fields
1020
- CURRENT_FIELD_NAMES=$(echo "$FIELDS_STATE" | python3 -c "
1021
- import json,sys
1022
- d = json.load(sys.stdin)
1023
- data = d.get('data', {})
1024
- proj = (data.get('user') or data.get('organization', {})).get('projectV2', {})
1025
- nodes = proj.get('fields', {}).get('nodes', [])
1026
- print(json.dumps([n.get('name') for n in nodes]))
1027
- " 2>/dev/null || echo "[]")
1028
-
1029
- REQUIRED_TEXT_FIELDS='["AI Agent State","Milestone","Phase"]'
1030
- MISSING_TEXT=$(python3 -c "
1031
- import json
1032
- required = json.loads('${REQUIRED_TEXT_FIELDS}')
1033
- current = json.loads('''${CURRENT_FIELD_NAMES}''')
1034
- missing = [f for f in required if f not in current]
1035
- if missing:
1036
- print('Missing text fields: ' + ', '.join(missing))
1037
- else:
1038
- print('Text fields: all present')
1039
- " 2>/dev/null)
1040
-
1041
- echo " ${MISSING_TEXT}"
1042
- echo ""
1043
-
1044
- # Report: no automated field addition (GitHub Projects v2 API does not support
1045
- # updating existing single-select field options — must delete and recreate)
1046
- echo "Note: GitHub Projects v2 GraphQL does not support adding options to an"
1047
- echo "existing single-select field. To add new pipeline stages:"
1048
- echo " 1. Delete the existing Status field on the board UI"
1049
- echo " 2. Run /mgw:board create (idempotency check will be skipped for fields)"
1050
- echo " Or: manually add options via GitHub Projects UI at ${BOARD_URL}"
1051
- echo ""
1052
- echo "For missing text fields, run /mgw:board create (it will create missing fields)."
1053
-
1054
- fi # end configure subcommand
1055
- ```
1056
- </step>
1057
-
1058
- <step name="subcommand_views">
1059
- **Execute 'views' subcommand:**
1060
-
1061
- Only run if `$SUBCOMMAND = "views"`.
1062
-
1063
- Creates GitHub Projects v2 layout views. Subcommand argument is the view type:
1064
- `kanban`, `table`, or `roadmap`. GitHub's API supports creating views but does NOT
1065
- support programmatic configuration of board grouping — that must be set in the UI.
1066
-
1067
- ```bash
1068
- if [ "$SUBCOMMAND" = "views" ]; then
1069
- if [ "$BOARD_CONFIGURED" = "false" ]; then
1070
- echo "No board configured. Run /mgw:board create first."
1071
- exit 1
1072
- fi
1073
-
1074
- VIEW_TYPE=$(echo "$ARGUMENTS" | awk '{print $2}')
1075
-
1076
- if [ -z "$VIEW_TYPE" ]; then
1077
- echo "Usage: /mgw:board views <kanban|table|roadmap>"
1078
- echo ""
1079
- echo " kanban Create Board layout view (swimlanes by Status)"
1080
- echo " table Create Table layout view (flat list with all fields)"
1081
- echo " roadmap Create Roadmap layout view (timeline grouped by Milestone)"
1082
- exit 1
1083
- fi
1084
-
1085
- case "$VIEW_TYPE" in
1086
- kanban|table|roadmap) ;;
1087
- *)
1088
- echo "Unknown view type: ${VIEW_TYPE}"
1089
- echo "Valid: kanban, table, roadmap"
1090
- exit 1
1091
- ;;
1092
- esac
1093
- ```
1094
-
1095
- **Map view type to layout and name:**
1096
-
1097
- ```bash
1098
- case "$VIEW_TYPE" in
1099
- kanban)
1100
- VIEW_NAME="Kanban — Pipeline Stages"
1101
- VIEW_LAYOUT="BOARD_LAYOUT"
1102
- VIEW_KEY="kanban"
1103
- ;;
1104
- table)
1105
- VIEW_NAME="Triage Table — Team Planning"
1106
- VIEW_LAYOUT="TABLE_LAYOUT"
1107
- VIEW_KEY="table"
1108
- ;;
1109
- roadmap)
1110
- VIEW_NAME="Roadmap — Milestone Timeline"
1111
- VIEW_LAYOUT="ROADMAP_LAYOUT"
1112
- VIEW_KEY="roadmap"
1113
- ;;
1114
- esac
1115
-
1116
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1117
- echo " MGW ► BOARD VIEWS: ${VIEW_NAME}"
1118
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1119
- echo ""
1120
- echo "Board: #${BOARD_NUMBER} — ${BOARD_URL}"
1121
- echo "Creating ${VIEW_LAYOUT} view: '${VIEW_NAME}'..."
1122
- echo ""
1123
- ```
1124
-
1125
- **Create the view via GraphQL:**
1126
-
1127
- ```bash
1128
- CREATE_VIEW_RESULT=$(gh api graphql -f query='
1129
- mutation($projectId: ID!, $name: String!, $layout: ProjectV2ViewLayout!) {
1130
- createProjectV2View(input: {
1131
- projectId: $projectId
1132
- name: $name
1133
- layout: $layout
1134
- }) {
1135
- projectV2View {
1136
- id
1137
- name
1138
- layout
1139
- }
1140
- }
1141
- }
1142
- ' -f projectId="$BOARD_NODE_ID" \
1143
- -f name="$VIEW_NAME" \
1144
- -f layout="$VIEW_LAYOUT" 2>&1)
1145
-
1146
- VIEW_ID=$(echo "$CREATE_VIEW_RESULT" | python3 -c "
1147
- import json,sys
1148
- d = json.load(sys.stdin)
1149
- print(d['data']['createProjectV2View']['projectV2View']['id'])
1150
- " 2>/dev/null)
1151
-
1152
- VIEW_LAYOUT_RETURNED=$(echo "$CREATE_VIEW_RESULT" | python3 -c "
1153
- import json,sys
1154
- d = json.load(sys.stdin)
1155
- print(d['data']['createProjectV2View']['projectV2View']['layout'])
1156
- " 2>/dev/null)
1157
-
1158
- if [ -z "$VIEW_ID" ]; then
1159
- echo "ERROR: Failed to create view."
1160
- echo "GraphQL response: ${CREATE_VIEW_RESULT}"
1161
- exit 1
1162
- fi
1163
-
1164
- echo "View created:"
1165
- echo " Name: ${VIEW_NAME}"
1166
- echo " Layout: ${VIEW_LAYOUT_RETURNED}"
1167
- echo " ID: ${VIEW_ID}"
1168
- echo ""
1169
- ```
1170
-
1171
- **Store view ID in project.json:**
1172
-
1173
- ```bash
1174
- python3 << PYEOF
1175
- import json
1176
-
1177
- with open('${MGW_DIR}/project.json') as f:
1178
- project = json.load(f)
1179
-
1180
- # Ensure views dict exists under project_board
1181
- board = project.setdefault('project', {}).setdefault('project_board', {})
1182
- views = board.setdefault('views', {})
1183
-
1184
- views['${VIEW_KEY}'] = {
1185
- 'view_id': '${VIEW_ID}',
1186
- 'name': '${VIEW_NAME}',
1187
- 'layout': '${VIEW_LAYOUT}'
1188
- }
1189
-
1190
- with open('${MGW_DIR}/project.json', 'w') as f:
1191
- json.dump(project, f, indent=2)
1192
-
1193
- print('project.json updated with view ID')
1194
- PYEOF
1195
- ```
1196
-
1197
- **Output instructions and next steps:**
1198
-
1199
- ```bash
1200
- echo "View ID stored in .mgw/project.json under project.project_board.views.${VIEW_KEY}"
1201
- echo ""
1202
-
1203
- case "$VIEW_TYPE" in
1204
- kanban)
1205
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1206
- echo " NEXT STEP: Configure Group By in GitHub UI"
1207
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1208
- echo ""
1209
- echo "GitHub's API does not support setting board grouping programmatically."
1210
- echo "To create swimlanes by pipeline stage:"
1211
- echo ""
1212
- echo " 1. Open the board: ${BOARD_URL}"
1213
- echo " 2. Click '${VIEW_NAME}' in the view tabs"
1214
- echo " 3. Click the view settings (down-arrow next to view name)"
1215
- echo " 4. Select 'Group by' -> 'Status'"
1216
- echo ""
1217
- echo "Each pipeline stage will become a swimlane column:"
1218
- echo " New / Triaged / Planning / Executing / Verifying / PR Created / Done"
1219
- echo " + Needs Info / Needs Security Review / Discussing / Approved / Failed / Blocked"
1220
- echo ""
1221
- echo "See docs/BOARD-SCHEMA.md for full view configuration reference."
1222
- ;;
1223
- table)
1224
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1225
- echo " NEXT STEP: Configure Columns in GitHub UI"
1226
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1227
- echo ""
1228
- echo "Triage Table view created for team planning visibility."
1229
- echo "GitHub's API does not support setting table columns or sort order"
1230
- echo "programmatically — configure in the GitHub UI:"
1231
- echo ""
1232
- echo " 1. Open the board: ${BOARD_URL}"
1233
- echo " 2. Click '${VIEW_NAME}' in the view tabs"
1234
- echo " 3. Click the view settings (down-arrow next to view name)"
1235
- echo " 4. Add these columns in order:"
1236
- echo " Status (sort ascending — pipeline order)"
1237
- echo " Milestone"
1238
- echo " Phase"
1239
- echo " GSD Route"
1240
- echo " AI Agent State"
1241
- echo " 5. Set 'Sort by' -> 'Status' ascending"
1242
- echo ""
1243
- echo "This column order surfaces triage planning context:"
1244
- echo " Status first shows pipeline position at a glance."
1245
- echo " Milestone + Phase + GSD Route give scope and routing context."
1246
- echo " AI Agent State shows live execution activity."
1247
- echo ""
1248
- echo "See docs/BOARD-SCHEMA.md for full column and sort configuration reference."
1249
- ;;
1250
- roadmap)
1251
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1252
- echo " NEXT STEP: Configure Roadmap in GitHub UI"
1253
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1254
- echo ""
1255
- echo "Roadmap view created for milestone-based timeline visualization."
1256
- echo "GitHub's API does not support setting roadmap grouping or date fields"
1257
- echo "programmatically — configure in the GitHub UI:"
1258
- echo ""
1259
- echo " 1. Open the board: ${BOARD_URL}"
1260
- echo " 2. Click '${VIEW_NAME}' in the view tabs"
1261
- echo " 3. Click the view settings (down-arrow next to view name)"
1262
- echo " 4. Set 'Group by' -> 'Milestone'"
1263
- echo " Items will be grouped by the Milestone field value."
1264
- echo ""
1265
- echo "Timeline date field limitation:"
1266
- echo " GitHub Roadmap requires date fields (start date + end date) to render"
1267
- echo " items on the timeline. MGW uses iteration-based tracking without"
1268
- echo " explicit date fields — items will appear in the roadmap grouped by"
1269
- echo " Milestone but without timeline bars unless date fields are added."
1270
- echo ""
1271
- echo " To enable timeline bars, set milestone due dates via:"
1272
- echo " gh api repos/{owner}/{repo}/milestones/{number} --method PATCH \\"
1273
- echo " -f due_on='YYYY-MM-DDT00:00:00Z'"
1274
- echo " GitHub Projects v2 can read milestone due dates as a date source."
1275
- echo ""
1276
- echo "See docs/BOARD-SCHEMA.md for full roadmap configuration reference."
1277
- ;;
1278
- esac
1279
-
1280
- fi # end views subcommand
1281
- ```
1282
- </step>
1283
-
1284
- <step name="subcommand_sync">
1285
- **Execute 'sync' subcommand:**
1286
-
1287
- Only run if `$SUBCOMMAND = "sync"`.
1288
-
1289
- Reconcile all `.mgw/active/*.json` state files with their GitHub Projects v2 board items.
1290
- Adds missing issues to the board, then updates Status, AI Agent State, Phase, and
1291
- Milestone fields to match current local state. Prints a reconciliation diff table.
1292
-
1293
- ```bash
1294
- if [ "$SUBCOMMAND" = "sync" ]; then
1295
- if [ "$BOARD_CONFIGURED" = "false" ]; then
1296
- echo "No board configured. Run /mgw:board create first."
1297
- exit 1
1298
- fi
1299
-
1300
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1301
- echo " MGW ► BOARD SYNC: ${PROJECT_NAME}"
1302
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1303
- echo ""
1304
- ```
1305
-
1306
- **Collect active state files:**
1307
-
1308
- ```bash
1309
- ACTIVE_DIR="${MGW_DIR}/active"
1310
-
1311
- if ! ls "${ACTIVE_DIR}"/*.json 1>/dev/null 2>&1; then
1312
- echo "No active issues found in ${ACTIVE_DIR}/"
1313
- echo "Nothing to sync."
1314
- exit 0
1315
- fi
1316
-
1317
- ACTIVE_FILES=$(ls "${ACTIVE_DIR}"/*.json 2>/dev/null)
1318
- ACTIVE_COUNT=$(echo "$ACTIVE_FILES" | wc -l)
1319
-
1320
- echo "Reconciling ${ACTIVE_COUNT} active issues against board..."
1321
- echo ""
1322
- ```
1323
-
1324
- **Read field IDs from project.json:**
1325
-
1326
- ```bash
1327
- STATUS_FIELD_ID=$(echo "$FIELDS_JSON" | python3 -c "
1328
- import json,sys
1329
- fields = json.load(sys.stdin)
1330
- print(fields.get('status', {}).get('field_id', ''))
1331
- " 2>/dev/null)
1332
-
1333
- AI_STATE_FIELD_ID=$(echo "$FIELDS_JSON" | python3 -c "
1334
- import json,sys
1335
- fields = json.load(sys.stdin)
1336
- print(fields.get('ai_agent_state', {}).get('field_id', ''))
1337
- " 2>/dev/null)
1338
-
1339
- PHASE_FIELD_ID=$(echo "$FIELDS_JSON" | python3 -c "
1340
- import json,sys
1341
- fields = json.load(sys.stdin)
1342
- print(fields.get('phase', {}).get('field_id', ''))
1343
- " 2>/dev/null)
1344
-
1345
- MILESTONE_FIELD_ID=$(echo "$FIELDS_JSON" | python3 -c "
1346
- import json,sys
1347
- fields = json.load(sys.stdin)
1348
- print(fields.get('milestone', {}).get('field_id', ''))
1349
- " 2>/dev/null)
1350
-
1351
- STATUS_OPTIONS=$(echo "$FIELDS_JSON" | python3 -c "
1352
- import json,sys
1353
- fields = json.load(sys.stdin)
1354
- print(json.dumps(fields.get('status', {}).get('options', {})))
1355
- " 2>/dev/null || echo "{}")
1356
- ```
1357
-
1358
- **Fetch all current board items in a single GraphQL call:**
1359
-
1360
- ```bash
1361
- echo "Fetching current board items from GitHub..."
1362
-
1363
- BOARD_ITEMS_RESULT=$(gh api graphql -f query='
1364
- query($projectId: ID!) {
1365
- node(id: $projectId) {
1366
- ... on ProjectV2 {
1367
- items(first: 100) {
1368
- nodes {
1369
- id
1370
- content {
1371
- ... on Issue {
1372
- number
1373
- id
1374
- }
1375
- ... on PullRequest {
1376
- number
1377
- id
1378
- }
1379
- }
1380
- fieldValues(first: 10) {
1381
- nodes {
1382
- ... on ProjectV2ItemFieldSingleSelectValue {
1383
- name
1384
- field { ... on ProjectV2SingleSelectField { name } }
1385
- }
1386
- ... on ProjectV2ItemFieldTextValue {
1387
- text
1388
- field { ... on ProjectV2Field { name } }
1389
- }
1390
- }
1391
- }
1392
- }
1393
- }
1394
- }
1395
- }
1396
- }
1397
- ' -f projectId="$BOARD_NODE_ID" 2>/dev/null)
1398
-
1399
- # Build: issue_number → {item_id, current_status, current_ai_state, current_phase, current_milestone}
1400
- BOARD_ITEM_MAP=$(echo "$BOARD_ITEMS_RESULT" | python3 -c "
1401
- import json,sys
1402
- d = json.load(sys.stdin)
1403
- nodes = d.get('data', {}).get('node', {}).get('items', {}).get('nodes', [])
1404
- result = {}
1405
- for node in nodes:
1406
- content = node.get('content', {})
1407
- if not content:
1408
- continue
1409
- num = content.get('number')
1410
- if num is None:
1411
- continue
1412
- item_id = node.get('id', '')
1413
- status = ''
1414
- ai_state = ''
1415
- phase = ''
1416
- milestone = ''
1417
- for fv in node.get('fieldValues', {}).get('nodes', []):
1418
- fname = fv.get('field', {}).get('name', '')
1419
- if fname == 'Status':
1420
- status = fv.get('name', '')
1421
- elif fname == 'AI Agent State':
1422
- ai_state = fv.get('text', '')
1423
- elif fname == 'Phase':
1424
- phase = fv.get('text', '')
1425
- elif fname == 'Milestone':
1426
- milestone = fv.get('text', '')
1427
- result[str(num)] = {
1428
- 'item_id': item_id,
1429
- 'status': status,
1430
- 'ai_agent_state': ai_state,
1431
- 'phase': phase,
1432
- 'milestone': milestone
1433
- }
1434
- print(json.dumps(result))
1435
- " 2>/dev/null || echo "{}")
1436
-
1437
- if [ "$BOARD_ITEM_MAP" = "{}" ] && [ -n "$BOARD_ITEMS_RESULT" ]; then
1438
- echo "WARNING: Could not parse board items. Continuing with empty map."
1439
- fi
1440
- ```
1441
-
1442
- **Reconcile each active state file:**
1443
-
1444
- ```bash
1445
- SYNC_RESULTS=()
1446
- UPDATED_COUNT=0
1447
- ADDED_COUNT=0
1448
- ERROR_COUNT=0
1449
-
1450
- for STATE_FILE in $ACTIVE_FILES; do
1451
- # Parse state file
1452
- ISSUE_DATA=$(python3 -c "
1453
- import json,sys
1454
- try:
1455
- s = json.load(open('${STATE_FILE}'))
1456
- num = str(s.get('issue', {}).get('number', ''))
1457
- title = s.get('issue', {}).get('title', 'Unknown')[:45]
1458
- stage = s.get('pipeline_stage', 'new')
1459
- route = s.get('gsd_route', '') or ''
1460
- labels = s.get('issue', {}).get('labels', [])
1461
- # Extract phase from labels matching 'phase:*'
1462
- phase_val = ''
1463
- for lbl in labels:
1464
- if isinstance(lbl, str) and lbl.startswith('phase:'):
1465
- phase_val = lbl.replace('phase:', '')
1466
- break
1467
- elif isinstance(lbl, dict) and lbl.get('name', '').startswith('phase:'):
1468
- phase_val = lbl['name'].replace('phase:', '')
1469
- break
1470
- print(json.dumps({'number': num, 'title': title, 'stage': stage, 'route': route, 'phase': phase_val}))
1471
- except Exception as e:
1472
- print(json.dumps({'error': str(e)}))
1473
- " 2>/dev/null || echo '{"error":"parse failed"}')
1474
-
1475
- ISSUE_NUMBER=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('number',''))" 2>/dev/null)
1476
- ISSUE_TITLE=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('title','Unknown'))" 2>/dev/null)
1477
- PIPELINE_STAGE=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('stage','new'))" 2>/dev/null)
1478
- PHASE_VALUE=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('phase',''))" 2>/dev/null)
1479
-
1480
- if [ -z "$ISSUE_NUMBER" ]; then
1481
- SYNC_RESULTS+=("| ? | (parse error: ${STATE_FILE##*/}) | — | ERROR: could not read state |")
1482
- ERROR_COUNT=$((ERROR_COUNT + 1))
1483
- continue
1484
- fi
1485
-
1486
- # Look up board item
1487
- ITEM_DATA=$(echo "$BOARD_ITEM_MAP" | python3 -c "
1488
- import json,sys
1489
- m = json.load(sys.stdin)
1490
- d = m.get('${ISSUE_NUMBER}', {})
1491
- print(json.dumps(d))
1492
- " 2>/dev/null || echo "{}")
1493
-
1494
- BOARD_ITEM_ID=$(echo "$ITEM_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('item_id',''))" 2>/dev/null)
1495
- CURRENT_STATUS=$(echo "$ITEM_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status',''))" 2>/dev/null)
1496
- CURRENT_PHASE=$(echo "$ITEM_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('phase',''))" 2>/dev/null)
1497
- CURRENT_MILESTONE=$(echo "$ITEM_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('milestone',''))" 2>/dev/null)
1498
-
1499
- CHANGED_FIELDS=""
1500
-
1501
- # If issue is not on board, add it
1502
- if [ -z "$BOARD_ITEM_ID" ]; then
1503
- ISSUE_NODE_ID=$(gh issue view "$ISSUE_NUMBER" --json id -q .id 2>/dev/null || echo "")
1504
- if [ -n "$ISSUE_NODE_ID" ]; then
1505
- ADD_RESULT=$(gh api graphql -f query='
1506
- mutation($projectId: ID!, $contentId: ID!) {
1507
- addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
1508
- item { id }
1509
- }
1510
- }
1511
- ' -f projectId="$BOARD_NODE_ID" -f contentId="$ISSUE_NODE_ID" \
1512
- --jq '.data.addProjectV2ItemById.item.id' 2>/dev/null || echo "")
1513
-
1514
- if [ -n "$ADD_RESULT" ]; then
1515
- BOARD_ITEM_ID="$ADD_RESULT"
1516
- CHANGED_FIELDS="added to board"
1517
- ADDED_COUNT=$((ADDED_COUNT + 1))
1518
- else
1519
- SYNC_RESULTS+=("| #${ISSUE_NUMBER} | ${ISSUE_TITLE} | ${PIPELINE_STAGE} | ERROR: could not add to board |")
1520
- ERROR_COUNT=$((ERROR_COUNT + 1))
1521
- continue
1522
- fi
1523
- else
1524
- SYNC_RESULTS+=("| #${ISSUE_NUMBER} | ${ISSUE_TITLE} | ${PIPELINE_STAGE} | ERROR: could not resolve issue node ID |")
1525
- ERROR_COUNT=$((ERROR_COUNT + 1))
1526
- continue
1527
- fi
1528
- fi
1529
-
1530
- # Get milestone title from project.json for this issue's milestone
1531
- MILESTONE_VALUE=$(python3 -c "
1532
- import json,sys
1533
- try:
1534
- p = json.load(open('${MGW_DIR}/project.json'))
1535
- current_ms = p.get('current_milestone', 1)
1536
- for i, m in enumerate(p.get('milestones', []), 1):
1537
- for issue in m.get('issues', []):
1538
- if str(issue.get('github_number', '')) == '${ISSUE_NUMBER}':
1539
- print(m.get('title', ''))
1540
- sys.exit(0)
1541
- print('')
1542
- except:
1543
- print('')
1544
- " 2>/dev/null)
1545
-
1546
- # Update Status field if it differs
1547
- if [ -n "$STATUS_FIELD_ID" ]; then
1548
- DESIRED_OPTION_ID=$(echo "$STATUS_OPTIONS" | python3 -c "
1549
- import json,sys
1550
- opts = json.load(sys.stdin)
1551
- print(opts.get('${PIPELINE_STAGE}', ''))
1552
- " 2>/dev/null)
1553
-
1554
- if [ -n "$DESIRED_OPTION_ID" ]; then
1555
- # Map current board status name back to stage for comparison
1556
- CURRENT_STAGE=$(echo "$CURRENT_STATUS" | python3 -c "
1557
- import sys
1558
- stage_map = {
1559
- 'New': 'new', 'Triaged': 'triaged', 'Needs Info': 'needs-info',
1560
- 'Needs Security Review': 'needs-security-review', 'Discussing': 'discussing',
1561
- 'Approved': 'approved', 'Planning': 'planning', 'Executing': 'executing',
1562
- 'Verifying': 'verifying', 'PR Created': 'pr-created', 'Done': 'done',
1563
- 'Failed': 'failed', 'Blocked': 'blocked'
1564
- }
1565
- label = sys.stdin.read().strip()
1566
- print(stage_map.get(label, ''))
1567
- " 2>/dev/null)
1568
-
1569
- if [ "$CURRENT_STAGE" != "$PIPELINE_STAGE" ]; then
1570
- gh api graphql -f query='
1571
- mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
1572
- updateProjectV2ItemFieldValue(input: {
1573
- projectId: $projectId
1574
- itemId: $itemId
1575
- fieldId: $fieldId
1576
- value: { singleSelectOptionId: $optionId }
1577
- }) { projectV2Item { id } }
1578
- }
1579
- ' -f projectId="$BOARD_NODE_ID" \
1580
- -f itemId="$BOARD_ITEM_ID" \
1581
- -f fieldId="$STATUS_FIELD_ID" \
1582
- -f optionId="$DESIRED_OPTION_ID" 2>/dev/null || true
1583
-
1584
- if [ -n "$CHANGED_FIELDS" ]; then
1585
- CHANGED_FIELDS="${CHANGED_FIELDS}, Status (${CURRENT_STATUS:-none}→${PIPELINE_STAGE})"
1586
- else
1587
- CHANGED_FIELDS="Status (${CURRENT_STATUS:-none}→${PIPELINE_STAGE})"
1588
- fi
1589
- UPDATED_COUNT=$((UPDATED_COUNT + 1))
1590
- fi
1591
- fi
1592
- fi
1593
-
1594
- # Update AI Agent State field — sync always clears it (ephemeral during execution)
1595
- if [ -n "$AI_STATE_FIELD_ID" ] && [ -n "$CURRENT_AI_STATE" ] && [ "$CURRENT_AI_STATE" != "" ]; then
1596
- CURRENT_AI_STATE=$(echo "$ITEM_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ai_agent_state',''))" 2>/dev/null)
1597
- if [ -n "$CURRENT_AI_STATE" ]; then
1598
- gh api graphql -f query='
1599
- mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $text: String!) {
1600
- updateProjectV2ItemFieldValue(input: {
1601
- projectId: $projectId
1602
- itemId: $itemId
1603
- fieldId: $fieldId
1604
- value: { text: $text }
1605
- }) { projectV2Item { id } }
1606
- }
1607
- ' -f projectId="$BOARD_NODE_ID" \
1608
- -f itemId="$BOARD_ITEM_ID" \
1609
- -f fieldId="$AI_STATE_FIELD_ID" \
1610
- -f text="" 2>/dev/null || true
1611
-
1612
- if [ -n "$CHANGED_FIELDS" ]; then
1613
- CHANGED_FIELDS="${CHANGED_FIELDS}, AI Agent State (cleared)"
1614
- else
1615
- CHANGED_FIELDS="AI Agent State (cleared)"
1616
- fi
1617
- fi
1618
- fi
1619
-
1620
- # Update Phase field if it differs
1621
- if [ -n "$PHASE_FIELD_ID" ] && [ -n "$PHASE_VALUE" ] && [ "$PHASE_VALUE" != "$CURRENT_PHASE" ]; then
1622
- gh api graphql -f query='
1623
- mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $text: String!) {
1624
- updateProjectV2ItemFieldValue(input: {
1625
- projectId: $projectId
1626
- itemId: $itemId
1627
- fieldId: $fieldId
1628
- value: { text: $text }
1629
- }) { projectV2Item { id } }
1630
- }
1631
- ' -f projectId="$BOARD_NODE_ID" \
1632
- -f itemId="$BOARD_ITEM_ID" \
1633
- -f fieldId="$PHASE_FIELD_ID" \
1634
- -f text="$PHASE_VALUE" 2>/dev/null || true
1635
-
1636
- if [ -n "$CHANGED_FIELDS" ]; then
1637
- CHANGED_FIELDS="${CHANGED_FIELDS}, Phase (${CURRENT_PHASE:-none}→${PHASE_VALUE})"
1638
- else
1639
- CHANGED_FIELDS="Phase (${CURRENT_PHASE:-none}→${PHASE_VALUE})"
1640
- fi
1641
- fi
1642
-
1643
- # Update Milestone field if it differs
1644
- if [ -n "$MILESTONE_FIELD_ID" ] && [ -n "$MILESTONE_VALUE" ] && [ "$MILESTONE_VALUE" != "$CURRENT_MILESTONE" ]; then
1645
- gh api graphql -f query='
1646
- mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $text: String!) {
1647
- updateProjectV2ItemFieldValue(input: {
1648
- projectId: $projectId
1649
- itemId: $itemId
1650
- fieldId: $fieldId
1651
- value: { text: $text }
1652
- }) { projectV2Item { id } }
1653
- }
1654
- ' -f projectId="$BOARD_NODE_ID" \
1655
- -f itemId="$BOARD_ITEM_ID" \
1656
- -f fieldId="$MILESTONE_FIELD_ID" \
1657
- -f text="$MILESTONE_VALUE" 2>/dev/null || true
1658
-
1659
- if [ -n "$CHANGED_FIELDS" ]; then
1660
- CHANGED_FIELDS="${CHANGED_FIELDS}, Milestone (${CURRENT_MILESTONE:-none}→${MILESTONE_VALUE})"
1661
- else
1662
- CHANGED_FIELDS="Milestone (${CURRENT_MILESTONE:-none}→${MILESTONE_VALUE})"
1663
- fi
1664
- fi
1665
-
1666
- if [ -z "$CHANGED_FIELDS" ]; then
1667
- CHANGED_FIELDS="no changes"
1668
- fi
1669
-
1670
- SYNC_RESULTS+=("| #${ISSUE_NUMBER} | ${ISSUE_TITLE} | ${PIPELINE_STAGE} | ${CHANGED_FIELDS} |")
1671
- done
1672
- ```
1673
-
1674
- **Print reconciliation diff table:**
1675
-
1676
- ```bash
1677
- echo "| Issue | Title | Stage | Changes |"
1678
- echo "|-------|-------|-------|---------|"
1679
- for ROW in "${SYNC_RESULTS[@]}"; do
1680
- echo "$ROW"
1681
- done
1682
-
1683
- echo ""
1684
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1685
- echo " Sync complete: ${ACTIVE_COUNT} checked, ${UPDATED_COUNT} updated, ${ADDED_COUNT} added, ${ERROR_COUNT} errors"
1686
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1687
-
1688
- if [ "$ERROR_COUNT" -gt 0 ]; then
1689
- echo ""
1690
- echo "WARNING: ${ERROR_COUNT} issue(s) had errors. Check board manually: ${BOARD_URL}"
1691
- fi
1692
-
1693
- fi # end sync subcommand
157
+ case "$SUBCOMMAND" in
158
+ create)
159
+ # @~/.claude/commands/mgw/board/create.md
160
+ ;;
161
+ show)
162
+ # @~/.claude/commands/mgw/board/show.md
163
+ ;;
164
+ configure)
165
+ # @~/.claude/commands/mgw/board/configure.md
166
+ ;;
167
+ views)
168
+ # @~/.claude/commands/mgw/board/views.md
169
+ ;;
170
+ sync)
171
+ # @~/.claude/commands/mgw/board/sync.md
172
+ ;;
173
+ esac
1694
174
  ```
1695
175
  </step>
1696
176