---
title: "Github Operations"
description: "GitHub CLI operations for issues, PRs, milestones, and Projects v2. Covers gh commands, REST API patterns, and automation scripts. Use when managing GitHub issues, PRs, milestones, or Projects with gh."
canonical: "https://orchestkit.yonyon.ai/docs/reference/skills/github-operations"
---

# Github Operations

GitHub CLI operations for issues, PRs, milestones, and Projects v2. Covers gh commands, REST API patterns, and automation scripts. Use when managing GitHub issues, PRs, milestones, or Projects with gh.

<span className="badge badge-gray">Reference</span> <span className="badge badge-yellow">medium</span>

> **Auto-activated** — this skill loads automatically when Claude detects matching context.

<ContextualSkillSidebar slug="github-operations" />

> **Github Operations** GitHub CLI operations for issues, PRs, milestones, and Projects v2. Covers gh commands, REST API patterns, and automation scripts. Use when managing GitHub issues, PRs, milestones, or Projects with gh.


# GitHub Operations

Comprehensive GitHub CLI (`gh`) operations for project management, from basic issue creation to advanced Projects v2 integration and milestone tracking via REST API.

## Overview

- Creating and managing GitHub issues and PRs
- Working with GitHub Projects v2 custom fields
- Managing milestones (sprints, releases) via REST API
- Automating bulk operations with `gh`
- Running GraphQL queries for complex operations

---

## CRITICAL: Task Management is MANDATORY (CC 2.1.16)

**BEFORE doing ANYTHING else, create tasks to track progress:**

```python
# 1. Create main task IMMEDIATELY
TaskCreate(
  subject="GitHub Operations: {target}",
  description="Managing GitHub issues, PRs, milestones, or Projects",
  activeForm="Managing GitHub resources"
)

# 2. Create subtasks matching the operation scope
TaskCreate(subject="Issue management", activeForm="Creating/updating issues")
TaskCreate(subject="PR management", activeForm="Managing pull requests")
TaskCreate(subject="Milestone tracking", activeForm="Updating milestones")

# 3. Set dependencies if operations are sequential
TaskUpdate(taskId="3", addBlockedBy=["2"])
TaskUpdate(taskId="4", addBlockedBy=["3"])

# 4. Update status as you progress
TaskUpdate(taskId="2", status="in_progress")  # When starting
TaskUpdate(taskId="2", status="completed")    # When done
```

## Quick Reference

### Issue Operations

```bash
# Create issue with labels and milestone
gh issue create --title "Bug: API returns 500" --body "..." --label "bug" --milestone "Sprint 5"

# List and filter issues
gh issue list --state open --label "backend" --assignee @me

# Edit issue metadata
gh issue edit 123 --add-label "high" --milestone "v2.0"
```

### PR Operations

```bash
# Create PR with reviewers
gh pr create --title "feat: Add search" --body "..." --base dev --reviewer @teammate

# Watch CI status and auto-merge
gh pr checks 456 --watch
gh pr merge 456 --auto --squash --delete-branch

# Resume a session linked to a PR (CC 2.1.27)
claude --from-pr 456           # Resume session with PR context (diff, comments, review status)
claude --from-pr https://github.com/org/repo/pull/456
```

> **Tip (CC 2.1.27):** Sessions created via `gh pr create` are automatically linked to the PR. Use `--from-pr` to resume with full PR context.

### Milestone Operations (REST API)

> **Footgun:** `gh issue edit --milestone` takes a **NAME** (string), not a number. The REST API uses a **NUMBER** (integer). Never pass a number to `--milestone`. Load `Read("$\{CLAUDE_PLUGIN_ROOT\}/skills/github-operations/references/cli-vs-api-identifiers.md")`.

```bash
# List milestones with progress
gh api repos/:owner/:repo/milestones --jq '.[] | "\(.title): \(.closed_issues)/\(.open_issues + .closed_issues)"'

# Create milestone with due date
gh api -X POST repos/:owner/:repo/milestones \
  -f title="Sprint 8" -f due_on="2026-02-15T00:00:00Z"

# Close milestone (API uses number, not name)
MILESTONE_NUM=$(gh api repos/:owner/:repo/milestones --jq '.[] | select(.title=="Sprint 8") | .number')
gh api -X PATCH repos/:owner/:repo/milestones/$MILESTONE_NUM -f state=closed

# Assign issues to milestone (CLI uses name, not number)
gh issue edit 123 124 125 --milestone "Sprint 8"
```

### Projects v2 Operations

```bash
# Add issue to project
gh project item-add 1 --owner @me --url https://github.com/org/repo/issues/123

# Set custom field (requires GraphQL)
gh api graphql -f query='mutation {...}' -f projectId="..." -f itemId="..."
```

---

## JSON Output Patterns

```bash
# Get issue numbers matching criteria
gh issue list --json number,labels --jq '[.[] | select(.labels[].name == "bug")] | .[].number'

# PR summary with author
gh pr list --json number,title,author --jq '.[] | "\(.number): \(.title) by \(.author.login)"'

# Find ready-to-merge PRs (statusCheckRollup is an ARRAY, so fold it first)
gh pr list --json number,reviewDecision,statusCheckRollup \
  --jq '[.[] | select(.reviewDecision == "APPROVED"
        and ([(.statusCheckRollup // [])[] | .conclusion // .state]
             | length > 0 and all(IN("SUCCESS","SKIPPED","NEUTRAL"))))]'
```

---

## Key Concepts

### Milestone vs Epic

| Milestones | Epics |
|------------|-------|
| Time-based (sprints, releases) | Topic-based (features) |
| Has due date | No due date |
| Progress bar | Task list checkbox |
| Native REST API | Needs workarounds |

**Rule**: Use milestones for "when", use parent issues for "what".

### Projects v2 Custom Fields

Projects v2 uses GraphQL for setting custom fields (Status, Priority, Domain). Basic `gh project` commands work for listing and adding items, but field updates require GraphQL mutations.

---

## Rules Quick Reference

| Rule | Impact | What It Covers |
|------|--------|----------------|
| issue-tracking-automation (load `$\{CLAUDE_PLUGIN_ROOT\}/skills/github-operations/rules/issue-tracking-automation.md`) | HIGH | Auto-progress from commits, sub-task completion, session summaries |
| issue-branch-linking (load `$\{CLAUDE_PLUGIN_ROOT\}/skills/github-operations/rules/issue-branch-linking.md`) | MEDIUM | Branch naming, commit references, PR linking patterns |

