@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,404 @@
|
|
|
1
|
+
<purpose>
|
|
2
|
+
Shared board sync utilities for MGW pipeline commands. Three functions are exported:
|
|
3
|
+
|
|
4
|
+
- update_board_status — Called after any pipeline_stage transition to update the board
|
|
5
|
+
item's Status (single-select) field.
|
|
6
|
+
- update_board_agent_state — Called around GSD agent spawns to surface the active agent
|
|
7
|
+
in the board item's "AI Agent State" (text) field. Cleared after PR creation.
|
|
8
|
+
- sync_pr_to_board — Called after PR creation to add the PR as a board item (PR-type
|
|
9
|
+
item linked to the issue's board item).
|
|
10
|
+
|
|
11
|
+
All board updates are non-blocking: if the board is not configured, if the issue has no
|
|
12
|
+
board_item_id, or if the API call fails, the function returns silently. A board sync
|
|
13
|
+
failure MUST NEVER block pipeline execution.
|
|
14
|
+
</purpose>
|
|
15
|
+
|
|
16
|
+
## update_board_status
|
|
17
|
+
|
|
18
|
+
Call this function after any `pipeline_stage` transition in any MGW command.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# update_board_status — Update board Status field after a pipeline_stage transition
|
|
22
|
+
# Args: ISSUE_NUMBER, NEW_PIPELINE_STAGE
|
|
23
|
+
# Non-blocking: all failures are silent no-ops
|
|
24
|
+
update_board_status() {
|
|
25
|
+
local ISSUE_NUMBER="$1"
|
|
26
|
+
local NEW_STAGE="$2"
|
|
27
|
+
|
|
28
|
+
if [ -z "$ISSUE_NUMBER" ] || [ -z "$NEW_STAGE" ]; then
|
|
29
|
+
return 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Read board project node ID from project.json (non-blocking — if not configured, skip)
|
|
33
|
+
BOARD_NODE_ID=$(python3 -c "
|
|
34
|
+
import json, sys
|
|
35
|
+
try:
|
|
36
|
+
p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
|
|
37
|
+
print(p.get('project', {}).get('project_board', {}).get('node_id', ''))
|
|
38
|
+
except:
|
|
39
|
+
print('')
|
|
40
|
+
" 2>/dev/null || echo "")
|
|
41
|
+
if [ -z "$BOARD_NODE_ID" ]; then return 0; fi
|
|
42
|
+
|
|
43
|
+
# Get board_item_id for this issue from project.json
|
|
44
|
+
ITEM_ID=$(python3 -c "
|
|
45
|
+
import json, sys
|
|
46
|
+
try:
|
|
47
|
+
p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
|
|
48
|
+
for m in p.get('milestones', []):
|
|
49
|
+
for i in m.get('issues', []):
|
|
50
|
+
if i.get('github_number') == ${ISSUE_NUMBER}:
|
|
51
|
+
print(i.get('board_item_id', ''))
|
|
52
|
+
sys.exit(0)
|
|
53
|
+
print('')
|
|
54
|
+
except:
|
|
55
|
+
print('')
|
|
56
|
+
" 2>/dev/null || echo "")
|
|
57
|
+
if [ -z "$ITEM_ID" ]; then return 0; fi
|
|
58
|
+
|
|
59
|
+
# Map pipeline_stage to Status field option ID
|
|
60
|
+
# Reads from board-schema.json first, falls back to project.json fields
|
|
61
|
+
FIELD_ID=$(python3 -c "
|
|
62
|
+
import json, sys, os
|
|
63
|
+
try:
|
|
64
|
+
schema_path = '${REPO_ROOT}/.mgw/board-schema.json'
|
|
65
|
+
if os.path.exists(schema_path):
|
|
66
|
+
s = json.load(open(schema_path))
|
|
67
|
+
print(s.get('fields', {}).get('status', {}).get('field_id', ''))
|
|
68
|
+
else:
|
|
69
|
+
p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
|
|
70
|
+
fields = p.get('project', {}).get('project_board', {}).get('fields', {})
|
|
71
|
+
print(fields.get('status', {}).get('field_id', ''))
|
|
72
|
+
except:
|
|
73
|
+
print('')
|
|
74
|
+
" 2>/dev/null || echo "")
|
|
75
|
+
if [ -z "$FIELD_ID" ]; then return 0; fi
|
|
76
|
+
|
|
77
|
+
OPTION_ID=$(python3 -c "
|
|
78
|
+
import json, sys, os
|
|
79
|
+
try:
|
|
80
|
+
stage = '${NEW_STAGE}'
|
|
81
|
+
schema_path = '${REPO_ROOT}/.mgw/board-schema.json'
|
|
82
|
+
if os.path.exists(schema_path):
|
|
83
|
+
s = json.load(open(schema_path))
|
|
84
|
+
options = s.get('fields', {}).get('status', {}).get('options', {})
|
|
85
|
+
print(options.get(stage, ''))
|
|
86
|
+
else:
|
|
87
|
+
p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
|
|
88
|
+
fields = p.get('project', {}).get('project_board', {}).get('fields', {})
|
|
89
|
+
options = fields.get('status', {}).get('options', {})
|
|
90
|
+
print(options.get(stage, ''))
|
|
91
|
+
except:
|
|
92
|
+
print('')
|
|
93
|
+
" 2>/dev/null || echo "")
|
|
94
|
+
if [ -z "$OPTION_ID" ]; then return 0; fi
|
|
95
|
+
|
|
96
|
+
# Update the Status field on the board item (non-blocking)
|
|
97
|
+
gh api graphql -f query='
|
|
98
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
99
|
+
updateProjectV2ItemFieldValue(input: {
|
|
100
|
+
projectId: $projectId
|
|
101
|
+
itemId: $itemId
|
|
102
|
+
fieldId: $fieldId
|
|
103
|
+
value: { singleSelectOptionId: $optionId }
|
|
104
|
+
}) { projectV2Item { id } }
|
|
105
|
+
}
|
|
106
|
+
' -f projectId="$BOARD_NODE_ID" \
|
|
107
|
+
-f itemId="$ITEM_ID" \
|
|
108
|
+
-f fieldId="$FIELD_ID" \
|
|
109
|
+
-f optionId="$OPTION_ID" 2>/dev/null || true
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## update_board_agent_state
|
|
114
|
+
|
|
115
|
+
Call this function before spawning each GSD agent and after PR creation to surface
|
|
116
|
+
real-time agent activity in the board item's "AI Agent State" text field.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# update_board_agent_state — Update AI Agent State text field on the board item
|
|
120
|
+
# Args: ISSUE_NUMBER, STATE_TEXT (empty string to clear the field)
|
|
121
|
+
# Non-blocking: all failures are silent no-ops
|
|
122
|
+
update_board_agent_state() {
|
|
123
|
+
local ISSUE_NUMBER="$1"
|
|
124
|
+
local STATE_TEXT="$2"
|
|
125
|
+
|
|
126
|
+
if [ -z "$ISSUE_NUMBER" ]; then return 0; fi
|
|
127
|
+
|
|
128
|
+
# Read board project node ID from project.json (non-blocking — if not configured, skip)
|
|
129
|
+
BOARD_NODE_ID=$(python3 -c "
|
|
130
|
+
import json, sys
|
|
131
|
+
try:
|
|
132
|
+
p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
|
|
133
|
+
print(p.get('project', {}).get('project_board', {}).get('node_id', ''))
|
|
134
|
+
except:
|
|
135
|
+
print('')
|
|
136
|
+
" 2>/dev/null || echo "")
|
|
137
|
+
if [ -z "$BOARD_NODE_ID" ]; then return 0; fi
|
|
138
|
+
|
|
139
|
+
# Get board_item_id for this issue from project.json
|
|
140
|
+
ITEM_ID=$(python3 -c "
|
|
141
|
+
import json, sys
|
|
142
|
+
try:
|
|
143
|
+
p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
|
|
144
|
+
for m in p.get('milestones', []):
|
|
145
|
+
for i in m.get('issues', []):
|
|
146
|
+
if i.get('github_number') == ${ISSUE_NUMBER}:
|
|
147
|
+
print(i.get('board_item_id', ''))
|
|
148
|
+
sys.exit(0)
|
|
149
|
+
print('')
|
|
150
|
+
except:
|
|
151
|
+
print('')
|
|
152
|
+
" 2>/dev/null || echo "")
|
|
153
|
+
if [ -z "$ITEM_ID" ]; then return 0; fi
|
|
154
|
+
|
|
155
|
+
# Get the AI Agent State field ID from board-schema.json or project.json
|
|
156
|
+
FIELD_ID=$(python3 -c "
|
|
157
|
+
import json, sys, os
|
|
158
|
+
try:
|
|
159
|
+
schema_path = '${REPO_ROOT}/.mgw/board-schema.json'
|
|
160
|
+
if os.path.exists(schema_path):
|
|
161
|
+
s = json.load(open(schema_path))
|
|
162
|
+
print(s.get('fields', {}).get('ai_agent_state', {}).get('field_id', ''))
|
|
163
|
+
else:
|
|
164
|
+
p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
|
|
165
|
+
fields = p.get('project', {}).get('project_board', {}).get('fields', {})
|
|
166
|
+
print(fields.get('ai_agent_state', {}).get('field_id', ''))
|
|
167
|
+
except:
|
|
168
|
+
print('')
|
|
169
|
+
" 2>/dev/null || echo "")
|
|
170
|
+
if [ -z "$FIELD_ID" ]; then return 0; fi
|
|
171
|
+
|
|
172
|
+
# Update the AI Agent State text field on the board item (non-blocking)
|
|
173
|
+
gh api graphql -f query='
|
|
174
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $text: String!) {
|
|
175
|
+
updateProjectV2ItemFieldValue(input: {
|
|
176
|
+
projectId: $projectId
|
|
177
|
+
itemId: $itemId
|
|
178
|
+
fieldId: $fieldId
|
|
179
|
+
value: { text: $text }
|
|
180
|
+
}) { projectV2Item { id } }
|
|
181
|
+
}
|
|
182
|
+
' -f projectId="$BOARD_NODE_ID" \
|
|
183
|
+
-f itemId="$ITEM_ID" \
|
|
184
|
+
-f fieldId="$FIELD_ID" \
|
|
185
|
+
-f text="$STATE_TEXT" 2>/dev/null || true
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Stage-to-Status Mapping
|
|
190
|
+
|
|
191
|
+
The Status field options correspond to pipeline_stage values:
|
|
192
|
+
|
|
193
|
+
| pipeline_stage | Board Status Option |
|
|
194
|
+
|----------------|-------------------|
|
|
195
|
+
| `new` | New |
|
|
196
|
+
| `triaged` | Triaged |
|
|
197
|
+
| `needs-info` | Needs Info |
|
|
198
|
+
| `needs-security-review` | Needs Security Review |
|
|
199
|
+
| `discussing` | Discussing |
|
|
200
|
+
| `approved` | Approved |
|
|
201
|
+
| `planning` | Planning |
|
|
202
|
+
| `executing` | Executing |
|
|
203
|
+
| `verifying` | Verifying |
|
|
204
|
+
| `pr-created` | PR Created |
|
|
205
|
+
| `done` | Done |
|
|
206
|
+
| `failed` | Failed |
|
|
207
|
+
| `blocked` | Blocked |
|
|
208
|
+
|
|
209
|
+
Option IDs for each stage are looked up at runtime from:
|
|
210
|
+
1. `.mgw/board-schema.json` → `fields.status.options.<stage>` (preferred)
|
|
211
|
+
2. `.mgw/project.json` → `project.project_board.fields.status.options.<stage>` (fallback)
|
|
212
|
+
|
|
213
|
+
## AI Agent State Values
|
|
214
|
+
|
|
215
|
+
The AI Agent State text field is set before each GSD agent spawn and cleared after PR creation:
|
|
216
|
+
|
|
217
|
+
| Trigger | Value |
|
|
218
|
+
|---------|-------|
|
|
219
|
+
| Before gsd-planner spawn (quick route) | `"Planning"` |
|
|
220
|
+
| Before gsd-executor spawn (quick route) | `"Executing"` |
|
|
221
|
+
| Before gsd-verifier spawn (quick route) | `"Verifying"` |
|
|
222
|
+
| Before gsd-planner spawn (milestone, phase N) | `"Planning phase N"` |
|
|
223
|
+
| Before gsd-executor spawn (milestone, phase N) | `"Executing phase N"` |
|
|
224
|
+
| Before gsd-verifier spawn (milestone, phase N) | `"Verifying phase N"` |
|
|
225
|
+
| After PR created | `""` (clears the field) |
|
|
226
|
+
|
|
227
|
+
## Data Sources
|
|
228
|
+
|
|
229
|
+
| Field | Source |
|
|
230
|
+
|-------|--------|
|
|
231
|
+
| `BOARD_NODE_ID` | `project.json` → `project.project_board.node_id` |
|
|
232
|
+
| `ITEM_ID` | `project.json` → `milestones[*].issues[*].board_item_id` (set by #73) |
|
|
233
|
+
| `FIELD_ID` (status) | `board-schema.json` or `project.json` → `fields.status.field_id` |
|
|
234
|
+
| `OPTION_ID` | `board-schema.json` or `project.json` → `fields.status.options.<stage>` |
|
|
235
|
+
| `FIELD_ID` (agent state) | `board-schema.json` or `project.json` → `fields.ai_agent_state.field_id` |
|
|
236
|
+
|
|
237
|
+
## Non-Blocking Contract
|
|
238
|
+
|
|
239
|
+
Every failure case returns 0 (success) without printing to stderr. The caller is never
|
|
240
|
+
aware of board sync failures. This guarantees:
|
|
241
|
+
|
|
242
|
+
- Board not configured (no `node_id` in project.json) → silent no-op
|
|
243
|
+
- Issue has no `board_item_id` → silent no-op (not yet added to board)
|
|
244
|
+
- Status field not configured → silent no-op
|
|
245
|
+
- AI Agent State field not configured → silent no-op
|
|
246
|
+
- Stage has no mapped option ID → silent no-op
|
|
247
|
+
- GraphQL API error → silent no-op (`|| true` suppresses exit code)
|
|
248
|
+
- Network error → silent no-op
|
|
249
|
+
|
|
250
|
+
## Touch Points
|
|
251
|
+
|
|
252
|
+
Source or inline both utilities in any MGW command that spawns GSD agents.
|
|
253
|
+
|
|
254
|
+
### update_board_status — in issue.md (triage stage transitions)
|
|
255
|
+
|
|
256
|
+
After writing `pipeline_stage` to the state file in the `write_state` step:
|
|
257
|
+
```bash
|
|
258
|
+
# After: pipeline_stage written to .mgw/active/<issue>.json
|
|
259
|
+
update_board_status $ISSUE_NUMBER "$pipeline_stage" # non-blocking
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Transitions in issue.md:
|
|
263
|
+
- `needs-info` — validity or detail gate blocked
|
|
264
|
+
- `needs-security-review` — security gate blocked
|
|
265
|
+
- `triaged` — all gates passed or user override
|
|
266
|
+
|
|
267
|
+
### update_board_status — in run.md (pipeline stage transitions)
|
|
268
|
+
|
|
269
|
+
After each `pipeline_stage` checkpoint write to project.json and state file:
|
|
270
|
+
```bash
|
|
271
|
+
# After: pipeline_stage checkpoint written (state.md "Update Issue Pipeline Stage" pattern)
|
|
272
|
+
update_board_status $ISSUE_NUMBER "$NEW_STAGE" # non-blocking
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Transitions in run.md:
|
|
276
|
+
- `planning` — GSD execution begins
|
|
277
|
+
- `executing` — executor agent active
|
|
278
|
+
- `verifying` — verifier agent active
|
|
279
|
+
- `pr-created` — PR created
|
|
280
|
+
- `done` — pipeline complete
|
|
281
|
+
- `blocked` — blocking comment detected in preflight_comment_check
|
|
282
|
+
|
|
283
|
+
### update_board_agent_state — in run.md (around agent spawns)
|
|
284
|
+
|
|
285
|
+
Called immediately before spawning each GSD agent and after PR creation:
|
|
286
|
+
```bash
|
|
287
|
+
# Before spawning gsd-planner
|
|
288
|
+
update_board_agent_state $ISSUE_NUMBER "Planning phase ${PHASE_NUM}"
|
|
289
|
+
|
|
290
|
+
# Before spawning gsd-executor
|
|
291
|
+
update_board_agent_state $ISSUE_NUMBER "Executing phase ${PHASE_NUM}"
|
|
292
|
+
|
|
293
|
+
# Before spawning gsd-verifier
|
|
294
|
+
update_board_agent_state $ISSUE_NUMBER "Verifying phase ${PHASE_NUM}"
|
|
295
|
+
|
|
296
|
+
# After PR created (clear the field)
|
|
297
|
+
update_board_agent_state $ISSUE_NUMBER ""
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### sync_pr_to_board — in run.md and pr.md (after PR creation)
|
|
301
|
+
|
|
302
|
+
Called immediately after `gh pr create` succeeds in both run.md and pr.md (linked mode):
|
|
303
|
+
```bash
|
|
304
|
+
# After PR created
|
|
305
|
+
sync_pr_to_board $ISSUE_NUMBER $PR_NUMBER # non-blocking board PR link
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## sync_pr_to_board
|
|
309
|
+
|
|
310
|
+
Call this function after PR creation to add the PR as a board item. In GitHub Projects v2,
|
|
311
|
+
`addProjectV2ItemById` with a PR's node ID creates a PR-type item that GitHub Projects
|
|
312
|
+
tracks separately from the issue item.
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# sync_pr_to_board — Add PR as a board item, linked to the issue's board item
|
|
316
|
+
# Args: ISSUE_NUMBER, PR_NUMBER
|
|
317
|
+
# Non-blocking: all failures are silent no-ops
|
|
318
|
+
sync_pr_to_board() {
|
|
319
|
+
local ISSUE_NUMBER="$1"
|
|
320
|
+
local PR_NUMBER="$2"
|
|
321
|
+
|
|
322
|
+
if [ -z "$ISSUE_NUMBER" ] || [ -z "$PR_NUMBER" ]; then return 0; fi
|
|
323
|
+
|
|
324
|
+
# Read board project node ID from project.json (non-blocking — if not configured, skip)
|
|
325
|
+
BOARD_NODE_ID=$(python3 -c "
|
|
326
|
+
import json, sys
|
|
327
|
+
try:
|
|
328
|
+
p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
|
|
329
|
+
print(p.get('project', {}).get('project_board', {}).get('node_id', ''))
|
|
330
|
+
except:
|
|
331
|
+
print('')
|
|
332
|
+
" 2>/dev/null || echo "")
|
|
333
|
+
if [ -z "$BOARD_NODE_ID" ]; then return 0; fi
|
|
334
|
+
|
|
335
|
+
# Get PR node ID from GitHub (non-blocking)
|
|
336
|
+
PR_NODE_ID=$(gh pr view "$PR_NUMBER" --json id -q .id 2>/dev/null || echo "")
|
|
337
|
+
if [ -z "$PR_NODE_ID" ]; then return 0; fi
|
|
338
|
+
|
|
339
|
+
# Add PR to board as a PR-type item (creates a separate board entry linked to the PR)
|
|
340
|
+
ITEM_ID=$(gh api graphql -f query='
|
|
341
|
+
mutation($projectId: ID!, $contentId: ID!) {
|
|
342
|
+
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
|
|
343
|
+
item { id }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
' -f projectId="$BOARD_NODE_ID" -f contentId="$PR_NODE_ID" \
|
|
347
|
+
--jq '.data.addProjectV2ItemById.item.id' 2>/dev/null || echo "")
|
|
348
|
+
|
|
349
|
+
if [ -n "$ITEM_ID" ]; then
|
|
350
|
+
echo "MGW: PR #${PR_NUMBER} added to board (item: ${ITEM_ID})"
|
|
351
|
+
fi
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## sync_pr_to_board — Board Reconciliation in sync.md
|
|
356
|
+
|
|
357
|
+
During `mgw:sync`, after cross-refs are loaded, check for any `implements` links
|
|
358
|
+
(issue → PR) that don't yet have a board item for the PR. For each such link, call
|
|
359
|
+
`sync_pr_to_board` to ensure the board reflects all linked PRs.
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
# Board reconciliation — ensure all PR cross-refs have board items (non-blocking)
|
|
363
|
+
if [ -f "${REPO_ROOT}/.mgw/project.json" ] && [ -f "${REPO_ROOT}/.mgw/cross-refs.json" ]; then
|
|
364
|
+
BOARD_NODE_ID=$(python3 -c "
|
|
365
|
+
import json, sys
|
|
366
|
+
try:
|
|
367
|
+
p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
|
|
368
|
+
print(p.get('project', {}).get('project_board', {}).get('node_id', ''))
|
|
369
|
+
except:
|
|
370
|
+
print('')
|
|
371
|
+
" 2>/dev/null || echo "")
|
|
372
|
+
|
|
373
|
+
if [ -n "$BOARD_NODE_ID" ]; then
|
|
374
|
+
# Find all issue→PR implements links in cross-refs
|
|
375
|
+
PR_LINKS=$(python3 -c "
|
|
376
|
+
import json
|
|
377
|
+
refs = json.load(open('${REPO_ROOT}/.mgw/cross-refs.json'))
|
|
378
|
+
for link in refs.get('links', []):
|
|
379
|
+
if link.get('type') == 'implements' and link['a'].startswith('issue:') and link['b'].startswith('pr:'):
|
|
380
|
+
issue_num = link['a'].split(':')[1]
|
|
381
|
+
pr_num = link['b'].split(':')[1]
|
|
382
|
+
print(f'{issue_num} {pr_num}')
|
|
383
|
+
" 2>/dev/null || echo "")
|
|
384
|
+
|
|
385
|
+
# For each issue→PR link, ensure the PR is on the board (sync_pr_to_board is idempotent)
|
|
386
|
+
while IFS=' ' read -r LINKED_ISSUE LINKED_PR; do
|
|
387
|
+
[ -z "$LINKED_PR" ] && continue
|
|
388
|
+
sync_pr_to_board "$LINKED_ISSUE" "$LINKED_PR" # non-blocking
|
|
389
|
+
done <<< "$PR_LINKS"
|
|
390
|
+
fi
|
|
391
|
+
fi
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Consumers
|
|
395
|
+
|
|
396
|
+
| Command | Function | When Called |
|
|
397
|
+
|---------|----------|-------------|
|
|
398
|
+
| issue.md | update_board_status | After writing pipeline_stage in write_state step |
|
|
399
|
+
| run.md | update_board_status | After each pipeline_stage checkpoint write |
|
|
400
|
+
| run.md | update_board_agent_state | Before each GSD agent spawn, and after PR creation |
|
|
401
|
+
| run.md | sync_pr_to_board | After PR creation (before cross-ref is recorded) |
|
|
402
|
+
| pr.md | sync_pr_to_board | After PR creation in create_pr step (linked mode only) |
|
|
403
|
+
| sync.md | sync_pr_to_board | Board reconciliation — for each PR link in cross-refs |
|
|
404
|
+
| board.md (sync) | bulk reconciliation | Iterates all .mgw/active/*.json, fetches board items via node ID query, applies updateProjectV2ItemFieldValue for Status/AI Agent State/Phase/Milestone fields. Uses same GraphQL mutations as the three utility functions above, but in a single bulk loop with diff tracking. |
|