@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.
@@ -0,0 +1,688 @@
1
+ ---
2
+ name: board:create
3
+ description: Create the GitHub Projects v2 board and custom fields (idempotent)
4
+ ---
5
+
6
+ <step name="subcommand_create">
7
+ **Execute 'create' subcommand:**
8
+
9
+ Only run if `$SUBCOMMAND = "create"`.
10
+
11
+ **Idempotency check:**
12
+
13
+ ```bash
14
+ if [ "$SUBCOMMAND" = "create" ]; then
15
+ if [ "$BOARD_CONFIGURED" = "true" ]; then
16
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
17
+ echo " MGW ► BOARD ALREADY CONFIGURED"
18
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
19
+ echo ""
20
+ echo "Board: #${BOARD_NUMBER} — ${BOARD_URL}"
21
+ echo "Node ID: ${BOARD_NODE_ID}"
22
+ echo ""
23
+ echo "Custom fields:"
24
+ echo "$FIELDS_JSON" | python3 -c "
25
+ import json,sys
26
+ fields = json.load(sys.stdin)
27
+ for name, data in fields.items():
28
+ print(f\" {name}: {data.get('field_id', 'unknown')} ({data.get('type','?')})\")
29
+ " 2>/dev/null
30
+ echo ""
31
+ echo "To update field options: /mgw:board configure"
32
+ echo "To see board items: /mgw:board show"
33
+ exit 0
34
+ fi
35
+ ```
36
+
37
+ **Board discovery: check GitHub for an existing board before creating a new one:**
38
+
39
+ One lightweight GraphQL list call. Searches the first 20 user/org projects for a title
40
+ containing the project name. If found, registers it in project.json and exits — no fields
41
+ created, no board duplicated. Only runs when `BOARD_CONFIGURED = false`.
42
+
43
+ ```bash
44
+ echo "Checking GitHub for existing boards..."
45
+ DISCOVERED=$(node -e "
46
+ const { findExistingBoard, getProjectFields } = require('./lib/github.cjs');
47
+ const board = findExistingBoard('${OWNER}', '${PROJECT_NAME}');
48
+ if (!board) { process.stdout.write(''); process.exit(0); }
49
+ const fields = getProjectFields('${OWNER}', board.number) || {};
50
+ console.log(JSON.stringify({ ...board, fields }));
51
+ " 2>/dev/null || echo "")
52
+
53
+ if [ -n "$DISCOVERED" ]; then
54
+ DISC_NUMBER=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['number'])")
55
+ DISC_URL=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['url'])")
56
+ DISC_NODE_ID=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['nodeId'])")
57
+ DISC_TITLE=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['title'])")
58
+ DISC_FIELDS=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin).get('fields', {})))")
59
+
60
+ echo " Found existing board: #${DISC_NUMBER} \"${DISC_TITLE}\" — ${DISC_URL}"
61
+
62
+ python3 << PYEOF
63
+ import json
64
+
65
+ with open('${MGW_DIR}/project.json') as f:
66
+ project = json.load(f)
67
+
68
+ fields = json.loads('''${DISC_FIELDS}''') if '${DISC_FIELDS}' not in ('', '{}') else {}
69
+
70
+ project['project']['project_board'] = {
71
+ 'number': int('${DISC_NUMBER}'),
72
+ 'url': '${DISC_URL}',
73
+ 'node_id': '${DISC_NODE_ID}',
74
+ 'fields': fields
75
+ }
76
+
77
+ with open('${MGW_DIR}/project.json', 'w') as f:
78
+ json.dump(project, f, indent=2)
79
+
80
+ print('project.json updated')
81
+ PYEOF
82
+
83
+ echo ""
84
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
85
+ echo " MGW ► EXISTING BOARD REGISTERED"
86
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
87
+ echo ""
88
+ echo "Board: #${DISC_NUMBER} — ${DISC_URL}"
89
+ echo "Node ID: ${DISC_NODE_ID}"
90
+ echo ""
91
+ if [ "$DISC_FIELDS" != "{}" ] && [ -n "$DISC_FIELDS" ]; then
92
+ echo "Fields registered:"
93
+ echo "$DISC_FIELDS" | python3 -c "
94
+ import json,sys
95
+ fields = json.load(sys.stdin)
96
+ for name, data in fields.items():
97
+ ftype = data.get('type', '?')
98
+ print(f' {name}: {data.get(\"field_id\",\"?\")} ({ftype})')
99
+ " 2>/dev/null
100
+ else
101
+ echo " (no custom fields found — run /mgw:board configure to add them)"
102
+ fi
103
+ echo ""
104
+ echo "To see board items: /mgw:board show"
105
+ exit 0
106
+ fi
107
+
108
+ echo " No existing board found — creating new board..."
109
+ echo ""
110
+ ```
111
+
112
+ **Get owner and repo node IDs (required for GraphQL mutations):**
113
+
114
+ ```bash
115
+ OWNER_ID=$(gh api graphql -f query='
116
+ query($login: String!) {
117
+ user(login: $login) { id }
118
+ }
119
+ ' -f login="$OWNER" --jq '.data.user.id' 2>/dev/null)
120
+
121
+ # Fall back to org if user lookup fails
122
+ if [ -z "$OWNER_ID" ]; then
123
+ OWNER_ID=$(gh api graphql -f query='
124
+ query($login: String!) {
125
+ organization(login: $login) { id }
126
+ }
127
+ ' -f login="$OWNER" --jq '.data.organization.id' 2>/dev/null)
128
+ fi
129
+
130
+ if [ -z "$OWNER_ID" ]; then
131
+ echo "ERROR: Cannot resolve owner ID for '${OWNER}'. Check your GitHub token permissions."
132
+ exit 1
133
+ fi
134
+
135
+ REPO_NODE_ID=$(gh api graphql -f query='
136
+ query($owner: String!, $name: String!) {
137
+ repository(owner: $owner, name: $name) { id }
138
+ }
139
+ ' -f owner="$OWNER" -f name="$REPO_NAME" --jq '.data.repository.id' 2>/dev/null)
140
+ ```
141
+
142
+ **Create the project board:**
143
+
144
+ ```bash
145
+ BOARD_TITLE="${PROJECT_NAME} — MGW Pipeline Board"
146
+ echo "Creating GitHub Projects v2 board: '${BOARD_TITLE}'..."
147
+
148
+ CREATE_RESULT=$(gh api graphql -f query='
149
+ mutation($ownerId: ID!, $title: String!) {
150
+ createProjectV2(input: {
151
+ ownerId: $ownerId
152
+ title: $title
153
+ }) {
154
+ projectV2 {
155
+ id
156
+ number
157
+ url
158
+ }
159
+ }
160
+ }
161
+ ' -f ownerId="$OWNER_ID" -f title="$BOARD_TITLE" 2>&1)
162
+
163
+ NEW_PROJECT_ID=$(echo "$CREATE_RESULT" | python3 -c "
164
+ import json,sys
165
+ d = json.load(sys.stdin)
166
+ print(d['data']['createProjectV2']['projectV2']['id'])
167
+ " 2>/dev/null)
168
+
169
+ NEW_PROJECT_NUMBER=$(echo "$CREATE_RESULT" | python3 -c "
170
+ import json,sys
171
+ d = json.load(sys.stdin)
172
+ print(d['data']['createProjectV2']['projectV2']['number'])
173
+ " 2>/dev/null)
174
+
175
+ NEW_PROJECT_URL=$(echo "$CREATE_RESULT" | python3 -c "
176
+ import json,sys
177
+ d = json.load(sys.stdin)
178
+ print(d['data']['createProjectV2']['projectV2']['url'])
179
+ " 2>/dev/null)
180
+
181
+ if [ -z "$NEW_PROJECT_ID" ]; then
182
+ echo "ERROR: Failed to create project board."
183
+ echo "GraphQL response: ${CREATE_RESULT}"
184
+ exit 1
185
+ fi
186
+
187
+ echo " Created board: #${NEW_PROJECT_NUMBER} — ${NEW_PROJECT_URL}"
188
+ echo " Board node ID: ${NEW_PROJECT_ID}"
189
+ ```
190
+
191
+ **Create custom fields (Status, AI Agent State, Milestone, Phase, GSD Route):**
192
+
193
+ Field definitions follow docs/BOARD-SCHEMA.md from issue #71.
194
+
195
+ ```bash
196
+ echo ""
197
+ echo "Creating custom fields..."
198
+
199
+ # Field 1: Status (SINGLE_SELECT — maps to pipeline_stage)
200
+ STATUS_RESULT=$(gh api graphql -f query='
201
+ mutation($projectId: ID!) {
202
+ createProjectV2Field(input: {
203
+ projectId: $projectId
204
+ dataType: SINGLE_SELECT
205
+ name: "Status"
206
+ singleSelectOptions: [
207
+ { name: "New", color: GRAY, description: "Issue created, not yet triaged" }
208
+ { name: "Triaged", color: BLUE, description: "Triage complete, ready for execution" }
209
+ { name: "Needs Info", color: YELLOW, description: "Blocked at triage gate" }
210
+ { name: "Needs Security Review", color: RED, description: "High security risk flagged" }
211
+ { name: "Discussing", color: PURPLE, description: "Awaiting stakeholder scope approval" }
212
+ { name: "Approved", color: GREEN, description: "Cleared for execution" }
213
+ { name: "Planning", color: BLUE, description: "GSD planner agent active" }
214
+ { name: "Executing", color: ORANGE, description: "GSD executor agent active" }
215
+ { name: "Verifying", color: BLUE, description: "GSD verifier agent active" }
216
+ { name: "PR Created", color: GREEN, description: "PR open, awaiting review" }
217
+ { name: "Done", color: GREEN, description: "PR merged, issue closed" }
218
+ { name: "Failed", color: RED, description: "Unrecoverable pipeline error" }
219
+ { name: "Blocked", color: RED, description: "Blocking comment detected" }
220
+ ]
221
+ }) {
222
+ projectV2Field {
223
+ ... on ProjectV2SingleSelectField {
224
+ id
225
+ name
226
+ options { id name }
227
+ }
228
+ }
229
+ }
230
+ }
231
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
232
+
233
+ STATUS_FIELD_ID=$(echo "$STATUS_RESULT" | python3 -c "
234
+ import json,sys
235
+ d = json.load(sys.stdin)
236
+ print(d['data']['createProjectV2Field']['projectV2Field']['id'])
237
+ " 2>/dev/null)
238
+
239
+ # Build option ID map from result
240
+ STATUS_OPTIONS=$(echo "$STATUS_RESULT" | python3 -c "
241
+ import json,sys
242
+ d = json.load(sys.stdin)
243
+ options = d['data']['createProjectV2Field']['projectV2Field']['options']
244
+ # Map lowercase pipeline_stage keys to option IDs
245
+ stage_map = {
246
+ 'new': 'New', 'triaged': 'Triaged', 'needs-info': 'Needs Info',
247
+ 'needs-security-review': 'Needs Security Review', 'discussing': 'Discussing',
248
+ 'approved': 'Approved', 'planning': 'Planning', 'executing': 'Executing',
249
+ 'verifying': 'Verifying', 'pr-created': 'PR Created', 'done': 'Done',
250
+ 'failed': 'Failed', 'blocked': 'Blocked'
251
+ }
252
+ name_to_id = {o['name']: o['id'] for o in options}
253
+ result = {stage: name_to_id.get(display, '') for stage, display in stage_map.items()}
254
+ print(json.dumps(result))
255
+ " 2>/dev/null || echo "{}")
256
+
257
+ if [ -n "$STATUS_FIELD_ID" ]; then
258
+ echo " Status field created: ${STATUS_FIELD_ID}"
259
+ else
260
+ echo " WARNING: Status field creation failed: ${STATUS_RESULT}"
261
+ fi
262
+
263
+ # Field 2: AI Agent State (TEXT)
264
+ AI_STATE_RESULT=$(gh api graphql -f query='
265
+ mutation($projectId: ID!) {
266
+ createProjectV2Field(input: {
267
+ projectId: $projectId
268
+ dataType: TEXT
269
+ name: "AI Agent State"
270
+ }) {
271
+ projectV2Field {
272
+ ... on ProjectV2Field {
273
+ id
274
+ name
275
+ }
276
+ }
277
+ }
278
+ }
279
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
280
+
281
+ AI_STATE_FIELD_ID=$(echo "$AI_STATE_RESULT" | python3 -c "
282
+ import json,sys
283
+ d = json.load(sys.stdin)
284
+ print(d['data']['createProjectV2Field']['projectV2Field']['id'])
285
+ " 2>/dev/null)
286
+
287
+ if [ -n "$AI_STATE_FIELD_ID" ]; then
288
+ echo " AI Agent State field created: ${AI_STATE_FIELD_ID}"
289
+ else
290
+ echo " WARNING: AI Agent State field creation failed"
291
+ fi
292
+
293
+ # Field 3: Milestone (TEXT)
294
+ MILESTONE_FIELD_RESULT=$(gh api graphql -f query='
295
+ mutation($projectId: ID!) {
296
+ createProjectV2Field(input: {
297
+ projectId: $projectId
298
+ dataType: TEXT
299
+ name: "Milestone"
300
+ }) {
301
+ projectV2Field {
302
+ ... on ProjectV2Field {
303
+ id
304
+ name
305
+ }
306
+ }
307
+ }
308
+ }
309
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
310
+
311
+ MILESTONE_FIELD_ID=$(echo "$MILESTONE_FIELD_RESULT" | python3 -c "
312
+ import json,sys
313
+ d = json.load(sys.stdin)
314
+ print(d['data']['createProjectV2Field']['projectV2Field']['id'])
315
+ " 2>/dev/null)
316
+
317
+ if [ -n "$MILESTONE_FIELD_ID" ]; then
318
+ echo " Milestone field created: ${MILESTONE_FIELD_ID}"
319
+ else
320
+ echo " WARNING: Milestone field creation failed"
321
+ fi
322
+
323
+ # Field 4: Phase (TEXT)
324
+ PHASE_FIELD_RESULT=$(gh api graphql -f query='
325
+ mutation($projectId: ID!) {
326
+ createProjectV2Field(input: {
327
+ projectId: $projectId
328
+ dataType: TEXT
329
+ name: "Phase"
330
+ }) {
331
+ projectV2Field {
332
+ ... on ProjectV2Field {
333
+ id
334
+ name
335
+ }
336
+ }
337
+ }
338
+ }
339
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
340
+
341
+ PHASE_FIELD_ID=$(echo "$PHASE_FIELD_RESULT" | python3 -c "
342
+ import json,sys
343
+ d = json.load(sys.stdin)
344
+ print(d['data']['createProjectV2Field']['projectV2Field']['id'])
345
+ " 2>/dev/null)
346
+
347
+ if [ -n "$PHASE_FIELD_ID" ]; then
348
+ echo " Phase field created: ${PHASE_FIELD_ID}"
349
+ else
350
+ echo " WARNING: Phase field creation failed"
351
+ fi
352
+
353
+ # Field 5: GSD Route (SINGLE_SELECT)
354
+ GSD_ROUTE_RESULT=$(gh api graphql -f query='
355
+ mutation($projectId: ID!) {
356
+ createProjectV2Field(input: {
357
+ projectId: $projectId
358
+ dataType: SINGLE_SELECT
359
+ name: "GSD Route"
360
+ singleSelectOptions: [
361
+ { name: "quick", color: BLUE, description: "Small/atomic task, direct execution" }
362
+ { name: "quick --full", color: BLUE, description: "Small task with plan-checker and verifier" }
363
+ { name: "plan-phase", color: PURPLE, description: "Medium task with phase planning" }
364
+ { name: "new-milestone", color: ORANGE, description: "Large task with full milestone lifecycle" }
365
+ ]
366
+ }) {
367
+ projectV2Field {
368
+ ... on ProjectV2SingleSelectField {
369
+ id
370
+ name
371
+ options { id name }
372
+ }
373
+ }
374
+ }
375
+ }
376
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
377
+
378
+ GSD_ROUTE_FIELD_ID=$(echo "$GSD_ROUTE_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
+ GSD_ROUTE_OPTIONS=$(echo "$GSD_ROUTE_RESULT" | python3 -c "
385
+ import json,sys
386
+ d = json.load(sys.stdin)
387
+ options = d['data']['createProjectV2Field']['projectV2Field']['options']
388
+ route_map = {
389
+ 'gsd:quick': 'quick', 'gsd:quick --full': 'quick --full',
390
+ 'gsd:plan-phase': 'plan-phase', 'gsd:new-milestone': 'new-milestone'
391
+ }
392
+ name_to_id = {o['name']: o['id'] for o in options}
393
+ result = {route: name_to_id.get(display, '') for route, display in route_map.items()}
394
+ print(json.dumps(result))
395
+ " 2>/dev/null || echo "{}")
396
+
397
+ if [ -n "$GSD_ROUTE_FIELD_ID" ]; then
398
+ echo " GSD Route field created: ${GSD_ROUTE_FIELD_ID}"
399
+ else
400
+ echo " WARNING: GSD Route field creation failed"
401
+ fi
402
+
403
+ # Field 6: Plan Summary (TEXT)
404
+ PLAN_SUMMARY_RESULT=$(gh api graphql -f query='
405
+ mutation($projectId: ID!) {
406
+ createProjectV2Field(input: {
407
+ projectId: $projectId
408
+ dataType: TEXT
409
+ name: "Plan Summary"
410
+ }) {
411
+ projectV2Field {
412
+ ... on ProjectV2Field {
413
+ id
414
+ name
415
+ }
416
+ }
417
+ }
418
+ }
419
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
420
+
421
+ PLAN_SUMMARY_FIELD_ID=$(echo "$PLAN_SUMMARY_RESULT" | python3 -c "
422
+ import json,sys
423
+ d = json.load(sys.stdin)
424
+ print(d['data']['createProjectV2Field']['projectV2Field']['id'])
425
+ " 2>/dev/null)
426
+
427
+ if [ -n "$PLAN_SUMMARY_FIELD_ID" ]; then
428
+ echo " Plan Summary field created: ${PLAN_SUMMARY_FIELD_ID}"
429
+ else
430
+ echo " WARNING: Plan Summary field creation failed"
431
+ fi
432
+
433
+ # Field 7: Priority (SINGLE_SELECT)
434
+ PRIORITY_RESULT=$(gh api graphql -f query='
435
+ mutation($projectId: ID!) {
436
+ createProjectV2Field(input: {
437
+ projectId: $projectId
438
+ dataType: SINGLE_SELECT
439
+ name: "Priority"
440
+ singleSelectOptions: [
441
+ { name: "P0", color: RED, description: "Critical — blocking other work" }
442
+ { name: "P1", color: ORANGE, description: "High — needed this milestone" }
443
+ { name: "P2", color: YELLOW, description: "Medium — important but not urgent" }
444
+ { name: "P3", color: GRAY, description: "Low — nice to have" }
445
+ ]
446
+ }) {
447
+ projectV2Field {
448
+ ... on ProjectV2SingleSelectField {
449
+ id
450
+ name
451
+ options { id name }
452
+ }
453
+ }
454
+ }
455
+ }
456
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
457
+
458
+ PRIORITY_FIELD_ID=$(echo "$PRIORITY_RESULT" | python3 -c "
459
+ import json,sys
460
+ d = json.load(sys.stdin)
461
+ print(d['data']['createProjectV2Field']['projectV2Field']['id'])
462
+ " 2>/dev/null)
463
+
464
+ PRIORITY_OPTIONS=$(echo "$PRIORITY_RESULT" | python3 -c "
465
+ import json,sys
466
+ d = json.load(sys.stdin)
467
+ options = d['data']['createProjectV2Field']['projectV2Field']['options']
468
+ name_to_id = {o['name']: o['id'] for o in options}
469
+ print(json.dumps(name_to_id))
470
+ " 2>/dev/null || echo "{}")
471
+
472
+ if [ -n "$PRIORITY_FIELD_ID" ]; then
473
+ echo " Priority field created: ${PRIORITY_FIELD_ID}"
474
+ else
475
+ echo " WARNING: Priority field creation failed"
476
+ fi
477
+
478
+ # Field 8: Start Date (DATE)
479
+ START_DATE_RESULT=$(gh api graphql -f query='
480
+ mutation($projectId: ID!) {
481
+ createProjectV2Field(input: {
482
+ projectId: $projectId
483
+ dataType: DATE
484
+ name: "Start Date"
485
+ }) {
486
+ projectV2Field {
487
+ ... on ProjectV2Field {
488
+ id
489
+ name
490
+ }
491
+ }
492
+ }
493
+ }
494
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
495
+
496
+ START_DATE_FIELD_ID=$(echo "$START_DATE_RESULT" | python3 -c "
497
+ import json,sys
498
+ d = json.load(sys.stdin)
499
+ print(d['data']['createProjectV2Field']['projectV2Field']['id'])
500
+ " 2>/dev/null)
501
+
502
+ if [ -n "$START_DATE_FIELD_ID" ]; then
503
+ echo " Start Date field created: ${START_DATE_FIELD_ID}"
504
+ else
505
+ echo " WARNING: Start Date field creation failed"
506
+ fi
507
+
508
+ # Field 9: Target Date (DATE)
509
+ TARGET_DATE_RESULT=$(gh api graphql -f query='
510
+ mutation($projectId: ID!) {
511
+ createProjectV2Field(input: {
512
+ projectId: $projectId
513
+ dataType: DATE
514
+ name: "Target Date"
515
+ }) {
516
+ projectV2Field {
517
+ ... on ProjectV2Field {
518
+ id
519
+ name
520
+ }
521
+ }
522
+ }
523
+ }
524
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
525
+
526
+ TARGET_DATE_FIELD_ID=$(echo "$TARGET_DATE_RESULT" | python3 -c "
527
+ import json,sys
528
+ d = json.load(sys.stdin)
529
+ print(d['data']['createProjectV2Field']['projectV2Field']['id'])
530
+ " 2>/dev/null)
531
+
532
+ if [ -n "$TARGET_DATE_FIELD_ID" ]; then
533
+ echo " Target Date field created: ${TARGET_DATE_FIELD_ID}"
534
+ else
535
+ echo " WARNING: Target Date field creation failed"
536
+ fi
537
+ ```
538
+
539
+ **Update project.json with board metadata:**
540
+
541
+ ```bash
542
+ echo ""
543
+ echo "Updating project.json with board metadata..."
544
+
545
+ python3 << PYEOF
546
+ import json
547
+
548
+ with open('${MGW_DIR}/project.json') as f:
549
+ project = json.load(f)
550
+
551
+ # Build field schema
552
+ status_options = json.loads('''${STATUS_OPTIONS}''') if '${STATUS_OPTIONS}' != '{}' else {}
553
+ gsd_route_options = json.loads('''${GSD_ROUTE_OPTIONS}''') if '${GSD_ROUTE_OPTIONS}' != '{}' else {}
554
+
555
+ fields = {}
556
+
557
+ if '${STATUS_FIELD_ID}':
558
+ fields['status'] = {
559
+ 'field_id': '${STATUS_FIELD_ID}',
560
+ 'field_name': 'Status',
561
+ 'type': 'SINGLE_SELECT',
562
+ 'options': status_options
563
+ }
564
+
565
+ if '${AI_STATE_FIELD_ID}':
566
+ fields['ai_agent_state'] = {
567
+ 'field_id': '${AI_STATE_FIELD_ID}',
568
+ 'field_name': 'AI Agent State',
569
+ 'type': 'TEXT'
570
+ }
571
+
572
+ if '${MILESTONE_FIELD_ID}':
573
+ fields['milestone'] = {
574
+ 'field_id': '${MILESTONE_FIELD_ID}',
575
+ 'field_name': 'Milestone',
576
+ 'type': 'TEXT'
577
+ }
578
+
579
+ if '${PHASE_FIELD_ID}':
580
+ fields['phase'] = {
581
+ 'field_id': '${PHASE_FIELD_ID}',
582
+ 'field_name': 'Phase',
583
+ 'type': 'TEXT'
584
+ }
585
+
586
+ if '${GSD_ROUTE_FIELD_ID}':
587
+ fields['gsd_route'] = {
588
+ 'field_id': '${GSD_ROUTE_FIELD_ID}',
589
+ 'field_name': 'GSD Route',
590
+ 'type': 'SINGLE_SELECT',
591
+ 'options': gsd_route_options
592
+ }
593
+
594
+ if '${PLAN_SUMMARY_FIELD_ID}':
595
+ fields['plan_summary'] = {
596
+ 'field_id': '${PLAN_SUMMARY_FIELD_ID}',
597
+ 'field_name': 'Plan Summary',
598
+ 'type': 'TEXT'
599
+ }
600
+
601
+ priority_options = json.loads('''${PRIORITY_OPTIONS}''') if '${PRIORITY_OPTIONS}' != '{}' else {}
602
+ if '${PRIORITY_FIELD_ID}':
603
+ fields['priority'] = {
604
+ 'field_id': '${PRIORITY_FIELD_ID}',
605
+ 'field_name': 'Priority',
606
+ 'type': 'SINGLE_SELECT',
607
+ 'options': priority_options
608
+ }
609
+
610
+ if '${START_DATE_FIELD_ID}':
611
+ fields['start_date'] = {
612
+ 'field_id': '${START_DATE_FIELD_ID}',
613
+ 'field_name': 'Start Date',
614
+ 'type': 'DATE'
615
+ }
616
+
617
+ if '${TARGET_DATE_FIELD_ID}':
618
+ fields['target_date'] = {
619
+ 'field_id': '${TARGET_DATE_FIELD_ID}',
620
+ 'field_name': 'Target Date',
621
+ 'type': 'DATE'
622
+ }
623
+
624
+ # Update project_board section
625
+ project['project']['project_board'] = {
626
+ 'number': int('${NEW_PROJECT_NUMBER}') if '${NEW_PROJECT_NUMBER}'.isdigit() else None,
627
+ 'url': '${NEW_PROJECT_URL}',
628
+ 'node_id': '${NEW_PROJECT_ID}',
629
+ 'fields': fields
630
+ }
631
+
632
+ with open('${MGW_DIR}/project.json', 'w') as f:
633
+ json.dump(project, f, indent=2)
634
+
635
+ print('project.json updated')
636
+ PYEOF
637
+
638
+ echo ""
639
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
640
+ echo " MGW ► BOARD CREATED"
641
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
642
+ echo ""
643
+ echo "Board: #${NEW_PROJECT_NUMBER} — ${NEW_PROJECT_URL}"
644
+ echo "Node ID: ${NEW_PROJECT_ID}"
645
+ echo ""
646
+ echo "Custom fields created:"
647
+ echo " status ${STATUS_FIELD_ID:-FAILED} (SINGLE_SELECT, 13 options)"
648
+ echo " ai_agent_state ${AI_STATE_FIELD_ID:-FAILED} (TEXT)"
649
+ echo " milestone ${MILESTONE_FIELD_ID:-FAILED} (TEXT)"
650
+ echo " phase ${PHASE_FIELD_ID:-FAILED} (TEXT)"
651
+ echo " gsd_route ${GSD_ROUTE_FIELD_ID:-FAILED} (SINGLE_SELECT, 4 options)"
652
+ echo " plan_summary ${PLAN_SUMMARY_FIELD_ID:-FAILED} (TEXT)"
653
+ echo " priority ${PRIORITY_FIELD_ID:-FAILED} (SINGLE_SELECT, 4 options)"
654
+ echo " start_date ${START_DATE_FIELD_ID:-FAILED} (DATE)"
655
+ echo " target_date ${TARGET_DATE_FIELD_ID:-FAILED} (DATE)"
656
+ echo ""
657
+ echo "Field IDs stored in .mgw/project.json"
658
+ echo ""
659
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
660
+ echo " VIEW SETUP GUIDE"
661
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
662
+ echo ""
663
+ echo "Open your project board and create these views:"
664
+ echo ""
665
+ echo "1. Pipeline (Board)"
666
+ echo " - Layout: Board | Column field: Status"
667
+ echo " - Visible fields: Phase, GSD Route, Milestone, Priority"
668
+ echo ""
669
+ echo "2. Sprint Table (Table)"
670
+ echo " - Layout: Table | Group by: Milestone"
671
+ echo " - Columns: Status, Phase, Priority, Plan Summary, Assignees"
672
+ echo " - Sort by: Priority (ascending)"
673
+ echo ""
674
+ echo "3. Roadmap (Roadmap)"
675
+ echo " - Layout: Roadmap | Date field: Start Date -> Target Date"
676
+ echo " - Group by: Milestone"
677
+ echo ""
678
+ echo "4. My Work (Table)"
679
+ echo " - Layout: Table | Filter: assignee:@me"
680
+ echo " - Columns: Status, Phase, Plan Summary, Priority"
681
+ echo ""
682
+ echo "Next:"
683
+ echo " /mgw:board show Display board state"
684
+ echo " /mgw:run 73 Sync issues onto board items (#73)"
685
+
686
+ fi # end create subcommand
687
+ ```
688
+ </step>