Skip to content

Commit b6feab5

Browse files
committed
headlamp-plugin: add opt-in --with-claude-skills to create/upgrade
Scaffolds a Claude Code agent harness (CLAUDE.md, .claude/skills, .claude/settings.json, .mcp.json) when the flag is passed; default scaffold path is unchanged. CLAUDE.md replaces AGENTS.md when present. Signed-off-by: yolossn <sannagaraj@microsoft.com>
1 parent cbc0f31 commit b6feab5

18 files changed

Lines changed: 1801 additions & 5 deletions

File tree

.claude/skills/pr-triage/SKILL.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
---
2+
name: pr-triage
3+
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.
4+
---
5+
6+
# PR Triage
7+
8+
## Purpose
9+
10+
Sweep open PRs on the current repo and produce a short, actionable report:
11+
12+
1. **PRs missing a Copilot review request** — flag so the user can request one.
13+
2. **PRs whose commits do not follow Linux kernel-style** — flag and draft a ready-to-paste reply.
14+
15+
Output is read by the user to decide what to do next. Do not request reviews, push commits, or post comments without explicit instruction.
16+
17+
## Step 1 — Resolve the repo
18+
19+
Run from the current working directory:
20+
21+
```bash
22+
gh repo view --json nameWithOwner -q .nameWithOwner
23+
```
24+
25+
If this errors, stop and tell the user the directory is not a GitHub repo (or `gh` is not authenticated).
26+
27+
## Step 2 — List candidate PRs (open, not draft, no human reviews yet)
28+
29+
```bash
30+
gh pr list \
31+
--search "is:open is:pr draft:false review:none" \
32+
--json number,title,author,headRefName,url \
33+
--limit 50
34+
```
35+
36+
Notes:
37+
- `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.)
38+
- `draft:false` excludes drafts; mention this in the output.
39+
- If the list is empty, report "no open PRs needing triage" and stop.
40+
41+
## Step 3 — Detect missing Copilot review request
42+
43+
**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.
44+
45+
Run a second search that returns the candidate PRs **missing** Copilot involvement (neither requested nor already reviewed by Copilot):
46+
47+
```bash
48+
gh pr list \
49+
--search "is:open is:pr draft:false review:none -review-requested:app/copilot-pull-request-reviewer -reviewed-by:app/copilot-pull-request-reviewer" \
50+
--json number \
51+
--limit 50
52+
```
53+
54+
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).
55+
56+
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:
57+
58+
```bash
59+
gh pr list --search "is:open is:pr reviewed-by:app/copilot-pull-request-reviewer" --json number --limit 3
60+
```
61+
62+
If that returns nothing for a repo you know Copilot has reviewed, the slug is wrong for this org.
63+
64+
## Step 4 — Check commits for Linux kernel-style
65+
66+
For each PR, fetch its commits:
67+
68+
```bash
69+
gh pr view <number> --json commits -q '.commits[].messageHeadline'
70+
```
71+
72+
A commit **passes** if its subject line matches the convention used in this repo's `git log`:
73+
74+
- Format: `<area>[: <subarea>]*: <Capitalized imperative description>`
75+
- Areas seen in this repo: `frontend`, `backend`, `app`, `docs`, `chocolatey`, `ci`, plus nested forms like `backend: server:` or `frontend: common/ReleaseNotes/ReleaseNotes:`.
76+
- Subject ≤ ~75 characters, no trailing period, imperative mood (`Add`, `Fix`, `Bump`, `Refactor`, not `Added`/`Adding`/`Fixes`).
77+
- Conventional-commit prefixes (`feat:`, `fix:`, `chore:`, `feat(scope):`) are **not** this repo's style — flag them.
78+
- Lowercase-only or punctuation-only subjects (`update stuff`, `wip`, `.`) are flagged.
79+
80+
If unsure about borderline cases, sanity-check against recent history:
81+
82+
```bash
83+
git log --no-merges --format='%s' -30
84+
```
85+
86+
A PR **fails** the check if **any** of its commits fail. Record which commits failed and why (one short reason each).
87+
88+
## Step 5 — Report
89+
90+
Print a single compact report. Use this shape:
91+
92+
```
93+
## PR triage — <N> open PR(s) without a review (drafts excluded)
94+
95+
### ✅ Clean
96+
- #1234 — Title (author) — <url>
97+
98+
### ⚠️ Missing Copilot review
99+
- #1235 — Title (author) — <url>
100+
- #1236 — Title (author) — <url>
101+
102+
### ⚠️ Commit messages need cleanup
103+
- #1237 — Title (author) — <url>
104+
- "feat: add thing" — uses Conventional-commit prefix; expected `<area>: <Description>`
105+
- "wip" — non-descriptive
106+
- #1238 — Title (author) — <url>
107+
- "fixed bug" — past tense; expected imperative ("Fix …")
108+
109+
PRs in both ⚠️ sections appear in both.
110+
```
111+
112+
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:
113+
114+
```
115+
**Reply for #1237:**
116+
> 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.
117+
```
118+
119+
If there are zero issues across both checks, just say so in one line.
120+
121+
## Boundaries
122+
123+
- **Read-only.** Do not run `gh pr review`, `gh pr comment`, `gh pr edit`, or any mutating gh command. The user posts replies themselves.
124+
- **Do not** request Copilot as a reviewer automatically.
125+
- **Do not** suggest force-pushing or amending commits on the user's behalf.
126+
- If `gh` is not installed or not authenticated, say so and stop.
127+
- Cap at 50 PRs per run; if there are more, mention it and ask whether to paginate.

plugins/headlamp-plugin/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,39 @@ headlamp-plugin --help
6666
headlamp-plugin.js uninstall [pluginName] Uninstall the plugin.
6767
```
6868

