@snipcodeit/mgw 0.1.1 → 0.1.3
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 +20 -9
- package/commands/board.md +75 -0
- package/commands/issue.md +9 -10
- package/commands/milestone.md +180 -15
- package/commands/project.md +55 -1651
- package/commands/run.md +319 -20
- package/commands/sync.md +409 -1
- package/commands/workflows/github.md +19 -4
- package/dist/bin/mgw.cjs +2 -2
- package/dist/{claude-Vp9qvImH.cjs → claude-Dk1oVsaG.cjs} +156 -0
- package/dist/lib/index.cjs +237 -12
- package/package.json +1 -1
package/commands/sync.md
CHANGED
|
@@ -16,6 +16,11 @@ if the GitHub issue is still open, if linked PRs were merged/closed, and if trac
|
|
|
16
16
|
branches still exist. Moves completed items to .mgw/completed/, cleans up branches
|
|
17
17
|
and lingering worktrees, flags inconsistencies.
|
|
18
18
|
|
|
19
|
+
Also pulls board state to reconstruct missing local state files — enabling multi-machine
|
|
20
|
+
workflows. On a fresh machine with no .mgw/active/ files, sync reads the GitHub Projects
|
|
21
|
+
v2 board's Status field and rebuilds local state for every in-progress issue, so work
|
|
22
|
+
can continue without re-triaging from scratch.
|
|
23
|
+
|
|
19
24
|
Run periodically or when starting a new session to get a clean view.
|
|
20
25
|
</objective>
|
|
21
26
|
|
|
@@ -28,6 +33,321 @@ Run periodically or when starting a new session to get a clean view.
|
|
|
28
33
|
|
|
29
34
|
<process>
|
|
30
35
|
|
|
36
|
+
<step name="pull_board_state">
|
|
37
|
+
**Pull board state to reconstruct missing local .mgw/active/ files.**
|
|
38
|
+
|
|
39
|
+
This step runs first — before scan_active — so that any issues the board knows about
|
|
40
|
+
but this machine doesn't will be present by the time check_each runs. This is the
|
|
41
|
+
multi-machine sync mechanism: board is the distributed source of truth, .mgw/active/
|
|
42
|
+
is the local cache that sync rebuilds from it.
|
|
43
|
+
|
|
44
|
+
Two sub-operations, both non-blocking:
|
|
45
|
+
1. **Board discovery** — if project_board.node_id is missing, find and register it.
|
|
46
|
+
2. **Board pull** — fetch all board items, reconstruct .mgw/active/ for any issue not
|
|
47
|
+
present locally, detect stage drift for issues that exist on both sides.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
51
|
+
MGW_DIR="${REPO_ROOT}/.mgw"
|
|
52
|
+
OWNER=$(gh repo view --json owner -q .owner.login 2>/dev/null)
|
|
53
|
+
ACTIVE_DIR="${MGW_DIR}/active"
|
|
54
|
+
mkdir -p "$ACTIVE_DIR"
|
|
55
|
+
|
|
56
|
+
BOARD_NODE_ID=$(python3 -c "
|
|
57
|
+
import json
|
|
58
|
+
try:
|
|
59
|
+
p = json.load(open('${MGW_DIR}/project.json'))
|
|
60
|
+
print(p.get('project', {}).get('project_board', {}).get('node_id', ''))
|
|
61
|
+
except: print('')
|
|
62
|
+
" 2>/dev/null || echo "")
|
|
63
|
+
|
|
64
|
+
BOARD_DISCOVERED=""
|
|
65
|
+
BOARD_PULL_CREATED=0
|
|
66
|
+
BOARD_PULL_DRIFT="[]"
|
|
67
|
+
BOARD_PULL_ERRORS="[]"
|
|
68
|
+
|
|
69
|
+
# Sub-operation 1: board discovery
|
|
70
|
+
if [ -z "$BOARD_NODE_ID" ] && [ -f "${MGW_DIR}/project.json" ]; then
|
|
71
|
+
PROJECT_NAME=$(python3 -c "
|
|
72
|
+
import json
|
|
73
|
+
try:
|
|
74
|
+
p = json.load(open('${MGW_DIR}/project.json'))
|
|
75
|
+
print(p.get('project', {}).get('name', ''))
|
|
76
|
+
except: print('')
|
|
77
|
+
" 2>/dev/null || echo "")
|
|
78
|
+
|
|
79
|
+
if [ -n "$PROJECT_NAME" ]; then
|
|
80
|
+
DISCOVERED=$(node -e "
|
|
81
|
+
const { findExistingBoard, getProjectFields } = require('./lib/github.cjs');
|
|
82
|
+
const board = findExistingBoard('${OWNER}', '${PROJECT_NAME}');
|
|
83
|
+
if (!board) { process.stdout.write(''); process.exit(0); }
|
|
84
|
+
const fields = getProjectFields('${OWNER}', board.number) || {};
|
|
85
|
+
console.log(JSON.stringify({ ...board, fields }));
|
|
86
|
+
" 2>/dev/null || echo "")
|
|
87
|
+
|
|
88
|
+
if [ -n "$DISCOVERED" ]; then
|
|
89
|
+
python3 -c "
|
|
90
|
+
import json
|
|
91
|
+
with open('${MGW_DIR}/project.json') as f:
|
|
92
|
+
project = json.load(f)
|
|
93
|
+
d = json.loads('${DISCOVERED}')
|
|
94
|
+
project['project']['project_board'] = {
|
|
95
|
+
'number': d['number'], 'url': d['url'],
|
|
96
|
+
'node_id': d['nodeId'], 'fields': d.get('fields', {})
|
|
97
|
+
}
|
|
98
|
+
with open('${MGW_DIR}/project.json', 'w') as f:
|
|
99
|
+
json.dump(project, f, indent=2)
|
|
100
|
+
" 2>/dev/null
|
|
101
|
+
BOARD_NODE_ID=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['nodeId'])" 2>/dev/null)
|
|
102
|
+
DISC_NUMBER=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['number'])" 2>/dev/null)
|
|
103
|
+
DISC_URL=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['url'])" 2>/dev/null)
|
|
104
|
+
BOARD_DISCOVERED="#${DISC_NUMBER} — ${DISC_URL}"
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Sub-operation 2: board pull
|
|
110
|
+
if [ -n "$BOARD_NODE_ID" ]; then
|
|
111
|
+
echo "Pulling board state from GitHub..."
|
|
112
|
+
|
|
113
|
+
BOARD_ITEMS=$(gh api graphql -f query='
|
|
114
|
+
query($projectId: ID!) {
|
|
115
|
+
node(id: $projectId) {
|
|
116
|
+
... on ProjectV2 {
|
|
117
|
+
items(first: 100) {
|
|
118
|
+
nodes {
|
|
119
|
+
id
|
|
120
|
+
content {
|
|
121
|
+
... on Issue { number title url }
|
|
122
|
+
}
|
|
123
|
+
fieldValues(first: 10) {
|
|
124
|
+
nodes {
|
|
125
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
126
|
+
name
|
|
127
|
+
field { ... on ProjectV2SingleSelectField { name } }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
' -f projectId="$BOARD_NODE_ID" \
|
|
137
|
+
--jq '.data.node.items.nodes' 2>/dev/null || echo "[]")
|
|
138
|
+
|
|
139
|
+
# One Python call handles: map board items, detect missing local files,
|
|
140
|
+
# reconstruct state from GitHub issue data, detect drift on existing files.
|
|
141
|
+
PULL_RESULT=$(echo "$BOARD_ITEMS" | ACTIVE_DIR="$ACTIVE_DIR" MGW_DIR="$MGW_DIR" python3 << 'PYEOF'
|
|
142
|
+
import json, sys, os, subprocess, re
|
|
143
|
+
|
|
144
|
+
ACTIVE_DIR = os.environ['ACTIVE_DIR']
|
|
145
|
+
GSD_TOOLS = os.path.expanduser('~/.claude/get-shit-done/bin/gsd-tools.cjs')
|
|
146
|
+
|
|
147
|
+
STATUS_TO_STAGE = {
|
|
148
|
+
'New': 'new', 'Triaged': 'triaged',
|
|
149
|
+
'Needs Info': 'needs-info', 'Needs Security Review': 'needs-security-review',
|
|
150
|
+
'Discussing': 'discussing', 'Approved': 'approved',
|
|
151
|
+
'Planning': 'planning', 'Executing': 'executing',
|
|
152
|
+
'Verifying': 'verifying', 'PR Created': 'pr-created',
|
|
153
|
+
'Done': 'done', 'Failed': 'failed', 'Blocked': 'blocked'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
nodes = json.load(sys.stdin)
|
|
157
|
+
created = []
|
|
158
|
+
drift = []
|
|
159
|
+
errors = []
|
|
160
|
+
|
|
161
|
+
for node in nodes:
|
|
162
|
+
content = node.get('content', {})
|
|
163
|
+
num = content.get('number')
|
|
164
|
+
if num is None:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
status_label = ''
|
|
168
|
+
route_label = ''
|
|
169
|
+
for fv in node.get('fieldValues', {}).get('nodes', []):
|
|
170
|
+
fname = fv.get('field', {}).get('name', '')
|
|
171
|
+
if fname == 'Status':
|
|
172
|
+
status_label = fv.get('name', '')
|
|
173
|
+
elif fname == 'GSD Route':
|
|
174
|
+
route_label = fv.get('name', '')
|
|
175
|
+
|
|
176
|
+
board_stage = STATUS_TO_STAGE.get(status_label, 'new')
|
|
177
|
+
|
|
178
|
+
# Done items belong in completed/, not active/ — skip
|
|
179
|
+
if board_stage == 'done':
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
# Check for existing local active file
|
|
183
|
+
existing = None
|
|
184
|
+
for fname in os.listdir(ACTIVE_DIR):
|
|
185
|
+
if fname.startswith(f'{num}-') and fname.endswith('.json'):
|
|
186
|
+
existing = os.path.join(ACTIVE_DIR, fname)
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
if existing is None:
|
|
190
|
+
# No local file — reconstruct from GitHub issue data
|
|
191
|
+
try:
|
|
192
|
+
r = subprocess.run(
|
|
193
|
+
['gh', 'issue', 'view', str(num),
|
|
194
|
+
'--json', 'number,title,url,labels,assignees'],
|
|
195
|
+
capture_output=True, text=True
|
|
196
|
+
)
|
|
197
|
+
issue = json.loads(r.stdout)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
errors.append({'number': num, 'error': str(e)})
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
title = issue.get('title', content.get('title', ''))
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
slug = subprocess.run(
|
|
206
|
+
['node', GSD_TOOLS, 'generate-slug', title, '--raw'],
|
|
207
|
+
capture_output=True, text=True
|
|
208
|
+
).stdout.strip()[:40]
|
|
209
|
+
except:
|
|
210
|
+
slug = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-')[:40]
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
ts = subprocess.run(
|
|
214
|
+
['node', GSD_TOOLS, 'current-timestamp', '--raw'],
|
|
215
|
+
capture_output=True, text=True
|
|
216
|
+
).stdout.strip()
|
|
217
|
+
except:
|
|
218
|
+
from datetime import datetime
|
|
219
|
+
ts = datetime.utcnow().isoformat() + 'Z'
|
|
220
|
+
|
|
221
|
+
labels = [l.get('name', '') if isinstance(l, dict) else str(l)
|
|
222
|
+
for l in issue.get('labels', [])]
|
|
223
|
+
assignees = issue.get('assignees', [])
|
|
224
|
+
assignee = assignees[0].get('login') if assignees else None
|
|
225
|
+
|
|
226
|
+
state = {
|
|
227
|
+
'issue': {
|
|
228
|
+
'number': num,
|
|
229
|
+
'title': title,
|
|
230
|
+
'url': issue.get('url', content.get('url', '')),
|
|
231
|
+
'labels': labels,
|
|
232
|
+
'assignee': assignee
|
|
233
|
+
},
|
|
234
|
+
'triage': {
|
|
235
|
+
'scope': {'files': 0, 'systems': []},
|
|
236
|
+
'validity': 'confirmed',
|
|
237
|
+
'security_notes': '',
|
|
238
|
+
'conflicts': [],
|
|
239
|
+
'last_comment_count': 0,
|
|
240
|
+
'last_comment_at': None,
|
|
241
|
+
'gate_result': {
|
|
242
|
+
'status': 'passed',
|
|
243
|
+
'blockers': [],
|
|
244
|
+
'warnings': [f'Reconstructed from board state by mgw:sync — {ts}'],
|
|
245
|
+
'missing_fields': []
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
'gsd_route': route_label or None,
|
|
249
|
+
'gsd_artifacts': {'type': None, 'path': None},
|
|
250
|
+
'pipeline_stage': board_stage,
|
|
251
|
+
'reconstructed_from_board': True,
|
|
252
|
+
'comments_posted': [],
|
|
253
|
+
'linked_pr': None,
|
|
254
|
+
'linked_issues': [],
|
|
255
|
+
'linked_branches': []
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
state_path = os.path.join(ACTIVE_DIR, f'{num}-{slug}.json')
|
|
259
|
+
with open(state_path, 'w') as f:
|
|
260
|
+
json.dump(state, f, indent=2)
|
|
261
|
+
|
|
262
|
+
created.append({'number': num, 'title': title, 'stage': board_stage})
|
|
263
|
+
|
|
264
|
+
else:
|
|
265
|
+
# Local file exists — detect stage drift
|
|
266
|
+
try:
|
|
267
|
+
local = json.load(open(existing))
|
|
268
|
+
local_stage = local.get('pipeline_stage', 'new')
|
|
269
|
+
if local_stage != board_stage:
|
|
270
|
+
drift.append({
|
|
271
|
+
'number': num,
|
|
272
|
+
'title': content.get('title', ''),
|
|
273
|
+
'local': local_stage,
|
|
274
|
+
'board': board_stage
|
|
275
|
+
})
|
|
276
|
+
except:
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
print(json.dumps({'created': created, 'drift': drift, 'errors': errors}))
|
|
280
|
+
PYEOF
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
BOARD_PULL_CREATED=$(echo "$PULL_RESULT" | python3 -c "import json,sys; print(len(json.load(sys.stdin).get('created', [])))" 2>/dev/null || echo "0")
|
|
284
|
+
BOARD_PULL_DRIFT=$(echo "$PULL_RESULT" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin).get('drift', [])))" 2>/dev/null || echo "[]")
|
|
285
|
+
BOARD_PULL_ERRORS=$(echo "$PULL_RESULT" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin).get('errors', [])))" 2>/dev/null || echo "[]")
|
|
286
|
+
|
|
287
|
+
if [ "$BOARD_PULL_CREATED" -gt 0 ]; then
|
|
288
|
+
echo " Reconstructed ${BOARD_PULL_CREATED} issue(s) from board state"
|
|
289
|
+
echo "$PULL_RESULT" | python3 -c "
|
|
290
|
+
import json, sys
|
|
291
|
+
for item in json.load(sys.stdin).get('created', []):
|
|
292
|
+
print(f' ✓ #{item[\"number\"]} ({item[\"stage\"]}): {item[\"title\"][:60]}')
|
|
293
|
+
" 2>/dev/null
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
# Offer drift resolution if any issues have mismatched stages
|
|
297
|
+
DRIFT_COUNT=$(echo "$BOARD_PULL_DRIFT" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
|
|
298
|
+
|
|
299
|
+
if [ "$DRIFT_COUNT" -gt 0 ]; then
|
|
300
|
+
echo ""
|
|
301
|
+
echo "Stage drift detected (board vs local):"
|
|
302
|
+
echo "$BOARD_PULL_DRIFT" | python3 -c "
|
|
303
|
+
import json, sys
|
|
304
|
+
for d in json.load(sys.stdin):
|
|
305
|
+
print(f' #{d[\"number\"]} {d[\"title\"][:45]}: local={d[\"local\"]} board={d[\"board\"]}')
|
|
306
|
+
" 2>/dev/null
|
|
307
|
+
echo ""
|
|
308
|
+
|
|
309
|
+
AskUserQuestion(
|
|
310
|
+
header: "Stage Drift",
|
|
311
|
+
question: "Board and local state disagree on pipeline stage for ${DRIFT_COUNT} issue(s). How should we resolve?",
|
|
312
|
+
options: [
|
|
313
|
+
{ label: "Pull from board", description: "Update all local files to match board stages (board is source of truth)" },
|
|
314
|
+
{ label: "Keep local", description: "Leave local stages as-is — board will be updated next time pipeline runs" },
|
|
315
|
+
{ label: "Skip", description: "Ignore drift for now — flag in report only" }
|
|
316
|
+
]
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if [ "$USER_CHOICE" = "Pull from board" ]; then
|
|
320
|
+
echo "$BOARD_PULL_DRIFT" | python3 -c "
|
|
321
|
+
import json, sys, os
|
|
322
|
+
|
|
323
|
+
drift = json.load(sys.stdin)
|
|
324
|
+
active_dir = '${ACTIVE_DIR}'
|
|
325
|
+
|
|
326
|
+
for d in drift:
|
|
327
|
+
num = d['number']
|
|
328
|
+
board_stage = d['board']
|
|
329
|
+
for fname in os.listdir(active_dir):
|
|
330
|
+
if fname.startswith(f'{num}-') and fname.endswith('.json'):
|
|
331
|
+
path = os.path.join(active_dir, fname)
|
|
332
|
+
state = json.load(open(path))
|
|
333
|
+
state['pipeline_stage'] = board_stage
|
|
334
|
+
with open(path, 'w') as f:
|
|
335
|
+
json.dump(state, f, indent=2)
|
|
336
|
+
print(f' Updated #{num}: {d[\"local\"]} → {board_stage}')
|
|
337
|
+
break
|
|
338
|
+
" 2>/dev/null
|
|
339
|
+
BOARD_PULL_DRIFT="[]" # Resolved
|
|
340
|
+
fi
|
|
341
|
+
fi
|
|
342
|
+
fi
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Note:** `reconstructed_from_board: true` is set on any state file created this way.
|
|
346
|
+
Downstream commands (`/mgw:run`, `/mgw:issue`) will see this flag and know the state was
|
|
347
|
+
rebuilt from board data — triage results and GSD artifacts will need to be re-run if the
|
|
348
|
+
issue advances to `planning` or beyond.
|
|
349
|
+
</step>
|
|
350
|
+
|
|
31
351
|
<step name="scan_active">
|
|
32
352
|
**Scan all active issue states:**
|
|
33
353
|
|
|
@@ -144,6 +464,81 @@ Classify each issue into:
|
|
|
144
464
|
- **Unreviewed comments:** COMMENT_DELTA > 0 — new comments posted since triage that haven't been classified
|
|
145
465
|
</step>
|
|
146
466
|
|
|
467
|
+
<step name="label_drift_detection">
|
|
468
|
+
**Label Drift Detection:**
|
|
469
|
+
|
|
470
|
+
For each active issue state file in `.mgw/active/`, compare the stored `labels` field
|
|
471
|
+
against the live GitHub labels to detect MGW pipeline label drift:
|
|
472
|
+
|
|
473
|
+
```python
|
|
474
|
+
label_drift = []
|
|
475
|
+
|
|
476
|
+
for each state_file in .mgw/active/*.json:
|
|
477
|
+
stored_labels = state['labels'] # list stored at last pipeline write
|
|
478
|
+
live_labels_json = subprocess.run(
|
|
479
|
+
['gh', 'issue', 'view', str(issue_number), '--json', 'labels', '--jq', '[.labels[].name]'],
|
|
480
|
+
capture_output=True, text=True
|
|
481
|
+
).stdout.strip()
|
|
482
|
+
live_labels = json.loads(live_labels_json or '[]')
|
|
483
|
+
|
|
484
|
+
# Find MGW labels in stored vs live
|
|
485
|
+
stored_mgw = [l for l in stored_labels if l.startswith('mgw:')]
|
|
486
|
+
live_mgw = [l for l in live_labels if l.startswith('mgw:')]
|
|
487
|
+
|
|
488
|
+
if set(stored_mgw) != set(live_mgw):
|
|
489
|
+
label_drift.append({
|
|
490
|
+
'issue': issue_number,
|
|
491
|
+
'stored': stored_mgw,
|
|
492
|
+
'live': live_mgw,
|
|
493
|
+
'pipeline_stage': state['pipeline_stage']
|
|
494
|
+
})
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
If label drift detected, include in the drift report output:
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
Label drift detected for ${COUNT} issue(s):
|
|
501
|
+
#N: stored=[mgw:in-progress], live=[] — pipeline_stage=${STAGE}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Offer repair:**
|
|
505
|
+
```
|
|
506
|
+
AskUserQuestion(
|
|
507
|
+
header: "Label Drift Detected",
|
|
508
|
+
question: "Live GitHub labels don't match .mgw state for ${COUNT} issue(s). Repair?",
|
|
509
|
+
options: [
|
|
510
|
+
{ label: "Repair all", description: "Re-apply correct label for each issue's pipeline_stage via remove_mgw_labels_and_apply" },
|
|
511
|
+
{ label: "Skip", description: "Log drift only — no changes" }
|
|
512
|
+
]
|
|
513
|
+
)
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
If "Repair all":
|
|
517
|
+
```bash
|
|
518
|
+
# For each drifted issue, determine correct label from pipeline_stage and re-apply
|
|
519
|
+
CORRECT_LABEL=$(python3 -c "
|
|
520
|
+
stage_to_label = {
|
|
521
|
+
'triaged': 'mgw:triaged',
|
|
522
|
+
'needs-info': 'mgw:needs-info',
|
|
523
|
+
'needs-security-review': 'mgw:needs-security-review',
|
|
524
|
+
'discussing': 'mgw:discussing',
|
|
525
|
+
'approved': 'mgw:approved',
|
|
526
|
+
'planning': 'mgw:in-progress',
|
|
527
|
+
'executing': 'mgw:in-progress',
|
|
528
|
+
'verifying': 'mgw:in-progress',
|
|
529
|
+
'blocked': 'mgw:blocked',
|
|
530
|
+
'done': '',
|
|
531
|
+
'pr-created': '',
|
|
532
|
+
'failed': '',
|
|
533
|
+
}
|
|
534
|
+
print(stage_to_label.get('${PIPELINE_STAGE}', ''))
|
|
535
|
+
")
|
|
536
|
+
remove_mgw_labels_and_apply ${ISSUE_NUMBER} "${CORRECT_LABEL}"
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Log repair actions: "Repaired label for #N: applied ${CORRECT_LABEL} (pipeline_stage=${STAGE})"
|
|
540
|
+
</step>
|
|
541
|
+
|
|
147
542
|
<step name="health_check">
|
|
148
543
|
**GSD health check (if .planning/ exists):**
|
|
149
544
|
|
|
@@ -214,11 +609,13 @@ git push origin --delete ${BRANCH_NAME} 2>/dev/null
|
|
|
214
609
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
215
610
|
|
|
216
611
|
Active: ${active_count} issues in progress
|
|
612
|
+
Pulled: ${BOARD_PULL_CREATED} reconstructed from board (0 if board not configured)
|
|
217
613
|
Completed: ${completed_count} archived
|
|
218
614
|
Stale: ${stale_count} need attention
|
|
219
615
|
Orphaned: ${orphaned_count} need attention
|
|
220
616
|
Comments: ${comment_drift_count} issues with unreviewed comments
|
|
221
617
|
Branches: ${deleted_count} cleaned up
|
|
618
|
+
${BOARD_DISCOVERED ? 'Board: discovered + registered ' + BOARD_DISCOVERED : ''}
|
|
222
619
|
${HEALTH ? 'GSD Health: ' + HEALTH.status : ''}
|
|
223
620
|
|
|
224
621
|
${details_for_each_non_active_item}
|
|
@@ -231,7 +628,16 @@ ${gsd_milestone_consistency ? 'GSD Milestone Links:\n' + gsd_milestone_consisten
|
|
|
231
628
|
</process>
|
|
232
629
|
|
|
233
630
|
<success_criteria>
|
|
234
|
-
- [ ]
|
|
631
|
+
- [ ] pull_board_state runs before scan_active
|
|
632
|
+
- [ ] Board discovery: if project_board.node_id empty, findExistingBoard() + getProjectFields() called; if found, registered in project.json
|
|
633
|
+
- [ ] Board pull: all board items fetched in one GraphQL call; issues with no local .mgw/active/ file are reconstructed from GitHub issue data + board Status
|
|
634
|
+
- [ ] Reconstructed files have reconstructed_from_board:true, pipeline_stage from board Status, gsd_route from board GSD Route field
|
|
635
|
+
- [ ] "Done" board items are skipped (not written to active/)
|
|
636
|
+
- [ ] Stage drift detected and reported: local pipeline_stage differs from board Status
|
|
637
|
+
- [ ] Drift resolution offered: pull from board / keep local / skip
|
|
638
|
+
- [ ] Board pull errors (failed gh issue view calls) are collected and shown in report, never abort sync
|
|
639
|
+
- [ ] BOARD_PULL_CREATED count shown in sync report
|
|
640
|
+
- [ ] All .mgw/active/ files scanned (including any reconstructed by pull_board_state)
|
|
235
641
|
- [ ] GitHub state checked for each issue, PR, branch
|
|
236
642
|
- [ ] Comment delta checked for each active issue
|
|
237
643
|
- [ ] GSD milestone consistency checked for all maps-to links
|
|
@@ -239,5 +645,7 @@ ${gsd_milestone_consistency ? 'GSD Milestone Links:\n' + gsd_milestone_consisten
|
|
|
239
645
|
- [ ] Lingering worktrees cleaned up for completed items
|
|
240
646
|
- [ ] Branch deletion offered for completed items
|
|
241
647
|
- [ ] Stale/orphaned/drift items flagged (including comment drift and milestone inconsistencies)
|
|
648
|
+
- [ ] Label drift detected between .mgw/active labels field and live GitHub labels
|
|
649
|
+
- [ ] Repair offered for drifted issues via remove_mgw_labels_and_apply
|
|
242
650
|
- [ ] Summary presented
|
|
243
651
|
</success_criteria>
|
|
@@ -95,12 +95,15 @@ Seven labels for pipeline stage tracking. Created by init.md, managed by issue.m
|
|
|
95
95
|
| `mgw:blocked` | `b60205` | Pipeline blocked by stakeholder comment |
|
|
96
96
|
|
|
97
97
|
### Remove MGW Labels and Apply New
|
|
98
|
-
Used when transitioning pipeline stages. Removes all `mgw:*` pipeline labels, then applies the target label.
|
|
98
|
+
Used when transitioning pipeline stages. Removes all `mgw:*` pipeline labels, then applies the target label. Returns non-zero if any gh operation failed.
|
|
99
99
|
```bash
|
|
100
|
-
# Remove all mgw: pipeline labels from issue, then apply new one
|
|
100
|
+
# Remove all mgw: pipeline labels from issue, then apply new one.
|
|
101
|
+
# Pass empty string as NEW_LABEL to remove all MGW labels without applying a new one.
|
|
102
|
+
# Returns non-zero if any label operation failed.
|
|
101
103
|
remove_mgw_labels_and_apply() {
|
|
102
104
|
local ISSUE_NUMBER="$1"
|
|
103
105
|
local NEW_LABEL="$2"
|
|
106
|
+
local LABEL_FAILED=0
|
|
104
107
|
|
|
105
108
|
# Get current labels
|
|
106
109
|
CURRENT_LABELS=$(gh issue view "$ISSUE_NUMBER" --json labels --jq '.labels[].name' 2>/dev/null)
|
|
@@ -109,15 +112,27 @@ remove_mgw_labels_and_apply() {
|
|
|
109
112
|
for LABEL in $CURRENT_LABELS; do
|
|
110
113
|
case "$LABEL" in
|
|
111
114
|
mgw:triaged|mgw:needs-info|mgw:needs-security-review|mgw:discussing|mgw:approved|mgw:in-progress|mgw:blocked)
|
|
112
|
-
gh issue edit "$ISSUE_NUMBER" --remove-label "$LABEL"
|
|
115
|
+
gh issue edit "$ISSUE_NUMBER" --remove-label "$LABEL"
|
|
116
|
+
RC=$?
|
|
117
|
+
if [ $RC -ne 0 ]; then
|
|
118
|
+
echo "MGW WARNING: failed to remove label $LABEL from issue $ISSUE_NUMBER (exit $RC)" >&2
|
|
119
|
+
LABEL_FAILED=1
|
|
120
|
+
fi
|
|
113
121
|
;;
|
|
114
122
|
esac
|
|
115
123
|
done
|
|
116
124
|
|
|
117
125
|
# Apply new label
|
|
118
126
|
if [ -n "$NEW_LABEL" ]; then
|
|
119
|
-
gh issue edit "$ISSUE_NUMBER" --add-label "$NEW_LABEL"
|
|
127
|
+
gh issue edit "$ISSUE_NUMBER" --add-label "$NEW_LABEL"
|
|
128
|
+
RC=$?
|
|
129
|
+
if [ $RC -ne 0 ]; then
|
|
130
|
+
echo "MGW WARNING: failed to add label $NEW_LABEL to issue $ISSUE_NUMBER (exit $RC)" >&2
|
|
131
|
+
LABEL_FAILED=1
|
|
132
|
+
fi
|
|
120
133
|
fi
|
|
134
|
+
|
|
135
|
+
return $LABEL_FAILED
|
|
121
136
|
}
|
|
122
137
|
```
|
|
123
138
|
|
package/dist/bin/mgw.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var claude = require('../claude-
|
|
4
|
+
var claude = require('../claude-Dk1oVsaG.cjs');
|
|
5
5
|
var require$$0 = require('commander');
|
|
6
6
|
var require$$1 = require('path');
|
|
7
7
|
var require$$0$2 = require('fs');
|
|
@@ -9,7 +9,7 @@ var require$$0$1 = require('child_process');
|
|
|
9
9
|
|
|
10
10
|
var mgw$1 = {};
|
|
11
11
|
|
|
12
|
-
var version = "0.1.
|
|
12
|
+
var version = "0.1.3";
|
|
13
13
|
var require$$8 = {
|
|
14
14
|
version: version};
|
|
15
15
|
|