## Batch Issue Creation

When creating multiple issues at once (e.g., seeding a sprint), use an array-driven loop:

```bash
# Define issues as an array of "title|labels|milestone" entries
SPRINT="Sprint 9"
ISSUES=(
  "feat: Add user auth|enhancement,backend|$SPRINT"
  "fix: Login redirect loop|bug,high|$SPRINT"
  "chore: Update dependencies|maintenance|$SPRINT"
)

for entry in "${ISSUES[@]}"; do
  IFS='|' read -r title labels milestone <<< "$entry"
  NUM=$(gh issue create \
    --title "$title" \
    --label "$labels" \
    --milestone "$milestone" \
    --body "" \
    --json number --jq '.number')
  echo "Created #$NUM: $title"
done
```

> **Tip:** Capture the created issue number with `--json number --jq '.number'` so you can reference it immediately (e.g., add to Projects v2, link in PRs).

---

## Best Practices

1. **Always use `--json` for scripting** - Parse with `--jq` for reliability
2. **Non-interactive mode for automation** - Use `--title`, `--body` flags
3. **Check rate limits before bulk operations** - `gh api rate_limit`. On CC ≥ 2.1.116, the Bash tool surfaces a rate-limit hint in the transcript when `gh` hits 403 — **treat that hint as authoritative and back off**, don't blind-retry. Before 2.1.116, agents had no signal and would burn all retry attempts in ~13 s.
4. **Use heredocs for multi-line content** - `--body "$(cat &lt;&lt;'EOF'...EOF)"`
5. **Link issues in PRs** - `Closes #123`, `Fixes #456` — GitHub auto-closes on merge
6. **Use ISO 8601 dates** - `YYYY-MM-DDTHH:MM:SSZ` for milestone due_on
7. **Close milestones, don't delete** - Preserve history
8. **`--milestone` takes NAME, not number** - Load `Read("$\{CLAUDE_PLUGIN_ROOT\}/skills/github-operations/references/cli-vs-api-identifiers.md")`
9. **Never `gh issue close` directly** - Comment progress with `gh issue comment`; issues close only when their linked PR merges to the default branch

---

## 2026 CLI changes — what to know

### `gh-copilot` extension is retired

GitHub retired the `gh-copilot` extension in **October 2025**. Copilot is now a standalone binary:

```bash
# OLD — no longer supported
gh extension install github/gh-copilot   # fails
gh copilot suggest "revert last commit"   # fails

# NEW — standalone `copilot` binary
copilot suggest "revert last commit"
copilot explain "git rebase -i HEAD~5"
```

Install from `cli.github.com/copilot` or via Homebrew (`brew install github/gh/copilot`). Authentication is shared with `gh auth` when both are installed.

### `gh agent-task` (2026)

New subcommand for managing Copilot coding-agent tasks:

```bash
gh agent-task create --repo owner/repo --title "Fix flaky login test"
gh agent-task list --state open
gh agent-task view 42 --log          # stream agent log
gh agent-task watch 42                # live-follow until completion
gh agent-task cancel 42
```

Pairs with the REST endpoint `POST /repos/\{owner\}/\{repo\}/agent-tasks` for CI-driven task creation.

### Sub-issues (native, 2026)

Sub-issues are now a native GitHub concept — no extension required:

```bash
# List sub-issues of parent #123
gh api repos/{owner}/{repo}/issues/123/sub_issues

# Add an existing issue #456 as sub-issue of #123
gh api -X POST repos/{owner}/{repo}/issues/123/sub_issues \
  -f sub_issue_id=$(gh api repos/{owner}/{repo}/issues/456 --jq .node_id)

# Remove a sub-issue relationship
gh api -X DELETE repos/{owner}/{repo}/issues/123/sub_issue \
  -F sub_issue_id=<id>
```

The old `gh-sub-issue` third-party extension still works but is superseded. GraphQL sub-issue mutations still require the issue `node_id` (see `references/cli-vs-api-identifiers.md`).

---

## Related Skills

- `ork:create-pr` - Create pull requests with proper formatting and review assignments
- `ork:review-pr` - Comprehensive PR review with specialized agents
- `ork:release-management` - GitHub release workflow with semantic versioning and changelogs
- `ork:commit` - Stacked-PR workflow and rebase coordination live in `src/skills/commit/rules/` (`stacked-pr-workflow`, `stacked-pr-rebase`). There is no `stacked-prs` skill.
- `ork:issue-progress-tracking` - Automatic issue progress updates from commits

## Key Decisions

| Decision | Choice | Rationale |
|----------|--------|-----------|
| CLI vs API | gh CLI preferred | Simpler auth, better UX, handles pagination automatically |
| Output format | --json with --jq | Reliable parsing for automation, no regex parsing needed |
| Milestones vs Epics | Milestones for time | Milestones have due dates and progress bars, epics for topic grouping |
| Projects v2 fields | GraphQL mutations | gh project commands limited, GraphQL required for custom fields |
| Milestone lifecycle | Close, don't delete | Preserves history and progress tracking |

## Upstream coverage (do not restate)

These topics are documented first-party. Fetch them there instead of growing a copy
in this skill.

| Topic | Source |
|-------|--------|
| `gh issue` bulk create / edit / list loops, templates, transfer and pin | https://cli.github.com/manual/gh_issue |
| Cross-repo label create / edit / clone | https://cli.github.com/manual/gh_label |
| Org-wide issue search qualifiers | https://cli.github.com/manual/gh_search_issues |
| Milestone REST CRUD: endpoints, payload fields, list filters and sorting | https://docs.github.com/en/rest/issues/milestones |
| Projects v2 field mutations, value unions, field and option ID discovery | https://docs.github.com/en/issues/planning-and-tracking-with-projects/automating-your-project/using-the-api-to-manage-projects |
| Sub-issue endpoint payloads and pagination | https://docs.github.com/en/rest/issues/sub-issues |
| Rate-limit headers, reset semantics, secondary limits | https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api |

