@snipcodeit/mgw 0.2.1 → 0.3.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,496 @@
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
+
404
+ **Update project.json with board metadata:**
405
+
406
+ ```bash
407
+ echo ""
408
+ echo "Updating project.json with board metadata..."
409
+
410
+ python3 << PYEOF
411
+ import json
412
+
413
+ with open('${MGW_DIR}/project.json') as f:
414
+ project = json.load(f)
415
+
416
+ # Build field schema
417
+ status_options = json.loads('''${STATUS_OPTIONS}''') if '${STATUS_OPTIONS}' != '{}' else {}
418
+ gsd_route_options = json.loads('''${GSD_ROUTE_OPTIONS}''') if '${GSD_ROUTE_OPTIONS}' != '{}' else {}
419
+
420
+ fields = {}
421
+
422
+ if '${STATUS_FIELD_ID}':
423
+ fields['status'] = {
424
+ 'field_id': '${STATUS_FIELD_ID}',
425
+ 'field_name': 'Status',
426
+ 'type': 'SINGLE_SELECT',
427
+ 'options': status_options
428
+ }
429
+
430
+ if '${AI_STATE_FIELD_ID}':
431
+ fields['ai_agent_state'] = {
432
+ 'field_id': '${AI_STATE_FIELD_ID}',
433
+ 'field_name': 'AI Agent State',
434
+ 'type': 'TEXT'
435
+ }
436
+
437
+ if '${MILESTONE_FIELD_ID}':
438
+ fields['milestone'] = {
439
+ 'field_id': '${MILESTONE_FIELD_ID}',
440
+ 'field_name': 'Milestone',
441
+ 'type': 'TEXT'
442
+ }
443
+
444
+ if '${PHASE_FIELD_ID}':
445
+ fields['phase'] = {
446
+ 'field_id': '${PHASE_FIELD_ID}',
447
+ 'field_name': 'Phase',
448
+ 'type': 'TEXT'
449
+ }
450
+
451
+ if '${GSD_ROUTE_FIELD_ID}':
452
+ fields['gsd_route'] = {
453
+ 'field_id': '${GSD_ROUTE_FIELD_ID}',
454
+ 'field_name': 'GSD Route',
455
+ 'type': 'SINGLE_SELECT',
456
+ 'options': gsd_route_options
457
+ }
458
+
459
+ # Update project_board section
460
+ project['project']['project_board'] = {
461
+ 'number': int('${NEW_PROJECT_NUMBER}') if '${NEW_PROJECT_NUMBER}'.isdigit() else None,
462
+ 'url': '${NEW_PROJECT_URL}',
463
+ 'node_id': '${NEW_PROJECT_ID}',
464
+ 'fields': fields
465
+ }
466
+
467
+ with open('${MGW_DIR}/project.json', 'w') as f:
468
+ json.dump(project, f, indent=2)
469
+
470
+ print('project.json updated')
471
+ PYEOF
472
+
473
+ echo ""
474
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
475
+ echo " MGW ► BOARD CREATED"
476
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
477
+ echo ""
478
+ echo "Board: #${NEW_PROJECT_NUMBER} — ${NEW_PROJECT_URL}"
479
+ echo "Node ID: ${NEW_PROJECT_ID}"
480
+ echo ""
481
+ echo "Custom fields created:"
482
+ echo " status ${STATUS_FIELD_ID:-FAILED} (SINGLE_SELECT, 13 options)"
483
+ echo " ai_agent_state ${AI_STATE_FIELD_ID:-FAILED} (TEXT)"
484
+ echo " milestone ${MILESTONE_FIELD_ID:-FAILED} (TEXT)"
485
+ echo " phase ${PHASE_FIELD_ID:-FAILED} (TEXT)"
486
+ echo " gsd_route ${GSD_ROUTE_FIELD_ID:-FAILED} (SINGLE_SELECT, 4 options)"
487
+ echo ""
488
+ echo "Field IDs stored in .mgw/project.json"
489
+ echo ""
490
+ echo "Next:"
491
+ echo " /mgw:board show Display board state"
492
+ echo " /mgw:run 73 Sync issues onto board items (#73)"
493
+
494
+ fi # end create subcommand
495
+ ```
496
+ </step>