@snipcodeit/mgw 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -5
- package/bin/mgw-install.cjs +121 -24
- package/commands/board/configure.md +205 -0
- package/commands/board/create.md +688 -0
- package/commands/board/show.md +221 -0
- package/commands/board/sync.md +461 -0
- package/commands/board/views.md +253 -0
- package/commands/board.md +23 -1543
- package/commands/context.md +183 -0
- package/commands/handoff.md +169 -0
- package/commands/issue.md +62 -0
- package/commands/milestone.md +42 -43
- package/commands/project.md +19 -0
- package/commands/review.md +222 -42
- package/commands/run/execute.md +820 -0
- package/commands/run/pr-create.md +324 -0
- package/commands/run/triage.md +510 -0
- package/commands/run/worktree.md +95 -0
- package/commands/run.md +23 -1547
- package/commands/sync.md +69 -0
- package/commands/workflows/gsd.md +1 -13
- package/dist/bin/mgw.cjs +107 -15
- package/dist/{index-BiwU0uWA.cjs → index-B-_JvYpz.cjs} +885 -69
- package/dist/lib/index.cjs +653 -155
- package/package.json +5 -2
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: board:show
|
|
3
|
+
description: Display board state, field IDs, item counts grouped by pipeline stage, and configured views
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<step name="subcommand_show">
|
|
7
|
+
**Execute 'show' subcommand:**
|
|
8
|
+
|
|
9
|
+
Only run if `$SUBCOMMAND = "show"`.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
if [ "$SUBCOMMAND" = "show" ]; then
|
|
13
|
+
if [ "$BOARD_CONFIGURED" = "false" ]; then
|
|
14
|
+
echo "No board configured. Run /mgw:board create first."
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
19
|
+
echo " MGW ► BOARD STATE: ${PROJECT_NAME}"
|
|
20
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
21
|
+
echo ""
|
|
22
|
+
echo "Board: #${BOARD_NUMBER} — ${BOARD_URL}"
|
|
23
|
+
echo "Node ID: ${BOARD_NODE_ID}"
|
|
24
|
+
echo ""
|
|
25
|
+
echo "Custom Fields:"
|
|
26
|
+
echo "$FIELDS_JSON" | python3 -c "
|
|
27
|
+
import json,sys
|
|
28
|
+
fields = json.load(sys.stdin)
|
|
29
|
+
for name, data in fields.items():
|
|
30
|
+
fid = data.get('field_id', 'unknown')
|
|
31
|
+
ftype = data.get('type', 'unknown')
|
|
32
|
+
fname = data.get('field_name', name)
|
|
33
|
+
if ftype == 'SINGLE_SELECT':
|
|
34
|
+
opts = len(data.get('options', {}))
|
|
35
|
+
print(f' {fname:<20} {fid} ({ftype}, {opts} options)')
|
|
36
|
+
else:
|
|
37
|
+
print(f' {fname:<20} {fid} ({ftype})')
|
|
38
|
+
" 2>/dev/null
|
|
39
|
+
echo ""
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Fetch board items from GitHub to show current state:**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
echo "Fetching board items from GitHub..."
|
|
46
|
+
|
|
47
|
+
ITEMS_RESULT=$(gh api graphql -f query='
|
|
48
|
+
query($owner: String!, $number: Int!) {
|
|
49
|
+
user(login: $owner) {
|
|
50
|
+
projectV2(number: $number) {
|
|
51
|
+
title
|
|
52
|
+
items(first: 50) {
|
|
53
|
+
totalCount
|
|
54
|
+
nodes {
|
|
55
|
+
id
|
|
56
|
+
content {
|
|
57
|
+
... on Issue {
|
|
58
|
+
number
|
|
59
|
+
title
|
|
60
|
+
state
|
|
61
|
+
}
|
|
62
|
+
... on PullRequest {
|
|
63
|
+
number
|
|
64
|
+
title
|
|
65
|
+
state
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
fieldValues(first: 10) {
|
|
69
|
+
nodes {
|
|
70
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
71
|
+
name
|
|
72
|
+
field { ... on ProjectV2SingleSelectField { name } }
|
|
73
|
+
}
|
|
74
|
+
... on ProjectV2ItemFieldTextValue {
|
|
75
|
+
text
|
|
76
|
+
field { ... on ProjectV2Field { name } }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
' -f owner="$OWNER" -F number="$BOARD_NUMBER" 2>/dev/null)
|
|
86
|
+
|
|
87
|
+
# Fall back to org query if user query fails
|
|
88
|
+
if echo "$ITEMS_RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); _ = d['data']['user']['projectV2']" 2>/dev/null; then
|
|
89
|
+
ITEM_NODES=$(echo "$ITEMS_RESULT" | python3 -c "
|
|
90
|
+
import json,sys
|
|
91
|
+
d = json.load(sys.stdin)
|
|
92
|
+
print(json.dumps(d['data']['user']['projectV2']['items']['nodes']))
|
|
93
|
+
" 2>/dev/null || echo "[]")
|
|
94
|
+
TOTAL_ITEMS=$(echo "$ITEMS_RESULT" | python3 -c "
|
|
95
|
+
import json,sys
|
|
96
|
+
d = json.load(sys.stdin)
|
|
97
|
+
print(d['data']['user']['projectV2']['items']['totalCount'])
|
|
98
|
+
" 2>/dev/null || echo "0")
|
|
99
|
+
else
|
|
100
|
+
# Try organization lookup
|
|
101
|
+
ITEMS_RESULT=$(gh api graphql -f query='
|
|
102
|
+
query($owner: String!, $number: Int!) {
|
|
103
|
+
organization(login: $owner) {
|
|
104
|
+
projectV2(number: $number) {
|
|
105
|
+
title
|
|
106
|
+
items(first: 50) {
|
|
107
|
+
totalCount
|
|
108
|
+
nodes {
|
|
109
|
+
id
|
|
110
|
+
content {
|
|
111
|
+
... on Issue { number title state }
|
|
112
|
+
... on PullRequest { number title state }
|
|
113
|
+
}
|
|
114
|
+
fieldValues(first: 10) {
|
|
115
|
+
nodes {
|
|
116
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
117
|
+
name
|
|
118
|
+
field { ... on ProjectV2SingleSelectField { name } }
|
|
119
|
+
}
|
|
120
|
+
... on ProjectV2ItemFieldTextValue {
|
|
121
|
+
text
|
|
122
|
+
field { ... on ProjectV2Field { name } }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
' -f owner="$OWNER" -F number="$BOARD_NUMBER" 2>/dev/null)
|
|
132
|
+
|
|
133
|
+
ITEM_NODES=$(echo "$ITEMS_RESULT" | python3 -c "
|
|
134
|
+
import json,sys
|
|
135
|
+
d = json.load(sys.stdin)
|
|
136
|
+
org_data = d.get('data', {}).get('organization') or d.get('data', {}).get('user', {})
|
|
137
|
+
proj = org_data.get('projectV2', {})
|
|
138
|
+
print(json.dumps(proj.get('items', {}).get('nodes', [])))
|
|
139
|
+
" 2>/dev/null || echo "[]")
|
|
140
|
+
TOTAL_ITEMS=$(echo "$ITEMS_RESULT" | python3 -c "
|
|
141
|
+
import json,sys
|
|
142
|
+
d = json.load(sys.stdin)
|
|
143
|
+
org_data = d.get('data', {}).get('organization') or d.get('data', {}).get('user', {})
|
|
144
|
+
proj = org_data.get('projectV2', {})
|
|
145
|
+
print(proj.get('items', {}).get('totalCount', 0))
|
|
146
|
+
" 2>/dev/null || echo "0")
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
echo "Board Items (${TOTAL_ITEMS} total):"
|
|
150
|
+
echo ""
|
|
151
|
+
|
|
152
|
+
echo "$ITEM_NODES" | python3 -c "
|
|
153
|
+
import json,sys
|
|
154
|
+
nodes = json.load(sys.stdin)
|
|
155
|
+
|
|
156
|
+
if not nodes:
|
|
157
|
+
print(' No items on board yet.')
|
|
158
|
+
print(' Run /mgw:run 73 to sync issues as board items (#73).')
|
|
159
|
+
sys.exit(0)
|
|
160
|
+
|
|
161
|
+
# Group by Status field
|
|
162
|
+
by_status = {}
|
|
163
|
+
for node in nodes:
|
|
164
|
+
content = node.get('content', {})
|
|
165
|
+
num = content.get('number', '?')
|
|
166
|
+
title = content.get('title', 'Unknown')[:45]
|
|
167
|
+
status = 'No Status'
|
|
168
|
+
for fv in node.get('fieldValues', {}).get('nodes', []):
|
|
169
|
+
field = fv.get('field', {})
|
|
170
|
+
if field.get('name') == 'Status':
|
|
171
|
+
status = fv.get('name', 'No Status')
|
|
172
|
+
break
|
|
173
|
+
by_status.setdefault(status, []).append((num, title))
|
|
174
|
+
|
|
175
|
+
order = ['Executing', 'Planning', 'Verifying', 'PR Created', 'Triaged', 'Approved',
|
|
176
|
+
'Discussing', 'New', 'Needs Info', 'Needs Security Review', 'Blocked', 'Failed', 'Done', 'No Status']
|
|
177
|
+
|
|
178
|
+
for status in order:
|
|
179
|
+
items = by_status.pop(status, [])
|
|
180
|
+
if items:
|
|
181
|
+
print(f' {status} ({len(items)}):')
|
|
182
|
+
for num, title in items:
|
|
183
|
+
print(f' #{num} {title}')
|
|
184
|
+
|
|
185
|
+
for status, items in by_status.items():
|
|
186
|
+
print(f' {status} ({len(items)}):')
|
|
187
|
+
for num, title in items:
|
|
188
|
+
print(f' #{num} {title}')
|
|
189
|
+
" 2>/dev/null
|
|
190
|
+
|
|
191
|
+
echo ""
|
|
192
|
+
|
|
193
|
+
# Show configured views if any
|
|
194
|
+
VIEWS_JSON=$(echo "$PROJECT_JSON" | python3 -c "
|
|
195
|
+
import json,sys
|
|
196
|
+
p = json.load(sys.stdin)
|
|
197
|
+
board = p.get('project', {}).get('project_board', {})
|
|
198
|
+
views = board.get('views', {})
|
|
199
|
+
print(json.dumps(views))
|
|
200
|
+
" 2>/dev/null || echo "{}")
|
|
201
|
+
|
|
202
|
+
if [ "$VIEWS_JSON" != "{}" ] && [ -n "$VIEWS_JSON" ]; then
|
|
203
|
+
echo "Configured Views:"
|
|
204
|
+
echo "$VIEWS_JSON" | python3 -c "
|
|
205
|
+
import json,sys
|
|
206
|
+
views = json.load(sys.stdin)
|
|
207
|
+
for key, v in views.items():
|
|
208
|
+
print(f' {v[\"name\"]:<40} {v[\"layout\"]:<16} (ID: {v[\"view_id\"]})')
|
|
209
|
+
" 2>/dev/null
|
|
210
|
+
echo ""
|
|
211
|
+
else
|
|
212
|
+
echo "Views: none configured"
|
|
213
|
+
echo " Run /mgw:board views kanban to create the kanban view"
|
|
214
|
+
echo ""
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
echo "Open board: ${BOARD_URL}"
|
|
218
|
+
|
|
219
|
+
fi # end show subcommand
|
|
220
|
+
```
|
|
221
|
+
</step>
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: board:sync
|
|
3
|
+
description: Reconcile all board items with current .mgw/active/ state
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<step name="subcommand_sync">
|
|
7
|
+
**Execute 'sync' subcommand:**
|
|
8
|
+
|
|
9
|
+
Only run if `$SUBCOMMAND = "sync"`.
|
|
10
|
+
|
|
11
|
+
Reconcile all `.mgw/active/*.json` state files with their GitHub Projects v2 board items.
|
|
12
|
+
Adds missing issues to the board, then updates Status, AI Agent State, Phase, and
|
|
13
|
+
Milestone fields to match current local state. Prints a reconciliation diff table.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
if [ "$SUBCOMMAND" = "sync" ]; then
|
|
17
|
+
if [ "$BOARD_CONFIGURED" = "false" ]; then
|
|
18
|
+
echo "No board configured. Run /mgw:board create first."
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
23
|
+
echo " MGW ► BOARD SYNC: ${PROJECT_NAME}"
|
|
24
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
25
|
+
echo ""
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Collect active state files:**
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
ACTIVE_DIR="${MGW_DIR}/active"
|
|
32
|
+
|
|
33
|
+
if ! ls "${ACTIVE_DIR}"/*.json 1>/dev/null 2>&1; then
|
|
34
|
+
echo "No active issues found in ${ACTIVE_DIR}/"
|
|
35
|
+
echo "Nothing to sync."
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
ACTIVE_FILES=$(ls "${ACTIVE_DIR}"/*.json 2>/dev/null)
|
|
40
|
+
ACTIVE_COUNT=$(echo "$ACTIVE_FILES" | wc -l)
|
|
41
|
+
|
|
42
|
+
echo "Reconciling ${ACTIVE_COUNT} active issues against board..."
|
|
43
|
+
echo ""
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Read field IDs from project.json:**
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
STATUS_FIELD_ID=$(echo "$FIELDS_JSON" | python3 -c "
|
|
50
|
+
import json,sys
|
|
51
|
+
fields = json.load(sys.stdin)
|
|
52
|
+
print(fields.get('status', {}).get('field_id', ''))
|
|
53
|
+
" 2>/dev/null)
|
|
54
|
+
|
|
55
|
+
AI_STATE_FIELD_ID=$(echo "$FIELDS_JSON" | python3 -c "
|
|
56
|
+
import json,sys
|
|
57
|
+
fields = json.load(sys.stdin)
|
|
58
|
+
print(fields.get('ai_agent_state', {}).get('field_id', ''))
|
|
59
|
+
" 2>/dev/null)
|
|
60
|
+
|
|
61
|
+
PHASE_FIELD_ID=$(echo "$FIELDS_JSON" | python3 -c "
|
|
62
|
+
import json,sys
|
|
63
|
+
fields = json.load(sys.stdin)
|
|
64
|
+
print(fields.get('phase', {}).get('field_id', ''))
|
|
65
|
+
" 2>/dev/null)
|
|
66
|
+
|
|
67
|
+
MILESTONE_FIELD_ID=$(echo "$FIELDS_JSON" | python3 -c "
|
|
68
|
+
import json,sys
|
|
69
|
+
fields = json.load(sys.stdin)
|
|
70
|
+
print(fields.get('milestone', {}).get('field_id', ''))
|
|
71
|
+
" 2>/dev/null)
|
|
72
|
+
|
|
73
|
+
STATUS_OPTIONS=$(echo "$FIELDS_JSON" | python3 -c "
|
|
74
|
+
import json,sys
|
|
75
|
+
fields = json.load(sys.stdin)
|
|
76
|
+
print(json.dumps(fields.get('status', {}).get('options', {})))
|
|
77
|
+
" 2>/dev/null || echo "{}")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Fetch all current board items in a single GraphQL call:**
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
echo "Fetching current board items from GitHub..."
|
|
84
|
+
|
|
85
|
+
BOARD_ITEMS_RESULT=$(gh api graphql -f query='
|
|
86
|
+
query($projectId: ID!) {
|
|
87
|
+
node(id: $projectId) {
|
|
88
|
+
... on ProjectV2 {
|
|
89
|
+
items(first: 100) {
|
|
90
|
+
nodes {
|
|
91
|
+
id
|
|
92
|
+
content {
|
|
93
|
+
... on Issue {
|
|
94
|
+
number
|
|
95
|
+
id
|
|
96
|
+
}
|
|
97
|
+
... on PullRequest {
|
|
98
|
+
number
|
|
99
|
+
id
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
fieldValues(first: 10) {
|
|
103
|
+
nodes {
|
|
104
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
105
|
+
name
|
|
106
|
+
field { ... on ProjectV2SingleSelectField { name } }
|
|
107
|
+
}
|
|
108
|
+
... on ProjectV2ItemFieldTextValue {
|
|
109
|
+
text
|
|
110
|
+
field { ... on ProjectV2Field { name } }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
' -f projectId="$BOARD_NODE_ID" 2>/dev/null)
|
|
120
|
+
|
|
121
|
+
# Build: issue_number -> {item_id, current_status, current_ai_state, current_phase, current_milestone}
|
|
122
|
+
BOARD_ITEM_MAP=$(echo "$BOARD_ITEMS_RESULT" | python3 -c "
|
|
123
|
+
import json,sys
|
|
124
|
+
d = json.load(sys.stdin)
|
|
125
|
+
nodes = d.get('data', {}).get('node', {}).get('items', {}).get('nodes', [])
|
|
126
|
+
result = {}
|
|
127
|
+
for node in nodes:
|
|
128
|
+
content = node.get('content', {})
|
|
129
|
+
if not content:
|
|
130
|
+
continue
|
|
131
|
+
num = content.get('number')
|
|
132
|
+
if num is None:
|
|
133
|
+
continue
|
|
134
|
+
item_id = node.get('id', '')
|
|
135
|
+
status = ''
|
|
136
|
+
ai_state = ''
|
|
137
|
+
phase = ''
|
|
138
|
+
milestone = ''
|
|
139
|
+
for fv in node.get('fieldValues', {}).get('nodes', []):
|
|
140
|
+
fname = fv.get('field', {}).get('name', '')
|
|
141
|
+
if fname == 'Status':
|
|
142
|
+
status = fv.get('name', '')
|
|
143
|
+
elif fname == 'AI Agent State':
|
|
144
|
+
ai_state = fv.get('text', '')
|
|
145
|
+
elif fname == 'Phase':
|
|
146
|
+
phase = fv.get('text', '')
|
|
147
|
+
elif fname == 'Milestone':
|
|
148
|
+
milestone = fv.get('text', '')
|
|
149
|
+
result[str(num)] = {
|
|
150
|
+
'item_id': item_id,
|
|
151
|
+
'status': status,
|
|
152
|
+
'ai_agent_state': ai_state,
|
|
153
|
+
'phase': phase,
|
|
154
|
+
'milestone': milestone
|
|
155
|
+
}
|
|
156
|
+
print(json.dumps(result))
|
|
157
|
+
" 2>/dev/null || echo "{}")
|
|
158
|
+
|
|
159
|
+
if [ "$BOARD_ITEM_MAP" = "{}" ] && [ -n "$BOARD_ITEMS_RESULT" ]; then
|
|
160
|
+
echo "WARNING: Could not parse board items. Continuing with empty map."
|
|
161
|
+
fi
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Reconcile each active state file:**
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
SYNC_RESULTS=()
|
|
168
|
+
UPDATED_COUNT=0
|
|
169
|
+
ADDED_COUNT=0
|
|
170
|
+
ERROR_COUNT=0
|
|
171
|
+
|
|
172
|
+
for STATE_FILE in $ACTIVE_FILES; do
|
|
173
|
+
# Parse state file
|
|
174
|
+
ISSUE_DATA=$(python3 -c "
|
|
175
|
+
import json,sys
|
|
176
|
+
try:
|
|
177
|
+
s = json.load(open('${STATE_FILE}'))
|
|
178
|
+
num = str(s.get('issue', {}).get('number', ''))
|
|
179
|
+
title = s.get('issue', {}).get('title', 'Unknown')[:45]
|
|
180
|
+
stage = s.get('pipeline_stage', 'new')
|
|
181
|
+
route = s.get('gsd_route', '') or ''
|
|
182
|
+
labels = s.get('issue', {}).get('labels', [])
|
|
183
|
+
# Extract phase from labels matching 'phase:*'
|
|
184
|
+
phase_val = ''
|
|
185
|
+
for lbl in labels:
|
|
186
|
+
if isinstance(lbl, str) and lbl.startswith('phase:'):
|
|
187
|
+
phase_val = lbl.replace('phase:', '')
|
|
188
|
+
break
|
|
189
|
+
elif isinstance(lbl, dict) and lbl.get('name', '').startswith('phase:'):
|
|
190
|
+
phase_val = lbl['name'].replace('phase:', '')
|
|
191
|
+
break
|
|
192
|
+
print(json.dumps({'number': num, 'title': title, 'stage': stage, 'route': route, 'phase': phase_val}))
|
|
193
|
+
except Exception as e:
|
|
194
|
+
print(json.dumps({'error': str(e)}))
|
|
195
|
+
" 2>/dev/null || echo '{"error":"parse failed"}')
|
|
196
|
+
|
|
197
|
+
ISSUE_NUMBER=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('number',''))" 2>/dev/null)
|
|
198
|
+
ISSUE_TITLE=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('title','Unknown'))" 2>/dev/null)
|
|
199
|
+
PIPELINE_STAGE=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('stage','new'))" 2>/dev/null)
|
|
200
|
+
PHASE_VALUE=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('phase',''))" 2>/dev/null)
|
|
201
|
+
|
|
202
|
+
if [ -z "$ISSUE_NUMBER" ]; then
|
|
203
|
+
SYNC_RESULTS+=("| ? | (parse error: ${STATE_FILE##*/}) | — | ERROR: could not read state |")
|
|
204
|
+
ERROR_COUNT=$((ERROR_COUNT + 1))
|
|
205
|
+
continue
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Look up board item
|
|
209
|
+
ITEM_DATA=$(echo "$BOARD_ITEM_MAP" | python3 -c "
|
|
210
|
+
import json,sys
|
|
211
|
+
m = json.load(sys.stdin)
|
|
212
|
+
d = m.get('${ISSUE_NUMBER}', {})
|
|
213
|
+
print(json.dumps(d))
|
|
214
|
+
" 2>/dev/null || echo "{}")
|
|
215
|
+
|
|
216
|
+
BOARD_ITEM_ID=$(echo "$ITEM_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('item_id',''))" 2>/dev/null)
|
|
217
|
+
CURRENT_STATUS=$(echo "$ITEM_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status',''))" 2>/dev/null)
|
|
218
|
+
CURRENT_PHASE=$(echo "$ITEM_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('phase',''))" 2>/dev/null)
|
|
219
|
+
CURRENT_MILESTONE=$(echo "$ITEM_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('milestone',''))" 2>/dev/null)
|
|
220
|
+
|
|
221
|
+
CHANGED_FIELDS=""
|
|
222
|
+
|
|
223
|
+
# If issue is not on board, add it
|
|
224
|
+
if [ -z "$BOARD_ITEM_ID" ]; then
|
|
225
|
+
ISSUE_NODE_ID=$(gh issue view "$ISSUE_NUMBER" --json id -q .id 2>/dev/null || echo "")
|
|
226
|
+
if [ -n "$ISSUE_NODE_ID" ]; then
|
|
227
|
+
ADD_RESULT=$(gh api graphql -f query='
|
|
228
|
+
mutation($projectId: ID!, $contentId: ID!) {
|
|
229
|
+
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
|
|
230
|
+
item { id }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
' -f projectId="$BOARD_NODE_ID" -f contentId="$ISSUE_NODE_ID" \
|
|
234
|
+
--jq '.data.addProjectV2ItemById.item.id' 2>/dev/null || echo "")
|
|
235
|
+
|
|
236
|
+
if [ -n "$ADD_RESULT" ]; then
|
|
237
|
+
BOARD_ITEM_ID="$ADD_RESULT"
|
|
238
|
+
CHANGED_FIELDS="added to board"
|
|
239
|
+
ADDED_COUNT=$((ADDED_COUNT + 1))
|
|
240
|
+
else
|
|
241
|
+
SYNC_RESULTS+=("| #${ISSUE_NUMBER} | ${ISSUE_TITLE} | ${PIPELINE_STAGE} | ERROR: could not add to board |")
|
|
242
|
+
ERROR_COUNT=$((ERROR_COUNT + 1))
|
|
243
|
+
continue
|
|
244
|
+
fi
|
|
245
|
+
else
|
|
246
|
+
SYNC_RESULTS+=("| #${ISSUE_NUMBER} | ${ISSUE_TITLE} | ${PIPELINE_STAGE} | ERROR: could not resolve issue node ID |")
|
|
247
|
+
ERROR_COUNT=$((ERROR_COUNT + 1))
|
|
248
|
+
continue
|
|
249
|
+
fi
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# Get milestone title from project.json for this issue's milestone
|
|
253
|
+
MILESTONE_VALUE=$(python3 -c "
|
|
254
|
+
import json,sys
|
|
255
|
+
try:
|
|
256
|
+
p = json.load(open('${MGW_DIR}/project.json'))
|
|
257
|
+
current_ms = p.get('current_milestone', 1)
|
|
258
|
+
for i, m in enumerate(p.get('milestones', []), 1):
|
|
259
|
+
for issue in m.get('issues', []):
|
|
260
|
+
if str(issue.get('github_number', '')) == '${ISSUE_NUMBER}':
|
|
261
|
+
print(m.get('title', ''))
|
|
262
|
+
sys.exit(0)
|
|
263
|
+
print('')
|
|
264
|
+
except:
|
|
265
|
+
print('')
|
|
266
|
+
" 2>/dev/null)
|
|
267
|
+
|
|
268
|
+
# Update Status field if it differs
|
|
269
|
+
if [ -n "$STATUS_FIELD_ID" ]; then
|
|
270
|
+
DESIRED_OPTION_ID=$(echo "$STATUS_OPTIONS" | python3 -c "
|
|
271
|
+
import json,sys
|
|
272
|
+
opts = json.load(sys.stdin)
|
|
273
|
+
print(opts.get('${PIPELINE_STAGE}', ''))
|
|
274
|
+
" 2>/dev/null)
|
|
275
|
+
|
|
276
|
+
if [ -n "$DESIRED_OPTION_ID" ]; then
|
|
277
|
+
# Map current board status name back to stage for comparison
|
|
278
|
+
CURRENT_STAGE=$(echo "$CURRENT_STATUS" | python3 -c "
|
|
279
|
+
import sys
|
|
280
|
+
stage_map = {
|
|
281
|
+
'New': 'new', 'Triaged': 'triaged', 'Needs Info': 'needs-info',
|
|
282
|
+
'Needs Security Review': 'needs-security-review', 'Discussing': 'discussing',
|
|
283
|
+
'Approved': 'approved', 'Planning': 'planning', 'Executing': 'executing',
|
|
284
|
+
'Verifying': 'verifying', 'PR Created': 'pr-created', 'Done': 'done',
|
|
285
|
+
'Failed': 'failed', 'Blocked': 'blocked'
|
|
286
|
+
}
|
|
287
|
+
label = sys.stdin.read().strip()
|
|
288
|
+
print(stage_map.get(label, ''))
|
|
289
|
+
" 2>/dev/null)
|
|
290
|
+
|
|
291
|
+
if [ "$CURRENT_STAGE" != "$PIPELINE_STAGE" ]; then
|
|
292
|
+
gh api graphql -f query='
|
|
293
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
294
|
+
updateProjectV2ItemFieldValue(input: {
|
|
295
|
+
projectId: $projectId
|
|
296
|
+
itemId: $itemId
|
|
297
|
+
fieldId: $fieldId
|
|
298
|
+
value: { singleSelectOptionId: $optionId }
|
|
299
|
+
}) { projectV2Item { id } }
|
|
300
|
+
}
|
|
301
|
+
' -f projectId="$BOARD_NODE_ID" \
|
|
302
|
+
-f itemId="$BOARD_ITEM_ID" \
|
|
303
|
+
-f fieldId="$STATUS_FIELD_ID" \
|
|
304
|
+
-f optionId="$DESIRED_OPTION_ID" 2>/dev/null || true
|
|
305
|
+
|
|
306
|
+
if [ -n "$CHANGED_FIELDS" ]; then
|
|
307
|
+
CHANGED_FIELDS="${CHANGED_FIELDS}, Status (${CURRENT_STATUS:-none}→${PIPELINE_STAGE})"
|
|
308
|
+
else
|
|
309
|
+
CHANGED_FIELDS="Status (${CURRENT_STATUS:-none}→${PIPELINE_STAGE})"
|
|
310
|
+
fi
|
|
311
|
+
UPDATED_COUNT=$((UPDATED_COUNT + 1))
|
|
312
|
+
fi
|
|
313
|
+
fi
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
# Update AI Agent State field — sync always clears it (ephemeral during execution)
|
|
317
|
+
if [ -n "$AI_STATE_FIELD_ID" ] && [ -n "$CURRENT_AI_STATE" ] && [ "$CURRENT_AI_STATE" != "" ]; then
|
|
318
|
+
CURRENT_AI_STATE=$(echo "$ITEM_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ai_agent_state',''))" 2>/dev/null)
|
|
319
|
+
if [ -n "$CURRENT_AI_STATE" ]; then
|
|
320
|
+
gh api graphql -f query='
|
|
321
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $text: String!) {
|
|
322
|
+
updateProjectV2ItemFieldValue(input: {
|
|
323
|
+
projectId: $projectId
|
|
324
|
+
itemId: $itemId
|
|
325
|
+
fieldId: $fieldId
|
|
326
|
+
value: { text: $text }
|
|
327
|
+
}) { projectV2Item { id } }
|
|
328
|
+
}
|
|
329
|
+
' -f projectId="$BOARD_NODE_ID" \
|
|
330
|
+
-f itemId="$BOARD_ITEM_ID" \
|
|
331
|
+
-f fieldId="$AI_STATE_FIELD_ID" \
|
|
332
|
+
-f text="" 2>/dev/null || true
|
|
333
|
+
|
|
334
|
+
if [ -n "$CHANGED_FIELDS" ]; then
|
|
335
|
+
CHANGED_FIELDS="${CHANGED_FIELDS}, AI Agent State (cleared)"
|
|
336
|
+
else
|
|
337
|
+
CHANGED_FIELDS="AI Agent State (cleared)"
|
|
338
|
+
fi
|
|
339
|
+
fi
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# Update Phase field if it differs
|
|
343
|
+
if [ -n "$PHASE_FIELD_ID" ] && [ -n "$PHASE_VALUE" ] && [ "$PHASE_VALUE" != "$CURRENT_PHASE" ]; then
|
|
344
|
+
gh api graphql -f query='
|
|
345
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $text: String!) {
|
|
346
|
+
updateProjectV2ItemFieldValue(input: {
|
|
347
|
+
projectId: $projectId
|
|
348
|
+
itemId: $itemId
|
|
349
|
+
fieldId: $fieldId
|
|
350
|
+
value: { text: $text }
|
|
351
|
+
}) { projectV2Item { id } }
|
|
352
|
+
}
|
|
353
|
+
' -f projectId="$BOARD_NODE_ID" \
|
|
354
|
+
-f itemId="$BOARD_ITEM_ID" \
|
|
355
|
+
-f fieldId="$PHASE_FIELD_ID" \
|
|
356
|
+
-f text="$PHASE_VALUE" 2>/dev/null || true
|
|
357
|
+
|
|
358
|
+
if [ -n "$CHANGED_FIELDS" ]; then
|
|
359
|
+
CHANGED_FIELDS="${CHANGED_FIELDS}, Phase (${CURRENT_PHASE:-none}→${PHASE_VALUE})"
|
|
360
|
+
else
|
|
361
|
+
CHANGED_FIELDS="Phase (${CURRENT_PHASE:-none}→${PHASE_VALUE})"
|
|
362
|
+
fi
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
# Update Milestone field if it differs
|
|
366
|
+
if [ -n "$MILESTONE_FIELD_ID" ] && [ -n "$MILESTONE_VALUE" ] && [ "$MILESTONE_VALUE" != "$CURRENT_MILESTONE" ]; then
|
|
367
|
+
gh api graphql -f query='
|
|
368
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $text: String!) {
|
|
369
|
+
updateProjectV2ItemFieldValue(input: {
|
|
370
|
+
projectId: $projectId
|
|
371
|
+
itemId: $itemId
|
|
372
|
+
fieldId: $fieldId
|
|
373
|
+
value: { text: $text }
|
|
374
|
+
}) { projectV2Item { id } }
|
|
375
|
+
}
|
|
376
|
+
' -f projectId="$BOARD_NODE_ID" \
|
|
377
|
+
-f itemId="$BOARD_ITEM_ID" \
|
|
378
|
+
-f fieldId="$MILESTONE_FIELD_ID" \
|
|
379
|
+
-f text="$MILESTONE_VALUE" 2>/dev/null || true
|
|
380
|
+
|
|
381
|
+
if [ -n "$CHANGED_FIELDS" ]; then
|
|
382
|
+
CHANGED_FIELDS="${CHANGED_FIELDS}, Milestone (${CURRENT_MILESTONE:-none}→${MILESTONE_VALUE})"
|
|
383
|
+
else
|
|
384
|
+
CHANGED_FIELDS="Milestone (${CURRENT_MILESTONE:-none}→${MILESTONE_VALUE})"
|
|
385
|
+
fi
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
# Update Plan Summary field from latest plan comment (if field exists)
|
|
389
|
+
PLAN_SUMMARY_FIELD_ID=$(echo "$FIELDS_JSON" | python3 -c "
|
|
390
|
+
import json,sys
|
|
391
|
+
fields = json.load(sys.stdin)
|
|
392
|
+
print(fields.get('plan_summary', {}).get('field_id', ''))
|
|
393
|
+
" 2>/dev/null)
|
|
394
|
+
|
|
395
|
+
if [ -n "$PLAN_SUMMARY_FIELD_ID" ] && [ -n "$BOARD_ITEM_ID" ]; then
|
|
396
|
+
PLAN_SUMMARY=$(node -e "
|
|
397
|
+
const ic = require('./lib/issue-context.cjs');
|
|
398
|
+
ic.findLatestComment(${ISSUE_NUMBER}, 'plan')
|
|
399
|
+
.then(plan => {
|
|
400
|
+
if (plan) {
|
|
401
|
+
const body = plan.body.replace(/<!--[\\s\\S]*?-->\\n?/, '').trim();
|
|
402
|
+
const summary = body.split('\\n').slice(0, 3).join(' ').substring(0, 200);
|
|
403
|
+
process.stdout.write(summary);
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
.catch(() => {});
|
|
407
|
+
" 2>/dev/null || echo "")
|
|
408
|
+
|
|
409
|
+
if [ -n "$PLAN_SUMMARY" ]; then
|
|
410
|
+
gh api graphql -f query='
|
|
411
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $text: String!) {
|
|
412
|
+
updateProjectV2ItemFieldValue(input: {
|
|
413
|
+
projectId: $projectId
|
|
414
|
+
itemId: $itemId
|
|
415
|
+
fieldId: $fieldId
|
|
416
|
+
value: { text: $text }
|
|
417
|
+
}) { projectV2Item { id } }
|
|
418
|
+
}
|
|
419
|
+
' -f projectId="$BOARD_NODE_ID" \
|
|
420
|
+
-f itemId="$BOARD_ITEM_ID" \
|
|
421
|
+
-f fieldId="$PLAN_SUMMARY_FIELD_ID" \
|
|
422
|
+
-f text="$PLAN_SUMMARY" 2>/dev/null || true
|
|
423
|
+
|
|
424
|
+
if [ -n "$CHANGED_FIELDS" ]; then
|
|
425
|
+
CHANGED_FIELDS="${CHANGED_FIELDS}, Plan Summary (updated)"
|
|
426
|
+
else
|
|
427
|
+
CHANGED_FIELDS="Plan Summary (updated)"
|
|
428
|
+
fi
|
|
429
|
+
fi
|
|
430
|
+
fi
|
|
431
|
+
|
|
432
|
+
if [ -z "$CHANGED_FIELDS" ]; then
|
|
433
|
+
CHANGED_FIELDS="no changes"
|
|
434
|
+
fi
|
|
435
|
+
|
|
436
|
+
SYNC_RESULTS+=("| #${ISSUE_NUMBER} | ${ISSUE_TITLE} | ${PIPELINE_STAGE} | ${CHANGED_FIELDS} |")
|
|
437
|
+
done
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Print reconciliation diff table:**
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
echo "| Issue | Title | Stage | Changes |"
|
|
444
|
+
echo "|-------|-------|-------|---------|"
|
|
445
|
+
for ROW in "${SYNC_RESULTS[@]}"; do
|
|
446
|
+
echo "$ROW"
|
|
447
|
+
done
|
|
448
|
+
|
|
449
|
+
echo ""
|
|
450
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
451
|
+
echo " Sync complete: ${ACTIVE_COUNT} checked, ${UPDATED_COUNT} updated, ${ADDED_COUNT} added, ${ERROR_COUNT} errors"
|
|
452
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
453
|
+
|
|
454
|
+
if [ "$ERROR_COUNT" -gt 0 ]; then
|
|
455
|
+
echo ""
|
|
456
|
+
echo "WARNING: ${ERROR_COUNT} issue(s) had errors. Check board manually: ${BOARD_URL}"
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
fi # end sync subcommand
|
|
460
|
+
```
|
|
461
|
+
</step>
|