skills/troykelly/claude-skills/project-board-enforcement

project-board-enforcement

SKILL.md

Project Board Enforcement

Overview

The GitHub Project board is THE source of truth for all work state. Not labels. Not comments. Not memory. The project board.

Core principle: If it's not in the project board with correct fields, it doesn't exist.

This skill is called by other skills at gate points. It is not invoked directly.

API Optimization Requirement

CRITICAL: All read operations MUST use cached data from github-api-cache.

The following environment variables MUST be set before using this skill:

  • GH_CACHE_ITEMS - Cached project items JSON
  • GH_CACHE_FIELDS - Cached project fields JSON
  • GH_PROJECT_ID - Project node ID
  • GH_STATUS_FIELD_ID - Status field ID
  • GH_STATUS_*_ID - Status option IDs

If these are not set, invoke session-start first to initialize the cache.

The Rule

Every issue, epic, and initiative MUST be in the project board BEFORE work begins.

This is not optional. This is not a suggestion. This is a hard gate.

Required Environment

# These MUST be set. Work cannot proceed without them.
echo $GITHUB_PROJECT      # Full URL: https://github.com/users/USER/projects/N
echo $GITHUB_PROJECT_NUM  # Just the number: N
echo $GH_PROJECT_OWNER    # Owner: @me or org name

If any are missing, stop and configure them before proceeding.

Project Field Requirements

Mandatory Fields

Every project MUST have these fields configured:

Field Type Required Values
Status Single select Backlog, Ready, In Progress, In Review, Done, Blocked
Type Single select Feature, Bug, Chore, Research, Spike, Epic, Initiative
Priority Single select Critical, High, Medium, Low

Recommended Fields

Field Type Purpose
Verification Single select Not Verified, Failing, Partial, Passing
Criteria Met Number Count of completed acceptance criteria
Criteria Total Number Total acceptance criteria
Last Verified Date When verification last ran
Epic Text Parent epic issue number
Initiative Text Parent initiative issue number

Verification Functions

All read operations use cached data (0 API calls). Only writes require API calls.

Verify Issue in Project

GATE FUNCTION - Called before any work begins. 0 API calls (uses cache).

verify_issue_in_project() {
  local issue=$1

  # Get project item ID FROM CACHE (0 API calls)
  ITEM_ID=$(echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.content.number == $issue) | .id")

  if [ -z "$ITEM_ID" ] || [ "$ITEM_ID" = "null" ]; then
    echo "BLOCKED: Issue #$issue is not in the project board."
    echo ""
    echo "Add it with:"
    echo "  gh project item-add $GITHUB_PROJECT_NUM --owner $GH_PROJECT_OWNER --url \$(gh issue view $issue --json url -q .url)"
    return 1
  fi

  echo "$ITEM_ID"
  return 0
}

Verify Status Field Set

GATE FUNCTION - Called before work proceeds past issue check. 0 API calls (uses cache).

verify_status_set() {
  local issue=$1
  local item_id=$2

  # Get current status FROM CACHE (0 API calls)
  STATUS=$(echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.id == \"$item_id\") | .status.name")

  if [ -z "$STATUS" ] || [ "$STATUS" = "null" ]; then
    echo "BLOCKED: Issue #$issue has no Status set in project board."
    echo ""
    echo "Set status before proceeding."
    return 1
  fi

  echo "$STATUS"
  return 0
}

Add Issue to Project

Called by issue-prerequisite after issue creation. 1 API call + cache refresh.

add_issue_to_project() {
  local issue_url=$1

  # Add to project (1 API call - unavoidable write)
  gh project item-add "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" --url "$issue_url"

  if [ $? -ne 0 ]; then
    echo "ERROR: Failed to add issue to project."
    return 1
  fi

  # Refresh cache after adding (1 API call)
  export GH_CACHE_ITEMS=$(gh project item-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" --format json)

  # Get the item ID from refreshed cache
  local issue_num=$(echo "$issue_url" | grep -oE '[0-9]+$')
  ITEM_ID=$(echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.content.number == $issue_num) | .id")

  echo "$ITEM_ID"
  return 0
}

Set Project Status

Called at every status transition. 1 API call (uses cached IDs).

set_project_status() {
  local item_id=$1
  local new_status=$2  # Backlog, Ready, In Progress, In Review, Done, Blocked

  # Use cached IDs (0 API calls for lookups)
  # GH_PROJECT_ID, GH_STATUS_FIELD_ID set by session-start

  # Get option ID from cache
  local option_id
  case "$new_status" in
    "Backlog")     option_id="$GH_STATUS_BACKLOG_ID" ;;
    "Ready")       option_id="$GH_STATUS_READY_ID" ;;
    "In Progress") option_id="$GH_STATUS_IN_PROGRESS_ID" ;;
    "In Review")   option_id="$GH_STATUS_IN_REVIEW_ID" ;;
    "Done")        option_id="$GH_STATUS_DONE_ID" ;;
    "Blocked")     option_id="$GH_STATUS_BLOCKED_ID" ;;
    *)
      # Fallback: look up from cached fields (0 API calls)
      option_id=$(echo "$GH_CACHE_FIELDS" | jq -r ".fields[] | select(.name == \"Status\") | .options[] | select(.name == \"$new_status\") | .id")
      ;;
  esac

  if [ -z "$option_id" ] || [ "$option_id" = "null" ]; then
    echo "ERROR: Status '$new_status' not found in project."
    return 1
  fi

  # Single API call to update status
  gh project item-edit --project-id "$GH_PROJECT_ID" --id "$item_id" \
    --field-id "$GH_STATUS_FIELD_ID" --single-select-option-id "$option_id"

  return $?
}

