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