| name | project-board |
| description | Manage GitHub Project board โ issues, sprints, status tracking, acceptance verification, and release management. White-labeled template for any project. |
| user-invocable | true |
| allowed-tools | Bash, Read, Grep, Glob |
| argument-hint | [list|create|close|cancel|assign|sprint|sprint-plan|triage|board|move|verify] [options] |
| catalog_description | GitHub Project board โ issues, sprints, releases, and triage. |
Project Board โ GitHub Issue & Sprint Management
Manage GitHub Issues and Project board from Claude Code. Provides full lifecycle management from roadmap ideas through sprint execution to completion, including acceptance criteria verification.
Configuration
Set these values in your project's cognitive-core.conf or replace {{placeholders}} after installation:
CC_GITHUB_OWNER="owner"
CC_GITHUB_REPO="owner/repo"
CC_PROJECT_NUMBER=3
CC_PROJECT_ID="PVT_xxx"
CC_STATUS_FIELD_ID="PVTSSF_xxx"
CC_AREA_FIELD_ID="PVTSSF_xxx"
CC_SPRINT_FIELD_ID="PVTIF_xxx"
CC_BRANCH_AUTO_CREATE="false"
CC_BRANCH_AUTO_CHECKOUT="true"
CC_BRANCH_BASE="main"
CC_BRANCH_HOTFIX_BASE="main"
CC_BRANCH_DEFAULT_TYPE="feature"
CC_BRANCH_SLUG_MAX_LENGTH="40"
CC_BRANCH_LABEL_MAP="bug=fix|enhancement=feature|documentation=docs"
Project Guard โ Cross-Project Contamination Prevention
CRITICAL: Before ANY GraphQL mutation that references a projectId, verify it matches the configured CC_PROJECT_ID exactly. Users often have multiple GitHub projects and field IDs from the wrong project will silently add/move items to unrelated boards.
Validation rules:
- All
projectId values in mutations MUST equal CC_PROJECT_ID
- All field IDs (
fieldId) MUST belong to the configured project โ they typically contain a substring of the project ID
- When discovering field IDs via
gh project field-list, ALWAYS specify --owner CC_GITHUB_OWNER and the correct CC_PROJECT_NUMBER
- If a
gh project field-list response returns IDs that don't match the expected project ID substring, ABORT and report the mismatch
If wrong project is detected: Stop immediately and report: "Wrong project detected โ field IDs do not match configured project. Aborting to prevent cross-project contamination."
Board Structure
Status (Columns) โ Issue Lifecycle
Roadmap โ Backlog โ Todo โ In Progress โ To Be Tested โ Done
โ Canceled
| Column | Meaning | Sprint Required |
|---|
| Roadmap | Feature ideas and future enhancements | No |
| Backlog | Accepted work, ready for sprint planning | No |
| Todo | Committed to a sprint, not yet started | Yes |
| In Progress | Actively being developed | Yes |
| To Be Tested | Code complete, needs verification | Yes |
| Done | Verified and closed (terminal) | โ |
| Canceled | Abandoned or deferred (terminal) | โ |
Status Option IDs
Replace with your project's actual IDs after running setup.sh:
roadmap โ {{STATUS_ROADMAP_ID}}
backlog โ {{STATUS_BACKLOG_ID}}
todo โ {{STATUS_TODO_ID}}
progress โ {{STATUS_PROGRESS_ID}}
testing โ {{STATUS_TESTING_ID}}
done โ {{STATUS_DONE_ID}}
canceled โ {{STATUS_CANCELED_ID}}
Workflow Transition Rules
Based on Linear/Jira/Kanban best practices. The move command MUST enforce these rules.
Allowed Transitions Matrix
FROM โ TO Roadmap Backlog Todo In Progress To Be Tested Done Canceled
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Roadmap - โ โ - - - โ
Backlog โ - โ - - - โ
Todo - โ - โ - - โ
In Progress - - - - โ - โ
To Be Tested - - - โ* - โ โ
Done - - - โ* โ* - -
Canceled - โ* โ* - - - -
โ = Allowed | โ* = Allowed but warn (reopen/rework) | - = Blocked
Key Rules
- Forward flow is primary: Roadmap/Backlog โ Todo โ In Progress โ To Be Tested โ Done
- Backward transitions with warning: To Be Tested โ In Progress (rework), Done โ In Progress/To Be Tested (reopen), Canceled โ Backlog/Todo (reopen)
- Canceled reachable from anywhere except Done
- Reopen from Done/Canceled: Allowed with warning. The
move command automatically reopens the GitHub issue when moving out of Done or Canceled.
- No skipping: Cannot jump Backlog โ In Progress (must pass through Todo first)
- Reopen syncs GitHub state: When moving from Done or Canceled to an active column, the
move command automatically runs gh issue reopen to sync the GitHub issue state with the board status.
- Auto-sprint assignment: When moving to Todo, In Progress, or To Be Tested, the
move command automatically assigns the issue to the current sprint if it has no sprint set. Requires CC_SPRINT_FIELD_ID to be configured.
- Auto-assignee: Sprint items must have an owner. When moving to a sprint-required column and the issue has no assignee, auto-assign to the current user (initiator of the change).
CI Automation
The project-board-automation.yml workflow (in cicd/workflows/) handles:
- PR opened with
Closes #N โ issue moves to In Progress (from Todo only)
- PR merged โ issue moves to Done
- Issue assigned (from Backlog/Roadmap) โ moves to Todo
- New issue opened โ added to board in Backlog
- Issue reopened โ moves to In Progress
- Issue closed โ moves to Done
Area (Row Grouping)
Customizable per project. Default domains:
| Area | Scope | Option ID |
|---|
| CI/CD | Build pipeline, containers, deployment | {{AREA_CICD_ID}} |
| Monitoring | Metrics, alerting, dashboards | {{AREA_MONITORING_ID}} |
| Testing | Test framework, coverage, QA | {{AREA_TESTING_ID}} |
| Security | Access control, scanning, encryption | {{AREA_SECURITY_ID}} |
| Infrastructure | Servers, backup, networking | {{AREA_INFRASTRUCTURE_ID}} |
Sprint (Time-boxed Iterations)
- Default: 14-day iterations
- Issues assigned to sprints should be in Todo or later
- Use
sprint command to view current sprint progress
- Use
sprint-plan to assign issues to iterations
Labels
| Type | Values |
|---|
| Priority | priority:p0-critical, priority:p1-high, priority:p2-medium, priority:p3-low |
| Area | area:cicd, area:monitoring, area:testing, area:security, area:infrastructure |
| Kind | bug, enhancement, documentation |
Commands
Parse the user's arguments to determine which command to run. Default (no args) = list.
list (default)
List open issues grouped by priority.
gh issue list --repo {{CC_GITHUB_REPO}} --state open --label "priority:p0-critical" --json number,title,labels,assignees
gh issue list --repo {{CC_GITHUB_REPO}} --state open --label "priority:p1-high" --json number,title,labels,assignees
gh issue list --repo {{CC_GITHUB_REPO}} --state open --label "priority:p2-medium" --json number,title,labels,assignees
gh issue list --repo {{CC_GITHUB_REPO}} --state open --label "priority:p3-low" --json number,title,labels,assignees
Format as priority-grouped table:
## Open Issues
### P0 โ Critical
| # | Title | Area | Assignee |
|---|-------|------|----------|
### P1 โ High
...
Support --area=<area> filter (adds --label "area:<area>") and --state=closed (changes to --state closed --limit 10).
create
Syntax: /project-board create "title" [--priority p0|p1|p2|p3] [--area cicd|monitoring|testing|security|infrastructure] [--body "description"] [--plan <path>]
Map --priority pN to labels: p0โpriority:p0-critical, p1โpriority:p1-high, p2โpriority:p2-medium, p3โpriority:p3-low.
Map --area to label area:<value>.
- Create the GitHub issue with labels:
gh issue create --repo {{CC_GITHUB_REPO}} --title "<title>" --label "<labels>" --body "<body>"
- Add to project board and set Area field:
ISSUE_ID=$(gh issue view <number> --repo {{CC_GITHUB_REPO}} --json id --jq '.id')
ITEM_ID=$(gh api graphql -f query='mutation { addProjectV2ItemById(input: { projectId: "{{CC_PROJECT_ID}}" contentId: "'$ISSUE_ID'" }) { item { id } } }' --jq '.data.addProjectV2ItemById.item.id')
gh api graphql -f query='mutation { updateProjectV2ItemFieldValue(input: { projectId: "{{CC_PROJECT_ID}}" itemId: "'$ITEM_ID'" fieldId: "{{CC_AREA_FIELD_ID}}" value: { singleSelectOptionId: "<AREA_OPTION_ID>" } }) { projectV2Item { id } } }'
-
Default status: Backlog (unless --status specified)
-
Attach implementation plan (if --plan provided or CC_ISSUE_ATTACH_PLAN=true):
If --plan <path> is provided, read the file and post it as a comment on the newly created issue:
gh issue comment <number> --repo {{CC_GITHUB_REPO}} --body "$(cat <<'PLAN'
## Implementation Plan
$(cat <plan-path>)
---
*Attached by `/project-board create`. Source: `<plan-path>`*
PLAN
)"
If no --plan flag but CC_ISSUE_ATTACH_PLAN=true, check for an active plan file in ~/.claude/plans/. If exactly one .md file exists, attach it automatically. If multiple exist, skip (ambiguous).
close
Syntax: /project-board close <number> [number2 ...] [--comment "reason"]
Closure Guard: If the issue has acceptance criteria (checkbox list in body), run verification FIRST. NEVER close an issue that has PARTIAL or FAIL criteria. If any criteria are not PASS, block the close and report the gaps. This prevents premature closure that hides unfinished work.
- Check for acceptance criteria โ if present, verify all are PASS before proceeding
- Close the GitHub issue:
gh issue close <number> --repo {{CC_GITHUB_REPO}} --comment "<comment>"
- Update board status to Done:
ITEMS=$(gh project item-list {{CC_PROJECT_NUMBER}} --owner {{CC_GITHUB_OWNER}} --format json --limit 500)
ITEM_ID=$(echo "$ITEMS" | jq -r --argjson n <N> '.items[] | select(.content.number == $n) | .id')
gh api graphql -f query='mutation { updateProjectV2ItemFieldValue(input: { projectId: "{{CC_PROJECT_ID}}" itemId: "'$ITEM_ID'" fieldId: "{{CC_STATUS_FIELD_ID}}" value: { singleSelectOptionId: "{{STATUS_DONE_ID}}" } }) { projectV2Item { id } } }'
cancel
Cancel one or more issues. Moves to Canceled on the board. Requires a reason.
Syntax: /project-board cancel <number> [number2 ...] --reason "why"
- Check current status โ block if already Done (create new issue instead)
- Add comment with cancellation reason
- Close the issue
- Move to Canceled on the board
ITEMS=$(gh project item-list {{CC_PROJECT_NUMBER}} --owner {{CC_GITHUB_OWNER}} --format json --limit 500)
CURRENT=$(echo "$ITEMS" | jq -r --argjson n <N> '.items[] | select(.content.number == $n) | .status')
if [ "$CURRENT" = "Done" ]; then echo "Cannot cancel a Done issue. Create a new issue instead."; exit 1; fi
gh issue close <number> --repo {{CC_GITHUB_REPO}} --comment "Canceled: <reason>"
ITEM_ID=$(echo "$ITEMS" | jq -r --argjson n <N> '.items[] | select(.content.number == $n) | .id')
gh api graphql -f query='mutation { updateProjectV2ItemFieldValue(input: { projectId: "{{CC_PROJECT_ID}}" itemId: "'$ITEM_ID'" fieldId: "{{CC_STATUS_FIELD_ID}}" value: { singleSelectOptionId: "{{STATUS_CANCELED_ID}}" } }) { projectV2Item { id } } }'
assign
Syntax: /project-board assign <number> <username>
gh issue edit <number> --repo {{CC_GITHUB_REPO}} --add-assignee <username>
sprint
Show current sprint progress. Query the Sprint iteration field, filter items, group by status.
gh api graphql -f query='query {
user(login: "{{CC_GITHUB_OWNER}}") {
projectV2(number: {{CC_PROJECT_NUMBER}}) {
field(name: "Sprint") {
... on ProjectV2IterationField {
configuration { iterations { id title startDate duration } }
}
}
}
}
}'
gh project item-list {{CC_PROJECT_NUMBER}} --owner {{CC_GITHUB_OWNER}} --format json
Filter items matching current iteration. Group by status:
## Sprint: <title> (<date range>)
### In Progress
| # | Title | Area | Assignee |
### To Be Tested
| # | Title | Area | Pending |
### Done
| # | Title | Area |
### Not started
| # | Title | Area |
**Progress**: X/Y items done (Z%)
Support --all (show all sprints) and --backlog (include unassigned items).
sprint-plan
Syntax: /project-board sprint-plan "<sprint-title>" <issue-numbers...>
Example: /project-board sprint-plan "Sprint 2" 22 23 24
- Get the iteration ID for the sprint title:
gh api graphql -f query='query {
user(login: "{{CC_GITHUB_OWNER}}") {
projectV2(number: {{CC_PROJECT_NUMBER}}) {
field(name: "Sprint") {
... on ProjectV2IterationField {
configuration { iterations { id title startDate duration } }
}
}
}
}
}' --jq '.data.user.projectV2.field.configuration.iterations[] | select(.title == "<SPRINT_TITLE>") | .id'
-
If sprint doesn't exist, create a new iteration via updateProjectV2 mutation. New iterations get startDate = previous sprint endDate, same duration (14 days).
-
For each issue, get its project item ID and assign the sprint:
ITEM_ID=$(gh project item-list {{CC_PROJECT_NUMBER}} --owner {{CC_GITHUB_OWNER}} --format json --jq '.items[] | select(.content.number == <N>) | .id')
gh api graphql -f query='mutation { updateProjectV2ItemFieldValue(input: { projectId: "{{CC_PROJECT_ID}}" itemId: "'$ITEM_ID'" fieldId: "{{CC_SPRINT_FIELD_ID}}" value: { iterationId: "<ITERATION_ID>" } }) { projectV2Item { id } } }'
triage
Find issues without priority or area labels and suggest labels.
gh issue list --repo {{CC_GITHUB_REPO}} --state open --json number,title,labels,body
For each issue missing priority:* or area:* labels, analyze the title and body to suggest appropriate labels. Present suggestions for the user to confirm before applying.
board
Show the project board URL and a summary of items per column.
gh project item-list {{CC_PROJECT_NUMBER}} --owner {{CC_GITHUB_OWNER}} --format json
Output:
## Project Board
URL: https://github.com/users/{{CC_GITHUB_OWNER}}/projects/{{CC_PROJECT_NUMBER}}
| Column | Count |
|----------------|-------|
| Roadmap | N |
| Backlog | N |
| Todo | N |
| In Progress | N |
| To Be Tested | N |
| Done | N |
| Canceled | N |
move
Move an issue to a different board column. Enforces transition rules.
Syntax: /project-board move <number> <roadmap|backlog|todo|progress|testing|done|canceled>
Map column names to Status Option IDs and execute.
Before moving, check the transition is allowed:
ITEMS=$(gh project item-list {{CC_PROJECT_NUMBER}} --owner {{CC_GITHUB_OWNER}} --format json --limit 500)
CURRENT=$(echo "$ITEMS" | jq -r --argjson n <N> '.items[] | select(.content.number == $n) | .status')
TARGET="<target_status>"
ITEM_ID=$(echo "$ITEMS" | jq -r --argjson n <N> '.items[] | select(.content.number == $n) | .id')
gh api graphql -f query='mutation { updateProjectV2ItemFieldValue(input: { projectId: "{{CC_PROJECT_ID}}" itemId: "'$ITEM_ID'" fieldId: "{{CC_STATUS_FIELD_ID}}" value: { singleSelectOptionId: "<STATUS_OPTION_ID>" } }) { projectV2Item { id } } }'
if echo "todo progress testing" | grep -qw "$TARGET_KEY"; then
CURRENT_SPRINT=$(echo "$ITEMS" | jq -r --argjson n <N> '.items[] | select(.content.number == $n) | .sprint // empty')
if [ -z "$CURRENT_SPRINT" ] && [ -n "{{CC_SPRINT_FIELD_ID}}" ]; then
ITERATION_ID=$(gh api graphql -f query='query {
user(login: "{{CC_GITHUB_OWNER}}") {
projectV2(number: {{CC_PROJECT_NUMBER}}) {
field(name: "Sprint") {
... on ProjectV2IterationField {
configuration { iterations { id title startDate duration } }
}
}
}
}
}' --jq '[.data.user.projectV2.field.configuration.iterations[] | select((.startDate | strptime("%Y-%m-%d") | mktime) <= now and ((.startDate | strptime("%Y-%m-%d") | mktime) + (.duration * 86400)) > now)] | .[0].id')
if [ -n "$ITERATION_ID" ]; then
gh api graphql -f query='mutation { updateProjectV2ItemFieldValue(input: { projectId: "{{CC_PROJECT_ID}}" itemId: "'$ITEM_ID'" fieldId: "{{CC_SPRINT_FIELD_ID}}" value: { iterationId: "'$ITERATION_ID'" } }) { projectV2Item { id } } }'
echo "Auto-assigned to current sprint"
fi
fi
ASSIGNEES=$(gh issue view <N> --repo {{CC_GITHUB_REPO}} --json assignees --jq '.assignees | length')
if [ "$ASSIGNEES" = "0" ]; then
CURRENT_USER=$(gh api user --jq '.login')
gh issue edit <N> --repo {{CC_GITHUB_REPO}} --add-assignee "$CURRENT_USER"
echo "Auto-assigned to $CURRENT_USER"
fi
fi
if [ "$TARGET_KEY" = "progress" ] && [ "{{CC_BRANCH_AUTO_CREATE}}" = "true" ]; then
ISSUE_JSON=$(gh issue view <N> --repo {{CC_GITHUB_REPO}} --json title,labels)
ISSUE_TITLE=$(echo "$ISSUE_JSON" | jq -r '.title')
ISSUE_LABELS=$(echo "$ISSUE_JSON" | jq -r '[.labels[].name] | join(",")')
BRANCH_TYPE="{{CC_BRANCH_DEFAULT_TYPE}}"
IFS='|' read -ra LABEL_PAIRS <<< "{{CC_BRANCH_LABEL_MAP}}"
for pair in "${LABEL_PAIRS[@]}"; do
LABEL="${pair%%=*}"
TYPE="${pair##*=}"
if echo "$ISSUE_LABELS" | grep -q "$LABEL"; then
BRANCH_TYPE="$TYPE"
break
fi
done
BASE_BRANCH="{{CC_BRANCH_BASE}}"
if echo "$ISSUE_LABELS" | grep -q "priority:p0-critical" && [ "$BRANCH_TYPE" = "fix" ]; then
BRANCH_TYPE="hotfix"
BASE_BRANCH="{{CC_BRANCH_HOTFIX_BASE}}"
echo "P0 Critical โ creating hotfix branch from $BASE_BRANCH"
fi
SLUG=$(echo "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]' | \
sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//' | \
cut -c1-{{CC_BRANCH_SLUG_MAX_LENGTH}})
BRANCH_NAME="${BRANCH_TYPE}/<N>-${SLUG}"
EXISTING=$(gh issue develop <N> --repo {{CC_GITHUB_REPO}} --list 2>/dev/null | head -1)
if [ -n "$EXISTING" ]; then
echo "Branch already exists: $EXISTING"
if [ "{{CC_BRANCH_AUTO_CHECKOUT}}" = "true" ]; then
git fetch origin && git checkout "$EXISTING"
echo "Checked out existing branch: $EXISTING"
fi
else
CHECKOUT_FLAG=""
if [ "{{CC_BRANCH_AUTO_CHECKOUT}}" = "true" ]; then
CHECKOUT_FLAG="--checkout"
fi
gh issue develop <N> \
--repo {{CC_GITHUB_REPO}} \
--base "$BASE_BRANCH" \
--name "$BRANCH_NAME" \
$CHECKOUT_FLAG
echo "Created branch: $BRANCH_NAME (from $BASE_BRANCH)"
fi
fi
Branch Naming Convention
When CC_BRANCH_AUTO_CREATE="true", moving to In Progress auto-creates branches using gh issue develop:
<type>/<issue-number>-<kebab-case-slug>
| Type | When | Base Branch |
|---|
feature/ | enhancement label or default | CC_BRANCH_BASE |
fix/ | bug label | CC_BRANCH_BASE |
hotfix/ | bug + priority:p0-critical | CC_BRANCH_HOTFIX_BASE |
docs/ | documentation label | CC_BRANCH_BASE |
Label mapping is configurable via CC_BRANCH_LABEL_MAP (pipe-separated label=type pairs).
Hotfix workflow: After merging hotfix to production branch, also merge to development branch to keep branches in sync.
Disabling: Set CC_BRANCH_AUTO_CREATE="false" (default) to skip branch creation entirely.
verify
Verify acceptance criteria for an issue. Delegates to the acceptance-verification skill.
Syntax: /project-board verify <number> [--strict] [--dry-run]
This reads the issue's acceptance criteria, searches the codebase for evidence (commits, code, tests, docs), and posts a structured verification comment on the issue with PASS/PARTIAL/FAIL status per criterion.
See the acceptance-verification skill for full workflow details.
Error Handling
- If
gh commands fail with auth errors, suggest: gh auth refresh -h github.com -s project
- If an issue number doesn't exist, report it clearly
- Confirm destructive actions (close, cancel) when affecting more than 2 issues at once
- If a move is blocked by transition rules, explain WHY and show allowed targets
- CRITICAL: Wrong project guard โ Before every GraphQL mutation, verify
projectId matches CC_PROJECT_ID. If field IDs don't match the configured project, ABORT immediately. See "Project Guard" section above.
CI Automation
The project-board-automation.yml workflow requires a PROJECT_PAT repository secret (classic PAT with repo + project scopes). Without it, the automation jobs will fail silently.
Integration with Agents
The project-coordinator agent can invoke this skill for project planning workflows.
The solution-architect agent references the board for feature tracking.
The skill-updater agent can verify board status during sprint reviews.