**What still stays ours, in full, in this skill:** PR review / merge gating including
the `statusCheckRollup` array fold (`references/pr-workflows.md`); GraphQL queries,
pagination, and node-id lookup (`references/graphql-api.md`); the CLI-NAME vs
API-NUMBER identifier mapping for milestones and Projects v2
(`references/cli-vs-api-identifiers.md`); the sub-issue quick reference above; and
the rate-limit pre-flight guard (`examples/automation-scripts.md`). House rules with
their rationale are collected in `references/ork-delta.md`.

## References

Load on demand with `Read("$\{CLAUDE_PLUGIN_ROOT\}/skills/github-operations/references/&lt;file&gt;")`:
| File | Content |
|------|---------|
| `ork-delta.md` | House rules: hook contracts, close-don't-delete, rate-limit discipline, the file-set contract |
| `pr-workflows.md` | Reviews, merge strategies, auto-merge, statusCheckRollup folding |
| `graphql-api.md` | Complex queries, pagination, bulk operations |
| `cli-vs-api-identifiers.md` | NAME vs NUMBER footguns, milestone/project ID mapping |
| `issue-management.md` | Pointer stub: issue delta plus upstream links |
| `milestone-api.md` | Pointer stub: milestone delta plus upstream links |
| `projects-v2.md` | Pointer stub: Projects v2 delta plus upstream links |

## Examples

Load: `Read("$\{CLAUDE_PLUGIN_ROOT\}/skills/github-operations/examples/automation-scripts.md")` - Rate-limit discipline for long `gh` loops, plus pointers for the bulk-loop recipes


---

## Rules (2)

### Linking Issues to Branches and PRs — MEDIUM


## Linking Issues to Branches and PRs

Establish bidirectional links between issues, branches, commits, and PRs for full traceability and automatic issue closure.

### Branch Naming Convention

```bash
# Pattern: {type}/{issue-number}-{description}
issue/123-implement-feature
fix/456-resolve-timeout-bug
feature/789-add-search-api

# Creates automatic link: branch -> issue
```

### Commit Linking

```bash
# Reference in commit message
git commit -m "feat(#123): Add user validation"

# Auto-close keywords (in commit or PR body)
git commit -m "fix: Resolve timeout (closes #456)"
git commit -m "feat: Add search (fixes #789)"
```

### PR Linking

```bash
# Create PR that auto-closes issue on merge
gh pr create --title "feat(#123): Add search" \
  --body "Closes #123

## Changes
- Added search API endpoint
- Added search UI component"

# Link existing PR to issue
gh issue edit 123 --add-label "has-pr"
```

### PR-Aware Session Resumption

```bash
# Resume with full PR context (CC 2.1.27+)
claude --from-pr 42    # Loads PR diff, comments, review status
```

### Linking Checklist

| Link | How | Automatic? |
|------|-----|------------|
| Branch to issue | Branch name `issue/N-*` | Yes (hooks) |
| Commit to issue | `#N` in commit message | Yes (GitHub) |
| PR to issue | `Closes #N` in PR body | Yes (GitHub) |
| Issue to PR | `has-pr` label | Manual or hook |

**Incorrect — Branch without issue number prefix:**
```bash
git checkout -b implement-feature
git commit -m "Add user validation"
# No automatic linking - reviewer lacks context
```

**Correct — Issue-prefixed branch with linked commit:**
```bash
git checkout -b issue/123-implement-feature
git commit -m "feat(#123): Add user validation"
# Auto-links: branch → issue, commit → issue
```

### Key Rules

- Always start branches with **issue number prefix** for automatic detection
- Use **`Closes #N`** in PR body for automatic issue closure on merge
- Include **`#N`** in every commit that relates to an issue
- Use **conventional commit format** for consistent linking
- Add **`has-pr` label** to issues when a PR is created
- Use **`--from-pr`** to resume sessions with full PR context


### Automate issue progress updates so stakeholders always see current status — HIGH


## Automated Issue Progress Updates

Track issue progress automatically through commit detection, sub-task matching, and session summaries. Eliminates manual status updates.

### Three-Hook Pipeline

| Hook | Trigger | Action |
|------|---------|--------|
| Commit Detection | Each commit | Extracts issue number, queues for batch comment |
| Sub-task Updater | Commit message match | Checks off matching `- [ ]` items in issue body |
| Session Summary | Session end | Posts consolidated progress comment |

### Issue Number Extraction

```bash
# From branch name (priority)
issue/123-implement-feature  # Extracts: 123
fix/456-resolve-bug          # Extracts: 456
feature/789-add-tests        # Extracts: 789

# From commit message (fallback)
"feat(#123): Add user validation"     # Extracts: 123
"fix: Resolve bug (closes #456)"      # Extracts: 456
```

### Sub-task Auto-Completion

Commit messages are matched against issue checkboxes using normalized text comparison:

```markdown
# Issue body (before)
- [ ] Add input validation
- [ ] Write unit tests

# Commit: "feat(#123): Add input validation"

# Issue body (after)
- [x] Add input validation
- [ ] Write unit tests
```

### Session Summary Format

```markdown
## Claude Code Progress Update

**Session**: `abc12345...`
**Branch**: `issue/123-implement-feature`

### Commits (3)
- `abc1234`: feat(#123): Add input validation
- `def5678`: test(#123): Add unit tests

### Files Changed
- `src/validation.ts` (+45, -12)
- `tests/validation.test.ts` (+89, -0)

### Sub-tasks Completed
- [x] Add input validation
- [x] Write unit tests
```

**Incorrect — Manual issue updates without automation:**
```bash
# Commit without issue reference
git commit -m "Add validation"

# Manually comment on issue #123:
"Added validation - see commit abc1234"
[Time-consuming, error-prone]
```

**Correct — Automated progress tracking:**
```bash
# Issue-prefixed branch
git checkout -b issue/123-validation

# Conventional commit
git commit -m "feat(#123): Add input validation"

# Hook auto-posts to issue:
"[Session abc123] feat(#123): Add input validation
Files: src/validation.ts (+45)"
```

### Key Rules

- Use **issue-prefixed branches** (`issue/N-`, `fix/N-`, `feature/N-`) for automatic detection
- Include **`#N`** in commit messages as fallback for issue linking
- Use **conventional commits** (`feat(#123):`, `fix(#123):`) for reliable matching
- Match commit message text to **checkbox descriptions** for auto-completion
- Post **consolidated summaries** at session end, not per-commit



