Install any skill in seconds. Free to start, no credit card required.
Get Started Free →GitHub issue and project-board management for the dotnet/msbuild repo. Use when asked to file/triage/update issues, post comments, amend issue bodies, move sprints, bulk-update project board fields, or audit items by sprint/status/assignee.
.claude/skills/dotnet-project-management/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 102% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 129% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 190% | 0% |
| case-20 | ✓→✗ | ▼ Worse | 158% | 0% |
| case-21 | ✓→✗ | ▼ Worse | 65% | 0% |
This skill covers everything you need to manage issues and the project board for the MSBuild repository: querying, commenting, editing, creating issues, and bulk operations on the project board (sprints, status, assignees, etc.).
dotnet/msbuild — all issues, PRs, and discussions.dotnet/projects/117 — title "MSBuild". This is the canonical project board for the team. Most issues created in dotnet/msbuild are auto-added or manually added here for sprint planning.2026 May I, 2026 May II, 2026 June I, etc. (roughly two-week iterations).> Note: there are/were other auxiliary project boards for specific initiatives. Use project 117 unless the user explicitly points at another one.
| Identifier | Value | |---|---| | Project node ID | PVT_kwDOAIt-yc4ABM5F | | Sprint field ID (iteration) | PVTIF_lADOAIt-yc4ABM5FzgAtMGI |
These IDs are stable but can be re-derived if the project is ever recreated or migrated:
bash# Project node ID gh api graphql -f query='{ organization(login:"dotnet") { projectV2(number:117) { id } } }' # All field IDs (Sprint, Status, Area, Priority, …) gh project field-list 117 --owner dotnet --format json | jq '.fields[] | {id,name,type}'
For GitHub work, prefer gh CLI over MCP tools (per repo convention).
gh issue ... for issue-level operations (create / view / comment / edit / close).gh project ... for project-board CRUD.gh api graphql for anything gh doesn't expose directly (sprint values per item, project-level field values, bulk reads).The default gh token may not include the project scope. If a gh project ... write call fails with a scope error, ask the user to run:
bashgh auth refresh -s project
bash# View gh issue view 13315 --repo dotnet/msbuild --json title,body,state,assignees,labels # Comment (use --body-file to avoid shell-quoting traps with multi-line markdown) gh issue comment 13315 --repo dotnet/msbuild --body-file ./comment.md # Amend body (overwrite — there is no append; use view → edit → write) gh issue edit 13597 --repo dotnet/msbuild --body-file ./new-body.md # Create gh issue create --repo dotnet/msbuild --title "Title" --body-file ./body.md # Add labels / assignees gh issue edit 13315 --repo dotnet/msbuild --add-label "Area: Server" --add-assignee someuser
When drafting comments / bodies that summarize internal discussions:
--body-file. Don't try to inline multi-line markdown.gh issue view does not return project-board field values. Use GraphQL via the repository → issue → projectItems path:
bashgh api graphql -f query=' query { r: repository(owner:"dotnet", name:"msbuild") { issue(number: 13315) { id projectItems(first: 5) { nodes { id project { number title } sprint: fieldValueByName(name: "Sprint") { ... on ProjectV2ItemFieldIterationValue { title iterationId } } status: fieldValueByName(name: "Status") { ... on ProjectV2ItemFieldSingleSelectValue { name optionId } } } } } } }'
> fieldValueByName returns null when the field has no value set on that item — handle accordingly.
The gh project item-list CLI command has internal limits and is unreliable for large boards (project 117 is in the thousands of items). Use GraphQL with pagination.
pythonimport subprocess, json PROJECT_NUMBER = 117 ORG = "dotnet" results = [] cursor = None while True: after = f', after: "{cursor}"' if cursor else '' query = f'''query {{ organization(login: "{ORG}") {{ projectV2(number: {PROJECT_NUMBER}) {{ items(first: 100{after}) {{ pageInfo {{ hasNextPage endCursor }} nodes {{ id fieldValues(first: 20) {{ nodes {{ ... on ProjectV2ItemFieldIterationValue {{ field {{ ... on ProjectV2IterationField {{ name }} }} title }} ... on ProjectV2ItemFieldUserValue {{ field {{ ... on ProjectV2Field {{ name }} }} users(first: 10) {{ nodes {{ login }} }} }} ... on ProjectV2ItemFieldSingleSelectValue {{ field {{ ... on ProjectV2SingleSelectField {{ name }} }} name }} }} }} content {{ ... on Issue {{ number title }} ... on PullRequest {{ number title }} ... on DraftIssue {{ title }} }} }} }} }} }} }}''' try: r = subprocess.run(['gh','api','graphql','-f',f'query={query}'], capture_output=True, text=True, check=True) except subprocess.CalledProcessError as e: raise RuntimeError(f"GraphQL query failed: {e.stderr}") from e page = json.loads(r.stdout)['data']['organization']['projectV2']['items'] for node in page['nodes']: sprint = status = None assignees = [] for fv in node.get('fieldValues', {}).get('nodes', []): if not fv: # null entries for unset fields continue fname = (fv.get('field') or {}).get('name', '') if fname == 'Sprint': sprint = fv.get('title') elif fname == 'Status': status = fv.get('name') elif fname == 'Assignees': assignees = [u['login'] for u in (fv.get('users') or {}).get('nodes', [])] results.append({'id': node['id'], 'sprint': sprint, 'status': status, 'assignees': assignees, 'content': node.get('content')}) if not page['pageInfo']['hasNextPage']: break cursor = page['pageInfo']['endCursor']
Iteration IDs are short hex strings (e.g., 303c2425). They are not the same across projects; always look them up from project 117.
bashgh api graphql -f query=' query { node(id: "PVTIF_lADOAIt-yc4ABM5FzgAtMGI") { ... on ProjectV2IterationField { configuration { iterations { id title startDate } completedIterations { id title startDate } } } } }' | jq '.data.node.configuration | (.iterations + .completedIterations)[] | {id,title}'
gh project item-editbash# Iteration / sprint gh project item-edit \ --project-id PVT_kwDOAIt-yc4ABM5F \ --id <PROJECT_ITEM_ID> \ --field-id PVTIF_lADOAIt-yc4ABM5FzgAtMGI \ --iteration-id <ITERATION_ID> # Single-select (Status, Area, Priority): use --single-select-option-id # Text: --text | Number: --number | Date: --date YYYY-MM-DD
bashgh api graphql -f query=' mutation { updateProjectV2ItemFieldValue(input: { projectId: "PVT_kwDOAIt-yc4ABM5F", itemId: "<PROJECT_ITEM_ID>", fieldId: "PVTIF_lADOAIt-yc4ABM5FzgAtMGI", value: { iterationId: "<ITERATION_ID>" } }) { projectV2Item { id } } }'
A newly-created issue is not always auto-added. To add and set its sprint:
bashISSUE_NODE_ID=$(gh api graphql -f query=' query { repository(owner:"dotnet",name:"msbuild") { issue(number:13707) { id } } }' \ | jq -r '.data.repository.issue.id') ITEM_ID=$(gh api graphql -f query=' mutation { addProjectV2ItemById(input:{ projectId:"PVT_kwDOAIt-yc4ABM5F", contentId:"'"$ISSUE_NODE_ID"'" }) { item { id } } }' | jq -r '.data.addProjectV2ItemById.item.id') gh api graphql -f query=' mutation { updateProjectV2ItemFieldValue(input:{ projectId:"PVT_kwDOAIt-yc4ABM5F", itemId:"'"$ITEM_ID"'", fieldId:"PVTIF_lADOAIt-yc4ABM5FzgAtMGI", value:{iterationId:"<ITERATION_ID>"} }) { projectV2Item { id } } }'
Field 'fieldValueByName' has an argument conflictgh api graphql --paginate (and several jq-style aliases) re-uses field names. If you call fieldValueByName(name:"Sprint") and fieldValueByName(name:"Status") in the same selection set without aliases, you'll get:
Field 'fieldValueByName' has an argument conflict: {name:"Sprint"} or {name:"Status"}?Fix: alias each call.
graphqlsprint: fieldValueByName(name: "Sprint") { ... } status: fieldValueByName(name: "Status") { ... }
gh api graphql --paginate produces concatenated JSON, not one documentEach page is emitted as a separate JSON object on stdout; piping straight into jq fails with "Invalid numeric literal". Either:
jq --slurp to read the stream as an array.field on a project field value is a union; you can't select name directly. Use a typed inline fragment per concrete field type (ProjectV2Field, ProjectV2IterationField, ProjectV2SingleSelectField, …).
The board's "No Assignees" slice is driven by the project-level Assignees field, not by Issue.assignees. They can diverge. When filtering by board state, always read ProjectV2ItemFieldUserValue from fieldValues.
null entries in fieldValues.nodesGraphQL returns null for items where a particular field is unset. Always guard if not fv: continue before dereferencing.
number / repositorycontent may be a DraftIssue that only exposes title, body, assignees. Code that joins to issue numbers must handle None.
gh project item-list is unreliable for large boardsIt silently truncates. Always use the GraphQL pagination loop above instead.
project scope on gh tokenReads work; writes (gh project item-edit, the addProjectV2ItemById / updateProjectV2ItemFieldValue mutations) fail. Have the user run gh auth refresh -s project.
gh issue edit --body overwrites — there is no appendTo "append" to a body, fetch with gh issue view --json body, append, write back with --body-file. For status updates, prefer a comment.
When summarizing meetings or chat threads into GitHub:
| Goal | Sketch | |---|---| | Add a status comment to N issues | for n in 13315 13702 …; do gh issue comment $n --repo dotnet/msbuild --body-file ./c-$n.md; done | | Replace a one-line issue body with a full Context / Exit-criteria / Out-of-scope spec | gh issue edit <n> --repo dotnet/msbuild --body-file ./body.md | | Move all unassigned items from sprint X to sprint Y | GraphQL pagination loop above + gh project item-edit --iteration-id per item | | File a new issue and put it in the current sprint | gh issue create → addProjectV2ItemById → updateProjectV2ItemFieldValue | | Audit "what's in the current sprint that has no owner" | GraphQL pagination loop, filter on Sprint == "<current>" and empty Assignees |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-14 | pass→pass | 14,012 | 7,066 | -50% | 1 | 1 | 0% | 2,485 | 4,835 | +95% | 0 | 0 | — |
case-15 | pass→pass | 14,864 | 6,681 | -55% | 1 | 1 | 0% | 2,577 | 5,001 | +94% | 0 | 0 | — |
case-01 | fail→fail | 9,856 | 18,663 | +89% | 1 | 1 | 0% | 790 | 3,965 | +402% | 0 | 0 | — |
case-02 | fail→fail | 11,632 | 17,587 | +51% | 1 | 1 | 0% | 1,863 | 4,767 | +156% | 0 | 0 | — |
case-03 | fail→fail | 13,773 | 8,651 | -37% | 1 | 1 | 0% | 757 | 4,079 | +439% | 0 | 0 | — |
case-04 | fail→pass | 33,650 | 3,084 | -91% | 1 | 1 | 0% | 2,011 | 4,055 | +102% | 0 | 0 | — |
case-05 | fail→pass | 10,350 | 5,446 | -47% | 1 | 1 | 0% | 1,976 | 4,521 | +129% | 0 | 0 | — |
case-06 | pass→pass | 10,075 | 6,254 | -38% | 1 | 1 | 0% | 1,866 | 4,429 | +137% | 0 | 0 | — |
case-07 | pass→pass | 10,945 | 4,160 | -62% | 1 | 1 | 0% | 1,631 | 4,380 | +169% | 0 | 0 | — |
case-08 | pass→pass | 9,576 | 2,634 | -72% | 1 | 1 | 0% | 1,619 | 3,955 | +144% | 0 | 0 | — |
case-09 | pass→pass | 16,778 | 10,748 | -36% | 1 | 1 | 0% | 3,336 | 5,914 | +77% | 0 | 0 | — |
case-10 | fail→pass | 10,770 | 5,960 | -45% | 1 | 1 | 0% | 1,659 | 4,804 | +190% | 0 | 0 | — |
case-11 | pass→pass | 16,741 | 8,870 | -47% | 1 | 1 | 0% | 3,056 | 5,217 | +71% | 0 | 0 | — |
case-12 | pass→pass | 9,781 | 7,110 | -27% | 1 | 1 | 0% | 1,750 | 4,654 | +166% | 0 | 0 | — |
case-13 | pass→pass | 20,357 | 7,483 | -63% | 1 | 1 | 0% | 3,278 | 4,827 | +47% | 0 | 0 | — |
case-16 | pass→pass | 7,966 | 4,208 | -47% | 1 | 1 | 0% | 1,275 | 4,285 | +236% | 0 | 0 | — |
case-17 | pass→pass | 11,794 | 5,139 | -56% | 1 | 1 | 0% | 1,971 | 4,315 | +119% | 0 | 0 | — |
case-18 | pass→pass | 10,318 | 5,277 | -49% | 1 | 1 | 0% | 2,035 | 4,657 | +129% | 0 | 0 | — |
case-19 | pass→pass | 6,839 | 4,201 | -39% | 1 | 1 | 0% | 1,182 | 4,335 | +267% | 0 | 0 | — |
case-20 | pass→fail | 8,980 | 7,623 | -15% | 1 | 1 | 0% | 1,565 | 4,033 | +158% | 0 | 0 | — |
case-21 | pass→fail | 13,988 | 5,009 | -64% | 1 | 1 | 0% | 2,256 | 3,726 | +65% | 0 | 0 | — |
case-22 | pass→pass | 15,348 | 15,113 | -2% | 1 | 1 | 0% | 2,958 | 6,729 | +127% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 16 counted toward the lift figure. The other 6 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +5 percentage points is the difference between those two pass rates over the 16 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.