mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
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:
@@ -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/)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user