---

## References (7)

### Cli Vs Api Identifiers

# CLI vs REST API Identifier Mapping

GitHub CLI (`gh`) and the REST API use **different identifier types** for the same resources. Mixing them is the #1 source of silent failures in automation.

## Quick Reference

| Resource | `gh` CLI flag | REST API field | Example |
|----------|---------------|----------------|---------|
| Milestone | `--milestone "Sprint 8"` (NAME) | `milestones/:number` (INTEGER) | CLI: `"Sprint 8"` → API: `/milestones/5` |
| Issue | `gh issue view 123` (number) | `issues/:number` (INTEGER) | Same — issue number works in both |
| PR | `gh pr view 456` (number) | `pulls/:number` (INTEGER) | Same — PR number works in both |
| User | `--assignee "username"` (LOGIN) | `assignees/:username` (STRING) | Same format, no confusion |
| Label | `--label "bug"` (NAME) | labels by name only | Same — no number in API either |
| Project | `gh project` uses **NUMBER** | Projects v2 uses **node_id** | Different! GraphQL needs `node_id` |

---

## The Milestone Footgun

`gh issue edit --milestone` and `gh issue list --milestone` accept a **milestone NAME (string)**, not a number.

The REST API endpoint is `repos/:owner/:repo/milestones/:number` — it uses the **milestone NUMBER (integer)**.

```bash
# CORRECT: gh CLI uses milestone NAME
gh issue edit 123 --milestone "Sprint 8"         # ✓ Name
gh issue list --milestone "Sprint 8"             # ✓ Name

# CORRECT: REST API uses milestone NUMBER
gh api -X PATCH repos/:owner/:repo/milestones/5 -f state=closed  # ✓ Number

# WRONG: don't pass a number to --milestone
gh issue edit 123 --milestone 5    # ✗ Silently fails or wrong milestone
```

### Look Up a Milestone Number

When you need a number (for REST API calls), look it up from the name:

```bash
# Get milestone number from name
MILESTONE_NUM=$(gh api repos/:owner/:repo/milestones \
  --jq '.[] | select(.title == "Sprint 8") | .number')

# Then use the number for REST API calls
gh api -X PATCH repos/:owner/:repo/milestones/$MILESTONE_NUM -f state=closed
```

---

## Projects v2 Identifier Confusion

Projects v2 has an extra layer: the project **number** (shown in URL) vs the project **node_id** (needed for GraphQL mutations).

```bash
# List projects — shows both number and id
gh project list --owner @me --format json --jq '.projects[] | {number, id}'
# Output: {"number": 1, "id": "PVT_kwDOAbc123"}

# gh project commands use NUMBER
gh project item-add 1 --owner @me --url https://github.com/org/repo/issues/123

# GraphQL mutations need node_id (the "PVT_..." value)
gh api graphql -f query='
  mutation {
    updateProjectV2ItemFieldValue(input: {
      projectId: "PVT_kwDOAbc123"   # node_id, not number
      ...
    })
  }
'
```

---

## Issue and PR Numbers

Issues and PRs use the same number in both CLI and REST API — no translation needed.

```bash
# Both work identically
gh issue view 123
gh api repos/:owner/:repo/issues/123

# JSON output includes both number and node_id
gh issue view 123 --json number,id
# {"number": 123, "id": "I_kwDOAbc123"}
```

GraphQL sub-issue mutations require the **node_id** (`I_kwDO...`), not the number.

---

## Safe Patterns

### Always resolve milestone names to numbers before REST API calls

```bash
get_milestone_number() {
  local name="$1"
  gh api repos/:owner/:repo/milestones \
    --jq --arg name "$name" '.[] | select(.title == $name) | .number'
}

MS_NUM=$(get_milestone_number "Sprint 8")
gh api -X PATCH repos/:owner/:repo/milestones/$MS_NUM -f state=closed
```

### Use `--json` to capture the created resource's identifier

```bash
# Capture milestone number at creation time
MILESTONE_NUM=$(gh api -X POST repos/:owner/:repo/milestones \
  -f title="Sprint 9" \
  --jq '.number')

# Now you have the number for REST API calls
gh api repos/:owner/:repo/milestones/$MILESTONE_NUM
```


### Graphql Api

# GraphQL API with gh

## Basic GraphQL Query

```bash
gh api graphql -f query='
  query {
    viewer {
      login
      name
    }
  }
'
```

## Variables in Queries

```bash
gh api graphql \
  -F owner="org" \
  -F repo="repo-name" \
  -f query='
    query($owner: String!, $repo: String!) {
      repository(owner: $owner, name: $repo) {
        issues(first: 10, states: OPEN) {
          nodes {
            number
            title
          }
        }
      }
    }
  '
```

**Note**: Use `-F` for non-string values (numbers, booleans), `-f` for strings.

---

## Common Queries

### Repository Info

```bash
gh api graphql -f query='
  query($owner: String!, $repo: String!) {
    repository(owner: $owner, name: $repo) {
      name
      description
      stargazerCount
      forkCount
      issues(states: OPEN) { totalCount }
      pullRequests(states: OPEN) { totalCount }
    }
  }
' -f owner="org" -f repo="repo-name"
```

### Issue with Labels and Milestone

```bash
gh api graphql -f query='
  query($owner: String!, $repo: String!, $number: Int!) {
    repository(owner: $owner, name: $repo) {
      issue(number: $number) {
        title
        body
        state
        labels(first: 10) {
          nodes { name color }
        }
        milestone {
          title
          dueOn
        }
        assignees(first: 5) {
          nodes { login }
        }
      }
    }
  }
' -f owner="org" -f repo="repo-name" -F number=123
```

### PR with Reviews and Checks

```bash
gh api graphql -f query='
  query($owner: String!, $repo: String!, $number: Int!) {
    repository(owner: $owner, name: $repo) {
      pullRequest(number: $number) {
        title
        reviewDecision
        mergeable
        commits(last: 1) {
          nodes {
            commit {
              statusCheckRollup {
                state
                contexts(first: 10) {
                  nodes {
                    ... on CheckRun {
                      name
                      conclusion
                    }
                  }
                }
              }
            }
          }
        }
        reviews(last: 10) {
          nodes {
            author { login }
            state
            submittedAt
          }
        }
      }
    }
  }
' -f owner="org" -f repo="repo-name" -F number=456
```

