Add next-dev-loop and next-ppr-optimizer skills (#93943)

Two related skills for the `next dev` agentic workflow. They're paired:
one establishes a shared session and view of the running app, and the
other builds on it for a specific kind of work.

**`next-dev-loop`** is the edit-then-verify rhythm during `next dev`.
After making a change, the agent needs to know whether it actually works
at runtime — not just whether it type-checks or compiles. The skill
teaches the agent to use the two views Next.js exposes about itself:
`/_next/mcp` (framework-side — routes, segments, RSC, server logs,
errors) and `agent-browser` (browser-side — DOM, console, network, React
fiber, vitals). The two views cross-check each other.

The preflight is the load-bearing part. It opens a headed
`agent-browser` session at the user's target URL with React DevTools
enabled, then pauses — because the page might be behind a login wall or
need specific state the user has to set up. Once the user confirms, the
agent probes `/_next/mcp`, confirms Turbopack, and pulls the route map.
Sibling skills can assume this setup is done.

**`next-ppr-optimizer`** is the first sibling. It's the agentic loop for
optimizing the static shell of a `cacheComponents` page: diagnose the
shell, pick the highest-ROI refactor, apply it, confirm the shell grew.
Two levers — push down (extract an I/O into a Suspense-wrapped child so
the parent stays static; autonomous for trivial cases) or cache (wrap in
`'use cache'` with a `cacheLife` profile chosen *with* the user — the
skill never invents one). Diagnosis uses the suspense tree +
`/__nextjs_original-stack-frames` for source resolution, with the H4
anti-pattern guardrail (one dominant boundary covering the viewport is
not a push-down candidate; recurse inside it instead).

<!-- NEXT_JS_LLM_PR -->
This commit is contained in:
Jude Gao
2026-05-21 20:28:51 -04:00
committed by GitHub
parent fa2ec3838a
commit 2e753e236c
6 changed files with 292 additions and 131 deletions
+1
View File
@@ -74,6 +74,7 @@ test-timings.json
.claude/plans/
.claude/todos.json
.claude/worktrees
.claude/scheduled_tasks.lock
CLAUDE.local.md
# Conductor (worktrees created by Conductor app, but keep scripts/)
+1 -12
View File
@@ -46,11 +46,6 @@ export async function copy_docs(task, opts) {
.target('dist/docs')
}
export async function copy_skills(task, opts) {
const skillsSource = join(__dirname, '../../skills')
await task.source(join(skillsSource, '**/*')).target('dist/skills')
}
export async function copy_styled_jsx_assets(task, opts) {
// we copy the styled-jsx types so that we can reference them
// in the next-env.d.ts file so it doesn't matter if the styled-jsx
@@ -2198,13 +2193,7 @@ export async function ncc_safe_stable_stringify(task, opts) {
export async function precompile(task, opts) {
await task.parallel(
[
'browser_polyfills',
'copy_ncced',
'copy_styled_jsx_assets',
'copy_docs',
'copy_skills',
],
['browser_polyfills', 'copy_ncced', 'copy_styled_jsx_assets', 'copy_docs'],
opts
)
}
-20
View File
@@ -1,20 +0,0 @@
---
name: next-compile
description: Check Next.js compilation errors via a running dev server. Turbopack only. MANDATORY after every code edit before reporting work complete. Replaces `next build`.
---
# Check Next.js compilation errors
**MANDATORY**: after any `Edit`/`Write`, run step 2 before telling the user the change is done. Never run `next build`. Turbopack only — the script bails on webpack.
1. Find the port of a running `next dev` — check listening processes. If you can't find it, ask the user.
2. Run:
```bash
node ./scripts/check.mjs <port>
```
3. Fix actionable errors. Ignore noise or pre-existing unrelated failures.
4. Only report the edit complete once step 2 returns clean (or only pre-existing failures).
-99
View File
@@ -1,99 +0,0 @@
#!/usr/bin/env node
/**
* Fetch compilation issues from a running Next.js dev server via its MCP
* endpoint. Prints the tool's raw JSON result to stdout. Cross-platform —
* uses only Node's built-in fetch (Node 18+; Next.js 16 requires Node 20+).
*
* Usage:
* node check.mjs <port>
*
* Output (stdout, on success):
* {"issues":[...]} no errors when issues array is empty
* {"error":"..."} Turbopack project not available (e.g. webpack)
*
* Exit codes:
* 0 MCP call succeeded; JSON printed to stdout
* 1 bad invocation, network failure, or unexpected MCP response shape
* (details on stderr)
*/
const port = process.argv[2]
if (!port || !/^\d+$/.test(port)) {
console.error('Usage: node check.mjs <port>')
process.exit(1)
}
const url = `http://localhost:${port}/_next/mcp`
const headers = {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
}
function parseSSE(text) {
for (const line of text.split('\n')) {
if (line.startsWith('data:')) {
return JSON.parse(line.slice(5).trim())
}
}
return null
}
async function post(body) {
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(body),
})
if (!res.ok) {
throw new Error(`HTTP ${res.status} from ${url}`)
}
return res.text()
}
try {
// MCP handshake (re-done on every invocation — endpoint is stateless per call)
await post({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2025-03-26',
capabilities: {},
clientInfo: { name: 'check-compilation-skill', version: '1.0.0' },
},
})
await post({
jsonrpc: '2.0',
method: 'notifications/initialized',
})
// Tool call
const raw = await post({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: { name: 'get_compilation_issues', arguments: {} },
})
const frame = parseSSE(raw)
if (frame?.error) {
console.error(`MCP error: ${frame.error.message || frame.error}`)
process.exit(1)
}
const text = frame?.result?.content?.[0]?.text
if (typeof text !== 'string') {
console.error('Unexpected MCP response shape')
process.exit(1)
}
// Print the tool result as-is.
process.stdout.write(text)
if (!text.endsWith('\n')) process.stdout.write('\n')
} catch (err) {
console.error(
`Cannot reach dev server on port ${port}: ${err.message}\n` +
'Is `next dev` running on that port?'
)
process.exit(1)
}
+136
View File
@@ -0,0 +1,136 @@
---
name: next-dev-loop
description: >
Verify Next.js runtime behavior after editing app code. Use this
skill to confirm a change actually works in a running app — not
just that it compiles or type-checks. Combines /_next/mcp
(Next.js's view) with agent-browser (the browser's view).
Requires a running `next dev`.
---
# next-dev-loop
The edit/verify rhythm during `next dev` — make a change, then
confirm it actually works at runtime, not only that the types or
the build are happy.
You verify through two views of the same running app:
- **`/_next/mcp`** — an HTTP endpoint Next.js exposes about itself.
Knows framework-specific things: routes, segments, RSC, server
actions, server logs, and errors as Next.js saw them. Call
`tools/list` for the current surface.
- **`agent-browser`** — a CLI that drives a real Chrome. Knows
framework-agnostic browser things: DOM, console, network, React
fiber, vitals. Run `agent-browser --help` for the current surface.
The two views cross-check each other.
## requires
- Next.js **16.3+** with **Turbopack** — `/_next/mcp` plus the
proactive compile check via `get_compilation_issues`.
- `agent-browser` **>= 0.27.0** — when React introspection landed.
These are hard floors, not soft preferences. If anything is missing,
tell the user how to upgrade and stop. Don't fall back to grepping
source or to a weaker probe — this skill assumes both views are live
at the versions above.
- Upgrade Next.js: `pnpm next upgrade` (or `npx next upgrade`).
Docs: https://nextjs.org/docs/app/getting-started/upgrading
(version-16 guide:
https://nextjs.org/docs/app/guides/upgrading/version-16)
- Upgrade `agent-browser`: `npm i -g agent-browser@latest`.
## preflight
Once per session, confirm both views are live.
1. **Open the user's `agent-browser` session at the target URL
with `--headed` and react-devtools enabled, then pause.** The
browser is the user's, not yours; `agent-browser open` is
headless by default, so `--headed` is required. If the page is
behind login, gated by a feature flag, or needs specific state,
the user drives that — log in, set state, navigate. Continue
only after they confirm. Session state is sticky per session:
you can't add `--enable react-devtools` after the session is
open, and `cookies set` on a not-yet-opened session creates a
sessionless cookie that silently fails to apply.
2. POST `tools/list` to `/_next/mcp`. Send
`Accept: application/json, text/event-stream`; responses are
SSE-framed, strip the `data: ` prefix before parsing JSON.
- Unreachable → either `next dev` isn't running, or Next.js is
below 16.3. Check `package.json` to disambiguate, then refuse.
- `get_compilation_issues` not in the list → Next.js below 16.3.
Refuse and tell the user to upgrade.
3. `mcp get_compilation_issues` doubles as a Turbopack probe.
An error response of `"Turbopack project is not available..."`
means the user is on webpack. Refuse — Turbopack is required.
4. `mcp get_routes` → your route map for the rest of the session.
## loop
### before the edit — narrow the scope
Ask the running app, not the codebase. `/_next/mcp` knows which
files rendered the current route; use those as your search scope.
Runtime introspection stays cheap as the codebase grows; agentic
search doesn't.
### after the edit — verify
Four failure modes. Check each:
- **Compiles** — `mcp get_compilation_issues`.
- **Runs without errors** — `/_next/mcp` (server and bubbled-up
browser errors both surface here).
- **Behaves as intended** — `agent-browser` drives the page; assert
what the user actually sees.
- **React-level behavior** — `agent-browser` with react-devtools
enabled exposes the component tree, props, state, and render
counts. Anchor framework-level checks here (extra renders,
server/client boundary shifts, suspense fallbacks) — DOM asserts
alone miss them.
Pick the specific tool from `tools/list` or `agent-browser
--help` rather than from memory.
## gotchas
- React introspection output is stale after navigation. Re-run.
- Non-3000 dev server: read the `next dev` banner; set
`NEXT_MCP_URL=http://localhost:<port>/_next/mcp`.
- `get_errors` and `get_page_metadata` need at least one navigation
to populate.
## reference
All tools below are present once preflight passes. If `tools/list`
is missing any of them, preflight should have refused — re-check.
```
# /_next/mcp notes
get_project_metadata projectPath, devServerUrl, bundler
get_routes fs-scan; no browser session needed
get_errors runtime + build; needs a browser session;
includes browser-side errors caught by the
dev server
get_page_metadata segment trie + routerType; needs a browser
session; use as a discovery shortcut for
which files power a route
get_logs returns logFilePath
get_server_action_by_id hashed id → file + functionName
get_compilation_issues Turbopack only; errors on webpack
("Turbopack project is not available")
```
## teardown
Close the `agent-browser` session. Leave `next dev` up for the next
loop.
---
`next-dev-loop-<topic>` siblings (e.g. `next-dev-loop-rsc`, `next-dev-loop-debug`)
assume this preflight already ran; they pick up at the loop.
+154
View File
@@ -0,0 +1,154 @@
---
name: next-ppr-optimizer
description: >
Optimize the static shell of a `cacheComponents` page. Use this
skill when a Next.js page feels slow on first paint and you want
to maximize what's prerendered before any dynamic data streams in.
---
# next-ppr-optimizer
The agentic optimization loop for PPR: diagnose the shell, pick
the highest-ROI refactor, apply it, confirm the shell actually
grew.
Two refactor levers:
- **Push down** — extract an I/O into a child wrapped in Suspense,
so the parent stays static. Mechanical; see `decide` for the
autonomy threshold.
- **Cache** — wrap the I/O in `'use cache'` and pick a `cacheLife`
profile. Freshness is a domain judgment — never invent a profile;
always ask the user with the named presets.
The hard part is **prioritization**: rank candidates by the
visible pixel area they cover. Largest gap first.
## requires
- `next-dev-loop` already initiated for this session.
- `cacheComponents: true` in `next.config.ts`. Without it there's
no static shell to optimize — refuse.
## preflight
1. Confirm `cacheComponents: true` in `next.config.ts`. The
user-driven browser session from `next-dev-loop`'s preflight
must already be open at the target URL.
2. On that open session, set the `next-instant-navigation-testing`
cookie via `agent-browser cookies set` — pass name and value as
separate positional args, not as a `name=value` blob. Value
format `[0,"p<random>"]`, scoped to the dev server hostname.
Reload.
While the cookie is held, dynamic streaming pauses and what's
visible equals the static shell + Suspense fallbacks.
## loop
### diagnose
1. **Sanity-check the view.** Confirm the visible content is the
shell, not the dev error overlay (which can paint a sparse
viewport that mimics a partially-suspended shell). Any of these
signals means it's a no-shell state — surface the bailout from
`mcp get_logs` (look for `NEXT_STATIC_GEN_BAILOUT` /
_blocking-route_) and stop:
- `mcp get_page_metadata` returns `segments: []` or
`routerType: "pages"` on what should be an app route.
- The page request returns HTTP 500.
- The suspense tree reports zero boundaries on a page that
visibly rendered something.
2. **List candidates.** `agent-browser react suspense` lists every
Suspense boundary on the current page and points at the JSX
site. The source paths are compiled chunks; resolve them to
user source via `POST /__nextjs_original-stack-frames` (the
endpoint the dev error overlay uses). That returns user file +
line + column for each boundary, including ones declared in
layouts and in imported component files. Blocker classification
is a soft hint only — plain `fetch()` often classifies as
`unknown` — so rely on the resolved JSX site and the source you
read, not on the classification.
3. **Rank by rendered area.** Take both renders: shell-only
(instant cookie set) and full (instant cookie removed by name
— see teardown). For each candidate, use the larger of its
fallback rect (shell-only) and rendered subtree rect (full)
as the per-candidate ROI. Sort descending. The fallback rect
alone misleads when developers used an undersized spinner.
4. **If one boundary dominates, re-diagnose deeper.** When a
candidate's rect covers roughly the entire viewport AND it's
the only dynamic boundary, push-down is not the move — that
wrapper _is_ the shell. Resolve the boundary's JSX site, read
the source it wraps, recurse into the wrapped component, and
enumerate each `await` and each async child. _Those_ are the
real candidates; loop back to step 3 with them.
### decide
For each candidate, pick a lever.
- **Push down** when the I/O sits inside a component that also
contains static content (heading, layout frame, image
scaffolding) — the static siblings can rise into the shell.
Apply autonomously when trivial; otherwise propose the diff.
- **Cache** when the I/O has no extractable static structure
around it, or when caching is genuinely the right answer.
Ask the user for a freshness budget, map their answer to a
named `cacheLife` profile (`seconds`, `minutes`, `hours`,
`days`, `weeks`, `max`, or `default`). Never invent.
### apply
Trivial push-down: extract the awaited call into an `async` child;
replace the parent with a sync function rendering the static
shell plus `<Suspense fallback={…}>` around the new child; pass
the values the child needs as props.
Cache: insert `'use cache'` at the top of the function body, then
`cacheLife('<profile>')` from the agreed preset.
### verify
Re-take the shell-only render. The targeted candidate's gap
should be strictly smaller, or gone (content promoted to shell).
If neither, undo — a refactor that compiles but doesn't grow the
shell is still a regression. Then re-run the diagnose-step-1
check — a botched extract can introduce a no-shell state where
there wasn't one before.
## gotchas
- `cacheLife` presets live in
`packages/next/src/server/config-shared.ts` — the source of
truth if the user asks what each preset means.
- Turbopack sometimes caches a stale compile error after the
underlying issue is fixed; touch the offending file to force
recompile if the dev server reports an error you've already
resolved.
## reference
```
next-instant-navigation-testing cookie freezes streaming; what's
visible = static shell + fallbacks
POST /__nextjs_original-stack-frames compiled chunk URL + line/col
→ user file + line/col
cacheLife('<profile>') default | seconds | minutes | hours
| days | weeks | max
```
## teardown
Remove the `next-instant-navigation-testing` cookie by name. Do
not use `agent-browser cookies clear` (no args) — that wipes the
user's auth and any other session state.
---
Sibling of `next-dev-loop` — initiate that first. Pick the loop
that matches the work: `next-dev-loop` for general edit/verify,
`next-ppr-optimizer` for shell-area optimization.