Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .claude/skills/board-ops/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@ belong to the field", so the mistake is at least loud.
**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 lookups below
cannot find one.** Every `select(.content.repository==… and .content.number==…)`
matches nothing against a draft, and `item-add --url` has no URL to be given.
Look it up by **title** instead, then feed that item id to `item-edit` or
`item-delete` exactly as usual:

```sh
ITEM_ID=$(gh project item-list 28 --owner modelcontextprotocol --format json --limit 500 \
--jq '.items[] | select(.content.type=="DraftIssue")
| select(.content.title | startswith("[GHSA-xxxx-yyyy-zzzz]")) | .id')
```

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** —
Expand Down
5 changes: 4 additions & 1 deletion .claude/skills/issue-create/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ query, and an unmilestoned one drops out of release planning silently.

**Never create a duplicate.** Check the board for a matching item first.
**Never create a draft card** (a board card with no issue number) — every board
item is a real GitHub issue.
item is a real GitHub issue. The single exception is a **GitHub security
advisory**, which is private until it is published and so cannot be tracked by
an issue at all; see `/security-advisory`. Nothing you reach through *this*
flow is that case.

## 0. Check the board first

Expand Down
48 changes: 46 additions & 2 deletions .claude/skills/issue-triage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,9 @@ count means the board contradicts a rule, not that the rule needs revisiting.
| Check | Invariant | Fix |
| --- | --- | --- |
| Double-boarded | An issue has a card on **one** board, the one matching its version label | Delete the wrong-board card |
| Non-Issue items | **Only issues go on a board** — never PRs, never drafts | Delete the item |
| Non-Issue items | **Only issues go on a board** — never PRs, never drafts, *except* a `[GHSA-…]` **draft** on **#28** | Delete the item |
| No Status | Every card carries a Status | Set one — `Incoming` if unmilestoned, else by where it actually is |
| GHSA draft missing Status/Priority | An exempted advisory draft still carries both | Set them — `/security-advisory` |
| `Incoming` **with** a milestone (#28) | Incoming ⇔ no milestone | Approval was never recorded: move to **Todo**, or clear the milestone |
| Past Incoming **without** a milestone (#28) | Everything past Incoming ⇔ milestoned | Claims an approval nobody made: milestone it, or move back to Incoming |
| Wrong board for label | `v1` → #11, `v2` → #28 | Move the card to the right board |
Expand Down Expand Up @@ -235,7 +236,25 @@ jq -nr --slurpfile o "$D/i.json" --slurpfile a "$D/b28.json" --slurpfile b "$D/b
| [own($b)[] | select(.content.type=="Issue") | {n:.content.number, s:.status}] as $B11
| {
"double-boarded": [$B28[].n | select(. as $n | [$B11[].n]|index($n))],
"non-Issue on a board": [(own($a)[], own($b)[]) | select(.content.type!="Issue") | .content.number],
# An advisory draft card is the ONE legitimate non-Issue item (see AGENTS.md).
# The exemption is narrowed three ways, and each one matters: DRAFTS only
# (a GHSA-titled PR is still reported), board #28 ONLY (an advisory has no
# business on #11), and the `[GHSA-` title prefix (a stray draft is still
# reported). Reports the TITLE, since a draft has no number.
"non-Issue on a board": [(own($a)[] | select(.content.type!="Issue"
and ((.content.type=="DraftIssue"
and ((.content.title // "") | startswith("[GHSA-"))) | not))),
(own($b)[] | select(.content.type!="Issue"))]
| map(.content.title // "(untitled)"),
# $B28/$B11 hold only Issue items, so the Status and Priority checks below
# cannot see an advisory draft. Exempting drafts from the check above would
# therefore have made a half-made advisory card invisible to the whole
# audit; this is the narrow replacement.
"GHSA draft missing Status/Priority":
[own($a)[] | select(.content.type=="DraftIssue"
and ((.content.title // "") | startswith("[GHSA-")))
| select(.status==null or .priority==null)
| (.content.title[0:24])],
"no Status": [($B28[], $B11[]) | select(.s==null) | .n],
"Incoming w/ milestone": [$B28[] | select(.s=="Incoming" and ms(.n)!=null) | .n],
"past Incoming, no ms": [$B28[] | select(.s!=null and .s!="Incoming" and .s!="Done"
Expand Down Expand Up @@ -286,6 +305,31 @@ Two things the queries must account for, both learned the hard way:
and that check then reports `0` while the invariant it states (no drafts) is
being violated (Copilot). The filter admits an item with no repository and
excludes only cards that name a *different* one.
- **Advisory drafts are carved out of that check by TITLE, not by type.** A
GitHub security advisory is private until it is published, so it is tracked by
a draft card titled `[GHSA-xxxx-yyyy-zzzz] - …` — the one exception `AGENTS.md`
grants to "no draft cards", and the `security-advisory` skill is the flow. There
are enough of them open at any time that counting them would pin this check
permanently non-zero, and a check that never prints `0` stops being read at
all. The discriminator is deliberately the **title prefix** and nothing
broader: exempting *all* drafts, or every card whose Status is `Incoming`,
would let an ordinary stray draft through, which is the defect the check
exists for. So a draft titled anything else is still reported — by title,
since a draft has no issue number to print.
⚠️ **The title prefix alone is not enough, because a title is not a type and
not a board.** Matched on its own it would also exempt a **pull request**
whose title happens to start `[GHSA-` — a plausible title for a security fix
— and an advisory draft misfiled on **#11**, where the replacement field check
below does not look either, so both checks would read `0`. The exemption is
therefore `DraftIssue` **and** `[GHSA-` **and** board #28; #11 still reports
every non-Issue item it carries.
⚠️ **The exemption had to come with a replacement check.** `$B28` and `$B11`
are built from `Issue` items only, so the `no Status` and `no Priority`
checks never see a draft — before the carve-out the non-Issue check was the
*only* thing looking at one, and exempting drafts there alone would have made
a half-made advisory card invisible to the entire audit. Hence
`GHSA draft missing Status/Priority`, which reads the item-level `.status`
and `.priority` that `item-list` exposes for a draft as it does for an issue.
- **`$M` holds closed issues too** — the lookup is built from
`gh issue list --state all`, which it has to be, because the last check reads
closed issues' state reasons. So `isopen` is not there to cope with a missing
Expand Down
Loading
Loading