---

## Pagination

```bash
# Use --paginate for automatic pagination
gh api graphql --paginate \
  -F owner="org" \
  -F repo="repo-name" \
  -f query='
    query($owner: String!, $repo: String!, $endCursor: String) {
      repository(owner: $owner, name: $repo) {
        issues(first: 100, after: $endCursor, states: OPEN) {
          nodes {
            number
            title
          }
          pageInfo {
            hasNextPage
            endCursor
          }
        }
      }
    }
  '
```

**Important**: For pagination to work:
- Include `$endCursor: String` in query variables
- Include `pageInfo \{ hasNextPage endCursor \}` in response
- Use `after: $endCursor` in the connection

---

## Mutations

### Add Label to Issue

```bash
# First get label ID
LABEL_ID=$(gh api graphql -f query='
  query($owner: String!, $repo: String!, $name: String!) {
    repository(owner: $owner, name: $repo) {
      label(name: $name) { id }
    }
  }
' -f owner="org" -f repo="repo-name" -f name="bug" \
  --jq '.data.repository.label.id')

# Get issue 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="org" -f repo="repo-name" -F number=123 \
  --jq '.data.repository.issue.id')

# Add label
gh api graphql -f query='
  mutation($issueId: ID!, $labelIds: [ID!]!) {
    addLabelsToLabelable(input: {
      labelableId: $issueId
      labelIds: $labelIds
    }) {
      labelable {
        ... on Issue { title }
      }
    }
  }
' -f issueId="$ISSUE_ID" -f labelIds="[\"$LABEL_ID\"]"
```

### Create Issue with GraphQL

```bash
gh api graphql -f query='
  mutation($repoId: ID!, $title: String!, $body: String) {
    createIssue(input: {
      repositoryId: $repoId
      title: $title
      body: $body
    }) {
      issue {
        number
        url
      }
    }
  }
' -f repoId="MDEwOlJlcG9zaXRvcnkxMjM0NTY3ODk=" \
  -f title="New issue via GraphQL" \
  -f body="Description here"
```

### Close Issue

```bash
gh api graphql -f query='
  mutation($issueId: ID!) {
    closeIssue(input: { issueId: $issueId }) {
      issue {
        state
        closedAt
      }
    }
  }
' -f issueId="I_kwDOABCD1234"
```

---

## JQ Processing

```bash
# Extract specific field
gh api graphql -f query='...' --jq '.data.repository.issues.nodes'

# Filter results
gh api graphql -f query='...' \
  --jq '.data.repository.issues.nodes[] | select(.labels.nodes[].name == "bug")'

# Transform to custom format
gh api graphql -f query='...' \
  --jq '.data.repository.issues.nodes[] | {num: .number, title: .title}'
```

---

## Error Handling

```bash
# Check for errors in response
RESULT=$(gh api graphql -f query='...')

if echo "$RESULT" | jq -e '.errors' > /dev/null 2>&1; then
  echo "GraphQL Error:"
  echo "$RESULT" | jq '.errors[].message'
  exit 1
fi

# Process successful result
echo "$RESULT" | jq '.data'
```

---

## Useful Fragments

### Reusable Issue Fragment

```graphql
fragment IssueFields on Issue {
  number
  title
  state
  createdAt
  updatedAt
  labels(first: 10) {
    nodes { name }
  }
  assignees(first: 5) {
    nodes { login }
  }
  milestone {
    title
  }
}

query {
  repository(owner: "org", name: "repo-name") {
    issues(first: 10) {
      nodes {
        ...IssueFields
      }
    }
  }
}
```

---

## Bulk Operations

### Update Multiple Issues

```bash
# Get all issues to update
ISSUES=$(gh api graphql -f query='
  query {
    repository(owner: "org", name: "repo-name") {
      issues(first: 50, states: OPEN, labels: ["stale"]) {
        nodes { id number }
      }
    }
  }
' --jq '.data.repository.issues.nodes[]')

# Close each one
echo "$ISSUES" | while read -r issue; do
  ISSUE_ID=$(echo "$issue" | jq -r '.id')
  gh api graphql -f query='
    mutation($id: ID!) {
      closeIssue(input: { issueId: $id }) {
        issue { number state }
      }
    }
  ' -f id="$ISSUE_ID"
done
```

---

## Rate Limit Checking

```bash
gh api graphql -f query='
  query {
    rateLimit {
      limit
      remaining
      resetAt
      used
    }
  }
'
```

---

## Get Node IDs

Many GraphQL mutations require node IDs (not numbers):

```bash
# 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="org" -f repo="repo-name" -F number=123 \
  --jq '.data.repository.issue.id'

# Repository ID
gh api graphql -f query='
  query($owner: String!, $repo: String!) {
    repository(owner: $owner, name: $repo) { id }
  }
' -f owner="org" -f repo="repo-name" \
  --jq '.data.repository.id'

# Label ID
gh api graphql -f query='
  query($owner: String!, $repo: String!, $name: String!) {
    repository(owner: $owner, name: $repo) {
      label(name: $name) { id }
    }
  }
' -f owner="org" -f repo="repo-name" -f name="bug" \
  --jq '.data.repository.label.id'
```


### Issue Management

# Issue Management

Vendor mechanics for `gh issue` are not restated here. This file carries only the
house delta and the pointers.

> Kept on disk deliberately: `tests/skills/test-github-operations-completeness.sh`
> and `tests/unit/test-git-enforcement-hooks.sh` assert this exact filename.

## Upstream

| Topic | Source |
|-------|--------|
| `gh issue create/edit/list/close/view` flags, templates, `--body-file` | https://cli.github.com/manual/gh_issue |
| Issue search qualifiers (`is:open`, `label:`, `no:assignee`) | https://cli.github.com/manual/gh_search_issues |
| Native sub-issue endpoints and payloads | https://docs.github.com/en/rest/issues/sub-issues |
| Keyword auto-close semantics (`Closes #N`) | https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue |

## Our delta

Full rules with rationale: [ork-delta.md](ork-delta.md).

1. **`--label` is mandatory.** `src/hooks/src/pretool/bash/gh-label-enforcer.ts`
   denies a bare `gh issue create`. A seeding loop without it dies partway.
