| name | managing-project-backlog |
| description | Plan and maintain a GitHub Projects v2 board with the deckhand workflow: create epics, break them into linked sub-issues, prioritise, assign owners, and move tickets across the kanban (Backlog, Prioritized, Doing, Review, Done). Use eagerly when working a board: 'the backlog', 'the board', 'pick up #N', 'work on #N', 'what's next', 'what should I do next', 'create an epic', 'break this into tickets', 'prioritise', a pasted GitHub Projects URL, or /managing-project-backlog. The skill is board-aware: it confirms a GitHub Projects board is in play (configured for this repo, or present on it) before acting, and steps aside if there is no board, so it never barges into an unrelated repo. |
| user-invocable | true |
Managing the Project Backlog
Automate the GitHub Projects kanban workflow — from epic creation through to merged. Every piece of work traces back to an epic; every epic connects to the product roadmap.
⛔ Invocation rule — read first. This skill governs the deckhand GitHub Projects board workflow. Fire eagerly when the user is doing board work — "the board", "the backlog", "pick up #N", "work on #N", "what's next", epics, sub-issues, prioritising, a pasted GitHub Projects URL. Then apply the board-awareness gate in the first-invocation protocol below: if a board is in play (configured, or present on the repo) act on it; if there is genuinely no board, step aside rather than barging in. Once you're doing board operations, follow this skill's conventions consistently for the rest of the session.
If you find yourself about to run gh issue ..., gh project ..., or pick up an issue without having read this skill in the current session — STOP and load it first. The board IDs, column mappings, and the "read epic + siblings before starting" protocol below are not optional context.
⚠ First-invocation protocol (read this BEFORE running any board operation).
deckhand only operates on a GitHub Projects board, so first establish which situation you're in by reading board-config.md next to this file:
- Configured —
board-config.md has no <...> placeholders (<OWNER>, <REPO>, <PROJECT_NUMBER>, <PROJECT_ID>, <STATUS_FIELD_ID>, <EPICS_OPTION_ID>, etc.). Proceed as normal — this is the everyday case, and "pick up #N" / "what's next" should just work.
- Not configured, but a board is in play — placeholders remain, but the user is clearly doing GitHub Projects board work, or the repo's owner has a board (a quick
gh project list --owner <owner> returns one). Offer to wire deckhand up: run setup (below), then proceed. Ask first; don't force it.
- No board in play — placeholders remain AND there is no GitHub Projects board to drive (not a GitHub repo, or
gh project list comes back empty, and the user only said something generic). Step aside: briefly say deckhand runs off a GitHub Projects board and you don't see one here, offer to set one up if they'd like, and otherwise do nothing. Do NOT run board operations or push setup on someone who has no board.
When a board is in play but not yet configured, set it up before any board operation. You — the assisting Claude — must run setup before any board operation.
Two options, in order of preference:
-
Run the bundled script: bash <path-to-managing-project-backlog>/../ticket-lifecycle/setup.sh. It's interactive — it will ask the user for owner, repo, and project number, auto-discover the rest via gh api graphql, and write the populated config to both skills' board-config.md files. When invoking from Claude Code, ask the user the three questions in chat, then call the script — or perform the equivalent GraphQL discovery yourself and write board-config.md directly.
-
Do it inline: if the user can't run an interactive script, you can do the discovery yourself. Ask the user for owner / repo / project number, then run:
gh api graphql -f query='query{user(login:"<OWNER>"){projectV2(number:<NUM>){id title fields(first:50){nodes{... on ProjectV2SingleSelectField{id name options{id name}}}}}}}'
(swap user for organization if the owner is an org). Show the user the discovered column list, ask them to map each of the six workflow terms (Epics / Backlog / Prioritized / Doing / Review / Done) to one of their actual columns, then write the populated board-config.md yourself.
Do not invent IDs. Do not skip setup and hope it works. Only proceed to the rest of the workflow once board-config.md has no <...> placeholders left. The "Project Discovery" section below shows how to look them up manually if needed.
🔄 Staying up to date (quiet by default).
On your first invocation in a session, run a throttled version check from the folder this skill is installed in (the one holding this SKILL.md and its VERSION). Suggested one-liner — adapt the path; on Codex/Cursor it's wherever this skill actually lives:
DIR="<this skill's folder>"
[ -f "$DIR/.no-update-check" ] && exit 0
[ -f "$DIR/.last-update-check" ] && [ -z "$(find "$DIR/.last-update-check" -mtime +0 2>/dev/null)" ] && exit 0
touch "$DIR/.last-update-check"
LOCAL=$(cat "$DIR/VERSION" 2>/dev/null)
REMOTE=$(curl -fsS --max-time 3 https://raw.githubusercontent.com/jonthebeef/deckhand/main/skills/managing-project-backlog/VERSION 2>/dev/null)
[ -n "$REMOTE" ] && [ "$REMOTE" != "$LOCAL" ] && echo "deckhand update available: $LOCAL -> $REMOTE"
- If a newer version is reported: tell the user once, briefly ("deckhand $REMOTE is available, you have $LOCAL — want me to update it?"), then get on with their actual request. Never block on it.
- If current, offline, or the check errors: say nothing. Silence is the default.
- Don't offer twice in one session — if
ticket-lifecycle already offered, skip.
- To disable: the user drops an empty
.no-update-check file in this folder, or just asks. See the README's "Staying up to date" section.
⬆️ Updating (only on request or an accepted offer) — update in place, don't break anything.
- Confirm with the user first.
- Fetch latest: download the repo (e.g.
curl -fsSL https://codeload.github.com/jonthebeef/deckhand/tar.gz/refs/heads/main to a temp dir, extract).
- Overwrite in place — replace
SKILL.md and VERSION in this skill's installed folder with the new copies.
- PRESERVE — never overwrite:
board-config.md (the user's real board IDs), the .no-update-check / .last-update-check markers, and anything else populated locally.
- Verify: confirm
board-config.md is untouched, confirm the new VERSION, list what changed. If anything would clobber the user's config, STOP and ask.
- Report the new version and that config was preserved. Do the same for
ticket-lifecycle if it's installed alongside.
Same procedure on Claude Code, Codex, and Cursor — just write to wherever this skill actually lives on that platform.
Workflow
digraph backlog_flow {
rankdir=TB;
"Epic exists?" [shape=diamond];
"Find epic" [shape=box];
"Create epic" [shape=box];
"Break into sub-issues" [shape=box];
"Add to board as Backlog" [shape=box];
"Prioritize" [shape=box];
"Pick up (Doing)" [shape=box];
"Review" [shape=box];
"Done" [shape=box];
"Epic exists?" -> "Find epic" [label="yes"];
"Epic exists?" -> "Create epic" [label="no"];
"Find epic" -> "Break into sub-issues";
"Create epic" -> "Break into sub-issues";
"Break into sub-issues" -> "Add to board as Backlog";
"Add to board as Backlog" -> "Prioritize";
"Prioritize" -> "Pick up (Doing)";
"Pick up (Doing)" -> "Review";
"Review" -> "Done";
}
Project Discovery
Before any board operation, discover the project structure:
gh project list --owner <OWNER>
gh project field-list <PROJECT_NUMBER> --owner <OWNER> --format json
gh project item-list <PROJECT_NUMBER> --owner <OWNER> --format json
Kanban Reference
The live IDs for this project live in board-config.md alongside this skill — populated by setup.sh on first run. The skills read column option IDs from there. If you haven't run setup yet, do that before invoking any board operation.
The workflow expects six logical column states (your actual column names can be anything; setup maps your names to these terms):
| Skill term | What it means |
|---|
| Epics | Top-level epic issues that group related sub-issues |
| Backlog | Planned work, not yet prioritized |
| Prioritized | Next up — ordered by priority |
| Doing | Actively being worked on |
| Review | PR raised, under review / QA |
| Done | Merged |
Extra columns (e.g. Icebox before Backlog, Live after Done) are fine — the skills just won't drive transitions through them. Some teams also add Priority / Size / Epic single-select fields; the skills don't require them but will use them if board-config.md lists their IDs.
1. Epic Management
Find existing epic
gh project item-list <PROJECT_NUMBER> --owner <OWNER> --format json | \
python3 -c "import json,sys; d=json.load(sys.stdin, strict=False); [print(f'#{i[\"content\"][\"number\"]} — {i[\"title\"]} ({i[\"status\"]})') for i in d['items'] if i.get('status')=='Epics']"
Create new epic
gh issue create --repo <OWNER>/<REPO> \
--title "Enable users to [capability]" \
--label "epic,[domain-label]" \
--body "$(cat <<'EOF'
# [Epic Title]
[1-2 sentence summary of the user-facing outcome]
---
## What's Been Built
[Section for completed work — update as sub-issues move to Done]
## What's Next
[Remaining work — drives sub-issue creation]
## Key Files
| File | Role |
|------|------|
| ... | ... |
EOF
)"
Then add to project as Epic:
item_id=$(gh project item-add <PROJECT_NUMBER> --owner <OWNER> \
--url "https://github.com/<OWNER>/<REPO>/issues/ISSUE_NUM" \
--format json | python3 -c "import json,sys; print(json.load(sys.stdin, strict=False)['id'])")
gh project item-edit --project-id <PROJECT_ID> \
--id "$item_id" \
--field-id <STATUS_FIELD_ID> \
--single-select-option-id <EPICS_OPTION_ID>
Epic description style
- Title: "Enable users to [do something]" — user-facing capability
- What's Been Built: Updated as sub-issues complete — tracks progress
- What's Next: Drives sub-issue creation — each bullet becomes a ticket
- Key Files: Map of important files for the epic's domain
2. Sub-Issue Creation
Writing sub-issue descriptions
Write as deliverables, not retrospectives. Describe what will be delivered, even for completed work. This keeps tickets useful as reference for anyone picking them up or reviewing them later.
## Overview
[What this delivers and why — 1-2 sentences]
## Deliverables
[Specific outcomes — tables, bullet lists, not prose]
## Why
[Business/UX justification — 1 sentence]
## Key Files
[Files that will be created or modified]
## Depends on
[Other sub-issue numbers, if any]
## Parent
Part of #EPIC_NUMBER
Create and link
gh issue create --repo <OWNER>/<REPO> \
--title "Verb phrase describing deliverable" \
--label "domain-label,enhancement" \
--body "..."
numeric_id=$(gh api "repos/<OWNER>/<REPO>/issues/ISSUE_NUM" --jq '.id')
gh api "repos/<OWNER>/<REPO>/issues/EPIC_NUM/sub_issues" \
--method POST \
-F "sub_issue_id=${numeric_id}"
gh api graphql -f query='mutation{addSubIssue(input:{issueId:"<EPIC_NODE_ID>",subIssueId:"<SUB_NODE_ID>"}){subIssue{number}}}'
gh api graphql -f query='query{repository(owner:"<O>",name:"<R>"){issue(number:ISSUE_NUM){parent{number}}}}'
Hard rule: every follow-up ticket created during a code review or iteration must end Phase 5 with a verified parent link to its epic. Without it, the epic can't auto-close from sub-issue progress, board epic-grouping is empty, and future agents reading the orphan in isolation lose the trail. Title-only references ("#449 follow-up") do not count.
Labels
Create domain labels if they don't exist:
gh label create "domain-name" \
--description "Short description" \
--color "1D76DB" \
--repo <OWNER>/<REPO>
Every sub-issue gets: domain label (e.g. auth, billing, onboarding) + type label (e.g. enhancement, bug, chore).
3. Board Operations
Add to project and set fields
item_id=$(gh project item-add <PROJECT_NUMBER> --owner <OWNER> \
--url "https://github.com/<OWNER>/<REPO>/issues/NUM" \
--format json | python3 -c "import json,sys; print(json.load(sys.stdin, strict=False)['id'])")
gh project item-edit --project-id <PROJECT_ID> \
--id "$item_id" \
--field-id <STATUS_FIELD_ID> \
--single-select-option-id OPTION_ID
gh project item-edit --project-id <PROJECT_ID> \
--id "$item_id" \
--field-id <PRIORITY_FIELD_ID> \
--single-select-option-id PRIORITY_OPTION_ID
gh project item-edit --project-id <PROJECT_ID> \
--id "$item_id" \
--field-id <SIZE_FIELD_ID> \
--single-select-option-id SIZE_OPTION_ID
Move status (find item ID first)
gh project item-list <PROJECT_NUMBER> --owner <OWNER> --format json | \
python3 -c "import json,sys; d=json.load(sys.stdin, strict=False); [print(i['id']) for i in d['items'] if i['content'].get('number')==ISSUE_NUM]"
gh project item-edit --project-id <PROJECT_ID> \
--id "ITEM_ID" \
--field-id <STATUS_FIELD_ID> \
--single-select-option-id NEW_STATUS_OPTION_ID
Close completed issues
gh issue close ISSUE_NUM --repo <OWNER>/<REPO> --reason completed
4. Context Gathering (Before Picking Up Work)
Before starting any ticket, understand its place in the bigger picture.
digraph context {
rankdir=LR;
"Read your ticket" [shape=box];
"Read parent epic" [shape=box];
"Read sibling sub-issues" [shape=box];
"Check cross-epic connections" [shape=box];
"Move to Doing" [shape=box];
"Start work" [shape=doublecircle];
"Read your ticket" -> "Read parent epic";
"Read parent epic" -> "Read sibling sub-issues";
"Read sibling sub-issues" -> "Check cross-epic connections";
"Check cross-epic connections" -> "Move to Doing";
"Move to Doing" -> "Start work";
}
Read your ticket
gh issue view ISSUE_NUM --repo <OWNER>/<REPO>
Understand: deliverables, dependencies (Depends on), key files.
Read the parent epic
gh issue view EPIC_NUM --repo <OWNER>/<REPO>
Understand: the user-facing outcome, what's been built, what's next, how your ticket fits.
Read sibling sub-issues
gh api "repos/<OWNER>/<REPO>/issues/EPIC_NUM/sub_issues" | \
python3 -c "import json,sys; d=json.load(sys.stdin, strict=False); [print(f'#{i[\"number\"]} — {i[\"title\"]} ({i[\"state\"]})') for i in d]"
Understand: what's already done (learn from it, don't duplicate), what's in progress (coordinate), what's blocked on you.
Check cross-epic connections
Epics don't exist in isolation. Look for overlapping concerns:
gh project item-list <PROJECT_NUMBER> --owner <OWNER> --format json | \
python3 -c "import json,sys; d=json.load(sys.stdin, strict=False); [print(f'#{i[\"content\"][\"number\"]} — {i[\"title\"]}') for i in d['items'] if i.get('status')=='Epics']"
gh issue view RELATED_EPIC_NUM --repo <OWNER>/<REPO>
5. Completing Work
When a ticket moves through the workflow:
| Transition | Actions |
|---|
| → Doing | Assign the issue to the picking-up user (see below) — MANDATORY, move ticket on board, create feature branch |
| → Review | PR raised, CI green, move ticket on board, link PR to issue |
| → Done | PR merged, move ticket on board, close issue, add PR reference as comment |
Assigning the picker — non-negotiable
Every ticket moved to Doing must be assigned to the GitHub user picking it up, in the same step. This is how multiple collaborators avoid treading on each other's toes — an unassigned "Doing" ticket is invisible to teammates and leads to duplicate work.
Get the currently authenticated GitHub user and assign the issue:
GH_USER=$(gh api user --jq .login)
gh issue edit ISSUE_NUM --repo <OWNER>/<REPO> --add-assignee "$GH_USER"
Rules:
- Use the authenticated
gh user — never assume a name. Different machines / sessions = different users.
- Assign BEFORE moving the status to Doing, so the board never shows an unassigned "Doing" card even briefly.
- If a ticket is already assigned to someone else and you weren't asked to take it over, STOP and ask the user — that person may already be working on it. Don't reassign silently.
- If picking up an existing PR's review feedback, the assignee stays whoever opened the PR. Don't reassign on Phase 4 fix loops.
- This applies whether the ticket is picked up manually, via Ticket Lifecycle, or any other workflow.
Add PR reference to completed ticket
gh issue comment ISSUE_NUM --repo <OWNER>/<REPO> \
--body "Delivered in PR #PR_NUM."
Update epic progress
After sub-issues complete, update the epic body to reflect progress. Move items from "What's Next" to "What's Been Built".
Common Mistakes
| Mistake | Fix |
|---|
| Sub-issue not linked to epic | Always use the sub_issues API after creation |
| Labels missing from sub-issue | Every ticket gets domain label + type label |
| Description written as retrospective | Write as deliverable — "what we plan to deliver" not "what we did" |
| Picking up work without reading epic | Always read epic + siblings first for context |
| Forgetting to move ticket on board | Update status at every transition |
| Creating issues without adding to project | Issue creation and project addition are separate steps |
json.JSONDecodeError: Invalid control character | Always use json.load(sys.stdin, strict=False) — issue bodies contain tabs/control chars |