Set Project Type

Called when creating issues. 1 API call (uses cached IDs).

set_project_type() {
  local item_id=$1
  local type=$2  # Feature, Bug, Chore, Research, Spike, Epic, Initiative

  # Get type field ID and option from cache (0 API calls)
  local type_field_id=$(echo "$GH_CACHE_FIELDS" | jq -r '.fields[] | select(.name == "Type") | .id')
  local option_id=$(echo "$GH_CACHE_FIELDS" | jq -r ".fields[] | select(.name == \"Type\") | .options[] | select(.name == \"$type\") | .id")

  if [ -z "$option_id" ] || [ "$option_id" = "null" ]; then
    echo "ERROR: Type '$type' not found in project."
    return 1
  fi

  # Single API call to update type
  gh project item-edit --project-id "$GH_PROJECT_ID" --id "$item_id" \
    --field-id "$type_field_id" --single-select-option-id "$option_id"
}

State Queries via Project Board

All queries use cached data. 0 API calls.

Get Issues by Status

USE THIS instead of label queries. 0 API calls (uses cache).

get_issues_by_status() {
  local status=$1  # Ready, In Progress, etc.

  # Use cached data (0 API calls)
  echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.status.name == \"$status\") | .content.number"
}

# Examples:
# get_issues_by_status "Ready"
# get_issues_by_status "In Progress"
# get_issues_by_status "Blocked"

Get Issues by Type

0 API calls (uses cache).

get_issues_by_type() {
  local type=$1  # Epic, Feature, etc.

  echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.type.name == \"$type\") | .content.number"
}

Get Epic Children

0 API calls (uses cache).

get_epic_children() {
  local epic_num=$1

  echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.epic == \"#$epic_num\") | .content.number"
}

Count by Status

0 API calls (uses cache).

count_by_status() {
  local status=$1

  echo "$GH_CACHE_ITEMS" | jq "[.items[] | select(.status.name == \"$status\")] | length"
}

Gate Points

These are the points in workflows where project board verification is MANDATORY:

Workflow Point Gate Skill
Before any work Issue in project issue-driven-development Step 1
After issue creation Add to project, set fields issue-prerequisite
Starting work Status → In Progress issue-driven-development Step 6
Creating branch Verify project membership branch-discipline
PR created Status → In Review pr-creation
Work complete Status → Done issue-driven-development completion
Blocked Status → Blocked error-recovery
Epic created Add epic to project, set Type=Epic epic-management
Child issue created Add to project, link to parent issue-decomposition

Transition Rules

Valid transitions:

Backlog → Ready → In Progress → In Review → Done
   ↓        ↓          ↓            ↓
   └────────┴──────────┴────────────┴──→ Blocked
                                    (return to previous)

Transition Enforcement

validate_transition() {
  local current=$1
  local target=$2

  case "$current$target" in
    "Backlog→Ready"|"Ready→In Progress"|"In Progress→In Review"|"In Review→Done")
      return 0 ;;
    *"→Blocked")
      return 0 ;;
    "Blocked→Backlog"|"Blocked→Ready"|"Blocked→In Progress")
      return 0 ;;
    *)
      echo "INVALID_TRANSITION: $current$target"
      return 1 ;;
  esac
}

Labels vs Project Board

WRONG: Using labels for state (status:in-progress) RIGHT: Using project board Status field

Labels are only for supplementary info: epic, epic-[name], spawned-from:#N, review-finding

Sync Verification

Detect drift by comparing git branches to project board status:

  • Issues with branches should be In Progress or In Review
  • In Progress issues should have active branches

Use cached data (GH_CACHE_ITEMS) for 0 API calls. Example:

# Check if branch status matches project board
status=$(echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.content.number == $issue) | .status.name")

Error Messages

All project board errors provide actionable fixes:

Error Code Message Fix
NOT_IN_PROJECT Issue not in project board gh project item-add ...
NO_STATUS Status field not set Update Status field
INVALID_TRANSITION Invalid state change Use valid transition
PROJECT_NOT_FOUND Project not accessible Verify GITHUB_PROJECT_NUM

Integration

This skill is called by:

  • issue-driven-development - All status transitions
  • issue-prerequisite - After issue creation
  • epic-management - Epic and child issue setup
  • autonomous-orchestration - State queries and updates
  • session-start - Sync verification
  • work-intake - Project readiness check

This skill requires cache from:

  • github-api-cache - Provides GH_CACHE_ITEMS, GH_CACHE_FIELDS, and field IDs

Checklist for Callers

Before proceeding past any gate:

  • GitHub API cache initialized (GH_CACHE_ITEMS, GH_CACHE_FIELDS set)
  • Issue exists in project (verified from cache, not API call)
  • Status field is set
  • Type field is set
  • Priority field is set (for new issues)
  • Epic linkage set (if child of epic)
  • Transition is valid (if changing status)

API Cost Summary

Operation Before Caching After Caching
verify_issue_in_project 1 call 0 calls
verify_status_set 1 call 0 calls
add_issue_to_project 2 calls 2 calls
set_project_status 4 calls 1 call
set_project_type 3 calls 1 call
get_issues_by_status 1 call 0 calls
count_by_status 1 call 0 calls
verify_project_sync (10 branches) 10 calls 0 calls
Weekly Installs
13
GitHub Stars
6
First Seen
Jan 24, 2026
Installed on
gemini-cli8
codex7
cursor7
claude-code6
antigravity6
opencode6