Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions .agents/skills/triage-board/SKILL.md
Original file line number Diff line number Diff line change
@@ -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 <non-emu-account>`.

**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/<repo>/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/<handle>` is the owning team; any `@<individual-user>` 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 (`#<num>` / `<repo-short>` / `<title-truncated>` / 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="<PVTI_... from fetch>" \
-f fieldId="PVTSSF_lADOAF3p4s4AD4d_zgCPFLY" \
-f optionId="<team option id from team-mapping.md>"
```

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 <num> --repo <owner/repo> --add-assignee <login>
```

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.
74 changes: 74 additions & 0 deletions .agents/skills/triage-board/references/team-mapping.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading