# Project Board Enforcement

> MANDATORY for all work - the project board is THE source of truth. This skill provides verification functions and gates that other skills MUST call. No work proceeds without project board compliance.

- Skill: `majiayu000/project-board-enforcement-3` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add majiayu000/project-board-enforcement-3`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/project-board-enforcement-3/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/majiayu000/project-board-enforcement-3

---


# 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.**

## 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

```bash
# 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

### Verify Issue in Project

**GATE FUNCTION** - Called before any work begins.

```bash
verify_issue_in_project() {
  local issue=$1

  # Get project item ID
  ITEM_ID=$(gh project item-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" \
    --format json 2>/dev/null | \
    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.

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

  # Get current status
  STATUS=$(gh project item-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" \
    --format json 2>/dev/null | \
    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.**

```bash
add_issue_to_project() {
  local issue_url=$1

  # Add to project
  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

  # Get the item ID
  local issue_num=$(echo "$issue_url" | grep -oE '[0-9]+$')
  ITEM_ID=$(gh project item-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" \
    --format json | \
    jq -r ".items[] | select(.content.number == $issue_num) | .id")

  echo "$ITEM_ID"
  return 0
}
```

### Set Project Status

**Called at every status transition.**

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

  # Get project ID and field IDs (cache these in practice)
  PROJECT_ID=$(gh project list --owner "$GH_PROJECT_OWNER" --format json | \
    jq -r ".projects[] | select(.number == $GITHUB_PROJECT_NUM) | .id")

  STATUS_FIELD_ID=$(gh project field-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" \
    --format json | \
    jq -r '.fields[] | select(.name == "Status") | .id')

  OPTION_ID=$(gh project field-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" \
    --format json | \
    jq -r ".fields[] | select(.name == \"Status\") | .options[] | select(.name == \"$new_status\") | .id")

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

  gh project item-edit --project-id "$PROJECT_ID" --id "$item_id" \
    --field-id "$STATUS_FIELD_ID" --single-select-option-id "$OPTION_ID"

  return $?
}
```

### Set Project Type

**Called when creating issues.**

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

  PROJECT_ID=$(gh project list --owner "$GH_PROJECT_OWNER" --format json | \
    jq -r ".projects[] | select(.number == $GITHUB_PROJECT_NUM) | .id")

  TYPE_FIELD_ID=$(gh project field-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" \
    --format json | \
    jq -r '.fields[] | select(.name == "Type") | .id')

  OPTION_ID=$(gh project field-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" \
    --format json | \
    jq -r ".fields[] | select(.name == \"Type\") | .options[] | select(.name == \"$type\") | .id")

  gh project item-edit --project-id "$PROJECT_ID" --id "$item_id" \
    --field-id "$TYPE_FIELD_ID" --single-select-option-id "$OPTION_ID"
}
```

## State Queries via Project Board

### Get Issues by Status

**USE THIS instead of label queries.**

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

  gh project item-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" \
    --format json | \
    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

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

  gh project item-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" \
    --format json | \
    jq -r ".items[] | select(.type.name == \"$type\") | .content.number"
}
```

### Get Epic Children

```bash
get_epic_children() {
  local epic_num=$1

  gh project item-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" \
    --format json | \
    jq -r ".items[] | select(.epic == \"#$epic_num\") | .content.number"
}
```

### Count by Status

```bash
count_by_status() {
  local status=$1

  gh project item-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" \
    --format json | \
    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 Status Transitions

```
Backlog ──► Ready ──► In Progress ──► In Review ──► Done
    │         │            │              │
    │         │            │              │
    └─────────┴────────────┴──────────────┴──► Blocked
                                               │
                                               ▼
                                        (any previous state)
```

### Transition Enforcement

```bash
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 - Do Not Use Labels for State

```bash
# WRONG - labels are NOT the source of truth
gh issue list --label "status:pending"
gh issue edit 123 --add-label "status:in-progress"
```

### RIGHT - Use Project Board

```bash
# RIGHT - project board IS the source of truth
get_issues_by_status "Ready"
set_project_status "$ITEM_ID" "In Progress"
```

### When Labels Are Acceptable

Labels are still used for:
- `epic` - Identifying epic issues (supplementary)
- `epic-[name]` - Grouping issues in an epic (supplementary)
- `spawned-from:#N` - Lineage tracking (supplementary)
- `review-finding` - Origin tracking (supplementary)

But **state** (Ready, In Progress, Blocked, etc.) lives in the project board.

## Sync Verification

Run periodically to detect drift:

```bash
verify_project_sync() {
  echo "## Project Board Sync Check"
  echo ""

  # Check for issues with branches but Status != In Progress
  echo "### Issues with branches but not 'In Progress':"
  for branch in $(git branch -r | grep -E 'feature/[0-9]+' | sed 's/.*feature\///' | cut -d- -f1); do
    status=$(gh project item-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" \
      --format json | \
      jq -r ".items[] | select(.content.number == $branch) | .status.name")

    if [ "$status" != "In Progress" ] && [ "$status" != "In Review" ]; then
      echo "- #$branch: Status='$status' but has active branch"
    fi
  done

  # Check for In Progress issues with no recent activity
  echo ""
  echo "### 'In Progress' issues with no recent commits:"
  for issue in $(get_issues_by_status "In Progress"); do
    branch=$(git branch -r | grep -E "feature/$issue-" | head -1)
    if [ -z "$branch" ]; then
      echo "- #$issue: In Progress but no branch exists"
    fi
  done
}
```

## Error Messages

All project board errors should be clear and actionable:

```bash
project_error() {
  local code=$1
  local context=$2

  case "$code" in
    "NOT_IN_PROJECT")
      echo "BLOCKED: Issue $context is not in the project board."
      echo "Fix: gh project item-add $GITHUB_PROJECT_NUM --owner $GH_PROJECT_OWNER --url \$(gh issue view $context --json url -q .url)"
      ;;
    "NO_STATUS")
      echo "BLOCKED: Issue $context has no Status field set."
      echo "Fix: Update the issue's Status field in the project board."
      ;;
    "INVALID_TRANSITION")
      echo "BLOCKED: Cannot transition $context - invalid state change."
      ;;
    "PROJECT_NOT_FOUND")
      echo "BLOCKED: Project $GITHUB_PROJECT_NUM not found or not accessible."
      echo "Fix: Verify GITHUB_PROJECT_NUM and GH_PROJECT_OWNER are correct."
      ;;
  esac

  return 1
}
```

## 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

## Checklist for Callers

Before proceeding past any gate:

- [ ] Issue exists in project (verified, not assumed)
- [ ] 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)

