Skip to content
Open
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
127 changes: 127 additions & 0 deletions .claude/skills/pr-triage/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
name: pr-triage
description: Use this skill when the user asks to "triage PRs", "check new PRs", "review open PRs", "find PRs missing copilot review", or otherwise wants a sweep of open pull requests on the current GitHub repository to surface ones missing a Copilot review request and ones whose commits do not follow the Linux kernel-style format used in this repo.
---

# PR Triage

## Purpose

Sweep open PRs on the current repo and produce a short, actionable report:

1. **PRs missing a Copilot review request** — flag so the user can request one.
2. **PRs whose commits do not follow Linux kernel-style** — flag and draft a ready-to-paste reply.

Output is read by the user to decide what to do next. Do not request reviews, push commits, or post comments without explicit instruction.

## Step 1 — Resolve the repo

Run from the current working directory:

```bash
gh repo view --json nameWithOwner -q .nameWithOwner
```

If this errors, stop and tell the user the directory is not a GitHub repo (or `gh` is not authenticated).

## Step 2 — List candidate PRs (open, not draft, no human reviews yet)

```bash
gh pr list \
--search "is:open is:pr draft:false review:none" \
--json number,title,author,headRefName,url \
--limit 50
```

Notes:
- `review:none` excludes PRs that already have any **human** review. (A Copilot review does **not** count as a human review for this filter, so PRs Copilot has reviewed but no human has are still in scope — that is intentional.)
- `draft:false` excludes drafts; mention this in the output.
- If the list is empty, report "no open PRs needing triage" and stop.

## Step 3 — Detect missing Copilot review request

**Do not parse `reviewRequests` JSON for this.** Copilot does **not** appear in the `reviewRequests` field returned by `gh pr view --json reviewRequests` — neither as a User nor as a Bot. The only reliable signal is GitHub's search filters.

Run a second search that returns the candidate PRs **missing** Copilot involvement (neither requested nor already reviewed by Copilot):

```bash
gh pr list \
--search "is:open is:pr draft:false review:none -review-requested:app/copilot-pull-request-reviewer -reviewed-by:app/copilot-pull-request-reviewer" \
--json number \
--limit 50
```

Any PR number in this result is **missing Copilot**. PR numbers in Step 2's list but **not** in this list have Copilot involved (either requested or already reviewed).

The reviewer slug is `app/copilot-pull-request-reviewer`. If your org uses a different Copilot app login, change the slug — but verify by running:

```bash
gh pr list --search "is:open is:pr reviewed-by:app/copilot-pull-request-reviewer" --json number --limit 3
```

If that returns nothing for a repo you know Copilot has reviewed, the slug is wrong for this org.

## Step 4 — Check commits for Linux kernel-style

For each PR, fetch its commits:

```bash
gh pr view <number> --json commits -q '.commits[].messageHeadline'
```

A commit **passes** if its subject line matches the convention used in this repo's `git log`:

- Format: `<area>[: <subarea>]*: <Capitalized imperative description>`
- Areas seen in this repo: `frontend`, `backend`, `app`, `docs`, `chocolatey`, `ci`, plus nested forms like `backend: server:` or `frontend: common/ReleaseNotes/ReleaseNotes:`.
- Subject ≤ ~75 characters, no trailing period, imperative mood (`Add`, `Fix`, `Bump`, `Refactor`, not `Added`/`Adding`/`Fixes`).
- Conventional-commit prefixes (`feat:`, `fix:`, `chore:`, `feat(scope):`) are **not** this repo's style — flag them.
- Lowercase-only or punctuation-only subjects (`update stuff`, `wip`, `.`) are flagged.

If unsure about borderline cases, sanity-check against recent history:

```bash
git log --no-merges --format='%s' -30
```

A PR **fails** the check if **any** of its commits fail. Record which commits failed and why (one short reason each).

## Step 5 — Report

Print a single compact report. Use this shape:

```
## PR triage — <N> open PR(s) without a review (drafts excluded)

### ✅ Clean
- #1234 — Title (author) — <url>

### ⚠️ Missing Copilot review
- #1235 — Title (author) — <url>
- #1236 — Title (author) — <url>

### ⚠️ Commit messages need cleanup
- #1237 — Title (author) — <url>
- "feat: add thing" — uses Conventional-commit prefix; expected `<area>: <Description>`
- "wip" — non-descriptive
- #1238 — Title (author) — <url>
- "fixed bug" — past tense; expected imperative ("Fix …")

PRs in both ⚠️ sections appear in both.
```

For each PR in **Commit messages need cleanup**, also include a ready-to-paste reply block. Use the user's preferred wording verbatim — do not paraphrase:

```
**Reply for #1237:**
> The commit messages just need a quick cleanup to match our Linux kernel–style guidelines. The contributing guide and git log both have good examples.
```

If there are zero issues across both checks, just say so in one line.

