agentsclimarketplace

Github project management

Skill dustinkeeton/wafflestack/stacks/github-workflow/skills/github-project-management

GitHub project management operations using gh CLI and GraphQL. Covers Projects v2 boards, milestones, issue relationships, labels, status reporting, and sprint planning. Used by the project-manager agent to coordinate work.From its SKILL.md

Install
npx -y skills add dustinkeeton/wafflestack --skill github-project-management

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
  • runs commandsInstructs the agent to run 8 commands, including `gh repo view --json owner,name` and 7 more.

SKILL.md

15.1 KB, ~4.1k tokens by cl100k_base, as published. Nobody here has run it

GitHub Project Management

Operations for managing GitHub Projects v2, milestones, labels, issue relationships, and sprint planning using the gh CLI. Projects v2 requires GraphQL via gh api graphql since the REST API has limited support.

Prerequisites

  • gh CLI authenticated with appropriate scopes (project, repo)
  • Repository owner and name available via gh repo view --json owner,name

Projects v2 Operations

Discover the project

Find the project ID (required for all subsequent operations):

# List projects for the repo owner
gh api graphql -f query='
  query($owner: String!) {
    user(login: $owner) {
      projectsV2(first: 20) {
        nodes { id number title }
      }
    }
  }
' -f owner="$(gh repo view --json owner -q .owner.login)"

For organization-owned repos, replace user with organization.

Get project fields

Retrieve custom field IDs (Status, Priority, Sprint, etc.):

gh api graphql -f query='
  query($projectId: ID!) {
    node(id: $projectId) {
      ... on ProjectV2 {
        fields(first: 50) {
          nodes {
            ... on ProjectV2Field { id name }
            ... on ProjectV2SingleSelectField {
              id name
              options { id name }
            }
            ... on ProjectV2IterationField {
              id name
              configuration {
                iterations { id title startDate duration }
              }
            }
          }
        }
      }
    }
  }
' -f projectId="PROJECT_ID"

Add an issue to the project

gh api graphql -f query='
  mutation($projectId: ID!, $contentId: ID!) {
    addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
      item { id }
    }
  }
' -f projectId="PROJECT_ID" -f contentId="ISSUE_NODE_ID"

Get the issue's node ID first:

gh api graphql -f query='
  query($owner: String!, $repo: String!, $number: Int!) {
    repository(owner: $owner, name: $repo) {
      issue(number: $number) { id }
    }
  }
' -f owner="OWNER" -f repo="REPO" -F number=42

Update item status (single-select field)

gh api graphql -f query='
  mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
    updateProjectV2ItemFieldValue(input: {
      projectId: $projectId
      itemId: $itemId
      fieldId: $fieldId
      value: {singleSelectOptionId: $optionId}
    }) {
      projectV2Item { id }
    }
  }
' -f projectId="PROJECT_ID" -f itemId="ITEM_ID" -f fieldId="STATUS_FIELD_ID" -f optionId="OPTION_ID"

Update item date field (Start / Target)

gh api graphql -f query='
  mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $date: Date!) {
    updateProjectV2ItemFieldValue(input: {
      projectId: $projectId
      itemId: $itemId
      fieldId: $fieldId
      value: {date: $date}
    }) {
      projectV2Item { id }
    }
  }
' -f projectId="PROJECT_ID" -f itemId="ITEM_ID" -f fieldId="DATE_FIELD_ID" -f date="2026-04-01"

Batch-update dates using aliased mutations

Update multiple items in a single API call using GraphQL aliases. Up to ~20 operations per call:

gh api graphql -f query='
  mutation {
    s1: updateProjectV2ItemFieldValue(input: {
      projectId: "PROJECT_ID", itemId: "ITEM_1", fieldId: "START_FIELD_ID",
      value: {date: "2026-03-20"}
    }) { projectV2Item { id } }
    t1: updateProjectV2ItemFieldValue(input: {
      projectId: "PROJECT_ID", itemId: "ITEM_1", fieldId: "TARGET_FIELD_ID",
      value: {date: "2026-03-25"}
    }) { projectV2Item { id } }
    s2: updateProjectV2ItemFieldValue(input: {
      projectId: "PROJECT_ID", itemId: "ITEM_2", fieldId: "START_FIELD_ID",
      value: {date: "2026-03-22"}
    }) { projectV2Item { id } }
    t2: updateProjectV2ItemFieldValue(input: {
      projectId: "PROJECT_ID", itemId: "ITEM_2", fieldId: "TARGET_FIELD_ID",
      value: {date: "2026-03-28"}
    }) { projectV2Item { id } }
  }