69+
## Scaffolding the Claude Code agent harness
70+
71+
`create` accepts an opt-in `--with-claude-skills` flag:
72+
73+
```
74+
headlamp-plugin create my-plugin --with-claude-skills
75+
```
76+
77+
In addition to the default scaffold, this adds a Claude Code agent harness for
78+
building the plugin with an AI agent:
79+
80+
- `CLAUDE.md` — always-on agent policy for a Headlamp plugin (replaces the
81+
default `AGENTS.md`).
82+
- `.claude/skills/` — step-by-step skills for the CNCF/CRD plugin workflow
83+
(`create-crd-plugin`, `plan-plugin`, `define-resource`, `add-list-view`,
84+
`add-detail-view`, `add-settings`, `ensure-dependency`, `seed-test-data`,
85+
`run-and-verify`, `document-plugin`).
86+
- `.claude/settings.json` — a permission allowlist for the common dev commands.
87+
- `.mcp.json` — the `kubernetes`, `helm` and `chrome-devtools` MCP servers the
88+
skills use.
89+
90+
Without the flag, `create` behaves exactly as before (default `AGENTS.md`, no
91+
`.claude/` or `.mcp.json`).
92+
93+
To add the harness to an **existing** plugin, pass the same flag to `upgrade`:
94+
95+
```
96+
headlamp-plugin upgrade --with-claude-skills
97+
```
98+
99+
Existing harness files are left untouched, so it is safe to re-run; it only adds
100+
what is missing and drops `AGENTS.md` once `CLAUDE.md` is present.
101+
69102
## Template for installing plugins from a configuration file
70103

71104
plugins.yaml:

