board-ops

GitHub

提供GitHub项目板(v1/v2)的CLI操作指南,涵盖卡片增删、状态/优先级变更及ID管理。重点解决列表截断陷阱与单选项删除后的数据恢复问题,确保板操作安全准确。

.claude/skills/board-ops/SKILL.md modelcontextprotocol/inspector

Trigger Scenarios

需要添加或移动项目板卡片 修改项目板卡片的状态或优先级 处理被删除的单选选项导致的ID失效 从已删除项目中恢复卡片数据

Install

npx skills add modelcontextprotocol/inspector --skill board-ops -g -y
More Options

Non-standard path

npx skills add https://github.com/modelcontextprotocol/inspector/tree/main/.claude/skills/board-ops -g -y

Use without installing

npx skills use modelcontextprotocol/inspector@board-ops

指定 Agent (Claude Code)

npx skills add modelcontextprotocol/inspector --skill board-ops -a claude-code -g -y

安装 repo 全部 skill

npx skills add modelcontextprotocol/inspector --all -g -y

预览 repo 内 skill

npx skills add modelcontextprotocol/inspector --list

SKILL.md

Frontmatter
{
    "name": "board-ops",
    "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.",
    "disable-model-invocation": false
}

Board operations

The rules about what a card's Status and Priority should be live in AGENTS.md under Issue-driven Work Style. This skill is the mechanics: the exact gh calls, the IDs, and the ways the board can be damaged.

Related: /issue-create (the five-step create flow), /issue-triage (sweeping unboarded issues in, the priority rubric, the board audit).

Which board

Version label Board Owner Has a Priority field?
v2 #28 modelcontextprotocol Yes
v1 #11 modelcontextprotocol No

Both are org projects, so every command takes --owner modelcontextprotocol. The two projects have their own field and option IDs and none of them are interchangeable — a #28 id passed to #11 is rejected with "option Id does not belong to the field", so the mistake is at least loud.

Finding a card without trusting --limit