2. **Never `gh issue close`.** Issues close only when their linked PR merges to the
   default branch. Comment progress with `gh issue comment` instead.
3. **Milestones go in by NAME, never by number.** The identifier mapping stays in
   this skill: [cli-vs-api-identifiers.md](cli-vs-api-identifiers.md).
4. **Sub-issues are native.** Do not install `yahsan2/gh-sub-issue`.

House pattern worth keeping, capture the number at creation so the issue can be
wired into Projects v2 or a PR body in the same run:

```bash
NUM=$(gh issue create --title "$title" --label "$labels" \
  --milestone "$SPRINT" --body "" --json number --jq '.number')
echo "Created #$NUM"
```


### Milestone Api

# Milestone API

`gh` has no native milestone commands, so milestones are REST-only. The endpoint
list, payload fields, and query parameters are not restated here.

> Kept on disk deliberately: `tests/skills/test-github-operations-completeness.sh`
> and `tests/unit/test-git-enforcement-hooks.sh` assert this exact filename.

## Upstream

| Topic | Source |
|-------|--------|
| Milestone REST CRUD, payload fields (`title`, `state`, `description`, `due_on`), list filters and sorting | https://docs.github.com/en/rest/issues/milestones |
| `gh api` invocation, `:owner`/`:repo` placeholders, `--jq`, `--paginate` | https://cli.github.com/manual/gh_api |
| Assigning a milestone from the CLI | https://cli.github.com/manual/gh_issue_create |

## Our delta

Full rules with rationale: [ork-delta.md](ork-delta.md).

1. **Close, never DELETE.** `DELETE .../milestones/:number` detaches the milestone
   from every issue that carried it and the progress history is unrecoverable.
   Use `-f state=closed`.
2. **NAME for the CLI, NUMBER for the API.** The full identifier mapping and the
   name-to-number lookup helper stay in this skill:
   [cli-vs-api-identifiers.md](cli-vs-api-identifiers.md).
3. **ISO 8601 for `due_on`** (`YYYY-MM-DDTHH:MM:SSZ`). Anything else is accepted
   and then silently misread.

```bash
# Close a sprint: resolve the number from the title, then PATCH state.
MS=$(gh api repos/:owner/:repo/milestones \
  --jq '.[] | select(.title=="Sprint 8") | .number')
gh api -X PATCH "repos/:owner/:repo/milestones/$MS" -f state=closed
```


### Ork Delta

# OrchestKit delta for GitHub operations

What this repo does differently from the stock `gh` CLI and REST docs. Everything
here is a house rule, a hook contract, or a scar. Vendor mechanics live upstream,
linked per entry.

---

## Pass `--label` on every `gh issue create`

Why: `src/hooks/src/pretool/bash/gh-label-enforcer.ts`, registered in
`src/hooks/src/entries/pretool.ts`, returns a `deny` permission decision for a bare
`gh issue create`. A label-less create inside a seeding loop does not warn, it is
blocked, and the batch dies partway with issues already filed.
Upstream: https://cli.github.com/manual/gh_issue_create

## Treat the missing-`--milestone` message as advisory, not a failure

