Skip to main content

board-ops

gh recipes for the Servers V2 project board (#43): add an issue's card, move its Status, set or change its Priority, delete a card, and recover from a deleted single-select option. Use when moving a card between columns; when setting Status or Priority; when an option ID is rejected; before editing a board field's options; or when cards lost their Status or Priority.

소스 정보

저장소
modelcontextprotocol/servers
최근 소스 활동
2026년 10월 24일 18:02
감지된 SKILL.md 언어
영어
스타
91,652
포크
11,880

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
2 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
board-ops
description
gh recipes for the Servers V2 project board (#43): add an issue's card, move its Status, set or change its Priority, delete a card, and recover from a deleted single-select option. Use when moving a card between columns; when setting Status or Priority; when an option ID is rejected; before editing a board field's options; or when cards lost their Status or Priority.
disable-model-invocation
false
# Board operations The rules about _what_ a card's Status and Priority should be live in [`AGENTS.md`](../../../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 create flow, which boards a new issue through the add-card recipe below). **This file is the only place the board's IDs are written down.** Every other skill and document names a column or a field and points here. Two copies of an ID are worse than one, because the stale copy is indistinguishable from the live one. ## The board | Board | Owner | Fields this workflow uses | | --- | --- | --- | | [Servers V2 (#43)](https://github.com/orgs/modelcontextprotocol/projects/43) | `modelcontextprotocol` | Status, Priority | There is one board. It is an **org** project, so every `gh project` command takes `--owner modelcontextprotocol`. The board is **private**: treat a dump of its items as private data (see the snapshot section below). **Only issues go on the board, never PRs, never draft cards.** A PR is tracked through the card of the issue it closes. The one exception is a security advisory's `[GHSA-…]` draft card; see [Advisory draft cards](#advisory-draft-cards). ## IDs The project node id and the field ids are stable: | Thing | ID | | --- | --- | | Project node ID | `PVT_kwDOCt2Azc4BcZgq` | | Status field ID | `PVTSSF_lADOCt2Azc4BcZgqzhXCpm0` | | Priority field ID | `PVTSSF_lADOCt2Azc4BcZgqzhjiM9M` | The **option** ids are **not** stable. They are regenerated whenever a single-select field's option list is edited (see the hazard below), so **this skill does not list them. Resolve an option id by its name, at run time, every time**: ```sh # The board's fields, once per session. Refuses to go on from a failed call. FIELDS=$(gh project field-list 43 --owner modelcontextprotocol --format json) || FIELDS= # opt <Field> <Option name> → the option's current id, or nothing. # The arguments reach jq through --args, never as shell positional parameters: # Claude Code rewrites those in a skill invoked with arguments (#5160). opt() { jq -r '$ARGS.positional as [$f, $o] | .fields[] | select(.name == $f) | .options[] | select(.name == $o) | .id' \ --args "$@" <<<"$FIELDS" } STATUS_OPT=$(opt Status "Todo") PRIORITY_OPT=$(opt Priority "Medium") [ -n "$STATUS_OPT" ] && [ -n "$PRIORITY_OPT" ] \ || echo "option not found — check the name against the list below" >&2 ``` The option **names** are what the workflow depends on: | Field | Options, in board order | | --- | --- | | Status | `Incoming`, `Todo`, `In Progress`, `In Review`, `Done` | | Priority | `Urgent`, `High`, `Medium`, `Low` | What each Status means: | Status | Means | | --- | --- | | Incoming | Arrived unboarded, awaiting review — **no milestone** | | Todo | Approved (a milestone was assigned) | | In Progress | Active work | | In Review | A PR is open | | Done | **Shipped** — a merged PR, or a parent whose last sub-issue closed | If a name above no longer resolves, list what the field actually holds rather than guessing: ```sh # Swap "Status" for "Priority" to see the other field's options. gh project field-list 43 --owner modelcontextprotocol --format json \ | jq '.fields[] | select(.name=="Status") | .options' ``` A renamed option is a change to this table, made in the same change that renames it. ## 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. 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](#move-an-existing-card). - **A whole-board dump** (the snapshot and the recovery dump below) 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. ## Recipes Every recipe below assumes `FIELDS` and `opt` from [IDs](#ids) are defined in the same shell. ### Add a card and set its fields ```sh # Resolve both options FIRST, so a failed field lookup or a renamed option # never leaves a card on the board with no Status or Priority. STATUS_OPT=$(opt Status "Todo") # an issue you filed through the create flow is approved by definition PRIORITY_OPT=$(opt Priority "Medium") ITEM_ID= if [ -n "$STATUS_OPT" ] && [ -n "$PRIORITY_OPT" ]; then # Prints the item id (PVTI_…); capture it. ITEM_ID=$(gh project item-add 43 --owner modelcontextprotocol --url <issue-url> \ --format json --jq '.id') || ITEM_ID= fi if [ -n "$ITEM_ID" ] && [ -n "$STATUS_OPT" ] && [ -n "$PRIORITY_OPT" ]; then # Chained: a failed Status edit stops before Priority, and the failure shows. gh project item-edit --project-id PVT_kwDOCt2Azc4BcZgq --id "$ITEM_ID" \ --field-id PVTSSF_lADOCt2Azc4BcZgqzhXCpm0 --single-select-option-id "$STATUS_OPT" \ && gh project item-edit --project-id PVT_kwDOCt2Azc4BcZgq --id "$ITEM_ID" \ --field-id PVTSSF_lADOCt2Azc4BcZgqzhjiM9M --single-select-option-id "$PRIORITY_OPT" \ || echo "item-edit FAILED — card $ITEM_ID may be half set; read it back" >&2 else echo "missing option id (nothing added) or item-add failed — nothing edited" >&2 fi ``` Each `item-edit` sets **one** field, so setting both takes two calls; there is no combined form. `item-add` on an issue that already has a card returns the existing card's id rather than adding a second one. For an issue swept in at triage, the only difference is Status → **Incoming** and that you do **not** set a milestone. ### 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`](#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 43. Querying through the repository also means the issue number cannot match another repo's issue. 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. ```sh N=<ISSUE_NUMBER> ITEM_ID=$(gh api graphql -F n="$N" -f query='query($n:Int!){ repository(owner:"modelcontextprotocol",name:"servers"){issue(number:$n){ projectItems(first:100){nodes{id project{id}}}}}}' \ --jq '.data.repository.issue.projectItems.nodes[] | select(.project.id=="PVT_kwDOCt2Azc4BcZgq") | .id') || ITEM_ID= [ -n "$ITEM_ID" ] || echo "#$N has no card on #43 (or the lookup failed)" >&2 ``` Then edit it. For example, Status → In Review, when its PR opens: ```sh STATUS_OPT=$(opt Status "In Review") if [ -n "$ITEM_ID" ] && [ -n "$STATUS_OPT" ]; then gh project item-edit --project-id PVT_kwDOCt2Azc4BcZgq --id "$ITEM_ID" \ --field-id PVTSSF_lADOCt2Azc4BcZgqzhXCpm0 --single-select-option-id "$STATUS_OPT" else echo "no ITEM_ID or option id — nothing edited" >&2 fi ``` Setting Priority is the same call with `opt Priority "<level>"` and the Priority field id `PVTSSF_lADOCt2Azc4BcZgqzhjiM9M`. ### Read a card back To confirm what a card holds, read it from the issue: ```sh N=<ISSUE_NUMBER> gh api graphql -F n="$N" -f query='query($n:Int!){ repository(owner:"modelcontextprotocol",name:"servers"){issue(number:$n){ projectItems(first:100){nodes{project{id number} fieldValues(first:20){nodes{... on ProjectV2ItemFieldSingleSelectValue{ name field{... on ProjectV2SingleSelectField{name}}}}}}}}}}' \ --jq '.data.repository.issue.projectItems.nodes[] | select(.project.id=="PVT_kwDOCt2Azc4BcZgq") | [.fieldValues.nodes[] | select(.field) | {(.field.name): .name}] | add' ``` ### 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: ```sh # 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 43 --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**: it 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**: ```sh gh api repos/modelcontextprotocol/servers/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). ### Advisory draft cards A private security advisory is tracked by a **draft card** titled with the bare id, `[GHSA-xxxx-yyyy-zzzz]`, because a real issue would disclose it before a fix exists. No summary goes in the title until the advisory is published. What goes on the card, and when it moves, is the `/security-advisory` flow; these are only the mechanics. An advisory can also have a **follow-up card**, titled `[GHSA-xxxx-yyyy-zzzz follow-up]`, for work its fix leaves open. The recipes below handle it with one change: put `[$GHSA follow-up]` wherever they have `[$GHSA]`, both in the lookup's `--arg p` and in `item-create`'s `--title`. The advisory card's own lookup matches up to the closing bracket, so it never finds the follow-up. ⚠️ **A draft card has no repository and no issue number**, so the issue-side lookup above cannot find one. Look it up by its **bracketed GHSA id** in the full listing, checked for truncation, then feed that item id to `item-edit` or `item-delete` exactly as usual: ```sh GHSA=GHSA-xxxx-yyyy-zzzz # the advisory's real id ITEM_ID= # never let an earlier lookup's id survive a failed one LOOKED= # set only by a lookup over a COMPLETE listing BOARD=$(gh project item-list 43 --owner modelcontextprotocol --format json --limit 2000) if jq -e '(.items | length) == .totalCount' <<<"$BOARD" >/dev/null; then ITEM_ID=$(jq -r --arg p "[$GHSA]" '.items[] | select(.content.type=="DraftIssue") | select(.content.title | startswith($p)) | .id' <<<"$BOARD") && LOOKED=1 [ -n "$ITEM_ID" ] || echo "no draft card titled [$GHSA] on #43" >&2 else echo "item-list incomplete or failed — raise --limit; not concluding anything" >&2 fi ``` **Create** one only when that lookup ran over a complete listing **and** found none (a second card for the same advisory is a duplicate). Run it in the same shell, straight after the lookup: it refuses on a missing `LOOKED` or a found `ITEM_ID`. It is the add-card recipe with `item-create` in place of `item-add`, Status `Incoming`, and the provisional Priority: ```sh STATUS_OPT=$(opt Status "Incoming") PRIORITY_OPT=$(opt Priority "<provisional level>") if [ "$LOOKED" != 1 ] || [ -n "$ITEM_ID" ]; then echo "lookup incomplete, or [$GHSA] already has a card ($ITEM_ID) — not creating" >&2 elif [ -n "$STATUS_OPT" ] && [ -n "$PRIORITY_OPT" ]; then ITEM_ID=$(gh project item-create 43 --owner modelcontextprotocol \ --title "[$GHSA]" --body "<link and triage lines only>" \ --format json --jq '.id') || ITEM_ID= fi # Then the two chained item-edit calls from "Add a card and set its fields". ``` **Convert** it to an issue only after the advisory is **published**. The web UI has "Convert to issue" on the card; the API equivalent is: ```sh REPO_ID=$(gh api repos/modelcontextprotocol/servers --jq .node_id) || REPO_ID= [ -n "$ITEM_ID" ] && [ -n "$REPO_ID" ] && gh api graphql \ -f item="$ITEM_ID" -f repo="$REPO_ID" -f query='mutation($item:ID!,$repo:ID!){ convertProjectV2DraftIssueItemToIssue(input:{itemId:$item,repositoryId:$repo}){
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기