⚠️ gh project item-list --limit N truncates silently. Past N it returns the first N items with no error and no warning, so a select over the result matches nothing and a card that exists reads as missing. Board #28 passed 500 items in September 2026 — double the figure quoted here two months earlier — and the old --limit 500 lookups reported a carded issue as unboarded and 16 GHSA drafts as absent in one session (#2451). A limit is a guess about the board's size; don't make the recipes depend on it being right.

  • An issue's card is looked up from the issue, which is independent of board size — see Move an existing card.
  • A draft card or a whole-board dump (the GHSA lookup, the snapshot, the recovery dump, /issue-triage's sweep and audit) genuinely needs the full listing. Those recipes use a limit with headroom and compare the result's .items | length against the .totalCount that item-list --format json also returns, so a truncated listing fails loudly instead of passing as complete. The check also catches a failed gh call, whose empty output has neither key. Where a later step reads the dump from a file, an incomplete dump is deleted, so that step fails on the missing file rather than running on partial data.

Only issues go on a board — never PRs, never draft cards. A PR is tracked through the card of the issue it closes.

The one exception is a GitHub security advisory, tracked by a draft card titled [GHSA-xxxx-yyyy-zzzz] - … because a real issue would disclose it before a fix exists. The flow is /security-advisory.

⚠️ A draft card has no repository and no issue number, so the issue-side lookup below cannot find one, and item-add --url has no URL to be given. Look it up by title in the full listing instead, then feed that item id to item-edit or item-delete exactly as usual:

GHSA=GHSA-xxxx-yyyy-zzzz   # the advisory's real id
ITEM_ID=   # never let an earlier lookup's id survive a failed one
BOARD=$(gh project item-list 28 --owner modelcontextprotocol --format json --limit 2000)
if jq -e '(.items | length) == .totalCount' <<<"$BOARD" >/dev/null; then
  ITEM_ID=$(jq -r '.items[] | select(.content.type=="DraftIssue")
        | select(.content.title | startswith("['"$GHSA"']")) | .id' <<<"$BOARD")
  [ -n "$ITEM_ID" ] || echo "no draft card titled [$GHSA] on #28" >&2
else
  echo "item-list incomplete or failed — raise --limit; not concluding anything" >&2
fi

Match on the bracketed GHSA id, not on words from the summary — a summary is free text and two advisories can share one. Advisory drafts live on #28 only; /issue-triage's audit reports one found anywhere else.

V2 board (#28) IDs

The project node id and the field ids are stable. The option ids are not — they are regenerated whenever a single-select field's option list is edited (see the hazard below). If any option id here is rejected, re-fetch:

# Swap "Status" for "Priority" to fetch the other field's options.
gh project field-list 28 --owner modelcontextprotocol --format json \
  | jq '.fields[] | select(.name=="Status") | .options'
Thing ID
Project node ID PVT_kwDOCt2Azc4BJVxt
Status field ID PVTSSF_lADOCt2Azc4BJVxtzg5iI8c
Priority field ID PVTSSF_lADOCt2Azc4BJVxtzg5iJE4

Status option IDs (--single-select-option-id) — last verified 2026-08-01.

Status Option ID Means
Incoming 721a3d4c Arrived unboarded, awaiting review — no milestone
Todo fbdaf21e Approved (a milestone was assigned)
In Progress 195df262 Active work, whatever the surface
In Review 159c8a02 A PR is open
Done 259d6aab Shipped — a merged PR, or a parent whose last sub-issue closed

Priority option IDs — last verified 2026-08-01. Derive the level with the rubric in /issue-triage; don't eyeball it.

Priority Option ID Rubric total
Urgent 79628723 12+
High 0a877460 9–11
Medium da944a9c 6–8
Low d67ac7ce ≤5

V1 board (#11) IDs

The v1 line takes security fixes only, so this board sees little traffic — but a v1 issue still gets a card, and the same Incoming/Todo split applies.

Thing ID
Project node ID PVT_kwDOCt2Azc4BA5sz
Status field ID PVTSSF_lADOCt2Azc4BA5szzgzkS-g

Status option IDs — last verified 2026-08-01.

Status Option ID
Incoming 831820cf
Todo f75ad846
In Progress 47fc9ee4
In Review 0439b2bf
Done 98236657

There is no Priority field on this board — the priority rubric is v2-only. Don't try to set one here; the field id doesn't exist.

Recipes

Add a card and set its fields

# Prints the item id (PVTI_…); capture it.
ITEM_ID=$(gh project item-add 28 --owner modelcontextprotocol --url <issue-url> --format json --jq '.id')

# Status → Todo (an issue you filed through the create flow is approved by definition)
gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" \
  --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iI8c --single-select-option-id fbdaf21e

# Priority → Medium
gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" \
  --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iJE4 --single-select-option-id da944a9c

Each item-edit sets one field, so setting both takes two calls — there is no combined form.

For an issue swept in at triage, the only difference is Status → Incoming (721a3d4c) and that you do not set a milestone.

For v1, the same shape against board #11:

ITEM_ID=$(gh project item-add 11 --owner modelcontextprotocol --url <issue-url> --format json --jq '.id')
gh project item-edit --project-id PVT_kwDOCt2Azc4BA5sz --id "$ITEM_ID" \
  --field-id PVTSSF_lADOCt2Azc4BA5szzgzkS-g --single-select-option-id f75ad846

Move an existing card

Look the item id up from the issue rather than re-adding it. An issue's projectItems lists the cards it has on every board, so the lookup does not depend on how many items the board holds (see Finding a card without trusting --limit). Select the card by the board's node id, not its number: project numbers are per-owner, and an issue can also sit on a user-owned project that happens to be numbered 28. Querying through the repository also means the issue number cannot match another repo's issue — board #11 really does carry a modelcontextprotocol/servers card.

For a v1 card on #11, swap every #28 id, not just the lookup's. #11's node id PVT_kwDOCt2Azc4BA5sz goes in both the lookup's select and the edit's --project-id; the edit also takes #11's own Status field PVTSSF_lADOCt2Azc4BA5szzgzkS-g and an option id from its table; and a delete is item-delete 11.

The mutation runs only on a non-empty id: item-edit --id "" fails with an opaque node-resolution error rather than saying the card was not found. The || ITEM_ID= matters too — on a GraphQL error (a number that is a PR, not an issue; a rate limit) gh api still prints the raw error JSON to stdout, which would otherwise land in ITEM_ID as a non-empty "id". first:100 is the connection's maximum page; it counts the boards one issue is on, not the cards on a board, so it has no board-size exposure.

N=<ISSUE_NUMBER>
ITEM_ID=$(gh api graphql -F n="$N" -f query='query($n:Int!){
  repository(owner:"modelcontextprotocol",name:"inspector"){issue(number:$n){
    projectItems(first:100){nodes{id project{id}}}}}}' \
  --jq '.data.repository.issue.projectItems.nodes[]
        | select(.project.id=="PVT_kwDOCt2Azc4BJVxt") | .id') || ITEM_ID=
[ -n "$ITEM_ID" ] || echo "#$N has no card on #28 (or the lookup failed)" >&2

Then edit it — e.g. Status → In Review, when its PR opens:

if [ -n "$ITEM_ID" ]; then
  gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" \
    --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iI8c --single-select-option-id 159c8a02
else
  echo "no ITEM_ID — nothing edited" >&2
fi

Delete a card

Done means the work shipped. An issue closed as duplicate / won't fix / not planned / obsolete / superseded shipped nothing, so its card is deleted, not parked in Done:

# ITEM_ID from the issue-side LOOKUP block in "Move an existing card" above —
# the lookup only, not the item-edit that follows it.
if [ -n "$ITEM_ID" ]; then
  gh project item-delete 28 --owner modelcontextprotocol --id "$ITEM_ID"
else
  echo "no ITEM_ID — nothing deleted" >&2
fi

Deleting the card removes it from the board only — the issue itself is untouched, keeps its labels and comments, and stays searchable and linkable forever. Nothing is lost; the board simply stops claiming the work was delivered. Done is read as the record of what a milestone actually delivered, so a duplicate sitting there makes that record wrong in a way nobody can detect later.

The close reason is the machine-readable form of the same distinction. gh issue close --reason accepts only completed and not planned, so duplicate must be set through the API:

gh api repos/modelcontextprotocol/inspector/issues/<N> -X PATCH \
  -f state=closed -f state_reason=duplicate

(or "Mark as duplicate" in the web UI, which additionally records a duplicate-of link).

⚠️ The option-deletion hazard

Never add, rename, or remove an option on a single-select board field (Status or Priority) with the updateProjectV2Field GraphQL mutation unless you pass every existing option's id. That mutation does a full replace of the option list: resending options by name/color/description without their ids makes GitHub delete all existing options and mint new ones, which orphans that field's value on every card on the board and invalidates every option id in the tables above. This has happened once, on Status (~197 items reconstructed by inference).

Safe alternatives, in order of preference:

  1. Add or rename an option in the GitHub web UI (Project → the field's settings). This preserves the ids of untouched options. ⚠️ Deleting is different, in the UI as much as in the API: removing an option blanks that field's value on every card that held it, with no undo and no warning that says so.
  2. If you must script it, first gh api graphql the current options with their ids, then call updateProjectV2Field echoing back every existing option including its id, appending only the new one. ProjectV2SingleSelectFieldOptionInput.id is an optional String, so a mixed list works. Verify afterward that no card lost its value — snapshot gh project item-list … --format json --limit 2000 before and after, check each is complete the way the snapshot below does, and diff; don't just spot-check. Send those dumps to $BOARD_TMP too, for the reason above.

Both the Incoming Status option and the Urgent/High/Medium/Low Priority options were added this way (#1891), with the before/after diff confirming all 264 cards kept their Status.

gh project item-add and gh project item-edit are always safe — they set a card's value and never touch the field schema.

Always snapshot before touching a field's options

One command, and it is the difference between a five-minute restore and reconstructing statuses by inference:

⚠️ Write it outside the repo. The boards are private, so a snapshot is a full dump of item IDs and every card's Status and Priority. Left in the working tree it is one git add -A away from being published in a PR (Copilot).

BOARD_TMP=$(mktemp -d)
gh project item-list 28 --owner modelcontextprotocol --format json --limit 2000 \
  > "$BOARD_TMP/board-snapshot.json"
# A truncated snapshot cannot restore the cards it dropped — refuse to proceed on one.
jq -e '(.items | length) == .totalCount' "$BOARD_TMP/board-snapshot.json" >/dev/null \
  && echo "snapshot: $BOARD_TMP/board-snapshot.json" \
  || { echo "SNAPSHOT INCOMPLETE — raise --limit and retake it before editing options" >&2
       rm -f "$BOARD_TMP/board-snapshot.json"; false; }

Note the printed path; you need it to recover.

Recovering from a deleted option

This has happened twice — once via the API (~197 items, reconstructed by inference) and once via the UI (the Done column, 247 items, restored from a snapshot in minutes). With a snapshot the recovery is mechanical.

The recipe below is written for a deleted Status option. For a deleted Priority option it is the same three steps with two substitutions: read .priority instead of .status (gh project item-list --format json exposes each single-select field under its lowercased name, so both keys are present), and pass the Priority field id PVTSSF_lADOCt2Azc4BJVxtzg5iJE4.

# 0. Same temp dir the snapshot went to — keep every dump out of the worktree.
BOARD_TMP=${BOARD_TMP:-$(mktemp -d)}

# 1. Which cards lost their value, and what did they hold? lost-ids.json is
#    kept ONLY when the dump is complete AND the snapshot reports what those cards
#    held — step 3 refuses to run without it, so neither a truncated dump nor a
#    missing snapshot can turn into a silent no-op or an unconfirmed re-apply.
rm -f "$BOARD_TMP/lost-ids.json"
gh project item-list 28 --owner modelcontextprotocol --format json --limit 2000 \
  > "$BOARD_TMP/board-broken.json"
if jq -e '(.items | length) == .totalCount' "$BOARD_TMP/board-broken.json" >/dev/null; then
  jq -r '[.items[]|select(.status==null)|.id]' "$BOARD_TMP/board-broken.json" \
    > "$BOARD_TMP/lost-ids.json" || rm -f "$BOARD_TMP/lost-ids.json"
  jq -r --slurpfile L "$BOARD_TMP/lost-ids.json" '($L[0]) as $lost
    | [.items[] | select(.id as $i | $lost|index($i)) | .status // "(none)"]
    | group_by(.) | map({s:.[0],c:length}) | .[] | "was \(.s): \(.c)"' \
    "$BOARD_TMP/board-snapshot.json" \
    || { echo "no usable snapshot — cannot confirm what these cards held; not re-applying" >&2
         rm -f "$BOARD_TMP/lost-ids.json"; }
else
  echo "board-broken.json INCOMPLETE — raise --limit and re-run step 1" >&2
  rm -f "$BOARD_TMP/board-broken.json"
fi

# 2. Recreate the option, echoing every surviving option's id (see above).
#    NOTE: the recreated option gets a NEW id — the deleted one never comes back.

# 3. Re-apply it to the orphaned cards.
if [ -s "$BOARD_TMP/lost-ids.json" ]; then
  for id in $(jq -r '.[]' "$BOARD_TMP/lost-ids.json"); do
    gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$id" \
      --field-id PVTSSF_lADOCt2Azc4BJVxtzg5iI8c --single-select-option-id <NEW_OPTION_ID>
    sleep 0.4
  done
else
  echo "no lost-ids.json — step 1 did not complete; nothing re-applied" >&2
fi

Step 1's grouping is the safety check: confirm the orphaned set is exactly the cards that held the deleted option, so you don't overwrite a card someone legitimately moved in the meantime.

Because the recreated option carries a new id, the tables above and every reference to it must be updated in the same change — grep the old id across the repo. The Done id has been 248a3910 and is now 259d6aab for exactly this reason.

⚠️ Two different "Priority" fields

An issue page shows two fields named Priority, and they are unrelated. Ours is the one under Projects → Inspector V2.

Where it appears What it is Ours?
Projects → Inspector V2 → Priority The project board field on #28 (PVTSSF_lADOCt2Azc4BJVxtzg5iJE4) ✅ Yes
Fields → Priority (above Projects) A GitHub issue field, IFSS_kgDOAdAWeg, defined at the org level and shared by every repo in it ❌ No

Nothing syncs them, in either direction, and they will happily disagree. Never delete the org-level field — it belongs to the whole org. Don't set it either; a value there is a reporter's opinion and feeds the rubric as a capped +1 signal bonus and nothing more (see /issue-triage).

Version History

  • 1e31c78 Current 2026-09-28 17:12

    修复了因item-list截断导致卡片查找失败的问题,强化了快照完整性校验与错误处理机制,完善了v1板ID命名规范。

  • 2e90a62 2026-09-23 06:47

Same Skill Collection

.claude/skills/issue-create/SKILL.md
.claude/skills/issue-triage/SKILL.md
.claude/skills/local-dev/SKILL.md
.claude/skills/pr-flow/SKILL.md
.claude/skills/pre-push-gate/SKILL.md
.claude/skills/project-structure/SKILL.md
.claude/skills/release/SKILL.md
.claude/skills/security-advisory/SKILL.md
.claude/skills/test-servers/SKILL.md
.claude/skills/testing/SKILL.md

Metadata

Files
0
Version
1e31c78
Hash
4b69e769
Indexed
2026-09-23 06:47

ホーム - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-10-02 18:09
浙ICP备14020137号-1