'

Query items with date fields

gh api graphql -f query='
  query($projectId: ID!) {
    node(id: $projectId) {
      ... on ProjectV2 {
        items(first: 100) {
          nodes {
            id
            content { ... on Issue { number title } }
            fieldValues(first: 20) {
              nodes {
                ... on ProjectV2ItemFieldDateValue {
                  field { ... on ProjectV2Field { name } }
                  date
                }
                ... on ProjectV2ItemFieldSingleSelectValue {
                  field { ... on ProjectV2SingleSelectField { name } }
                  name
                }
              }
            }
          }
        }
      }
    }
  }
' -f projectId="PROJECT_ID"

Clear a date field

Set the date value to null (empty string) to clear it:

gh api graphql -f query='
  mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!) {
    clearProjectV2ItemFieldValue(input: {
      projectId: $projectId
      itemId: $itemId
      fieldId: $fieldId
    }) {
      projectV2Item { id }
    }
  }
' -f projectId="PROJECT_ID" -f itemId="ITEM_ID" -f fieldId="DATE_FIELD_ID"

Predict dates for items

When assigning predicted Start and Target dates, use these heuristics:

  1. Done items — use actual createdAt as Start and closedAt as Target (from gh issue list --state closed --json number,createdAt,closedAt)
  2. In Review / In Progress — Start = when work began (estimate from PR open date or status change), Target = 3-7 days from now depending on size
  3. Todo — Start = next available slot after current In Progress items finish
  4. Backlog — schedule within the milestone window:
    • Priority order: Critical → High → Medium → Low → unset
    • Size → duration: S = 2-3 days, M = 5 days, L = 10 days, unset = 5 days
    • Dependencies: items that block others go first
    • Parallel capacity: assume 2-3 items can be worked in parallel
  5. No milestone — schedule after all milestoned work

Update item iteration (sprint)

gh api graphql -f query='
  mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $iterationId: String!) {
    updateProjectV2ItemFieldValue(input: {
      projectId: $projectId
      itemId: $itemId
      fieldId: $fieldId
      value: {iterationId: $iterationId}
    }) {
      projectV2Item { id }
    }
  }
' -f projectId="PROJECT_ID" -f itemId="ITEM_ID" -f fieldId="ITERATION_FIELD_ID" -f iterationId="ITERATION_ID"

Query project items with status

gh api graphql -f query='
  query($projectId: ID!) {
    node(id: $projectId) {
      ... on ProjectV2 {
        items(first: 100) {
          nodes {
            id
            content {
              ... on Issue { number title state }
              ... on PullRequest { number title state }
            }
            fieldValues(first: 20) {
              nodes {
                ... on ProjectV2ItemFieldSingleSelectValue {
                  field { ... on ProjectV2SingleSelectField { name } }
                  name
                }
                ... on ProjectV2ItemFieldIterationValue {
                  field { ... on ProjectV2IterationField { name } }
                  title
                }
              }
            }
          }
        }
      }
    }
  }
' -f projectId="PROJECT_ID"

Remove an item from the project

gh api graphql -f query='
  mutation($projectId: ID!, $itemId: ID!) {
    deleteProjectV2Item(input: {projectId: $projectId, itemId: $itemId}) {
      deletedItemId
    }
  }
' -f projectId="PROJECT_ID" -f itemId="ITEM_ID"

Milestones

Milestones use the REST API via gh api.

Create a milestone

gh api repos/{owner}/{repo}/milestones \
  -f title="v0.2.0" \
  -f description="Audio transcription and elaboration improvements" \
  -f due_on="2026-04-01T00:00:00Z" \
  -f state="open"

List milestones

gh api repos/{owner}/{repo}/milestones --jq '.[] | {number, title, open_issues, closed_issues, due_on, state}'

Update a milestone

