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