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で見る