From ce086f4204eb287e2ee20a8d5d6b3a526dd3e56a Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 23 Sep 2026 23:46:36 -0400 Subject: [PATCH 1/3] docs(pr-flow): make the In Progress / In Review card moves runnable, with a check (#2470) Step 1 gave a command for the assignment only, and step 6 ended on a trailing "Move the card to In Review" sentence, so agents ran the code block and skipped the card move. Both steps now carry a block that moves the card and ends by printing its Status; the step is done only when it prints the target column. Every id is resolved by name at run time (project, Status field, option) and the card is found through issue.projectItems, so no /board-ops option id is copied and a recreated option still resolves. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: cliffhall --- .claude/skills/pr-flow/SKILL.md | 82 ++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/.claude/skills/pr-flow/SKILL.md b/.claude/skills/pr-flow/SKILL.md index 3da9a957cf..e4a4d99a37 100644 --- a/.claude/skills/pr-flow/SKILL.md +++ b/.claude/skills/pr-flow/SKILL.md @@ -21,15 +21,52 @@ PR with no linked issue has no board card, so the work is invisible to the project board and untracked. If there's no issue yet, create one first with `/issue-create` — don't open the PR and backfill. -**Assign the issue to yourself**, then move its card to **In Progress** -(`/board-ops`). A card in progress with nobody on it can't answer "who has -this?". `@me` resolves to whoever `gh` is authenticated as, so an agent assigns -the maintainer it is working for: +**Step 1 is two actions — assign the issue, and move its card to In Progress. +Both happen before you branch.** A card in progress with nobody on it can't +answer "who has this?", and an assigned issue whose card still says `Todo` tells +the board nobody has started. `@me` resolves to whoever `gh` is authenticated +as, so an agent assigns the maintainer it is working for. + +Run the whole block. It is the assignment, the card move, and a check; **the +step is done only when the last line prints `card: In Progress`.** ```sh -gh issue edit --repo modelcontextprotocol/inspector --add-assignee @me +N=; STATUS="In Progress" +gh issue edit "$N" --repo modelcontextprotocol/inspector --add-assignee @me + +# Every id is resolved BY NAME at run time, so none is copied from /board-ops +# and an option recreated after a deletion (its hazard) still resolves. +PROJECT_ID= FIELD_ID= OPTION_ID= ITEM_ID= # no id survives a failed lookup +PROJECT_ID=$(gh project view 28 --owner modelcontextprotocol --format json --jq .id) +FIELDS=$(gh project field-list 28 --owner modelcontextprotocol --format json) && + FIELD_ID=$(jq -r '.fields[] | select(.name=="Status") | .id' <<<"$FIELDS") && + OPTION_ID=$(jq -r --arg s "$STATUS" '.fields[] | select(.name=="Status") + | .options[] | select(.name==$s) | .id' <<<"$FIELDS") +# The card is found from the issue, not from a board listing (see /board-ops). +card() { + 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} + fieldValueByName(name:"Status"){... on ProjectV2ItemFieldSingleSelectValue{name}}}}}}}' \ + | jq -r --arg p "$PROJECT_ID" '.data.repository.issue.projectItems.nodes[] + | select(.project.id==$p) | "\(.id) \(.fieldValueByName.name // "(none)")"' +} +ITEM_ID=$(card | cut -d' ' -f1) +if [ -n "$PROJECT_ID" ] && [ -n "$FIELD_ID" ] && [ -n "$OPTION_ID" ] && [ -n "$ITEM_ID" ]; then + gh project item-edit --project-id "$PROJECT_ID" --id "$ITEM_ID" \ + --field-id "$FIELD_ID" --single-select-option-id "$OPTION_ID" >/dev/null +else + echo "lookup failed (project='$PROJECT_ID' field='$FIELD_ID' option='$OPTION_ID' item='$ITEM_ID') — nothing edited" >&2 +fi +NOW=$(card | cut -d' ' -f2-) +[ "$NOW" = "$STATUS" ] && echo "card: $NOW" || echo "card is '$NOW', not '$STATUS' — this step is NOT done" >&2 ``` +An issue with no card on #28 fails the lookup; board it first with +`/issue-create`'s card step rather than skipping the move. For a **v1** issue, +swap `28` for `11` in both `gh project` calls — board #11 has the same column +names. + ## 2. Branch **Branch names start with the target version segment** — the first path segment @@ -252,7 +289,40 @@ gh api graphql -F n= -f query='query($n:Int!){ The link does not change how the issue closes on a v2 merge; that is still step 9. `removeCloseIssueReferences` takes the same input and undoes the link. -Move the card to **In Review**, then go straight to step 7. +**Then move the card to In Review. Step 6 is done only when the PR is linked +_and_ the card says `In Review`.** It is step 1's block with a different +column and no assignment. Run it in full and check that the last line prints +`card: In Review`: + +```sh +N=; STATUS="In Review" # the ISSUE number, not the PR's + +PROJECT_ID= FIELD_ID= OPTION_ID= ITEM_ID= # no id survives a failed lookup +PROJECT_ID=$(gh project view 28 --owner modelcontextprotocol --format json --jq .id) +FIELDS=$(gh project field-list 28 --owner modelcontextprotocol --format json) && + FIELD_ID=$(jq -r '.fields[] | select(.name=="Status") | .id' <<<"$FIELDS") && + OPTION_ID=$(jq -r --arg s "$STATUS" '.fields[] | select(.name=="Status") + | .options[] | select(.name==$s) | .id' <<<"$FIELDS") +card() { + 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} + fieldValueByName(name:"Status"){... on ProjectV2ItemFieldSingleSelectValue{name}}}}}}}' \ + | jq -r --arg p "$PROJECT_ID" '.data.repository.issue.projectItems.nodes[] + | select(.project.id==$p) | "\(.id) \(.fieldValueByName.name // "(none)")"' +} +ITEM_ID=$(card | cut -d' ' -f1) +if [ -n "$PROJECT_ID" ] && [ -n "$FIELD_ID" ] && [ -n "$OPTION_ID" ] && [ -n "$ITEM_ID" ]; then + gh project item-edit --project-id "$PROJECT_ID" --id "$ITEM_ID" \ + --field-id "$FIELD_ID" --single-select-option-id "$OPTION_ID" >/dev/null +else + echo "lookup failed (project='$PROJECT_ID' field='$FIELD_ID' option='$OPTION_ID' item='$ITEM_ID') — nothing edited" >&2 +fi +NOW=$(card | cut -d' ' -f2-) +[ "$NOW" = "$STATUS" ] && echo "card: $NOW" || echo "card is '$NOW', not '$STATUS' — this step is NOT done" >&2 +``` + +Then go straight to step 7. ## 7. Run the Copilot review loop — immediately, every PR From cfdbe571c4dfcdbf24659df93bc2827f85e46086 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 24 Sep 2026 01:11:38 -0400 Subject: [PATCH 2/3] docs(pr-flow): fail step 1's check on a failed assignment; select the board in both blocks (#2472 review) Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: cliffhall --- .claude/skills/pr-flow/SKILL.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.claude/skills/pr-flow/SKILL.md b/.claude/skills/pr-flow/SKILL.md index e4a4d99a37..5e2a9e543c 100644 --- a/.claude/skills/pr-flow/SKILL.md +++ b/.claude/skills/pr-flow/SKILL.md @@ -32,13 +32,16 @@ step is done only when the last line prints `card: In Progress`.** ```sh N=; STATUS="In Progress" -gh issue edit "$N" --repo modelcontextprotocol/inspector --add-assignee @me +BOARD=28 # 11 for a v1 issue — board #11 has the same column names +ASSIGNED= +gh issue edit "$N" --repo modelcontextprotocol/inspector --add-assignee @me \ + && ASSIGNED=1 || echo "assignment failed — this step is NOT done" >&2 # Every id is resolved BY NAME at run time, so none is copied from /board-ops # and an option recreated after a deletion (its hazard) still resolves. PROJECT_ID= FIELD_ID= OPTION_ID= ITEM_ID= # no id survives a failed lookup -PROJECT_ID=$(gh project view 28 --owner modelcontextprotocol --format json --jq .id) -FIELDS=$(gh project field-list 28 --owner modelcontextprotocol --format json) && +PROJECT_ID=$(gh project view "$BOARD" --owner modelcontextprotocol --format json --jq .id) +FIELDS=$(gh project field-list "$BOARD" --owner modelcontextprotocol --format json) && FIELD_ID=$(jq -r '.fields[] | select(.name=="Status") | .id' <<<"$FIELDS") && OPTION_ID=$(jq -r --arg s "$STATUS" '.fields[] | select(.name=="Status") | .options[] | select(.name==$s) | .id' <<<"$FIELDS") @@ -59,13 +62,12 @@ else echo "lookup failed (project='$PROJECT_ID' field='$FIELD_ID' option='$OPTION_ID' item='$ITEM_ID') — nothing edited" >&2 fi NOW=$(card | cut -d' ' -f2-) -[ "$NOW" = "$STATUS" ] && echo "card: $NOW" || echo "card is '$NOW', not '$STATUS' — this step is NOT done" >&2 +[ "$NOW" = "$STATUS" ] && [ -n "$ASSIGNED" ] && echo "card: $NOW" \ + || echo "card is '$NOW', assigned='${ASSIGNED:-no}' — this step is NOT done" >&2 ``` An issue with no card on #28 fails the lookup; board it first with -`/issue-create`'s card step rather than skipping the move. For a **v1** issue, -swap `28` for `11` in both `gh project` calls — board #11 has the same column -names. +`/issue-create`'s card step rather than skipping the move. ## 2. Branch @@ -296,10 +298,11 @@ column and no assignment. Run it in full and check that the last line prints ```sh N=; STATUS="In Review" # the ISSUE number, not the PR's +BOARD=28 # 11 for a v1 issue — board #11 has the same column names PROJECT_ID= FIELD_ID= OPTION_ID= ITEM_ID= # no id survives a failed lookup -PROJECT_ID=$(gh project view 28 --owner modelcontextprotocol --format json --jq .id) -FIELDS=$(gh project field-list 28 --owner modelcontextprotocol --format json) && +PROJECT_ID=$(gh project view "$BOARD" --owner modelcontextprotocol --format json --jq .id) +FIELDS=$(gh project field-list "$BOARD" --owner modelcontextprotocol --format json) && FIELD_ID=$(jq -r '.fields[] | select(.name=="Status") | .id' <<<"$FIELDS") && OPTION_ID=$(jq -r --arg s "$STATUS" '.fields[] | select(.name=="Status") | .options[] | select(.name==$s) | .id' <<<"$FIELDS") From 412944d90f239ac2e3efc97dfecd73d2782295c8 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 24 Sep 2026 01:20:47 -0400 Subject: [PATCH 3/3] docs(pr-flow): name the selected board in the no-card fallback (#2472 review) Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: cliffhall --- .claude/skills/pr-flow/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/pr-flow/SKILL.md b/.claude/skills/pr-flow/SKILL.md index 5e2a9e543c..a0b818b59a 100644 --- a/.claude/skills/pr-flow/SKILL.md +++ b/.claude/skills/pr-flow/SKILL.md @@ -66,7 +66,7 @@ NOW=$(card | cut -d' ' -f2-) || echo "card is '$NOW', assigned='${ASSIGNED:-no}' — this step is NOT done" >&2 ``` -An issue with no card on #28 fails the lookup; board it first with +An issue with no card on board `$BOARD` fails the lookup; board it there first with `/issue-create`'s card step rather than skipping the move. ## 2. Branch