@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.
@@ -0,0 +1,333 @@
1
+ ---
2
+ name: mgw:assign
3
+ description: Claim an issue for a user — assigns via GitHub and updates board + state
4
+ argument-hint: "<issue-number> [username]"
5
+ allowed-tools:
6
+ - Bash
7
+ - Read
8
+ - Write
9
+ - Edit
10
+ ---
11
+
12
+ <objective>
13
+ Claim a GitHub issue for yourself or another team member. Three operations in one call:
14
+
15
+ 1. **GitHub assignment** — `gh issue edit --add-assignee` to set the issue assignee
16
+ 2. **State update** — write assignee to `.mgw/active/<issue>.json` (creates minimal entry
17
+ if not yet triaged)
18
+ 3. **Board confirmation** — if a board is configured, emit the board URL so the team
19
+ can verify the assignment is reflected on the board item
20
+
21
+ Usage:
22
+ - `mgw:assign 42` — assign issue #42 to yourself (@me)
23
+ - `mgw:assign 42 alice` — assign issue #42 to @alice
24
+
25
+ GitHub Projects v2 automatically syncs issue assignees to board items, so no direct
26
+ GraphQL mutation is needed for the board Assignees field.
27
+
28
+ Follows delegation boundary: only state and GitHub operations — no application code reads.
29
+ </objective>
30
+
31
+ <execution_context>
32
+ @~/.claude/commands/mgw/workflows/state.md
33
+ @~/.claude/commands/mgw/workflows/github.md
34
+ @~/.claude/commands/mgw/workflows/board-sync.md
35
+ </execution_context>
36
+
37
+ <context>
38
+ Arguments: $ARGUMENTS
39
+
40
+ State: .mgw/active/ (issue state — created if missing)
41
+ Board: .mgw/project.json (if configured — read for board URL only)
42
+ </context>
43
+
44
+ <process>
45
+
46
+ <step name="parse_args">
47
+ **Parse $ARGUMENTS into issue number and optional username:**
48
+
49
+ ```bash
50
+ ISSUE_NUMBER=$(echo "$ARGUMENTS" | awk '{print $1}')
51
+ USERNAME=$(echo "$ARGUMENTS" | awk '{print $2}')
52
+
53
+ # Validate issue number
54
+ if [ -z "$ISSUE_NUMBER" ]; then
55
+ echo "Usage: /mgw:assign <issue-number> [username]"
56
+ echo ""
57
+ echo " mgw:assign 42 — assign #42 to yourself"
58
+ echo " mgw:assign 42 alice — assign #42 to @alice"
59
+ exit 1
60
+ fi
61
+
62
+ if ! echo "$ISSUE_NUMBER" | grep -qE '^[0-9]+$'; then
63
+ echo "ERROR: Issue number must be numeric. Got: '${ISSUE_NUMBER}'"
64
+ exit 1
65
+ fi
66
+ ```
67
+ </step>
68
+
69
+ <step name="validate_and_load">
70
+ **Initialize .mgw/ and load existing state (from state.md):**
71
+
72
+ ```bash
73
+ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
74
+ if [ -z "$REPO_ROOT" ]; then
75
+ echo "ERROR: Not a git repository."
76
+ exit 1
77
+ fi
78
+
79
+ MGW_DIR="${REPO_ROOT}/.mgw"
80
+
81
+ # Ensure directory structure
82
+ mkdir -p "${MGW_DIR}/active" "${MGW_DIR}/completed"
83
+
84
+ # Ensure gitignore entries
85
+ for ENTRY in ".mgw/" ".worktrees/"; do
86
+ if ! grep -qF "${ENTRY}" "${REPO_ROOT}/.gitignore" 2>/dev/null; then
87
+ echo "${ENTRY}" >> "${REPO_ROOT}/.gitignore"
88
+ fi
89
+ done
90
+
91
+ # Initialize cross-refs if missing
92
+ if [ ! -f "${MGW_DIR}/cross-refs.json" ]; then
93
+ echo '{"links":[]}' > "${MGW_DIR}/cross-refs.json"
94
+ fi
95
+
96
+ # Find state file for this issue
97
+ STATE_FILE=$(ls "${MGW_DIR}/active/${ISSUE_NUMBER}-"*.json 2>/dev/null | head -1)
98
+ STATE_EXISTS=$( [ -n "$STATE_FILE" ] && echo "true" || echo "false" )
99
+ ```
100
+ </step>
101
+
102
+ <step name="resolve_user">
103
+ **Resolve the assignee username:**
104
+
105
+ ```bash
106
+ # If no username provided, use the authenticated user
107
+ if [ -z "$USERNAME" ]; then
108
+ RESOLVED_USER=$(gh api user -q .login 2>/dev/null)
109
+ if [ -z "$RESOLVED_USER" ]; then
110
+ echo "ERROR: Cannot resolve current GitHub user. Check your gh auth status."
111
+ exit 1
112
+ fi
113
+ else
114
+ RESOLVED_USER="$USERNAME"
115
+ # Validate user exists on GitHub
116
+ USER_EXISTS=$(gh api "users/${RESOLVED_USER}" -q .login 2>/dev/null)
117
+ if [ -z "$USER_EXISTS" ]; then
118
+ echo "ERROR: GitHub user '${RESOLVED_USER}' not found."
119
+ exit 1
120
+ fi
121
+ fi
122
+
123
+ echo "MGW: Assigning #${ISSUE_NUMBER} to @${RESOLVED_USER}..."
124
+ ```
125
+ </step>
126
+
127
+ <step name="resolve_coauthor">
128
+ **Build the Co-Authored-By tag for the assignee:**
129
+
130
+ GitHub assigns every account a noreply email: `{id}+{login}@users.noreply.github.com`.
131
+ This gets stored in the active state file so GSD commits for this issue use the right tag.
132
+ Falls back to the project-level default in `project.json` if resolution fails.
133
+
134
+ ```bash
135
+ # Fetch assignee's GitHub ID and display name
136
+ ASSIGNEE_DATA=$(gh api "users/${RESOLVED_USER}" --jq '{id: .id, name: .name}' 2>/dev/null)
137
+ ASSIGNEE_ID=$(echo "$ASSIGNEE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])" 2>/dev/null)
138
+ ASSIGNEE_NAME=$(echo "$ASSIGNEE_DATA" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['name'] or d.get('login',''))" 2>/dev/null)
139
+
140
+ if [ -n "$ASSIGNEE_ID" ]; then
141
+ COAUTHOR_TAG="${ASSIGNEE_NAME} <${ASSIGNEE_ID}+${RESOLVED_USER}@users.noreply.github.com>"
142
+ else
143
+ # Fall back to project-level default
144
+ COAUTHOR_TAG=$(python3 -c "
145
+ import json
146
+ try:
147
+ p = json.load(open('${MGW_DIR}/project.json'))
148
+ print(p.get('project', {}).get('coauthor', ''))
149
+ except:
150
+ print('')
151
+ " 2>/dev/null || echo "")
152
+ fi
153
+
154
+ echo "MGW: Co-author tag: ${COAUTHOR_TAG}"
155
+ ```
156
+ </step>
157
+
158
+ <step name="fetch_issue">
159
+ **Fetch issue metadata from GitHub:**
160
+
161
+ ```bash
162
+ ISSUE_DATA=$(gh issue view "$ISSUE_NUMBER" --json number,title,url,labels,assignees,state 2>/dev/null)
163
+ if [ -z "$ISSUE_DATA" ]; then
164
+ echo "ERROR: Issue #${ISSUE_NUMBER} not found in this repo."
165
+ exit 1
166
+ fi
167
+
168
+ ISSUE_TITLE=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['title'])" 2>/dev/null)
169
+ ISSUE_URL=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['url'])" 2>/dev/null)
170
+ ISSUE_STATE=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['state'])" 2>/dev/null)
171
+
172
+ # Check if user is already assigned (idempotent)
173
+ ALREADY_ASSIGNED=$(echo "$ISSUE_DATA" | python3 -c "
174
+ import json,sys
175
+ d = json.load(sys.stdin)
176
+ assignees = [a['login'] for a in d.get('assignees', [])]
177
+ print('true' if '${RESOLVED_USER}' in assignees else 'false')
178
+ " 2>/dev/null)
179
+ ```
180
+ </step>
181
+
182
+ <step name="assign_github">
183
+ **Assign the issue on GitHub:**
184
+
185
+ ```bash
186
+ if [ "$ALREADY_ASSIGNED" = "true" ]; then
187
+ echo "MGW: @${RESOLVED_USER} is already assigned to #${ISSUE_NUMBER} — confirming state."
188
+ else
189
+ if ! gh issue edit "$ISSUE_NUMBER" --add-assignee "$RESOLVED_USER" 2>/dev/null; then
190
+ echo "ERROR: Failed to assign @${RESOLVED_USER} to #${ISSUE_NUMBER}."
191
+ echo " Check that the user has access to this repo."
192
+ exit 1
193
+ fi
194
+ echo "MGW: Assigned @${RESOLVED_USER} to #${ISSUE_NUMBER}."
195
+ fi
196
+ ```
197
+ </step>
198
+
199
+ <step name="update_state">
200
+ **Write assignee to .mgw/active/ state (create minimal entry if needed):**
201
+
202
+ ```bash
203
+ TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --raw 2>/dev/null \
204
+ || date -u +"%Y-%m-%dT%H:%M:%S.000Z")
205
+
206
+ if [ "$STATE_EXISTS" = "true" ]; then
207
+ # Update existing state file: set issue.assignee and coauthor fields
208
+ python3 -c "
209
+ import json
210
+ with open('${STATE_FILE}') as f:
211
+ state = json.load(f)
212
+ state['issue']['assignee'] = '${RESOLVED_USER}'
213
+ state['coauthor'] = '${COAUTHOR_TAG}'
214
+ state['updated_at'] = '${TIMESTAMP}'
215
+ with open('${STATE_FILE}', 'w') as f:
216
+ json.dump(state, f, indent=2)
217
+ print('updated')
218
+ " 2>/dev/null
219
+
220
+ else
221
+ # No state file — generate slug and create minimal entry
222
+ SLUG=\$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs generate-slug "${ISSUE_TITLE}" --raw 2>/dev/null | cut -c1-40 \
223
+ || echo "issue-${ISSUE_NUMBER}")
224
+
225
+ NEW_STATE_FILE="${MGW_DIR}/active/${ISSUE_NUMBER}-${SLUG}.json"
226
+
227
+ python3 -c "
228
+ import json
229
+ state = {
230
+ 'issue': {
231
+ 'number': ${ISSUE_NUMBER},
232
+ 'title': '${ISSUE_TITLE}',
233
+ 'url': '${ISSUE_URL}',
234
+ 'labels': [],
235
+ 'assignee': '${RESOLVED_USER}'
236
+ },
237
+ 'coauthor': '${COAUTHOR_TAG}',
238
+ 'triage': {
239
+ 'scope': { 'size': 'unknown', 'file_count': 0, 'files': [], 'systems': [] },
240
+ 'validity': 'pending',
241
+ 'security_risk': 'unknown',
242
+ 'security_notes': '',
243
+ 'conflicts': [],
244
+ 'last_comment_count': 0,
245
+ 'last_comment_at': None,
246
+ 'gate_result': { 'status': 'pending', 'blockers': [], 'warnings': [], 'missing_fields': [] }
247
+ },
248
+ 'gsd_route': None,
249
+ 'gsd_artifacts': { 'type': None, 'path': None },
250
+ 'pipeline_stage': 'new',
251
+ 'comments_posted': [],
252
+ 'linked_pr': None,
253
+ 'linked_issues': [],
254
+ 'linked_branches': [],
255
+ 'created_at': '${TIMESTAMP}',
256
+ 'updated_at': '${TIMESTAMP}'
257
+ }
258
+ with open('${NEW_STATE_FILE}', 'w') as f:
259
+ json.dump(state, f, indent=2)
260
+ print('created')
261
+ " 2>/dev/null
262
+
263
+ STATE_FILE="$NEW_STATE_FILE"
264
+ echo "MGW: Created minimal state entry at ${STATE_FILE}"
265
+ fi
266
+ ```
267
+ </step>
268
+
269
+ <step name="check_board">
270
+ **Check if board is configured and emit board URL:**
271
+
272
+ GitHub Projects v2 automatically syncs issue assignees to board items. No direct
273
+ GraphQL mutation is needed — the board will reflect the new assignee when refreshed.
274
+
275
+ ```bash
276
+ BOARD_URL=$(python3 -c "
277
+ import json, sys, os
278
+ try:
279
+ p = json.load(open('${MGW_DIR}/project.json'))
280
+ board = p.get('project', {}).get('project_board', {})
281
+ print(board.get('url', ''))
282
+ except:
283
+ print('')
284
+ " 2>/dev/null || echo "")
285
+
286
+ BOARD_ITEM_ID=$(python3 -c "
287
+ import json, sys
288
+ try:
289
+ p = json.load(open('${MGW_DIR}/project.json'))
290
+ for m in p.get('milestones', []):
291
+ for i in m.get('issues', []):
292
+ if i.get('github_number') == ${ISSUE_NUMBER}:
293
+ print(i.get('board_item_id', ''))
294
+ sys.exit(0)
295
+ print('')
296
+ except:
297
+ print('')
298
+ " 2>/dev/null || echo "")
299
+
300
+ BOARD_CONFIGURED=$( [ -n "$BOARD_URL" ] && echo "true" || echo "false" )
301
+ ```
302
+ </step>
303
+
304
+ <step name="confirm">
305
+ **Emit assignment confirmation:**
306
+
307
+ ```bash
308
+ echo ""
309
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
310
+ echo " MGW ► ISSUE ASSIGNED"
311
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
312
+ echo ""
313
+ echo " Issue : #${ISSUE_NUMBER} — ${ISSUE_TITLE}"
314
+ echo " URL : ${ISSUE_URL}"
315
+ echo " Assignee: @${RESOLVED_USER}"
316
+ echo " State : ${ISSUE_STATE}"
317
+ if [ "$BOARD_CONFIGURED" = "true" ]; then
318
+ echo " Board : ${BOARD_URL}"
319
+ if [ -n "$BOARD_ITEM_ID" ]; then
320
+ echo " (board item updated automatically by GitHub)"
321
+ else
322
+ echo " (issue not yet added to board — run /mgw:board show)"
323
+ fi
324
+ fi
325
+ echo ""
326
+ if [ "$ALREADY_ASSIGNED" = "true" ]; then
327
+ echo " Note: @${RESOLVED_USER} was already the assignee — state confirmed."
328
+ fi
329
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
330
+ ```
331
+ </step>
332
+
333
+ </process>