diff --git a/.agents/skills/triage-board/SKILL.md b/.agents/skills/triage-board/SKILL.md new file mode 100644 index 00000000000000..59d5e0bc1db6bb --- /dev/null +++ b/.agents/skills/triage-board/SKILL.md @@ -0,0 +1,210 @@ +--- +name: triage-board +description: Triage items on the Fluent UI Unified project board (org-level GitHub Project at microsoft/projects/395). Fetches open issues across microsoft/fluentui, microsoft/fluentui-system-icons, and microsoft/fluentui-contrib that currently have no `Team` field set on the board, reads CODEOWNERS for each issue's package to route it to the right team (cxe-prg / cxe-red / teams-prg / cxe-coastal / v-build / xc-uxe / fluentui-motion / …), proposes a specific GitHub assignee from the CODEOWNERS line, and applies both via `gh` after the user approves. Use this skill whenever the user asks to triage the Fluent board, route unassigned project items, assign teams to board items, process the board's New column, triage unified board, or any variation of project-board team routing — distinct from the `triage-issues` skill which handles the repo-level `Needs: Triage :mag:` queue. +allowed-tools: Bash Read Grep Glob +--- + +# Triage the Fluent Unified Board + +Your job is to walk the items on `microsoft/projects/395` (the "Fluent UI - Unified" board) that don't yet have a `Team` field set, work out which team owns each one based on `CODEOWNERS`, propose a GitHub assignee, and apply both after the user approves. + +This skill is distinct from **`triage-issues`**: + +- `triage-issues` handles repo-level issues labeled `Needs: Triage :mag:` (the Shield workflow) — adds labels, assigns area owners, sometimes validates repros, and removes the triage label. +- `triage-board` operates at the **project-board** layer — sets the `Team` single-select field on the board and (secondarily) adds a GitHub-issue assignee. It does not touch labels, does not close issues, and does not alter `Status`. + +Both skills can run in the same session without interfering, but don't conflate them. + +Operate in **recommend-then-apply** mode: never mutate anything until the user has approved the batch. CODEOWNERS can list multiple candidates per path, and in edge cases (cross-cutting issues, unclear area) a wrong auto-assignment is annoying to undo — the approval gate is worth the extra step. + +## Preflight: verify `gh` is installed and the account can write + +Three distinct problems can block Step 5 (apply) — all should be caught upfront so the user doesn't spend time reviewing recommendations that can't be applied: + +**1. GitHub CLI installed.** All three scripts and every preflight check below shell out to `gh`. Verify it's on `PATH` before anything else: + +```bash +command -v gh >/dev/null 2>&1 && gh --version | head -1 || echo "MISSING_GH_CLI" +``` + +If the check prints `MISSING_GH_CLI`, stop and ask the user to install it (`brew install gh` on macOS, see https://cli.github.com for other platforms). Do not try to substitute `curl` against the GraphQL API — auth handling diverges and the rest of the workflow assumes `gh`. + +**2. Account identity / repo permission.** The board and its linked repos are under the `microsoft` org; EMU (Enterprise Managed User) tokens typically cannot mutate them even though they can read: + +```bash +gh api graphql -f query='{ viewer { login } repository(owner:"microsoft", name:"fluentui") { viewerPermission } }' +``` + +If `viewerPermission` is `NONE` or the active viewer is an EMU account, stop and suggest `gh auth switch --user `. + +**3. Token scope for ProjectV2 mutations.** `read:project` is enough to fetch items, but the `updateProjectV2ItemFieldValue` mutation requires the unqualified `project` scope. Check scopes directly — the read-query above will pass even when write scope is missing: + +```bash +gh auth status 2>&1 | grep -A0 "Token scopes" | grep -q "'project'," || echo "MISSING_PROJECT_SCOPE" +``` + +If the check prints `MISSING_PROJECT_SCOPE`, stop and ask the user to run `gh auth refresh -s project` (interactive device flow). Tell them to paste `! gh auth refresh -s project` into the prompt so the output lands in this session. Do not try to attempt the mutation and recover — every apply call will fail the same way and waste time. + +Failing loud on any of these is much better than failing at apply time after the user has spent time reviewing recommendations. + +## What each item needs + +For every untriaged board item, produce a recommendation with: + +| Field | Possible values | +| ---------------------- | --------------------------------------------------------------------------------------- | +| `repo` | `microsoft/fluentui` / `microsoft/fluentui-system-icons` / `microsoft/fluentui-contrib` | +| `issue_number` | the issue number in that repo | +| `team` | one of the board's Team options (see `references/team-mapping.md`) | +| `team_confidence` | `high` (clear CODEOWNERS hit), `medium` (partial match), `low` (flag for human) | +| `assignee` | GitHub login from CODEOWNERS, or `null` if only a team handle appears | +| `assignee_reason` | which CODEOWNERS line gave you the user | +| `needs_human_followup` | anything you're unsure about — surface it, don't paper over it | + +Don't invent Team values. The board's Team field has a fixed option set — see `references/team-mapping.md` for the mapping and option IDs. If an item's ownership doesn't map to any listed Team option, mark `team_confidence: low` and flag it for the user. + +## The fetch + +The board is an org-level ProjectV2 with ID `PVT_kwDOAF3p4s4AD4d_` and project number `395`. Items span three repos. The GraphQL API doesn't let you filter on "field not set" directly, so fetch everything and filter client-side. + +Run the fetch script — it paginates through all items (the board has 600+) and applies the **"By team"** view (view 6) filter, which keeps items where ALL of these hold: + +- `content.__typename === "Issue"` (skip DraftIssue and PullRequest) +- `content.state === "OPEN"` +- No `fieldValues` node has `field.name === "Team"` (i.e. Team is unset) +- No `fieldValues` node has `field.name === "Status"` and value `"✅ Done"` +- Labels do NOT include any of: `Help Wanted ✨`, `Type: Epic`, `Needs: Triage :mag:`, `Resolution: Soft Close` + +```bash +node .agents/skills/triage-board/scripts/fetch-untriaged.js > /tmp/board-untriaged.jsonl +``` + +Pass `--count` if you only want the count for a quick sanity check. + +The label exclusions matter: + +- `Needs: Triage :mag:` means the repo-level Shield queue hasn't cleared the issue yet — the `triage-issues` skill handles those first. Assigning a board Team to a repo-level-untriaged issue is premature. +- `Resolution: Soft Close` is applied to stale items that the inactivity bot has closed-adjacent. Routing these to a team wastes cycles on work nobody plans to do. +- `Type: Epic` and `Help Wanted ✨` are intentionally unowned categories on the board — the project owner manages those separately. + +To confirm the canonical filter hasn't drifted, sanity-check view 6's `filter` string before a big triage run: + +```bash +gh api graphql -f query='{ organization(login:"microsoft") { projectV2(number:395) { view(number:6) { filter } } } }' +``` + +## Routing logic + +### Step 1: identify the owning package/area + +For each issue, look at the **body** for the Area/Package field (our issue template has one), the **labels** (a `Package: charting` or `Component: Button` label is a strong signal), and the **title** (component names). For fluentui-contrib and fluentui-system-icons the signal set is smaller — usually the labels carry the area. + +If the issue targets a specific package path inside `fluentui`, grep CODEOWNERS for that path: + +```bash +# Example: react-combobox +grep -n 'react-combobox' .github/CODEOWNERS | head -3 +``` + +For issues in sibling repos (`fluentui-system-icons`, `fluentui-contrib`), fetch that repo's CODEOWNERS: + +```bash +gh api repos/microsoft//contents/.github/CODEOWNERS -H "Accept: application/vnd.github.raw" 2>/dev/null +``` + +CODEOWNERS lines look like: + +``` +packages/react-components/react-tooltip/library @microsoft/cxe-prg @mainframev +``` + +The first `@microsoft/` is the owning team; any `@` entries are the specific owners. + +### Step 2: map the team handle to a board Team value + +See `references/team-mapping.md` for the authoritative mapping. The confident mappings cover most traffic (`cxe-prg`, `cxe-red`, `teams-prg`, `cxe-coastal`, `v-build`, `fluentui-motion`, `xc-uxe`). Unmapped or ambiguous handles (`charting-team`, `fluentui-admins`, `fluentui-northstar`, `azure-design-engineering`, etc.) → `team_confidence: low`, flag for human. + +**Product-override rule — v9 issues never go to cxe-red.** The cxe-red team owns v8 exclusively. If an issue carries the `Fluent UI react-components (v9)` label — even alongside a v8 label, and even when CODEOWNERS resolves to `@microsoft/cxe-red` (e.g. via an older path reference) — route it to `cxe-prg` and flag it for human confirmation. The reverse is fine: v8-only issues stay on cxe-red. When both v8 and v9 labels are present on a single issue, the v9 label wins for routing purposes, since that's where active development happens. + +### Step 3: propose a specific GitHub assignee + +Prefer the individual user(s) named on the CODEOWNERS line. Rules: + +- **One individual + team handle** → propose that individual. This is the common case. +- **Multiple individuals + team handle** → propose the first individual, note the others in `needs_human_followup`. +- **Team handle only (no individual)** → leave `assignee: null` and flag. The user will assign manually since `gh issue edit --add-assignee` doesn't accept teams. +- **Existing assignee on the issue** → don't overwrite. Note the current assignee in the recommendation and only set the Team field. + +For cross-cutting issues (issue body mentions multiple components, or the author didn't pick one), default to team-only routing — flag for human rather than guess a person. + +## Workflow + +### Step 1 — Fetch + +Run the preflight auth check, then fetch all project items (paginated). Filter to the untriaged open-issue subset. Print the count and a one-line summary per item (`#` / `` / `` / current assignees if any). If the list is empty, tell the user and stop. + +### Step 2 — Classify + +For each item, go through the routing logic. Cache any CODEOWNERS file you fetch (at least per-session) so you don't re-fetch for every item in the same repo. + +Don't dump raw CODEOWNERS lines at the user — extract the team + user you care about and move on. + +### Step 3 — Present recommendations + +Show a single table the user can scan. Suggested columns: `#`, `repo-short` (e.g. `fluentui`, `contrib`, `icons`), `title` (truncated), `team`, `confidence`, `assignee`, `notes`. Group or sort by team so the user can spot clusters — it makes spot-checks faster. + +If there are low-confidence items or items needing human follow-up, call those out in a section above the table so they don't hide inside the noise. + +### Step 4 — Ask for approval + +Ask: "Apply all / apply specific numbers / skip / edit?". Accept `apply all`, `apply 36012 36016`, `skip 35998`, or free-form corrections (e.g. "for #35976 use cxe-prg instead"). Don't proceed to apply until the user responds. + +### Step 5 — Apply approved changes + +Two mutations per approved item: + +**Set the board Team field** (GraphQL mutation; `gh project item-edit` can also do this but GraphQL is more transparent): + +```bash +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="PVT_kwDOAF3p4s4AD4d_" \ + -f itemId="" \ + -f fieldId="PVTSSF_lADOAF3p4s4AD4d_zgCPFLY" \ + -f optionId="" +``` + +If you applied a wrong Team and need to undo it before the user notices, swap `updateProjectV2ItemFieldValue` for `clearProjectV2ItemFieldValue` and drop the `value` / `optionId` arg. + +**Set the GitHub-issue assignee** (only if the recommendation has a specific user, and the issue doesn't already have an assignee): + +```bash +gh issue edit --repo --add-assignee +``` + +Apply one item at a time and print a one-line result per item. Do not retry blindly on failure — stop and ask the user what to do. If a specific item fails (e.g. the assignee user isn't in the org), skip it and continue with the rest, then surface the failed ones in the summary. + +### Step 6 — Summarize + +Print: X items triaged (team set + assignee), Y items team-only (flagged for human to pick an assignee), Z items skipped or failed (with the specific reason per item). + +## Anti-patterns + +- **Don't touch `Status`.** The user wants items to stay in `🌱 New` after triage — only the Team field changes. Setting Status is out of scope. +- **Don't touch labels** on the underlying issue. Label triage belongs to the `triage-issues` skill. +- **Don't assume a single CODEOWNERS file.** Cross-repo items need the CODEOWNERS fetched from their own repo. +- **Don't guess an assignee for team-only CODEOWNERS lines.** Better to leave `assignee: null` and let the user pick. +- **Don't overwrite existing assignees.** If the issue already has one, respect it — your job is to set the Team, not re-assign work. +- **Don't retry on auth failure.** If the mutation returns `Unauthorized: As an Enterprise Managed User, you cannot access this content`, the whole batch will fail the same way. Surface it and stop. + +## Reference files + +- `references/team-mapping.md` — CODEOWNERS handle → board Team option (name + option ID) mapping, plus the known-ambiguous handles that should trip `team_confidence: low`. +- `scripts/fetch-untriaged.js` — paginated fetch + view-6 filter; emits JSONL of untriaged open issues. diff --git a/.agents/skills/triage-board/references/team-mapping.md b/.agents/skills/triage-board/references/team-mapping.md new file mode 100644 index 00000000000000..0a9b94583c8ec9 --- /dev/null +++ b/.agents/skills/triage-board/references/team-mapping.md @@ -0,0 +1,74 @@ +# CODEOWNERS handle → board Team mapping + +The "Fluent UI - Unified" board (org project `microsoft/projects/395`) has a fixed set of options for its `Team` single-select field. CODEOWNERS uses a wider set of team handles than the board tracks, so not every CODEOWNERS entry maps cleanly — the skill should flag uncertain ones for human review rather than guess. + +To refresh the option IDs against the live project (they're stable but not immortal): + +```bash +gh api graphql -f query='{ + organization(login:"microsoft") { + projectV2(number:395) { + id + fields(first:30) { + nodes { + ... on ProjectV2SingleSelectField { id name options { id name } } + } + } + } + } +}' | jq '.data.organization.projectV2 | {id, teamField: (.fields.nodes[] | select(.name=="Team"))}' +``` + +## Project + field IDs (as of this skill's authoring) + +- `projectId` = `PVT_kwDOAF3p4s4AD4d_` +- `teamFieldId` = `PVTSSF_lADOAF3p4s4AD4d_zgCPFLY` + +## Board Team options + +| Board option | Option ID | +| ----------------- | ---------- | +| `cxe-prg` | `5aacad01` | +| `cxe-red` | `d78a8f20` | +| `cxe-coastal` | `40933abb` | +| `teams-prg` | `64f7bd9e` | +| `v-a11y` | `4eda3bd1` | +| `v-build` | `82c4d92c` | +| `v-migration` | `3a22f81f` | +| `v-perf` | `15907658` | +| `v-pm` | `96a8ae0f` | +| `contributor` | `fe8e8988` | +| `xc-uxe` | `e7e0e0e0` | +| `fluentui-motion` | `207075c9` | + +## Confident mappings + +Use these for automatic routing. Evidence is either docs/team-routing.md, a well-known team purpose, or repeated usage in CODEOWNERS that corresponds to a single product area. + +| CODEOWNERS handle | Board Team | Notes | +| ------------------------------------ | ----------------- | -------------------------------------------------------------- | +| `@microsoft/cxe-prg` | `cxe-prg` | v9 component ownership (most of `packages/react-components/*`) | +| `@microsoft/cxe-red` | `cxe-red` | v8 component ownership (most of `packages/react/*`) | +| `@microsoft/teams-prg` | `teams-prg` | Teams-owned packages in fluentui + contrib | +| `@microsoft/fluentui-react-build` | `v-build` | build tooling, `tools/*`, `.github/*`, root configs | +| `@microsoft/fui-wc` | `cxe-coastal` | web components team (owns `packages/web-components/*`) | +| `@microsoft/fluent-motion-framework` | `fluentui-motion` | motion / animation primitives | +| `@microsoft/xc-uxe` | `xc-uxe` | appears in contrib for UXE-owned packages | +| `@microsoft/fluentui-react` | `cxe-red` | older v8 alias (docs/team-routing.md maps v8 to cxe-red) | + +## Ambiguous / flag-for-human + +Don't auto-route these. Set `team_confidence: low` and put a specific ask in `needs_human_followup` so the maintainer can pick. + +| CODEOWNERS handle | Why ambiguous | +| ------------------------------------- | ----------------------------------------------------------------------------------- | +| `@microsoft/charting-team` | No board Team option — charting doesn't have its own column in this project | +| `@microsoft/fluentui-northstar` | Northstar is EOL (v0); routing depends on whether the item should be closed instead | +| `@microsoft/fluentui-v` | Too generic — could be v8 or v9 | +| `@microsoft/fluentui-admins` | Meta / admin team, not a product team | +| `@microsoft/azure-design-engineering` | Azure partner team in contrib; unclear if they self-triage or roll up to xc-uxe | +| `@microsoft/cap-theme` | Contrib-only team; unclear board mapping | +| `@microsoft/ms-fabric` | Contrib-only team; unclear board mapping | +| `@microsoft/fluentui-variant-theme` | Contrib-only team; unclear board mapping | + +When you encounter an unmapped handle not in either table, treat it as ambiguous by default and surface it. The list above is expected to grow — add new confident mappings only when the user confirms them explicitly. diff --git a/.agents/skills/triage-board/scripts/fetch-untriaged.js b/.agents/skills/triage-board/scripts/fetch-untriaged.js new file mode 100755 index 00000000000000..bac0041ba51c35 --- /dev/null +++ b/.agents/skills/triage-board/scripts/fetch-untriaged.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node +// Fetch all items from the Fluent UI Unified board (microsoft/projects/395), +// page through them via GraphQL, and emit JSONL of items that match the +// board's "By team" view (view 6) filter: +// - content is an Issue (skip DraftIssue, PullRequest) +// - issue is OPEN +// - Team field is unset +// - Status is not "✅ Done" +// - Labels do not include Help Wanted ✨ / Type: Epic / Needs: Triage :mag: / Resolution: Soft Close +// +// Usage: +// node fetch-untriaged.js # JSONL to stdout +// node fetch-untriaged.js --count # just print the count +// +// Requires the GitHub CLI (`gh`) installed and authenticated. Run the +// SKILL.md preflight first. + +const { execFileSync } = require('node:child_process'); + +const PROJECT_NUMBER = 395; +const ORG = 'microsoft'; +const PAGE_SIZE = 100; +const MAX_PAGES = 20; + +const EXCLUDE_LABELS = new Set(['Help Wanted ✨', 'Type: Epic', 'Needs: Triage :mag:', 'Resolution: Soft Close']); + +const QUERY = ` +query ($cursor: String) { + organization(login: "${ORG}") { + projectV2(number: ${PROJECT_NUMBER}) { + items(first: ${PAGE_SIZE}, after: $cursor) { + pageInfo { hasNextPage endCursor } + nodes { + id + content { + __typename + ... on Issue { + number title url state body + repository { nameWithOwner } + assignees(first: 5) { nodes { login } } + labels(first: 20) { nodes { name } } + } + ... on DraftIssue { title } + ... on PullRequest { number title url state } + } + fieldValues(first: 30) { + nodes { + __typename + ... on ProjectV2ItemFieldSingleSelectValue { + field { ... on ProjectV2SingleSelectField { name } } + name + } + } + } + } + } + } + } +}`; + +function ghGraphQL(cursor) { + const args = ['api', 'graphql', '-f', `query=${QUERY}`]; + if (cursor) args.push('-F', `cursor=${cursor}`); + const out = execFileSync('gh', args, { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 }); + return JSON.parse(out); +} + +function isUntriagedOpenIssue(item) { + const c = item.content; + if (!c || c.__typename !== 'Issue' || c.state !== 'OPEN') return false; + + const labels = new Set((c.labels?.nodes ?? []).map(l => l.name)); + for (const ex of EXCLUDE_LABELS) if (labels.has(ex)) return false; + + let hasTeam = false; + let statusDone = false; + for (const fv of item.fieldValues?.nodes ?? []) { + const fieldName = fv.field?.name; + if (fieldName === 'Team') hasTeam = true; + if (fieldName === 'Status' && fv.name === '✅ Done') statusDone = true; + } + return !hasTeam && !statusDone; +} + +function main() { + const countOnly = process.argv.includes('--count'); + + let cursor = null; + let total = 0; + for (let page = 0; page < MAX_PAGES; page++) { + const resp = ghGraphQL(cursor); + if (resp.errors) { + console.error('GraphQL errors:', JSON.stringify(resp.errors, null, 2)); + process.exit(1); + } + const items = resp.data.organization.projectV2.items; + for (const node of items.nodes) { + if (isUntriagedOpenIssue(node)) { + total++; + if (!countOnly) process.stdout.write(JSON.stringify(node) + '\n'); + } + } + if (!items.pageInfo.hasNextPage) break; + cursor = items.pageInfo.endCursor; + } + + if (countOnly) console.log(total); + else console.error(`# ${total} untriaged item(s)`); +} + +main(); diff --git a/.claude/skills/triage-board/SKILL.md b/.claude/skills/triage-board/SKILL.md new file mode 100644 index 00000000000000..e954a19497cd11 --- /dev/null +++ b/.claude/skills/triage-board/SKILL.md @@ -0,0 +1 @@ +@../../../.agents/skills/triage-board/SKILL.md