diff --git a/docs/README.skills.md b/docs/README.skills.md
index 75d2269e6c..e10dab98f2 100644
--- a/docs/README.skills.md
+++ b/docs/README.skills.md
@@ -216,6 +216,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
| [github-codespaces-efficiency](../skills/github-codespaces-efficiency/SKILL.md)
`gh skills install github/awesome-copilot github-codespaces-efficiency` | Audit and improve GitHub Codespaces efficiency. Use this skill when a user wants faster Codespaces startup, lower Codespaces spend, slim devcontainers, right-size machines, tune idle timeout, or scope prebuilds to branches with sustained usage. | `references/codespaces.md`
`references/review-rubric.md` |
| [github-copilot-starter](../skills/github-copilot-starter/SKILL.md)
`gh skills install github/awesome-copilot github-copilot-starter` | Set up complete GitHub Copilot configuration for a new project based on technology stack | None |
| [github-issues](../skills/github-issues/SKILL.md)
`gh skills install github/awesome-copilot github-issues` | Create, update, and manage GitHub issues using MCP tools. Use this skill when users want to create bug reports, feature requests, or task issues, update existing issues, add labels/assignees/milestones, set issue fields (dates, priority, custom fields), set issue types, manage issue workflows, link issues, add dependencies, or track blocked-by/blocking relationships. Triggers on requests like "create an issue", "file a bug", "request a feature", "update issue X", "set the priority", "set the start date", "link issues", "add dependency", "blocked by", "blocking", or any GitHub issue management task. | `references/dependencies.md`
`references/images.md`
`references/issue-fields.md`
`references/issue-types.md`
`references/milestones.md`
`references/projects.md`
`references/search.md`
`references/sub-issues.md`
`references/templates.md` |
+| [github-projects](../skills/github-projects/SKILL.md)
`gh skills install github/awesome-copilot github-projects` | Create and administer GitHub Projects (v2) boards via gh CLI and GraphQL — project creation, single-select fields and options, views, workflows, and idempotent issue seeding. Covers the evergreen-board pattern (releases as a field, not per-release projects) and the API walls that trip agents up: input-key casing, required option color+description, full-replace option semantics, view/grouping read-only limits, and workflow automation being UI-only. Triggers on "set up a project board", "create a release view", "add issues to a project", "seed a project", "rename a board field", or any board-admin task beyond adding/updating items. | `scripts/seed-project.mjs` |
| [github-release](../skills/github-release/SKILL.md)
`gh skills install github/awesome-copilot github-release` | Guides IA through releasing a new version of a GitHub library end-to-end. Handles SemVer versioning and Keep a Changelog formatting automatically. | `references/commit-classification.md`
`references/semver-rules.md` |
| [gitmoji](../skills/gitmoji/SKILL.md)
`gh skills install github/awesome-copilot gitmoji` | Generates commit messages following the gitmoji convention (https://gitmoji.dev) — picks the right emoji for the intent of the change and writes a well-formed message. Use when asked to "write a gitmoji commit", "add an emoji to my commit message", "which gitmoji should I use", "gitmoji this change", or when a project uses gitmoji-style commit messages. Works from a git diff, staged changes, or a plain description of the change. Generates the message only — does not run git commands. | `references/gitmoji-reference.md` |
| [go-mcp-server-generator](../skills/go-mcp-server-generator/SKILL.md)
`gh skills install github/awesome-copilot go-mcp-server-generator` | Generate a complete Go MCP server project with proper structure, dependencies, and implementation using the official github.com/modelcontextprotocol/go-sdk. | None |
diff --git a/skills/github-projects/SKILL.md b/skills/github-projects/SKILL.md
new file mode 100644
index 0000000000..177734bff1
--- /dev/null
+++ b/skills/github-projects/SKILL.md
@@ -0,0 +1,110 @@
+---
+name: github-projects
+description: 'Create and administer GitHub Projects (v2) boards via gh CLI and GraphQL — project creation, single-select fields and options, views, workflows, and idempotent issue seeding. Covers the evergreen-board pattern (releases as a field, not per-release projects) and the API walls that trip agents up: input-key casing, required option color+description, full-replace option semantics, view/grouping read-only limits, and workflow automation being UI-only. Triggers on "set up a project board", "create a release view", "add issues to a project", "seed a project", "rename a board field", or any board-admin task beyond adding/updating items.'
+---
+
+# GitHub Projects (v2) board administration
+
+For adding/updating **items** on an existing board, the `github-issues`
+skill's Projects V2 reference is often enough. This skill covers
+**administering the board itself**.
+
+## Model: evergreen board, releases as fields
+
+- ONE long-lived org project per product. A release is a `Release`
+ single-select OPTION + a filtered view — NOT a per-release project.
+- Suggested fields: `Status` (Todo / In Progress / In Review / Done),
+ `Release` (one option per cycle), plus one grouping field with
+ human-readable options (workstream, area, epic — readable names, not
+ plan-document codes).
+- Mutations need the `project` scope: `gh auth refresh -s project`
+ (default token usually has only `read:project` → `INSUFFICIENT_SCOPES`).
+
+## Verified API walls (live-tested, 2026-08)
+
+1. **Input keys are camelCase and inconsistent per mutation:**
+ - `updateProjectV2Field(input: { fieldId, name?, singleSelectOptions? })`
+ — `fieldId`; no projectId.
+ - `createProjectV2View(input: { projectId, name, layout })` — `projectId`.
+ - CLI `gh project item-edit` requires BOTH `--project-id` and `--id`.
+2. **Every singleSelectOption requires name, color, AND description.**
+ Valid colors (introspected enum): GRAY, BLUE, GREEN, YELLOW, ORANGE,
+ RED, PINK, PURPLE. No TEAL/MAGENTA.
+3. **`singleSelectOptions` REPLACES the full option list** (empty input is
+ ignored, not cleared). To merge: read current options first, send the
+ merged set. Renaming an option while keeping item stamps stable: send
+ the same names (option IDs are preserved for unchanged names).
+4. **View grouping/filter/sort are read-only via API** (community
+ discussion 153532). Create views with layout only
+ (`BOARD_LAYOUT` / `TABLE_LAYOUT` / `ROADMAP_LAYOUT`) and set group-by /
+ filter in the web UI — a one-time manual step.
+5. **Workflows (board automation) are read + delete via API only** —
+ `updateProjectV2Workflow` does not exist (schema-introspected).
+ Auto-add issues, "item added → set Status", and close-archiving are
+ configured in the project's Workflows settings (web UI, one-time).
+
+## CLI fast paths
+
+```bash
+# create org-owned project
+gh project create --owner ORG --title NAME --format json
+
+# inspect fields + option IDs
+gh project field-list N --owner ORG --format json \
+ --jq '.fields[] | {name, id, options}'
+
+# create a single-select field
+gh project field-create N --owner ORG --name Release \
+ --data-type SINGLE_SELECT --single-select-options "v1.0.0,v1.1.0"
+
+# rename field / replace options (GraphQL; full-replace semantics!)
+gh api graphql -f query='mutation { updateProjectV2Field(input: {
+ fieldId: "PVTSSF_…"
+ name: "Workstream"
+ singleSelectOptions: [
+ {name: "Frontend", color: BLUE, description: "UI work"},
+ {name: "Security", color: RED, description: "Hardening"}
+ ]
+}) { projectV2Field { ... on ProjectV2SingleSelectField { options { id name } } } } }'
+
+# create views (layout only — group-by/filter are UI-only)
+gh api graphql -f query='mutation { createProjectV2View(input: {
+ projectId: "PVT_…", name: "vX Board", layout: BOARD_LAYOUT
+}) { projectV2View { id name } } }'
+
+# add + stamp items (idempotent add; item-edit prints nothing on success)
+gh project item-add N --owner ORG \
+ --url https://github.com/ORG/REPO/issues/42 --format json
+gh project item-edit --project-id PVT_… --id PVTI_… \
+ --field-id PVTSSF_… --single-select-option-id OPTION_ID
+
+# read board workflows (enabled state)
+gh api graphql -f query='{ organization(login: "ORG") { projectV2(number: N) {
+ workflows(first: 10) { nodes { id name enabled } } } } }' \
+ --jq '.data.organization.projectV2.workflows.nodes[]'
+```
+
+## Idempotent seeding pattern
+
+`addProjectV2ItemById` returns the existing item on re-add, so seed
+scripts are safe to re-run per release cycle. The bundled reference
+script resolves field/option IDs dynamically each run (never hardcode
+option IDs across cycles) and stamps `Release` + `Status` + grouping
+field per issue: see [scripts/seed-project.mjs](scripts/seed-project.mjs).
+
+## One-time manual UI steps (API cannot do these)
+
+- Project → Workflows: enable **Auto-add issues** (filter: your repo),
+ **Item added → Status = Todo**, optionally close-archiving.
+- Per view: group by `Status` / grouping field, filter `Release = vX`.
+
+## Common failure modes
+
+| Symptom | Cause |
+|---|---|
+| `missing required scopes [project]` | Token lacks write scope → `gh auth refresh -s project` |
+| `Argument 'fieldId' … required` on field update | Used `projectID` key — the field mutations take `fieldId` |
+| `invalid value (TEAL)` | Color enum has only GRAY/BLUE/GREEN/YELLOW/ORANGE/RED/PINK/PURPLE |
+| Options vanished after edit | `singleSelectOptions` replaces the whole list — merge, don't send partial |
+| `project-id must be provided` | `gh project item-edit` needs `--project-id` alongside `--id` |
+| Empty JSON error when scripting `item-edit` | Success prints nothing — don't parse its stdout |
diff --git a/skills/github-projects/scripts/seed-project.mjs b/skills/github-projects/scripts/seed-project.mjs
new file mode 100644
index 0000000000..53b718f7c2
--- /dev/null
+++ b/skills/github-projects/scripts/seed-project.mjs
@@ -0,0 +1,115 @@
+#!/usr/bin/env node
+/**
+ * Seed a GitHub Project (v2) for a release cycle — reference pattern.
+ *
+ * Idempotent: addProjectV2ItemById returns the existing item on re-add,
+ * and field stamps are upserts — safe to re-run every cycle.
+ *
+ * Usage:
+ * node scripts/seed-project.mjs vX.Y.Z
+ * PROJECT_OWNER=myorg PROJECT_NUMBER=3 node scripts/seed-project.mjs vX.Y.Z
+ *
+ * Requires gh CLI authenticated with the `project` scope
+ * (gh auth refresh -s project).
+ *
+ * Per-cycle edit: bump the ISSUE_MAP below (and add the new Release
+ * option + any new grouping options to the project fields FIRST —
+ * updateProjectV2Field replaces the full option list, so merge).
+ */
+import { execFileSync } from 'node:child_process';
+
+const OWNER = process.env.PROJECT_OWNER ?? 'ORG_NAME';
+const PROJECT_NUMBER = Number(process.env.PROJECT_NUMBER ?? 1);
+const REPO = process.env.SEED_REPO ?? 'ORG_NAME/REPO_NAME';
+const RELEASE = process.argv[2] ?? 'vX.Y.Z';
+
+/** issue number → grouping-field option name ('' = leave unstamped) */
+const ISSUE_MAP = {
+ // 18: 'Growth/SEO',
+ // 19: 'Security',
+};
+
+const GROUPING_FIELD = 'Workstream'; // rename to your grouping field
+const DEFAULT_STATUS = 'Todo';
+// ──────────────────────────────────────────────────────────────────────
+
+const gh = (args, { optionalJson = false } = {}) => {
+ const out = execFileSync('gh', args, {
+ encoding: 'utf8',
+ maxBuffer: 10 * 1024 * 1024,
+ });
+ if (!out.trim()) {
+ if (optionalJson) return null;
+ throw new Error(`gh ${args.join(' ')} returned empty output`);
+ }
+ return JSON.parse(out);
+};
+
+const fail = (msg) => {
+ console.error(`✗ ${msg}`);
+ process.exit(1);
+};
+
+// 1. Resolve project + fields dynamically (never hardcode option IDs)
+const project = gh([
+ 'project', 'view', String(PROJECT_NUMBER),
+ '--owner', OWNER, '--format', 'json',
+]);
+const projectId = project.id;
+console.log(`project #${PROJECT_NUMBER} → ${projectId} (${project.url})`);
+
+const fields = gh([
+ 'project', 'field-list', String(PROJECT_NUMBER),
+ '--owner', OWNER, '--format', 'json',
+]).fields;
+
+const fieldId = (name) => {
+ const f = fields.find((x) => x.name === name);
+ if (!f) fail(`field "${name}" not found. Have: ${fields.map((x) => x.name).join(', ')}`);
+ return f.id;
+};
+const optionId = (fieldName, optionName) => {
+ const f = fields.find((x) => x.name === fieldName);
+ const o = f?.options?.find((x) => x.name === optionName);
+ if (!o) fail(`option "${optionName}" not on field "${fieldName}" — add it first (updateProjectV2Field REPLACES the option list; merge).`);
+ return o.id;
+};
+
+const releaseField = fieldId('Release');
+const statusField = fieldId('Status');
+const autoStatus = fields.some((x) => x.name === 'Status');
+if (!releaseField || !autoStatus) fail('Release and Status fields must exist on the project first.');
+
+// 2. Add + stamp issues
+for (const [num, group] of Object.entries(ISSUE_MAP)) {
+ const url = `https://github.com/${REPO}/issues/${num}`;
+ const item = gh([
+ 'project', 'item-add', String(PROJECT_NUMBER),
+ '--owner', OWNER, '--url', url, '--format', 'json',
+ ]);
+ const itemId = item.id;
+
+ gh([
+ 'project', 'item-edit', '--project-id', projectId, '--id', itemId,
+ '--field-id', releaseField,
+ '--single-select-option-id', optionId('Release', RELEASE),
+ ], { optionalJson: true });
+ gh([
+ 'project', 'item-edit', '--project-id', projectId, '--id', itemId,
+ '--field-id', statusField,
+ '--single-select-option-id', optionId('Status', DEFAULT_STATUS),
+ ], { optionalJson: true });
+ if (group) {
+ gh([
+ 'project', 'item-edit', '--project-id', projectId, '--id', itemId,
+ '--field-id', fieldId(GROUPING_FIELD),
+ '--single-select-option-id', optionId(GROUPING_FIELD, group),
+ ], { optionalJson: true });
+ }
+ console.log(`✓ #${num} → ${RELEASE}${group ? ` · ${group}` : ''}`);
+}
+
+console.log(`\nDone: ${Object.keys(ISSUE_MAP).length} items seeded into ${project.url}`);
+console.log('One-time manual UI steps (API is read-only for these):');
+console.log(' - Workflows: Auto-add issues; Item added → Status=Todo');
+console.log(' - Board view: group by Status, filter Release=vX; Backlog view: group by grouping field');