|
| 1 | +--- |
| 2 | +name: board-ops |
| 3 | +description: "gh recipes for the Inspector project boards — add a card, move its Status, set or re-score its Priority, delete a card, and recover from a deleted single-select option. Covers board #28 (v2) and board #11 (v1), their node/field/option IDs, and the option-deletion hazard." |
| 4 | +disable-model-invocation: false |
| 5 | +--- |
| 6 | + |
| 7 | +# Board operations |
| 8 | + |
| 9 | +The rules about *what* a card's Status and Priority should be live in |
| 10 | +[`AGENTS.md`](../../../AGENTS.md) under **Issue-driven Work Style**. This skill is |
| 11 | +the *mechanics*: the exact `gh` calls, the IDs, and the ways the board can be |
| 12 | +damaged. |
| 13 | + |
| 14 | +Related: `/issue-create` (the five-step create flow), `/issue-triage` (sweeping |
| 15 | +unboarded issues in, the priority rubric, the board audit). |
| 16 | + |
| 17 | +## Which board |
| 18 | + |
| 19 | +| Version label | Board | Owner | Has a Priority field? | |
| 20 | +| --- | --- | --- | --- | |
| 21 | +| `v2` | [#28](https://github.com/orgs/modelcontextprotocol/projects/28) | `modelcontextprotocol` | Yes | |
| 22 | +| `v1` | [#11](https://github.com/orgs/modelcontextprotocol/projects/11) | `modelcontextprotocol` | **No** | |
| 23 | + |
| 24 | +Both are **org** projects, so every command takes `--owner modelcontextprotocol`. |
| 25 | +The two projects have their own field and option IDs and none of them are |
| 26 | +interchangeable — a #28 id passed to #11 is rejected with "option Id does not |
| 27 | +belong to the field", so the mistake is at least loud. |
| 28 | + |
| 29 | +**Only issues go on a board — never PRs, never draft cards.** A PR is tracked |
| 30 | +through the card of the issue it closes. |
| 31 | + |
| 32 | +## V2 board (#28) IDs |
| 33 | + |
| 34 | +The project node id and the field ids are stable. The **option** ids are **not** — |
| 35 | +they are regenerated whenever a single-select field's option list is edited (see |
| 36 | +the hazard below). If any option id here is rejected, re-fetch: |
| 37 | + |
| 38 | +```sh |
| 39 | +# Swap "Status" for "Priority" to fetch the other field's options. |
| 40 | +gh project field-list 28 --owner modelcontextprotocol --format json \ |
| 41 | + | jq '.fields[] | select(.name=="Status") | .options' |
| 42 | +``` |
| 43 | + |
| 44 | +| Thing | ID | |
| 45 | +| --- | --- | |
| 46 | +| Project node ID | `PVT_kwDOCt2Azc4BJVxt` | |
| 47 | +| Status field ID | `PVTSSF_lADOCt2Azc4BJVxtzg5iI8c` | |
| 48 | +| Priority field ID | `PVTSSF_lADOCt2Azc4BJVxtzg5iJE4` | |
| 49 | + |
| 50 | +Status option IDs (`--single-select-option-id`) — **last verified 2026-08-01**. |
| 51 | + |
| 52 | +| Status | Option ID | Means | |
| 53 | +| --- | --- | --- | |
| 54 | +| Incoming | `721a3d4c` | Arrived unboarded, awaiting review — **no milestone** | |
| 55 | +| Todo | `fbdaf21e` | Approved (a milestone was assigned) | |
| 56 | +| In Progress | `195df262` | Active work, whatever the surface | |
| 57 | +| In Review | `159c8a02` | A PR is open | |
| 58 | +| Done | `259d6aab` | **Shipped** — a merged PR, or a parent whose last sub-issue closed | |
| 59 | + |
| 60 | +Priority option IDs — **last verified 2026-08-01**. Derive the level with the |
| 61 | +rubric in `/issue-triage`; don't eyeball it. |
| 62 | + |
| 63 | +| Priority | Option ID | Rubric total | |
| 64 | +| --- | --- | --- | |
| 65 | +| Urgent | `79628723` | 12+ | |
| 66 | +| High | `0a877460` | 9–11 | |
| 67 | +| Medium | `da944a9c` | 6–8 | |
| 68 | +| Low | `d67ac7ce` | ≤5 | |
| 69 | + |
| 70 | +## V1 board (#11) IDs |
| 71 | + |
| 72 | +The v1 line takes security fixes only, so this board sees little traffic — but a |
| 73 | +v1 issue still gets a card, and the same Incoming/Todo split applies. |
| 74 | + |
| 75 | +| Thing | ID | |
| 76 | +| --- | --- | |
| 77 | +| Project node ID | `PVT_kwDOCt2Azc4BA5sz` | |
| 78 | +| Status field ID | `PVTSSF_lADOCt2Azc4BA5szzgzkS-g` | |
| 79 | + |
| 80 | +Status option IDs — **last verified 2026-08-01**. |
| 81 | + |
| 82 | +| Status | Option ID | |
| 83 | +| --- | --- | |
| 84 | +| Incoming | `831820cf` | |
| 85 | +| Todo | `f75ad846` | |
| 86 | +| In Progress | `47fc9ee4` | |
| 87 | +| In Review | `0439b2bf` | |
| 88 | +| Done | `98236657` | |
| 89 | + |
| 90 | +There is **no Priority field on this board** — the priority rubric is v2-only. |
| 91 | +Don't try to set one here; the field id doesn't exist. |
| 92 | + |
| 93 | +## Recipes |
| 94 | + |
| 95 | +### Add a card and set its fields |
| 96 | + |
| 97 | +```sh |
| 98 | +# Prints the item id (PVTI_…); capture it. |
| 99 | +ITEM_ID=$(gh project item-add 28 --owner modelcontextprotocol --url <issue-url> --format json --jq '.id') |
| 100 | + |
| 101 | +# Status → Todo (an issue you filed through the create flow is approved by definition) |
| 102 | +gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" \ |
| 103 | + --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iI8c --single-select-option-id fbdaf21e |
| 104 | + |
| 105 | +# Priority → Medium |
| 106 | +gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" \ |
| 107 | + --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iJE4 --single-select-option-id da944a9c |
| 108 | +``` |
| 109 | + |
| 110 | +Each `item-edit` sets **one** field, so setting both takes two calls — there is |
| 111 | +no combined form. |
| 112 | + |
| 113 | +For an issue swept in at triage, the only difference is Status → **Incoming** |
| 114 | +(`721a3d4c`) and that you do **not** set a milestone. |
| 115 | + |
| 116 | +For **v1**, the same shape against board #11: |
| 117 | + |
| 118 | +```sh |
| 119 | +ITEM_ID=$(gh project item-add 11 --owner modelcontextprotocol --url <issue-url> --format json --jq '.id') |
| 120 | +gh project item-edit --project-id PVT_kwDOCt2Azc4BA5sz --id "$ITEM_ID" \ |
| 121 | + --field-id PVTSSF_lADOCt2Azc4BA5szzgzkS-g --single-select-option-id f75ad846 |
| 122 | +``` |
| 123 | + |
| 124 | +### Move an existing card |
| 125 | + |
| 126 | +Look the item id up by issue number rather than re-adding it. Keep `--limit` |
| 127 | +above the board's item count (~265 as of 2026-08-01) — past it `item-list` |
| 128 | +truncates **silently**, `select` matches nothing, and `item-edit --id ""` fails |
| 129 | +with an opaque node-resolution error rather than saying the limit was too low. |
| 130 | + |
| 131 | +⚠️ **Filter by repository, not by number alone.** These are **org** projects and |
| 132 | +issue numbers are **repo-local**, so an unfiltered `select` can match another |
| 133 | +repo's issue that happens to share the number — board #11 really does carry a |
| 134 | +`modelcontextprotocol/servers` card — and then moves or deletes the wrong card, |
| 135 | +or passes two ids at once (Copilot). |
| 136 | + |
| 137 | +```sh |
| 138 | +ITEM_ID=$(gh project item-list 28 --owner modelcontextprotocol --format json --limit 500 \ |
| 139 | + --jq '.items[] | select(.content.repository=="modelcontextprotocol/inspector" |
| 140 | + and .content.number==<ISSUE_NUMBER>) | .id') |
| 141 | +# e.g. Status → In Review, when its PR opens |
| 142 | +gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" \ |
| 143 | + --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iI8c --single-select-option-id 159c8a02 |
| 144 | +``` |
| 145 | + |
| 146 | +### Delete a card |
| 147 | + |
| 148 | +**`Done` means the work shipped.** An issue closed as duplicate / won't fix / |
| 149 | +not planned / obsolete / superseded shipped nothing, so its card is **deleted**, |
| 150 | +not parked in Done: |
| 151 | + |
| 152 | +```sh |
| 153 | +ITEM_ID=$(gh project item-list 28 --owner modelcontextprotocol --format json --limit 500 \ |
| 154 | + --jq '.items[] | select(.content.repository=="modelcontextprotocol/inspector" |
| 155 | + and .content.number==<ISSUE_NUMBER>) | .id') |
| 156 | +gh project item-delete 28 --owner modelcontextprotocol --id "$ITEM_ID" |
| 157 | +``` |
| 158 | + |
| 159 | +Deleting the card removes it from the board only — **the issue itself is |
| 160 | +untouched**, keeps its labels and comments, and stays searchable and linkable |
| 161 | +forever. Nothing is lost; the board simply stops claiming the work was |
| 162 | +delivered. Done is read as the record of what a milestone actually delivered, so |
| 163 | +a duplicate sitting there makes that record wrong in a way nobody can detect |
| 164 | +later. |
| 165 | + |
| 166 | +The close **reason** is the machine-readable form of the same distinction. |
| 167 | +`gh issue close --reason` accepts only `completed` and `not planned`, so |
| 168 | +**`duplicate` must be set through the API**: |
| 169 | + |
| 170 | +```sh |
| 171 | +gh api repos/modelcontextprotocol/inspector/issues/<N> -X PATCH \ |
| 172 | + -f state=closed -f state_reason=duplicate |
| 173 | +``` |
| 174 | + |
| 175 | +(or "Mark as duplicate" in the web UI, which additionally records a |
| 176 | +duplicate-of link). |
| 177 | + |
| 178 | +## ⚠️ The option-deletion hazard |
| 179 | + |
| 180 | +**Never add, rename, or remove an option on a single-select board field (Status |
| 181 | +or Priority) with the `updateProjectV2Field` GraphQL mutation unless you pass |
| 182 | +every existing option's `id`.** That mutation does a **full replace** of the |
| 183 | +option list: resending options by name/color/description without their `id`s |
| 184 | +makes GitHub **delete all existing options and mint new ones**, which **orphans |
| 185 | +that field's value on every card on the board** *and* invalidates every option |
| 186 | +id in the tables above. This has happened once, on Status (~197 items |
| 187 | +reconstructed by inference). |
| 188 | + |
| 189 | +Safe alternatives, in order of preference: |
| 190 | + |
| 191 | +1. **Add or rename an option in the GitHub web UI** (Project → the field's |
| 192 | + settings). This preserves the ids of untouched options. |
| 193 | + ⚠️ **Deleting is different, in the UI as much as in the API**: removing an |
| 194 | + option blanks that field's value on every card that held it, with no undo and |
| 195 | + no warning that says so. |
| 196 | +2. If you must script it, first `gh api graphql` the current options **with their |
| 197 | + `id`s**, then call `updateProjectV2Field` echoing back every existing option |
| 198 | + **including its `id`**, appending only the new one. |
| 199 | + `ProjectV2SingleSelectFieldOptionInput.id` is an optional `String`, so a mixed |
| 200 | + list works. Verify afterward that no card lost its value — snapshot |
| 201 | + `gh project item-list … --format json` before and after and diff; don't just |
| 202 | + spot-check. Send those dumps to `$BOARD_TMP` too, for the reason above. |
| 203 | + |
| 204 | +Both the `Incoming` Status option and the Urgent/High/Medium/Low Priority |
| 205 | +options were added this way (#1891), with the before/after diff confirming all |
| 206 | +264 cards kept their Status. |
| 207 | + |
| 208 | +`gh project item-add` and `gh project item-edit` are always safe — they set a |
| 209 | +card's value and never touch the field schema. |
| 210 | + |
| 211 | +### Always snapshot before touching a field's options |
| 212 | + |
| 213 | +One command, and it is the difference between a five-minute restore and |
| 214 | +reconstructing statuses by inference: |
| 215 | + |
| 216 | +⚠️ **Write it outside the repo.** [The boards are private](../issue-triage/SKILL.md), |
| 217 | +so a snapshot is a full dump of item IDs and every card's Status and Priority. |
| 218 | +Left in the working tree it is one `git add -A` away from being published in a |
| 219 | +PR (Copilot). |
| 220 | + |
| 221 | +```sh |
| 222 | +BOARD_TMP=$(mktemp -d) |
| 223 | +gh project item-list 28 --owner modelcontextprotocol --format json --limit 600 \ |
| 224 | + > "$BOARD_TMP/board-snapshot.json" |
| 225 | +echo "snapshot: $BOARD_TMP/board-snapshot.json" # note the path; you need it to recover |
| 226 | +``` |
| 227 | + |
| 228 | +### Recovering from a deleted option |
| 229 | + |
| 230 | +This has happened twice — once via the API (~197 items, reconstructed by |
| 231 | +inference) and once via the UI (the `Done` column, 247 items, restored from a |
| 232 | +snapshot in minutes). With a snapshot the recovery is mechanical. |
| 233 | + |
| 234 | +The recipe below is written for a deleted **Status** option. For a deleted |
| 235 | +**Priority** option it is the same three steps with two substitutions: read |
| 236 | +`.priority` instead of `.status` (`gh project item-list --format json` exposes |
| 237 | +each single-select field under its lowercased name, so both keys are present), |
| 238 | +and pass the Priority field id `PVTSSF_lADOCt2Azc4BJVxtzg5iJE4`. |
| 239 | + |
| 240 | +```sh |
| 241 | +# 0. Same temp dir the snapshot went to — keep every dump out of the worktree. |
| 242 | +BOARD_TMP=${BOARD_TMP:-$(mktemp -d)} |
| 243 | + |
| 244 | +# 1. Which cards lost their value, and what did they hold? |
| 245 | +gh project item-list 28 --owner modelcontextprotocol --format json --limit 600 \ |
| 246 | + > "$BOARD_TMP/board-broken.json" |
| 247 | +jq -r '[.items[]|select(.status==null)|.id]' "$BOARD_TMP/board-broken.json" \ |
| 248 | + > "$BOARD_TMP/lost-ids.json" |
| 249 | +jq -r --slurpfile L "$BOARD_TMP/lost-ids.json" '($L[0]) as $lost |
| 250 | + | [.items[] | select(.id as $i | $lost|index($i)) | .status // "(none)"] |
| 251 | + | group_by(.) | map({s:.[0],c:length}) | .[] | "was \(.s): \(.c)"' \ |
| 252 | + "$BOARD_TMP/board-snapshot.json" |
| 253 | + |
| 254 | +# 2. Recreate the option, echoing every surviving option's id (see above). |
| 255 | +# NOTE: the recreated option gets a NEW id — the deleted one never comes back. |
| 256 | + |
| 257 | +# 3. Re-apply it to the orphaned cards. |
| 258 | +for id in $(jq -r '.[]' "$BOARD_TMP/lost-ids.json"); do |
| 259 | + gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$id" \ |
| 260 | + --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iI8c --single-select-option-id <NEW_OPTION_ID> |
| 261 | + sleep 0.4 |
| 262 | +done |
| 263 | +``` |
| 264 | + |
| 265 | +Step 1's grouping is the safety check: confirm the orphaned set is exactly the |
| 266 | +cards that held the deleted option, so you don't overwrite a card someone |
| 267 | +legitimately moved in the meantime. |
| 268 | + |
| 269 | +Because the recreated option carries a **new id**, the tables above and every |
| 270 | +reference to it must be updated in the same change — `grep` the old id across |
| 271 | +the repo. The `Done` id has been `248a3910` and is now `259d6aab` for exactly |
| 272 | +this reason. |
| 273 | + |
| 274 | +## ⚠️ Two different "Priority" fields |
| 275 | + |
| 276 | +An issue page shows two fields named Priority, and they are unrelated. **Ours is |
| 277 | +the one under _Projects → Inspector V2_.** |
| 278 | + |
| 279 | +| Where it appears | What it is | Ours? | |
| 280 | +| --- | --- | --- | |
| 281 | +| **Projects → Inspector V2 → Priority** | The **project board** field on #28 (`PVTSSF_lADOCt2Azc4BJVxtzg5iJE4`) | ✅ Yes | |
| 282 | +| **Fields → Priority** (above _Projects_) | A GitHub **issue field**, `IFSS_kgDOAdAWeg`, defined at the **org** level and shared by every repo in it | ❌ No | |
| 283 | + |
| 284 | +Nothing syncs them, in either direction, and they will happily disagree. |
| 285 | +**Never delete the org-level field** — it belongs to the whole org. Don't set it |
| 286 | +either; a value there is a *reporter's* opinion and feeds the rubric as a capped |
| 287 | ++1 signal bonus and nothing more (see `/issue-triage`). |
0 commit comments