gh api repos/{owner}/{repo}/milestones/{milestone_number} \
  -X PATCH \
  -f title="v0.2.0 - Revised" \
  -f state="open"

Close a milestone

gh api repos/{owner}/{repo}/milestones/{milestone_number} \
  -X PATCH \
  -f state="closed"

Assign an issue to a milestone

gh api repos/{owner}/{repo}/issues/{issue_number} \
  -X PATCH \
  -F milestone={milestone_number}

Get milestone progress

gh api repos/{owner}/{repo}/milestones/{milestone_number} \
  --jq '{title, open_issues, closed_issues, progress: ((.closed_issues / ((.open_issues + .closed_issues) | if . == 0 then 1 else . end)) * 100 | floor | tostring + "%")}'

Issue Relationships

Task lists (parent/child)

Create parent-child relationships using task lists in the issue body. Edit an issue body to include tracked tasks:

gh issue edit {parent_number} --body "$(cat <<'EOF'
## Tasks

- [ ] #{child_1}
- [ ] #{child_2}
- [ ] #{child_3}
EOF
)"

GitHub automatically tracks completion of referenced issues in task lists.

Cross-references (dependencies)

Add dependency notes as comments:

gh issue comment {issue_number} --body "Blocked by #42. This issue should be worked on after #42 is resolved."

Link related issues

Reference related issues in comments or the issue body:

gh issue comment {issue_number} --body "Related to #15 and #23."

Query sub-issues of a parent

gh api graphql -f query='
  query($owner: String!, $repo: String!, $number: Int!) {
    repository(owner: $owner, name: $repo) {
      issue(number: $number) {
        trackedIssues(first: 50) {
          nodes { number title state }
        }
      }
    }
  }
' -f owner="OWNER" -f repo="REPO" -F number=10

Labels

Create a label

gh label create "sprint:current" --description "Items in the current sprint" --color "0E8A16"
gh label create "priority:high" --description "High priority item" --color "D93F0B"
gh label create "priority:medium" --description "Medium priority item" --color "FBCA04"
gh label create "priority:low" --description "Low priority item" --color "0075CA"

List labels

gh label list

Apply labels to an issue

gh issue edit {issue_number} --add-label "priority:high,sprint:current"

Remove labels from an issue

gh issue edit {issue_number} --remove-label "sprint:current"

Query issues by label

gh issue list --label "priority:high" --state open --json number,title,labels

Query issues by multiple labels

gh issue list --label "priority:high" --label "sprint:current" --state open --json number,title

Status Reporting

Sprint progress report

Generate a summary of all open issues grouped by status:

# Get all open issues with labels and milestone
gh issue list --state open --json number,title,labels,milestone,assignees --limit 100 | \
  jq -r 'group_by(.milestone.title // "No Milestone") | .[] |
    "## \(.[0].milestone.title // "No Milestone")\n" +
    (map("- #\(.number) \(.title) [\(.labels | map(.name) | join(", "))]") | join("\n")) + "\n"'

Milestone burndown summary

gh api repos/{owner}/{repo}/milestones --jq '
  .[] | select(.state == "open") |
  "### \(.title)\n" +
  "  Open: \(.open_issues) | Closed: \(.closed_issues) | " +
  "Progress: \((.closed_issues / ((.open_issues + .closed_issues) | if . == 0 then 1 else . end)) * 100 | floor)%\n" +
  "  Due: \(.due_on // "No due date")\n"'

Closed issues since last report

gh issue list --state closed --json number,title,closedAt,labels --limit 50 | \
  jq --arg since "2026-03-10T00:00:00Z" '
    [.[] | select(.closedAt > $since)] |
    map("- #\(.number) \(.title)") | join("\n")'

PR merge activity

gh pr list --state merged --json number,title,mergedAt --limit 20 | \
  jq -r '.[] | "- PR #\(.number) \(.title) (merged \(.mergedAt | split("T")[0]))"'

Sprint Planning

Move items into a sprint

Assign the current iteration to a batch of issues. First, get the iteration field and current iteration IDs (see "Get project fields" above), then loop:

# For each issue number in the sprint
for ISSUE in 10 12 15 18; do
  # Get issue node ID
  ISSUE_ID=$(gh api graphql -f query='
    query($owner: String!, $repo: String!, $number: Int!) {
      repository(owner: $owner, name: $repo) {
        issue(number: $number) { id }
      }
    }
  ' -f owner="OWNER" -f repo="REPO" -F number=$ISSUE --jq '.data.repository.issue.id')

  # Add to project if not already there
  ITEM_ID=$(gh api graphql -f query='
    mutation($projectId: ID!, $contentId: ID!) {
      addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
        item { id }
      }
    }
  ' -f projectId="PROJECT_ID" -f contentId="$ISSUE_ID" --jq '.data.addProjectV2ItemById.item.id')

  # Set iteration
  gh api graphql -f query='
    mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $iterationId: String!) {
      updateProjectV2ItemFieldValue(input: {
        projectId: $projectId
        itemId: $itemId
        fieldId: $fieldId
        value: {iterationId: $iterationId}
      }) {
        projectV2Item { id }
      }
    }
  ' -f projectId="PROJECT_ID" -f itemId="$ITEM_ID" -f fieldId="ITERATION_FIELD_ID" -f iterationId="CURRENT_ITERATION_ID"
done

Prioritization

Apply priority labels and sort within the project:

# Set priority labels on issues
gh issue edit 10 --add-label "priority:high"
gh issue edit 12 --add-label "priority:medium"
gh issue edit 15 --add-label "priority:low"

Capacity check

Count issues assigned per sprint to avoid overloading:

gh issue list --label "sprint:current" --state open --json number,title,assignees | \
  jq 'length as $total | "Sprint items: \($total)"'

Sprint review checklist

Generate a checklist of done/not-done items for sprint review:

gh issue list --label "sprint:current" --state all --json number,title,state | \
  jq -r '.[] |
    (if .state == "CLOSED" then "- [x]" else "- [ ]" end) +
    " #\(.number) \(.title)"'

Practical Patterns

Full workflow: create milestone, label issues, add to project

OWNER=$(gh repo view --json owner -q .owner.login)
REPO=$(gh repo view --json name -q .name)

# 1. Create milestone
gh api repos/$OWNER/$REPO/milestones -f title="v0.3.0" -f due_on="2026-05-01T00:00:00Z"

# 2. Get milestone number
MILESTONE=$(gh api repos/$OWNER/$REPO/milestones --jq '.[] | select(.title=="v0.3.0") | .number')

# 3. Assign issues and label them
for ISSUE in 20 21 22; do
  gh api repos/$OWNER/$REPO/issues/$ISSUE -X PATCH -F milestone=$MILESTONE
  gh issue edit $ISSUE --add-label "priority:medium"
done

Error handling

Always check for errors in GraphQL responses:

RESULT=$(gh api graphql -f query='...' 2>&1)
if echo "$RESULT" | jq -e '.errors' > /dev/null 2>&1; then
  echo "GraphQL error: $(echo "$RESULT" | jq -r '.errors[0].message')"
fi

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Gives 0 of the 12 instructions most operations skills give in ~4.1k tokens

Counted across 483 of the 484 authors here whose files we hold, read 2026-08-07

  • Collect monitoring data throughout the simulationin 14 of 483, across 6 files
  • Set the random seed for reproducibilityin 14 of 483, across 6 files
  • Validate simulations against analytical solutionsin 12 of 483, across 4 files
  • Clarify goals, constraints, and inputsin 11 of 483, across 2 files
  • Implement contract tests for integration pointsin 11 of 483, across 2 files
  • Implement strangler fig infrastructure with API gatewayin 11 of 483, across 2 files
  • Audit modernized components for security vulnerabilitiesin 11 of 483, across 2 files
  • Avoid Python blocking calls in processesin 10 of 483, across 3 files
  • Use resource context managers for automatic cleanupin 9 of 483, across 2 files
  • Maintain consistent time unitsin 9 of 483, across 2 files
  • Validate outcomes against success criteriain 8 of 483, across 1 file
  • Analyze the legacy codebase for technical debtin 8 of 483, across 1 file

Said here and by no other author read

  • use GraphQL for Projects v2
  • use REST API for milestones
  • use task lists for parent-child relationships
  • use comments for cross-references
  • batch update dates using GraphQL aliases
  • schedule backlog items by priority and size

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.