plugins/headlamp-plugin/bin/headlamp-plugin.js

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,40 @@ const vitePromise = import('vite');
4747
* Copies the files within template, and modifies a couple.
4848
* Then runs "npm ci" inside of the folder.
4949
*
50+
/**
51+
* Adds the opt-in Claude Code agent harness to a plugin folder.
52+
*
53+
* Copies CLAUDE.md, .claude/ (skills + settings) and .mcp.json from the
54+
* "template-claude" folder into dstFolder, and drops AGENTS.md since CLAUDE.md
55+
* supersedes it as the single agent guide. Existing files are left untouched
56+
* (overwrite: false), so it is safe to run against an already-scaffolded plugin.
57+
*
58+
* @param {string} dstFolder - plugin folder to add the harness to.
59+
*/
60+
function addClaudeHarness(dstFolder) {
61+
const claudeTemplateFolder = path.resolve(__dirname, '..', 'template-claude');
62+
console.log('Adding Claude Code agent skills (CLAUDE.md, .claude/, .mcp.json)');
63+
fs.copySync(claudeTemplateFolder, dstFolder, {
64+
overwrite: false,
65+
errorOnExist: false,
66+
});
67+
const agentsPath = path.join(dstFolder, 'AGENTS.md');
68+
if (fs.existsSync(agentsPath)) {
69+
fs.removeSync(agentsPath);
70+
}
71+
}
72+
73+
/**
5074
* @param {string} name - name of package and output folder.
5175
* @param {boolean} link - if we link @kinvolk/headlamp-plugin for testing
5276
* @param {boolean} noInstall - if we skip installing with "npm ci"
77+
* @param {boolean} withClaudeSkills - if we also scaffold the Claude Code agent
78+
* harness (CLAUDE.md, .claude/skills, .claude/settings.json, .mcp.json) from
79+
* the "template-claude" folder. When set, the default template's AGENTS.md is
80+
* dropped in favour of CLAUDE.md.
5381
* @returns {0 | 1 | 2 | 3} Exit code, where 0 is success, 1, 2, and 3 are failures.
5482
*/
55-
function create(name, link, noInstall) {
83+
function create(name, link, noInstall, withClaudeSkills) {
5684
const dstFolder = name;
5785
const templateFolder = path.resolve(__dirname, '..', 'template');
5886
const indexPath = path.join(dstFolder, 'src', 'index.tsx');
@@ -98,6 +126,13 @@ function create(name, link, noInstall) {
98126
replaceFileVariables(indexPath);
99127
replaceFileVariables(readmePath);
100128

129+
// Opt-in Claude Code agent harness. Copied from a separate "template-claude"
130+
// folder so the default scaffold stays untouched unless --with-claude-skills
131+
// is passed.
132+
if (withClaudeSkills) {
133+
addClaudeHarness(dstFolder);
134+
}
135+
101136
// This can be used to make testing locally easier.
102137
if (link) {
103138
console.log('Linking @kinvolk/headlamp-plugin');
@@ -945,9 +980,12 @@ function getNpmOutdated() {
945980
* @param packageFolder {string} - folder where the package, or folder of packages is.
946981
* @parm skipPackageUpdates {boolean} - do not upgrade packages if true.
947982
* @param headlampPluginVersion {string} - tag or version of headlamp-plugin to upgrade to.
983+
* @param withClaudeSkills {boolean} - if true, add the Claude Code agent harness
984+
* (CLAUDE.md, .claude/, .mcp.json) to the package(s) being upgraded. Existing
985+
* harness files are left untouched; AGENTS.md is dropped in favour of CLAUDE.md.
948986
* @returns {0 | 1} Exit code, where 0 is success, 1 is failure.
949987
*/
950-
function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion) {
988+
function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion, withClaudeSkills) {
951989
/**
952990
* Files from the template might not be there.
953991
*
@@ -965,6 +1003,17 @@ function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion) {
9651003
'tsconfig.json',
9661004
'AGENTS.md',
9671005
];
1006+
1007+
// Plugins scaffolded with `create --with-claude-skills` use CLAUDE.md as the
1008+
// single agent guide instead of AGENTS.md, so don't reintroduce AGENTS.md on
1009+
// upgrade for them.
1010+
if (fs.existsSync('CLAUDE.md')) {
1011+
const agentsIndex = missingFiles.indexOf('AGENTS.md');
1012+
if (agentsIndex !== -1) {
1013+
missingFiles.splice(agentsIndex, 1);
1014+
}
1015+
}
1016+
9681017
const templateFolder = path.resolve(__dirname, '..', 'template');
9691018

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

1246+
// Opt-in: add the Claude Code harness. Run before addMissingTemplateFiles so
1247+
// that CLAUDE.md exists and AGENTS.md is not (re)added for harness plugins.
1248+
if (withClaudeSkills) {
1249+
addClaudeHarness('.');
1250+
}
1251+
11971252
addMissingTemplateFiles();
11981253
addMissingConfiguration();
11991254
removeFiles();
@@ -1756,11 +1811,17 @@ yargs(process.argv.slice(2))
17561811
.option('noinstall', {
17571812
describe: 'Skip installing dependencies with npm ci',
17581813
type: 'boolean',
1814+
})
1815+
.option('with-claude-skills', {
1816+
describe:
1817+
'Also scaffold the Claude Code agent harness (CLAUDE.md, .claude/skills, ' +
1818+
'.claude/settings.json, .mcp.json). Replaces the default AGENTS.md with CLAUDE.md.',
1819+
type: 'boolean',
17591820
});
17601821
},
17611822
argv => {
17621823
// @ts-ignore
1763-
process.exitCode = create(argv.name, argv.link, argv.noinstall);
1824+
process.exitCode = create(argv.name, argv.link, argv.noinstall, argv['with-claude-skills']);
17641825
}
17651826
)
17661827
.command(
@@ -1978,11 +2039,23 @@ yargs(process.argv.slice(2))
19782039
describe:
19792040
'Use a specific headlamp-plugin-version when upgrading packages. Defaults to "latest".',
19802041
type: 'string',
2042+
})
2043+
.option('with-claude-skills', {
2044+
describe:
2045+
'Add the Claude Code agent harness (CLAUDE.md, .claude/skills, ' +
2046+
'.claude/settings.json, .mcp.json) to the upgraded plugin(s). Replaces AGENTS.md ' +
2047+
'with CLAUDE.md. Existing harness files are left untouched.',
2048+
type: 'boolean',
19812049
});
19822050
},
19832051
argv => {
19842052
// @ts-ignore
1985-
process.exitCode = upgrade(argv.package, argv.skipPackageUpdates, argv.headlampPluginVersion);
2053+
process.exitCode = upgrade(
2054+
argv.package,
2055+
argv.skipPackageUpdates,
2056+
argv.headlampPluginVersion,
2057+
argv['with-claude-skills']
2058+
);
19862059
}
19872060
)
19882061
.command(

plugins/headlamp-plugin/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
"bin",
135135
"config",
136136
"template",
137+
"template-claude",
137138
"lib",
138139
"types",
139140
".storybook",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Skill",
5+
"Bash(npm install:*)",
6+
"Bash(npm start:*)",
7+
"Bash(npm run tsc:*)",
8+
"Bash(npm run lint:*)",
9+
"Bash(npm run lint-fix:*)",
10+
"Bash(npm run build:*)",
11+
"Bash(npm run format:*)",
12+
"Bash(npm run test:*)",
13+
"Bash(npm run i18n:*)",
14+
"Bash(npm run package:*)",
15+
"Bash(npx @kinvolk/headlamp-plugin:*)",
16+
"Bash(npx tsc:*)",
17+
"Read(.claude/**)",
18+
"Read(node_modules/@kinvolk/headlamp-plugin/**)",
19+
"Read(/tmp/**)",
20+
"Bash(echo:*)",
21+
"Bash(ls:*)",
22+
"Bash(cat:*)",
23+
"Bash(grep:*)",
24+
"Bash(find:*)",
25+
"Bash(head:*)",
26+
"Bash(tail:*)",
27+
"Bash(wc:*)",
28+
"Bash(which:*)",
29+
"Bash(sleep:*)",
30+
"Bash(mkdir:*)",
31+
"Bash(cp:*)",
32+
"Bash(kubectl get:*)",
33+
"Bash(kubectl describe:*)",
34+
"Bash(kubectl explain:*)",
35+
"Bash(kubectl wait:*)",
36+
"mcp__kubernetes__*",
37+
"mcp__chrome-devtools__*"
38+
],
39+
"ask": [],
40+
"deny": []
41+
}
42+
}

0 commit comments

Comments
 (0)