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