| name | github-project-release-manager |
| description | Manage the GitHub Project board and release process for the current repo using the gh CLI. Use when asked to manage github project, set up or sync project board, triage issues into project, check milestone progress, prepare release, cut a release, draft release notes, recommend a version bump, update CHANGELOG, or report project and release status. |
Act as a technical Project Manager and Release Manager working GitHub-natively via the gh CLI.
Never re-create structure that already exists. Every invocation starts by running the detection driver, which decides Setup vs Operational mode from the repo's actual state.
All paths are relative to the repository root. The driver is .claude/skills/github-project-release-manager/detect.sh.
Prerequisites
gh --version
gh auth status
jq --version
Authenticate if needed:
gh auth login
Reading Projects needs read:project. Writing to Projects needs project. Check before promising board mutations:
gh auth status 2>&1 | grep -i 'token scopes'
If 'project' is absent, board writes will fail. Get it with:
gh auth refresh -s project -h github.com
--hostname is required whenever this runs non-interactively — without it gh exits with --hostname required when not running interactively and prints usage. This is an OAuth device flow: it prints a one-time code to enter at github.com/login/device, so an agent cannot complete it. Hand the command to the user rather than attempting it.
Step 1 — Always run the driver first
./.claude/skills/github-project-release-manager/detect.sh
Read-only. Makes no writes. Safe to run repeatedly.
Real output from a fresh solo repo:
repo : tosin2013/repo-governor
MODE : setup (no config marker, no linked project, no releases/tags/milestones)
project write scope: false (scopes: 'gist', 'read:org', 'read:project', 'repo', 'workflow')
--- structure ---
config marker : false (.github/project-config.json)
linked projects : 0 []
releases / tags : 0 / 0 last: none / none
open milestones : 0
CHANGELOG.md : false
workflows : 0 (release automation: false, config: false)
--- activity ---
commits (90d) : 1
contributors : 1
open issues / PRs : 0 / 0
PRs merged (30d) : 0
--- recommendation ---
complexity score : 0/8
board : none
release process : none
For scripting, use --json and parse with jq:
./.claude/skills/github-project-release-manager/detect.sh --json > /tmp/snap.json
jq -r '.mode, .board_recommendation, .release_recommendation' /tmp/snap.json
| field | meaning |
|---|
mode | setup or operational — obey it |
project_write_scope | false → board mutations will fail; say so, don't attempt |
linked_projects | projects actually linked to this repo (not all your projects) |
complexity_score | 0–8; drives the board recommendation. Measures coordination need — contributors, open items, activity — not tracking need. A solo repository under heavy development scores low and may still warrant a board. |
board_decided / release_decided | the decision recorded in the marker. When present it outranks the computed recommendation — report it, do not re-argue it |
board_recommendation | none / lightweight / full |
release_recommendation | none / milestone / automated |
Enter Setup mode only when mode is setup, or when the user explicitly asks for a re-evaluation. Otherwise go to Operational mode.
A recorded decision outranks the computed recommendation. When board_decided is set, the driver prints it and flags any divergence:
board (decided) : full [human override]
computed would be 'none'; the recorded decision governs.
Report the decision. Do not re-open the question — a driver that surfaces only its own computation asks the human to re-litigate a settled choice every run, which is how a parallel decision surface starts.
Step 2a — Setup mode
Run only on first setup or explicit re-evaluation.
Decide the board — bias hard toward none
Take complexity_score from the driver:
| score | decision | what to create |
|---|
| 0–2 | none | No Project. Record the decision and stop. |
| 3–4 | lightweight | User-level Project with Status + Priority only. |
| 5–8 | full | Project with Status, Priority, Size, Target date, plus a board view and a table view. |
Do not create a Project for a solo repo with few open items. A board nobody reads is worse than no board. When the score says none, say so plainly and write the marker — that is a complete, successful Setup run.
Create only when warranted:
gh project create --owner "@me" --title "<repo> roadmap" --format json
Link it to the repo. Use the literal login, not @me (see Gotchas):
gh project link <number> --owner <login> --repo <owner>/<repo>
Add fields only for full:
gh project field-create <number> --owner <login> --name "Priority" \
--data-type SINGLE_SELECT --single-select-options "P0,P1,P2"
gh project field-create <number> --owner <login> --name "Size" \
--data-type SINGLE_SELECT --single-select-options "XS,S,M,L"
Enable the built-in workflows (auto-add items, auto-set status on close) in the Project's UI settings — gh cannot configure Project workflows.
Decide the release process — lightest that works
| situation | decision |
|---|
| No shipped artifact, or spec/docs only | none — do not create tags or a CHANGELOG |
| Ships occasionally, humans decide versions | milestone — version-named milestones + gh release create --generate-notes |
| Frequent releases, conventional commits already in use | automated — release-please or semantic-release |
| Signed artifacts, matrix builds, staged rollout | full — only when actually required |
Create a milestone only if the decision is milestone or heavier:
gh api repos/<owner>/<repo>/milestones -f title="v0.1.0" -f state=open
Write the marker — always, even when both decisions are "none"
This is what makes the next invocation Operational. Record the decision and what would change it.
mkdir -p .github
cat > .github/project-config.json <<'EOF'
{
"$comment": "Marker for the github-project-release-manager skill. Presence => Operational mode; do not re-run setup analysis.",
"version": 1,
"evaluated_at": "YYYY-MM-DD",
"repo": "<owner>/<repo>",
"board": {
"decision": "none",
"reason": "<why, citing the score and the numbers behind it>",
"revisit_when": "contributors > 2, or open issues+PRs > 5, or first release cut"
},
"release": {
"decision": "none",
"reason": "<why>",
"revisit_when": "<concrete trigger>"
}
}
EOF
jq -e . .github/project-config.json >/dev/null && echo "marker valid"
Verify the flip before reporting success:
./.claude/skills/github-project-release-manager/detect.sh | head -2
Step 2b — Operational mode
Default for every repeat call. Do useful maintenance. Do not re-analyze whether a board should exist unless asked.
Pick the work that matches the request. When the request is vague, check milestone risk and triage, then report.
Triage untriaged issues
gh issue list --state open --search "no:label" --limit 20
gh issue list --state open --search "no:milestone" --limit 20
Apply labels and milestones:
gh issue edit <n> --add-label "bug" --milestone "v0.1.0"
Add to the board (needs project scope):
gh project item-add <number> --owner <login> --url https://github.com/<owner>/<repo>/issues/<n>
Sync board status
List items and their current status:
gh project item-list <number> --owner <login> --limit 100 --format json \
| jq -r '.items[] | "\(.content.number // "-")\t\(.status // "no status")\t\(.content.title)"'
--limit is not optional. Without it the command returns 30 items and says nothing about the rest — see Gotchas.
Flag mismatches — a closed issue still in In Progress, a merged PR not marked Done — and fix them, or report them when write scope is missing.
Check milestone progress
gh api repos/<owner>/<repo>/milestones \
--jq '.[] | "\(.title): \(.closed_issues)/\(.open_issues + .closed_issues) due \(.due_on // "none")"'
Flag as at-risk any milestone whose due date is near with open issues remaining.
Recommend a version bump
Scan commits since the last tag. git describe fails on a repo with no tags, so fall back to full history:
RANGE=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/$/../'); RANGE=${RANGE:-HEAD}
echo "range: ${RANGE:-all history}"
git log ${RANGE} --format='%s' | grep -cE '^feat(\(.+\))?!?:'
git log ${RANGE} --format='%s' | grep -cE '^fix(\(.+\))?!?:'
git log ${RANGE} --format='%s' | grep -cE '(^[a-z]+(\(.+\))?!:|BREAKING CHANGE)'
Breaking > 0 → major (or minor while 0.x). Any feat → minor. Only fix → patch. If commits are not conventional, read the merged PR titles instead and say the recommendation is judgment-based.
Draft release notes without creating anything
This API generates notes and writes nothing:
gh api repos/<owner>/<repo>/releases/generate-notes \
-f tag_name=v0.1.0 -f target_commitish=main --jq '.body'
For the human-written summary, list what merged:
gh pr list --state merged --limit 100 \
--search "merged:>=$(date -v-30d +%Y-%m-%d 2>/dev/null || date -d '30 days ago' +%Y-%m-%d)" \
--json number,title,author --jq '.[] | "- \(.title) (#\(.number)) @\(.author.login)"'
Cut the release — only on explicit approval
Creating a release is public and hard to undo. Confirm the version with the user first, then:
gh release create v0.1.0 --generate-notes --draft --title "v0.1.0"
Keep --draft unless the user asked to publish. Publish separately:
gh release edit v0.1.0 --draft=false
Output format
Report in this order, every time:
- Mode — Setup or Operational, with the driver's one-line reason.
- Current snapshot — repo, board/release structure, activity numbers that matter.
- Actions taken / recommendations — what changed, or what should happen next.
- Release status — progress to next release, suggested version, blockers. Omit when no release process exists.
- Config update — only when something material changed.
Rules
- Prefer the simplest thing that works. Recommending "none" is a valid, complete outcome.
- Be idempotent. Check before creating; never produce a second Project, milestone, or marker.
- Ground every claim in driver output or a
gh command you ran. Do not assert board state you did not read.
- In Operational mode, when unsure, do maintenance rather than re-architecting.
- Confirm before public, hard-to-undo actions: publishing a release, deleting items, closing issues in bulk.
- When
project_write_scope is false, report what you would change and give the gh auth refresh -s project command. Do not attempt the write.
Gotchas
These were all hit while building this skill.
-
gh project link --owner "@me" fails with 'owner/repo' has different owner from '@me'. gh compares the --repo owner string against the literal @me instead of resolving it. Use the real login: --owner tosin2013.
-
gh project link succeeds silently and exits 0 — no output on success. It also works with only read:project. You cannot tell success from a no-op by exit code; verify with the GraphQL query below.
-
gh project item-list defaults to --limit 30, and silently truncates. Observed 2026-08-19 on this repository: the board appeared to "stop at issue 30", and comparing that list against gh issue list produced a confident, wrong diagnosis that 14 issues were missing from the board. The board held 34. Always pass --limit well above the item count before comparing board membership to anything:
gh project item-list <n> --owner <login> --limit 200 --format json
The failure mode is nasty because truncation looks exactly like a real gap, and the remedy for a real gap — adding items — is harmless enough that you may never notice you were wrong. Check the item count against --limit before believing a diff.
-
gh project list shows every project you own, not this repo's. Two untitled projects on an account are common. The only reliable repo↔project check is:
gh api graphql -f query='query{ repository(owner:"<owner>", name:"<repo>"){ projectsV2(first:20){ nodes{ number title url } } } }' \
--jq '.data.repository.projectsV2.nodes[]?'
-
gh release list, gh issue list, and gh pr list print nothing and exit 0 when empty. Never branch on their exit code. Count lines: gh release list | grep -c ..
-
read:project covers more than it looks, but not board mutations. Verified on this repo: gh project link and every list/view command succeed with alone, because linking is a repository-side operation. , because setting a field value is a Project mutation:
Troubleshooting
FATAL: gh not authenticated — run gh auth login.
FATAL: no GitHub remote resolved — the repo has no GitHub remote. git remote add origin https://github.com/<owner>/<repo>.git.
'owner/repo' has different owner from '@me' — replace @me with the literal login in --owner.
INSUFFICIENT_SCOPES ... requires ['project'] — the user must run gh auth refresh -s project -h github.com, then retry. An agent cannot do this: it is a browser device flow.
--hostname required when not running interactively — gh auth refresh was run without -h github.com. Add it.
- Driver reports
setup on a repo you already configured — the marker is missing or invalid. Check jq -e . .github/project-config.json; recreate it rather than re-running setup analysis.
could not resolve to a Repository — the token lacks repo scope for a private repo, or the name is wrong. Verify with gh repo view --json nameWithOwner.