Why: `src/hooks/src/pretool/bash/gh-milestone-enforcer.ts` is registered in the same
entries map but only emits context ("No --milestone set. Consider assigning to a
milestone for sprint tracking."). It never blocks. Do not abort or retry a create
because that line appeared in the transcript.
Upstream: https://cli.github.com/manual/gh_issue_create

## Never close an issue by hand; let the merged PR close it

Why: the repo CLAUDE.md "GitHub CLI" rule states issues close on merge from
`Closes #N` in the PR body, and closing by hand loses that PR link. The retired
`references/issue-management.md` and `examples/automation-scripts.md` both shipped
bulk `gh issue close` loops that strip the link from history permanently. Use
`gh issue comment` for progress instead.
Upstream: https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue

## Stop the loop on the first `gh` rate-limit hint, do not retry

Why: on Claude Code 2.1.116+ the Bash tool surfaces a rate-limit hint when `gh`
takes a 403, and that hint is the authoritative backoff signal. Before it existed,
agents had no signal and burned the whole retry budget in roughly 13 seconds.
`tests/evals/skills/github-operations-rate-limit.eval.yaml` grades this behaviour
and asserts the pre-flight guard, so the `gh api rate_limit` check and the
`remaining &lt; 100` threshold in `examples/automation-scripts.md` are contract, not
taste. (Eval added under issue #1436, "test(evals): add eval coverage for CC
2.1.116 behavioral knowledge in skills".)
Upstream: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api

## Close milestones, never DELETE them

Why: `DELETE /repos/:owner/:repo/milestones/:number` detaches the milestone from
every issue that carried it, so the sprint's progress history is gone with no undo.
The house Key Decision in SKILL.md is close-don't-delete; `-f state=closed` keeps
the closed/open counts queryable forever.
Upstream: https://docs.github.com/en/rest/issues/milestones

## Treat sub-issues as native; do not install `gh-sub-issue`

Why: distilled from the retired `references/issue-management.md`; no traced
incident. That file told agents to `gh extension install yahsan2/gh-sub-issue`
while the 2026 section of SKILL.md documents the native `sub_issues` REST
endpoints, so the skill contradicted itself depending on which file got loaded.
Upstream: https://docs.github.com/en/rest/issues/sub-issues

## Keep the six named sub-files on disk when thinning this skill

Why: `tests/skills/test-github-operations-completeness.sh` and
`tests/unit/test-git-enforcement-hooks.sh` assert the exact file set frozen by
issue #155 ("Skill: Create consolidated github-operations skill"), and the
completeness test additionally greps `examples/automation-scripts.md` for a
fenced bash block and resolves every relative markdown link in SKILL.md. Thin the
contents, keep the names, keep one fence in the example file.
Upstream: none, house-only contract enforced by
`tests/skills/test-github-operations-completeness.sh`


### Pr Workflows

# Pull Request Workflows

## Creating PRs

### Basic Creation

```bash
# Interactive (opens editor)
gh pr create

# Non-interactive with auto-fill from commits
gh pr create --fill

# Explicit title and body
gh pr create \
  --title "feat(#123): Add hybrid search with PGVector" \
  --body "Description..." \
  --base dev \
  --head feature/pgvector-search
```

### Full PR Creation Pattern

```bash
gh pr create \
  --title "feat(#${ISSUE_NUM}): Implement Langfuse tracing" \
  --body "$(cat <<'EOF'
## Summary
- Added @observe decorator to workflow functions
- Implemented CallbackHandler for LangChain
- Added session and user tracking

## Changes
- `backend/app/shared/services/langfuse/` - New Langfuse client
- `backend/app/workflows/nodes/` - Added tracing decorators
- `backend/tests/unit/services/` - Langfuse unit tests

## Test Plan
- [ ] Unit tests pass (`poetry run pytest tests/unit/`)
- [ ] Integration test with real Langfuse instance
- [ ] Verify traces appear in Langfuse UI

Closes #372

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EOF
)" \
  --base dev \
  --label "enhancement,backend" \
  --assignee "@me" \
  --reviewer "teammate"
```

### Using Body File

```bash
gh pr create --title "..." --body-file pr-description.md
```

---

## PR Checks and Status

### View Check Status

```bash
# List all checks
gh pr checks 456

# Watch checks in real-time
gh pr checks 456 --watch

# Wait for specific check
gh pr checks 456 --watch --fail-fast
```

### JSON Output for Automation

```bash
# Get check status. Valid fields: bucket, completedAt, description, event,
# link, name, startedAt, state, workflow. There is no `conclusion` field here.
gh pr checks 456 --json name,state,bucket

# Check if all passed (bucket is one of: pass, fail, pending, skipping, cancel)
gh pr checks 456 --json bucket \
  --jq 'any(.[]; .bucket == "fail") | not'
```

### Wait for Checks Pattern

```bash
PR_NUMBER=456

# `statusCheckRollup` returns an ARRAY of check objects, not a scalar state.
# CheckRun entries carry `.conclusion`, StatusContext entries carry `.state`,
# and an in-flight CheckRun has a null conclusion. Aggregate before comparing.
ROLLUP_JQ='
  [(.statusCheckRollup // [])[] | .conclusion // .state // "PENDING"]
  | if any(IN("FAILURE","TIMED_OUT","CANCELLED","ACTION_REQUIRED","STARTUP_FAILURE"))
    then "FAILURE"
    elif any(IN("PENDING","EXPECTED","QUEUED","IN_PROGRESS"))
    then "PENDING"
    else "SUCCESS"
    end'

while true; do
  STATUS=$(gh pr view $PR_NUMBER --json statusCheckRollup --jq "$ROLLUP_JQ")

  case "$STATUS" in
    "SUCCESS")
      echo "All checks passed!"
      break
      ;;
    "FAILURE")
      echo "Checks failed!"
      gh pr checks $PR_NUMBER
      exit 1
      ;;
    *)
      echo "Waiting... (status: $STATUS)"
      sleep 30
      ;;
  esac
done
```

---

## PR Reviews

### Requesting Reviews

```bash
# Request review
gh pr edit 456 --add-reviewer "username1,username2"

# Remove reviewer
gh pr edit 456 --remove-reviewer "username"
```

### Submitting Reviews

```bash
# Approve
gh pr review 456 --approve

# Approve with comment
gh pr review 456 --approve --body "LGTM! Clean implementation."

# Request changes
gh pr review 456 --request-changes --body "Need tests for edge cases"

# Comment without approval/rejection
gh pr review 456 --comment --body "Nice refactoring!"
```

### View Review Status

```bash
# Get review decision
gh pr view 456 --json reviewDecision

# List reviews
gh pr view 456 --json reviews \
  --jq '.reviews[] | "\(.author.login): \(.state)"'
```

---

## Merging PRs

### Merge Strategies

```bash
# Merge commit (default)
gh pr merge 456 --merge

# Squash merge (recommended for clean history)
gh pr merge 456 --squash

# Rebase merge
gh pr merge 456 --rebase

# With branch deletion
gh pr merge 456 --squash --delete-branch
```

### Auto-Merge

```bash
# Enable auto-merge (merges when checks pass + approved)
gh pr merge 456 --auto --squash --delete-branch

# Disable auto-merge
gh pr merge 456 --disable-auto
```

### Admin Merge (Bypass Protections)

```bash
# Bypass branch protection rules (requires admin)
gh pr merge 456 --admin --squash
```

---

## Safe Merge Pattern

```bash
#!/bin/bash
PR_NUMBER=$1

# 1. Verify checks passed. `statusCheckRollup` is an array, so every entry has
#    to be green; `length > 0` keeps a PR with no checks at all from sailing
#    through, because `all` on an empty array is true.
if ! gh pr view $PR_NUMBER --json statusCheckRollup \
  --jq '[(.statusCheckRollup // [])[] | .conclusion // .state]
        | length > 0 and all(IN("SUCCESS","SKIPPED","NEUTRAL"))' | grep -q true; then
  echo "ERROR: Checks not passed"
  gh pr checks $PR_NUMBER
  exit 1
fi

# 2. Verify approved
APPROVED=$(gh pr view $PR_NUMBER --json reviewDecision --jq '.reviewDecision')
if [[ "$APPROVED" != "APPROVED" ]]; then
  echo "ERROR: PR not approved (status: $APPROVED)"
  exit 1
fi

# 3. Verify mergeable
MERGEABLE=$(gh pr view $PR_NUMBER --json mergeable --jq '.mergeable')
if [[ "$MERGEABLE" != "MERGEABLE" ]]; then
  echo "ERROR: PR has conflicts"
  exit 1
fi

# 4. Merge
gh pr merge $PR_NUMBER --squash --delete-branch
echo "Successfully merged PR #$PR_NUMBER"
```

---

## PR Comments

```bash
# Add comment
gh pr comment 456 --body "Addressed review feedback in latest commit"

# View comments
gh pr view 456 --comments
```

---

## Checkout and Edit

```bash
# Checkout PR locally
gh pr checkout 456

# Edit PR metadata
gh pr edit 456 --title "New title" --add-label "urgent"

# Close without merging
gh pr close 456 --comment "Superseded by #789"

# Reopen
gh pr reopen 456
```

---

## PR Listing and Search

```bash
# My open PRs
gh pr list --author @me --state open

# PRs needing my review
gh pr list --search "review-requested:@me"

# Ready to merge (statusCheckRollup is an array, so fold it before filtering)
gh pr list --json number,title,reviewDecision,statusCheckRollup \
  --jq '[.[] | select(.reviewDecision == "APPROVED"
        and ([(.statusCheckRollup // [])[] | .conclusion // .state]
             | length > 0 and all(IN("SUCCESS","SKIPPED","NEUTRAL"))))]'

# Draft PRs
gh pr list --draft
```

---

## Convert Draft to Ready

```bash
# Mark ready for review
gh pr ready 456

# Convert to draft
gh pr ready 456 --undo
```

---

## PR Diff and Files

```bash
# View diff
gh pr diff 456

# List changed files
gh pr view 456 --json files --jq '.files[].path'

# View specific file
gh pr diff 456 -- path/to/file.py
```

---

## Common Patterns

### Create PR from Current Branch

```bash
# Push and create PR in one flow
git push -u origin $(git branch --show-current) && \
gh pr create --fill --base dev
```

### Find Stale PRs

```bash
# PRs not updated in 7 days
gh pr list --json number,title,updatedAt \
  --jq '[.[] | select(.updatedAt < (now - 604800 | todate))]'
```

### PR Statistics

```bash
# Average time to merge
gh pr list --state merged --limit 20 --json createdAt,mergedAt \
  --jq '[.[] | (.mergedAt | fromdateiso8601) - (.createdAt | fromdateiso8601)] | add / length / 3600 | "Average: \(.) hours"'
```


### Projects V2

# GitHub Projects v2

`gh project` covers listing and adding items; custom-field writes are GraphQL only.
The mutation shapes, field-value unions, and field/option ID discovery queries are
documented upstream and are not restated here.

> Kept on disk deliberately: `tests/skills/test-github-operations-completeness.sh`
> asserts this exact filename.

## Upstream

| Topic | Source |
|-------|--------|
| `updateProjectV2ItemFieldValue`, single-select / text / number / iteration value shapes, field and option ID discovery, org vs user project queries | https://docs.github.com/en/issues/planning-and-tracking-with-projects/automating-your-project/using-the-api-to-manage-projects |
| `gh project list/view/item-add/item-delete/field-list` | https://cli.github.com/manual/gh_project |
| Adding items to a project | https://docs.github.com/en/issues/planning-and-tracking-with-projects/managing-items-in-your-project/adding-items-to-your-project |
| GraphQL mutation reference | https://docs.github.com/en/graphql/reference/mutations |

## Our delta

Full rules with rationale: [ork-delta.md](ork-delta.md).

1. **Two identifiers, one project.** `gh project` subcommands take the project
   NUMBER from the URL; every GraphQL mutation needs the `PVT_...` node_id. Mixing
   them fails without a useful error. Mapping stays in this skill:
   [cli-vs-api-identifiers.md](cli-vs-api-identifiers.md).
2. **Field IDs are per project, never hardcode them across repos.** Resolve
   `field-list` output at run time, or the mutation silently writes the wrong field.
3. **Order matters:** create the issue, capture its URL, `item-add` to get the item
   id, then set fields. There is no single call that does all three.

```bash
# Discover ids for a project before any field mutation.
gh project field-list 1 --owner @me --format json
gh project list --owner @me --format json --jq '.projects[] | {number, id}'
```



---

## Examples (1)

### Automation Scripts

# GitHub Automation Scripts

Loop-and-`gh` recipes (bulk label, bulk assign, cross-repo label sync, org-wide
search, PR dashboards) are ordinary CLI usage and are not restated here. What
survives is the part the vendor docs do not tell you: how this repo wants an agent
to behave inside a long `gh` loop.

> Kept on disk deliberately: `tests/skills/test-github-operations-completeness.sh`
> and `tests/unit/test-git-enforcement-hooks.sh` assert this exact filename, and
> the completeness test greps it for a fenced bash block.

## Upstream

| Topic | Source |
|-------|--------|
| `gh issue`/`gh pr` list, edit, and search flags used by every bulk loop | https://cli.github.com/manual/gh_issue |
| Cross-repo label create/edit/clone | https://cli.github.com/manual/gh_label |
| Org-wide issue search | https://cli.github.com/manual/gh_search_issues |
| Rate-limit headers, reset semantics, secondary limits | https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api |

## Our delta: rate-limit discipline

Full rules with rationale: [../references/ork-delta.md](../references/ork-delta.md).

On Claude Code 2.1.116+ the Bash tool surfaces a rate-limit hint in the transcript
whenever a `gh` invocation takes a 403 rate-limit response. **That hint is the
authoritative backoff signal: stop the loop and wait for the reset, do not retry
the next call.** Before the hint existed, agents had no signal and exhausted the
whole retry budget in roughly 13 seconds. Never work around a limit by swapping
tokens.

The pre-flight guard below is the house pattern that keeps the loop from hitting
the first 403 at all. The `remaining &lt; 100` floor and the `gh api rate_limit`
probe are graded by `tests/evals/skills/github-operations-rate-limit.eval.yaml`,
so keep both:

```bash
#!/usr/bin/env bash
set -euo pipefail

check_rate_limit() {
  local remaining reset wait
  remaining=$(gh api rate_limit --jq '.rate.remaining')
  if [[ "$remaining" -lt 100 ]]; then
    reset=$(gh api rate_limit --jq '.rate.reset')
    wait=$((reset - $(date +%s)))
    echo "Rate limit low ($remaining). Waiting ${wait}s..."
    sleep "$wait"
  fi
}

for issue in $(gh issue list --json number --jq '.[].number'); do
  check_rate_limit
  gh issue edit "$issue" --add-label "processed"
done
```

Two more constraints that bite inside bulk loops:

- **Never bulk `gh issue close`.** Issues close only when their linked PR merges;
  a close loop strips the PR link from history.
- **Every `gh issue create` needs `--label`.** The `gh-label-enforcer` hook denies
  it otherwise, mid-batch.

## Related

- [Issue Management](../references/issue-management.md)
- [Milestone API](../references/milestone-api.md)
- [GraphQL API](../references/graphql-api.md)
- PR merge-gating (`statusCheckRollup` is an array, fold it before comparing):
  [PR Workflows](../references/pr-workflows.md)