## Boundaries

- **Read-only.** Do not run `gh pr review`, `gh pr comment`, `gh pr edit`, or any mutating gh command. The user posts replies themselves.
- **Do not** request Copilot as a reviewer automatically.
- **Do not** suggest force-pushing or amending commits on the user's behalf.
- If `gh` is not installed or not authenticated, say so and stop.
- Cap at 50 PRs per run; if there are more, mention it and ask whether to paginate.
33 changes: 33 additions & 0 deletions plugins/headlamp-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,39 @@ headlamp-plugin --help
headlamp-plugin.js uninstall [pluginName] Uninstall the plugin.
```

## Scaffolding the Claude Code agent harness

`create` accepts an opt-in `--with-claude-skills` flag:

```
headlamp-plugin create my-plugin --with-claude-skills
```

In addition to the default scaffold, this adds a Claude Code agent harness for
building the plugin with an AI agent:

- `CLAUDE.md` — always-on agent policy for a Headlamp plugin (replaces the
default `AGENTS.md`).
- `.claude/skills/` — step-by-step skills for the CNCF/CRD plugin workflow
(`create-crd-plugin`, `plan-plugin`, `define-resource`, `add-list-view`,
`add-detail-view`, `add-settings`, `ensure-dependency`, `seed-test-data`,
`run-and-verify`, `document-plugin`).
- `.claude/settings.json` — a permission allowlist for the common dev commands.
- `.mcp.json` — the `kubernetes`, `helm` and `chrome-devtools` MCP servers the
skills use.

Without the flag, `create` behaves exactly as before (default `AGENTS.md`, no
`.claude/` or `.mcp.json`).

To add the harness to an **existing** plugin, pass the same flag to `upgrade`:

```
headlamp-plugin upgrade --with-claude-skills
```

Existing harness files are left untouched, so it is safe to re-run; it only adds
what is missing and drops `AGENTS.md` once `CLAUDE.md` is present.

## Template for installing plugins from a configuration file

plugins.yaml:
Expand Down
81 changes: 77 additions & 4 deletions plugins/headlamp-plugin/bin/headlamp-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,40 @@ const vitePromise = import('vite');
* Copies the files within template, and modifies a couple.
* Then runs "npm ci" inside of the folder.
*
/**
* Adds the opt-in Claude Code agent harness to a plugin folder.
*
* Copies CLAUDE.md, .claude/ (skills + settings) and .mcp.json from the
* "template-claude" folder into dstFolder, and drops AGENTS.md since CLAUDE.md
* supersedes it as the single agent guide. Existing files are left untouched
* (overwrite: false), so it is safe to run against an already-scaffolded plugin.
*
* @param {string} dstFolder - plugin folder to add the harness to.
*/
function addClaudeHarness(dstFolder) {
const claudeTemplateFolder = path.resolve(__dirname, '..', 'template-claude');
console.log('Adding Claude Code agent skills (CLAUDE.md, .claude/, .mcp.json)');
fs.copySync(claudeTemplateFolder, dstFolder, {
overwrite: false,
errorOnExist: false,
});
const agentsPath = path.join(dstFolder, 'AGENTS.md');
if (fs.existsSync(agentsPath)) {
fs.removeSync(agentsPath);
}
}

/**
* @param {string} name - name of package and output folder.
* @param {boolean} link - if we link @kinvolk/headlamp-plugin for testing
* @param {boolean} noInstall - if we skip installing with "npm ci"
* @param {boolean} withClaudeSkills - if we also scaffold the Claude Code agent
* harness (CLAUDE.md, .claude/skills, .claude/settings.json, .mcp.json) from
* the "template-claude" folder. When set, the default template's AGENTS.md is
* dropped in favour of CLAUDE.md.
* @returns {0 | 1 | 2 | 3} Exit code, where 0 is success, 1, 2, and 3 are failures.
*/
function create(name, link, noInstall) {
function create(name, link, noInstall, withClaudeSkills) {
const dstFolder = name;
const templateFolder = path.resolve(__dirname, '..', 'template');
const indexPath = path.join(dstFolder, 'src', 'index.tsx');
Expand Down Expand Up @@ -98,6 +126,13 @@ function create(name, link, noInstall) {
replaceFileVariables(indexPath);
replaceFileVariables(readmePath);

// Opt-in Claude Code agent harness. Copied from a separate "template-claude"
// folder so the default scaffold stays untouched unless --with-claude-skills
// is passed.
if (withClaudeSkills) {
addClaudeHarness(dstFolder);
}

// This can be used to make testing locally easier.
if (link) {
console.log('Linking @kinvolk/headlamp-plugin');
Expand Down Expand Up @@ -945,9 +980,12 @@ function getNpmOutdated() {
* @param packageFolder {string} - folder where the package, or folder of packages is.
* @parm skipPackageUpdates {boolean} - do not upgrade packages if true.
* @param headlampPluginVersion {string} - tag or version of headlamp-plugin to upgrade to.
* @param withClaudeSkills {boolean} - if true, add the Claude Code agent harness
* (CLAUDE.md, .claude/, .mcp.json) to the package(s) being upgraded. Existing
* harness files are left untouched; AGENTS.md is dropped in favour of CLAUDE.md.
* @returns {0 | 1} Exit code, where 0 is success, 1 is failure.
*/
function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion) {
function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion, withClaudeSkills) {
/**
* Files from the template might not be there.
*
Expand All @@ -965,6 +1003,17 @@ function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion) {
'tsconfig.json',
'AGENTS.md',
];

// Plugins scaffolded with `create --with-claude-skills` use CLAUDE.md as the
// single agent guide instead of AGENTS.md, so don't reintroduce AGENTS.md on
// upgrade for them.
if (fs.existsSync('CLAUDE.md')) {
const agentsIndex = missingFiles.indexOf('AGENTS.md');
if (agentsIndex !== -1) {
missingFiles.splice(agentsIndex, 1);
}
}

const templateFolder = path.resolve(__dirname, '..', 'template');

missingFiles.forEach(pathToCheck => {
Expand Down Expand Up @@ -1194,6 +1243,12 @@ function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion) {
process.chdir(folder);
console.log(`Upgrading "${folder}"...`);

// Opt-in: add the Claude Code harness. Run before addMissingTemplateFiles so
// that CLAUDE.md exists and AGENTS.md is not (re)added for harness plugins.
if (withClaudeSkills) {
addClaudeHarness('.');
}

addMissingTemplateFiles();
addMissingConfiguration();
removeFiles();
Expand Down Expand Up @@ -1756,11 +1811,17 @@ yargs(process.argv.slice(2))
.option('noinstall', {
describe: 'Skip installing dependencies with npm ci',
type: 'boolean',
})
.option('with-claude-skills', {
describe:
'Also scaffold the Claude Code agent harness (CLAUDE.md, .claude/skills, ' +
'.claude/settings.json, .mcp.json). Replaces the default AGENTS.md with CLAUDE.md.',
type: 'boolean',
});
},
argv => {
// @ts-ignore
process.exitCode = create(argv.name, argv.link, argv.noinstall);
process.exitCode = create(argv.name, argv.link, argv.noinstall, argv['with-claude-skills']);
}
)
.command(
Expand Down Expand Up @@ -1978,11 +2039,23 @@ yargs(process.argv.slice(2))
describe:
'Use a specific headlamp-plugin-version when upgrading packages. Defaults to "latest".',
type: 'string',
})
.option('with-claude-skills', {
describe:
'Add the Claude Code agent harness (CLAUDE.md, .claude/skills, ' +
'.claude/settings.json, .mcp.json) to the upgraded plugin(s). Replaces AGENTS.md ' +
'with CLAUDE.md. Existing harness files are left untouched.',
type: 'boolean',
});
},
argv => {
// @ts-ignore
process.exitCode = upgrade(argv.package, argv.skipPackageUpdates, argv.headlampPluginVersion);
process.exitCode = upgrade(
argv.package,
argv.skipPackageUpdates,
argv.headlampPluginVersion,
argv['with-claude-skills']
);
}
)
.command(
Expand Down
1 change: 1 addition & 0 deletions plugins/headlamp-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
"bin",
"config",
"template",
"template-claude",
"lib",
"types",
".storybook",
Expand Down
42 changes: 42 additions & 0 deletions plugins/headlamp-plugin/template-claude/.claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"permissions": {
"allow": [
"Skill",
"Bash(npm install:*)",
"Bash(npm start:*)",
"Bash(npm run tsc:*)",
"Bash(npm run lint:*)",
"Bash(npm run lint-fix:*)",
"Bash(npm run build:*)",
"Bash(npm run format:*)",
"Bash(npm run test:*)",
"Bash(npm run i18n:*)",
"Bash(npm run package:*)",
"Bash(npx @kinvolk/headlamp-plugin:*)",
"Bash(npx tsc:*)",
"Read(.claude/**)",
"Read(node_modules/@kinvolk/headlamp-plugin/**)",
"Read(/tmp/**)",
"Bash(echo:*)",
"Bash(ls:*)",
"Bash(cat:*)",
"Bash(grep:*)",
"Bash(find:*)",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(wc:*)",
"Bash(which:*)",
"Bash(sleep:*)",
"Bash(mkdir:*)",
"Bash(cp:*)",
"Bash(kubectl get:*)",
"Bash(kubectl describe:*)",
"Bash(kubectl explain:*)",
"Bash(kubectl wait:*)",
"mcp__kubernetes__*",
"mcp__chrome-devtools__*"
],
"ask": [],
"deny": []
}
}
Loading
Loading