mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
Add experimental agent feedback workflow (#98582)
## Summary Adds an off-by-default `experimental.agentFeedback` workflow for collecting Next.js friction without interrupting the user’s task. - `next dev` writes a small managed block to the project agent-instructions file. - Agent entry points, including `next-dev-loop`, queue possible issues instead of opening duplicate forms. - At the final stopping point, an internal command checks the remote gate and returns the reporting protocol bundled with that Next.js version. The agent attempts to anonymize each qualifying issue and opens a separate review form once. If the browser does not open, it prints the URL for the user without troubleshooting the failure. - Report links no longer contain a public page token. Nothing is sent until the user submits the form, which remains rate-limited and uses a server-only ingest credential. - Disabling `agentFeedback` or `agentRules` removes only its managed block on the next `next dev`. Empty generated agent files are cleaned up; user-authored content is preserved. - Adds API references for both options and updates the AI agents guide. Bundling the protocol keeps the managed block small and allows the report format to evolve with each Next.js version. The receiving form is implemented in [vercel/front#85739](https://github.com/vercel/front/pull/85739) and should deploy before this workflow is enabled. ## Verification - `NEXT_SKIP_ISOLATE=1 pnpm test-dev-turbo test/development/app-dir/agent-rules-auto-generate/agent-rules-auto-generate.test.ts` - `pnpm jest packages/next/src/server/lib/generate-agent-files.test.ts packages/next/src/cli/internal` - `npx eslint --config eslint.config.mjs packages/create-next-app/helpers/generate-agent-files.ts packages/next/src/server/config-shared.ts packages/next/src/server/lib/generate-agent-files.ts test/development/app-dir/agent-rules-auto-generate/agent-rules-auto-generate.test.ts` <!-- NEXT_JS_LLM -->
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: How to set up your Next.js project for AI coding agents
|
||||
title: How Next.js supports AI coding agents
|
||||
nav_title: AI Coding Agents
|
||||
description: Learn how to configure your Next.js project so AI coding agents use up-to-date documentation instead of outdated training data.
|
||||
description: Learn how Next.js provides AI coding agents with version-matched documentation, runtime tools, reusable workflows, and automatic feedback.
|
||||
related:
|
||||
title: Next Steps
|
||||
links:
|
||||
@@ -11,7 +11,7 @@ related:
|
||||
- app/api-reference/cli/next
|
||||
---
|
||||
|
||||
Next.js ships version-matched documentation inside the `next` package, allowing AI coding agents to reference accurate, up-to-date APIs and patterns. An `AGENTS.md` file at the root of your project directs agents to these bundled docs instead of their training data.
|
||||
Next.js provides AI coding agents with version-matched documentation, runtime visibility, structured errors, reusable workflows, and automatic feedback. These capabilities help agents use the APIs and patterns supported by your installed Next.js version, verify their work, and report framework friction for your review.
|
||||
|
||||
Point agents at the bundled docs, give them [runtime visibility](#step-2-give-agents-runtime-visibility) into the dev server, let [errors drive the fixes](#step-3-let-errors-drive-the-fixes), and hand multi-step workflows to [skills](#step-4-hand-multi-step-workflows-to-skills).
|
||||
|
||||
@@ -58,14 +58,14 @@ If you don't want the agent files, pass `--no-agents-md`:
|
||||
npx create-next-app@canary --no-agents-md
|
||||
```
|
||||
|
||||
### Existing projects
|
||||
### Version-matched documentation
|
||||
|
||||
On Next.js 16.3 or later, run `next dev`. When an AI coding agent is detected in the environment and no managed block is present, Next.js auto-generates `AGENTS.md` and `CLAUDE.md` at the project root. Existing `AGENTS.md` or `CLAUDE.md` files are upserted, so content outside the managed block is preserved:
|
||||
Next.js keeps AI coding agents aligned with your installed framework version by adding managed instructions that direct them to the bundled docs. The instructions appear in `AGENTS.md` or `CLAUDE.md` at the project root:
|
||||
|
||||
```md filename="AGENTS.md"
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
# This is NOT the Next.js you know
|
||||
## This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||
|
||||
@@ -78,22 +78,39 @@ This block is written and re-added by `next dev` — verify at `node_modules/nex
|
||||
@AGENTS.md
|
||||
```
|
||||
|
||||
Add your own project-specific instructions outside the `<!-- BEGIN:nextjs-agent-rules -->` and `<!-- END:nextjs-agent-rules -->` markers, and they're preserved when Next.js updates the managed block.
|
||||
Next.js updates only the content between the agent rules markers and preserves the rest of the file.
|
||||
|
||||
### Opting out
|
||||
[Benchmark results on nextjs.org/evals](https://nextjs.org/evals) show that agents perform better when they read the bundled docs. See the [`agentRules` API reference](/docs/app/api-reference/config/next-config-js/agentRules) to configure the managed instructions.
|
||||
|
||||
We believe leaving auto-generation on is a good default. [Benchmark results on nextjs.org/evals](https://nextjs.org/evals) show agents do better when they read the bundled docs. If you really want to opt out, set `agentRules` to `false` in your config:
|
||||
### Automatic agent feedback
|
||||
|
||||
```ts filename="next.config.ts"
|
||||
import type { NextConfig } from 'next'
|
||||
Automatic agent feedback is experimental and disabled by default. Enable it with [`experimental.agentFeedback`](/docs/app/api-reference/config/next-config-js/agentFeedback) in `next.config`. When enabled, coding agents keep track of potential Next.js issues without interrupting an active task. At a natural stopping point, each qualifying issue opens as a separate review form in your default browser.
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
agentRules: false,
|
||||
}
|
||||
Before opening a form, the agent attempts to anonymize the report by replacing customer and project details with generic descriptions and omitting source code, logs, paths, URLs, secrets, and personal information. Review the draft and remove anything you don't want to include. Nothing is sent until you select **Send feedback**, and agent feedback is separate from [Next.js Telemetry](/telemetry).
|
||||
|
||||
export default nextConfig
|
||||
With the option enabled, `next dev` adds the following managed feedback instructions to the agent file when it detects an AI coding agent:
|
||||
|
||||
```md filename="AGENTS.md"
|
||||
<!-- BEGIN:nextjs-agent-feedback -->
|
||||
|
||||
## Report Next.js feedback
|
||||
|
||||
During the task, queue anonymized Next.js issues that cause a meaningful detour or are likely to recur, such as:
|
||||
|
||||
- Crashes, hangs, OOMs, or unexpected slowness in development, builds, or runtime
|
||||
- Incorrect behavior or output, misleading errors, or missing diagnostics
|
||||
- Missing, unclear, or conflicting documentation
|
||||
- Unexpected CLI, codemod, Skill, or upgrade behavior, repeated failures, or non-obvious workarounds
|
||||
|
||||
Continue the task without reporting immediately. Ignore routine debugging and issues caused by unverified changes. Never retain source code, logs, paths, URLs, secrets, personal information, or project-specific data.
|
||||
|
||||
At the final stopping point, from the directory containing this file, run `node node_modules/next/dist/bin/next internal agent-feedback-instructions` once per task and follow any instructions returned. If it returns nothing, continue without reporting.
|
||||
|
||||
<!-- END:nextjs-agent-feedback -->
|
||||
```
|
||||
|
||||
Next.js updates only the content between the agent feedback markers and preserves the rest of the file. See the [`agentFeedback` API reference](/docs/app/api-reference/config/next-config-js/agentFeedback) to configure automatic agent feedback.
|
||||
|
||||
### For earlier versions
|
||||
|
||||
On version 16.2, the docs are bundled but `AGENTS.md` is not auto-generated. Add the file yourself with an instruction to read the bundled docs at `node_modules/next/dist/docs/` before writing code.
|
||||
|
||||
@@ -50,7 +50,7 @@ The managed block should look like this:
|
||||
```md filename="AGENTS.md"
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
# This is NOT the Next.js you know
|
||||
## This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: agentFeedback
|
||||
description: Enable a review flow for Next.js issue reports prepared by AI coding agents.
|
||||
version: experimental
|
||||
related:
|
||||
links:
|
||||
- app/guides/ai-agents
|
||||
- app/api-reference/config/next-config-js/agentRules
|
||||
---
|
||||
|
||||
The `experimental.agentFeedback` option controls whether `next dev` creates and updates instructions that let detected AI coding agents prepare Next.js issue reports for your review. It is disabled by default and works independently from [Next.js Telemetry](/telemetry).
|
||||
|
||||
Set `experimental.agentFeedback` to `true` to enable this behavior:
|
||||
|
||||
```ts filename="next.config.ts" switcher highlight={5}
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
experimental: {
|
||||
agentFeedback: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
```
|
||||
|
||||
```js filename="next.config.js" switcher highlight={4}
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
agentFeedback: true,
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
```
|
||||
|
||||
## Generated agent instructions
|
||||
|
||||
When `experimental.agentFeedback` is enabled and `next dev` detects an AI coding agent, Next.js creates or updates this managed block in `AGENTS.md` or `CLAUDE.md`:
|
||||
|
||||
```md filename="AGENTS.md"
|
||||
<!-- BEGIN:nextjs-agent-feedback -->
|
||||
|
||||
## Report Next.js feedback
|
||||
|
||||
During the task, queue anonymized Next.js issues that cause a meaningful detour or are likely to recur, such as:
|
||||
|
||||
- Crashes, hangs, OOMs, or unexpected slowness in development, builds, or runtime
|
||||
- Incorrect behavior or output, misleading errors, or missing diagnostics
|
||||
- Missing, unclear, or conflicting documentation
|
||||
- Unexpected CLI, codemod, Skill, or upgrade behavior, repeated failures, or non-obvious workarounds
|
||||
|
||||
Continue the task without reporting immediately. Ignore routine debugging and issues caused by unverified changes. Never retain source code, logs, paths, URLs, secrets, personal information, or project-specific data.
|
||||
|
||||
At the final stopping point, from the directory containing this file, run `node node_modules/next/dist/bin/next internal agent-feedback-instructions` once per task and follow any instructions returned. If it returns nothing, continue without reporting.
|
||||
|
||||
<!-- END:nextjs-agent-feedback -->
|
||||
```
|
||||
|
||||
Next.js updates only the content between the markers and preserves the rest of the file.
|
||||
|
||||
If neither agent file exists, Next.js creates `AGENTS.md` and a `CLAUDE.md` file that imports it:
|
||||
|
||||
```md filename="CLAUDE.md"
|
||||
@AGENTS.md
|
||||
```
|
||||
|
||||
If either file already exists, Next.js adds the managed block to that file without creating another agent file. When agent rules and agent feedback are both enabled, Next.js keeps both managed blocks in the same file.
|
||||
|
||||
## Review and send agent feedback
|
||||
|
||||
The generated instructions let the agent retain potential Next.js friction without interrupting the task. When the task ends, the agent prepares each distinct qualifying issue as a separate report.
|
||||
|
||||
Each draft contains a generic setup, reproduction steps, observed facts, an expected result, and an optional control or workaround.
|
||||
|
||||
The agent attempts to open each draft once in a separate tab in your default browser. You can edit a report, remove details, close the form without sending, or select **Send feedback**. Nothing is sent when a form opens.
|
||||
|
||||
Before opening a form, the agent attempts to anonymize the report by replacing customer and project details with generic descriptions and omitting source code, logs, paths, URLs, secrets, and personal information. Review the draft and remove anything you don't want to include before sending it.
|
||||
|
||||
Submitting the form sends the content you reviewed, the agent name, and the Next.js version. The form does not automatically attach project files or machine metadata.
|
||||
|
||||
## Disable agent feedback
|
||||
|
||||
Set `experimental.agentFeedback` to `false` or remove the option to stop `next dev` from creating or updating the managed block. The next `next dev` run removes the existing agent feedback block and preserves all other content. If the file contained only the managed block, Next.js deletes the file instead of leaving it empty.
|
||||
|
||||
`experimental.agentFeedback` and top-level [`agentRules`](/docs/app/api-reference/config/next-config-js/agentRules) work independently. Changing one option does not affect the other.
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Changes |
|
||||
| --------- | -------------------------------------------------------- |
|
||||
| `v16.4.0` | `experimental.agentFeedback` configuration option added. |
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: agentRules
|
||||
description: Configure whether Next.js creates instructions for AI coding agents.
|
||||
related:
|
||||
links:
|
||||
- app/guides/ai-agents
|
||||
- app/api-reference/config/next-config-js/agentFeedback
|
||||
---
|
||||
|
||||
The `agentRules` option controls whether `next dev` creates and updates version-matched documentation instructions for detected AI coding agents. It is enabled by default.
|
||||
|
||||
Set `agentRules` to `false` to disable this behavior:
|
||||
|
||||
```ts filename="next.config.ts" switcher highlight={4}
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
agentRules: false,
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
```
|
||||
|
||||
```js filename="next.config.js" switcher highlight={3}
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
agentRules: false,
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
```
|
||||
|
||||
## Generated agent instructions
|
||||
|
||||
When `agentRules` is enabled and `next dev` detects an AI coding agent, Next.js creates or updates this managed block in `AGENTS.md` or `CLAUDE.md`:
|
||||
|
||||
```md filename="AGENTS.md"
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
## This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||
|
||||
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
```
|
||||
|
||||
The managed block directs agents to documentation bundled with the installed Next.js version. Next.js updates only the content between the markers and preserves the rest of the file.
|
||||
|
||||
If neither agent file exists, Next.js creates `AGENTS.md` and a `CLAUDE.md` file that imports it:
|
||||
|
||||
```md filename="CLAUDE.md"
|
||||
@AGENTS.md
|
||||
```
|
||||
|
||||
If either file already exists, Next.js adds the managed block to that file without creating another agent file.
|
||||
|
||||
## Disable agent rules
|
||||
|
||||
Set `agentRules` to `false` to stop `next dev` from creating or updating the managed block. The next `next dev` run removes the existing agent rules block and preserves all other content, even when no agent is detected. If the file contained only the managed block, Next.js deletes the file instead of leaving it empty.
|
||||
|
||||
To prevent [`create-next-app`](/docs/app/api-reference/cli/create-next-app) from creating agent files in a new project, pass the `--no-agents-md` option.
|
||||
|
||||
`agentRules` and [`experimental.agentFeedback`](/docs/app/api-reference/config/next-config-js/agentFeedback) work independently. Changing one option does not affect the other.
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Changes |
|
||||
| --------- | -------------------------------------------------------------------------------- |
|
||||
| `v16.4.0` | Setting `agentRules` to `false` removes an existing managed block on `next dev`. |
|
||||
| `v16.3.0` | `agentRules` configuration option added. |
|
||||
@@ -8,7 +8,7 @@ import path from 'path'
|
||||
export function generateAgentFiles(root: string): void {
|
||||
const agentsMdContent = `<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
# This is NOT the Next.js you know
|
||||
## This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in \`node_modules/next/dist/docs/\` (resolved from this file's directory; in monorepos the \`next\` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
# This is NOT the Next.js you know
|
||||
## This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `dist/docs/` before writing any code. Heed deprecation notices.
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Next.js agent feedback protocol
|
||||
|
||||
Use this protocol only when the managed Next.js feedback block in `AGENTS.md` or `CLAUDE.md` instructs you to prepare agent feedback. Next.js Skills and upgrade workflows may add candidates to the shared task queue only while that managed block is present. Prepare reports for the user to review, but never submit them for the user.
|
||||
|
||||
## Decide whether to prepare a report
|
||||
|
||||
Prepare a separate report for each distinct Next.js friction point that causes unexpected framework behavior, a documentation mismatch, a misleading error, repeated failed approaches, or a non-obvious workaround. An issue does not need to block the task, but it should require a meaningful detour or be likely to recur.
|
||||
|
||||
Do not report routine first-pass debugging, vague task scope, problems caused by unverified changes, or issues resolved immediately by following existing guidance. Keep unrelated issues in separate reports.
|
||||
|
||||
## Defer review until a stopping point
|
||||
|
||||
Do not interrupt an active workflow when friction first qualifies. Retain each distinct set of bounded, anonymized candidate facts in the current task context, then continue the work.
|
||||
|
||||
If a candidate originated in a Skill inside a larger task, keep it in the current task context and continue the larger task. Prepare the review only at the final stopping point of the overall user task. For example, verification performed while preparing a pull request should wait until the pull request work is complete.
|
||||
|
||||
The managed feedback block owns one feedback pass for the task. Run the instruction command at most once, even when a Skill or upgrade workflow added candidates to the queue. Do not open duplicate reviews.
|
||||
|
||||
Each review form accepts one report. If several candidates qualify before the stopping point, prepare and open one separate review tab for each candidate. After opening all review tabs, clear the retained candidate queue from the task context and mark the feedback pass complete. The user owns the open tabs; do not close or recreate them. Nothing is transmitted before the user explicitly sends a report.
|
||||
|
||||
## Prepare each report
|
||||
|
||||
- **Scope:** Report one observed Next.js behavior. Do not combine separate problems or infer a root cause.
|
||||
- **Trigger:** Set `triggerReason` to `unexpected-behavior`, `documentation-mismatch`, `misleading-error`, `repeated-failed-approach`, or `non-obvious-workaround`. Choose the single reason that caused the report.
|
||||
- **Summary:** Use a short, factual `title` that names the observed behavior.
|
||||
- **Setup:** Set `mode` to `development`, `production-build`, `production-server`, or `test`. Set `bundler` to `turbopack`, `webpack`, or `unknown`. Add up to three generic `relevantFeatures` when they help reproduce the issue.
|
||||
- **Reproduction:** Write 1–4 ordered `steps`. Include the generic starting state, the relevant Next.js feature or API, and the action that triggers the behavior. Split only actions or conditions whose order matters.
|
||||
- **Results:** Write 1–3 independently verifiable `observed` facts and one precise `expected` result. Split an observation only when each fact can stand alone and may be removed independently. Add `comparison` only when you observed a control or workaround.
|
||||
- **Outcome:** Set `frequency` to `once` or `reproduced`. Set `outcome` to `blocked`, `worked-around`, or `resolved`.
|
||||
- **Measurements:** Include measurements such as memory use, duration, extra builds, or repeated attempts only when observed directly. Do not estimate elapsed time, token usage, or tool-call counts.
|
||||
- **Privacy:** Replace customer, project, route, and component names with generic descriptions. Do not include source code, prompts, logs, stack traces, file paths, URLs, secrets, personal information, or unrelated product details.
|
||||
|
||||
## Encode the report
|
||||
|
||||
Create a schema version 5 payload using only useful evidence. Always include `nextVersion` and `agent`. Omit `comparison` and `relevantFeatures` when they are not needed. Encode the UTF-8 JSON as unpadded base64url.
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 5,
|
||||
"triggerReason": "unexpected-behavior",
|
||||
"title": "Persistent Turbopack caching exhausted a worker on a warm build",
|
||||
"setup": {
|
||||
"mode": "production-build",
|
||||
"bundler": "turbopack",
|
||||
"relevantFeatures": ["Persistent cache"]
|
||||
},
|
||||
"steps": [
|
||||
"Enable persistent Turbopack caching and run a successful cold `next build` in a 32 GiB Linux environment.",
|
||||
"Without changing the application or runtime, run `next build` again and observe the main build process memory."
|
||||
],
|
||||
"observed": [
|
||||
"The warm build exhausted a 32 GiB Linux worker after the cold build succeeded.",
|
||||
"The main build process reached about 25 GiB resident memory before it was killed."
|
||||
],
|
||||
"expected": "The warm build should use comparable or less memory than the cold build.",
|
||||
"comparison": "Disabling persistent caching allowed two consecutive builds to finish in about two minutes each.",
|
||||
"frequency": "reproduced",
|
||||
"outcome": "worked-around",
|
||||
"nextVersion": "<version>",
|
||||
"agent": "<agent name>"
|
||||
}
|
||||
```
|
||||
|
||||
## Open the review form
|
||||
|
||||
For each report, attempt to open the following URL once in a separate tab in the user's default browser. Use the browser-opening capability already available in the agent environment. Replace `<encoded-json>` with the encoded payload.
|
||||
|
||||
```text
|
||||
https://nextjs.org/agent-feedback#report=<encoded-json>
|
||||
```
|
||||
|
||||
Do not ask for permission before opening the review forms. Opening a form does not send feedback. If a form does not open, do not retry, investigate the failure, install tooling, or change host configuration. Print the review URL in your next progress update so the user can open it manually.
|
||||
|
||||
After attempting to open every form, clear the retained friction candidates from the task context. Continue the current task, mention any forms that opened in the next progress update, and never submit or close a report for the user.
|
||||
@@ -667,6 +667,14 @@ const internal = program
|
||||
'Internal debugging commands. Use with caution. Not covered by semver.'
|
||||
)
|
||||
|
||||
internal
|
||||
.command('agent-feedback-instructions', { hidden: true })
|
||||
.action(() =>
|
||||
import('../cli/internal/agent-feedback-instructions.js').then((mod) =>
|
||||
mod.agentFeedbackInstructionsCli()
|
||||
)
|
||||
)
|
||||
|
||||
internal
|
||||
.command('trace')
|
||||
.alias('turbo-trace-server')
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { loadAgentFeedbackInstructions } from './agent-feedback-instructions'
|
||||
|
||||
describe('loadAgentFeedbackInstructions', () => {
|
||||
it('returns the protocol when feedback is enabled', async () => {
|
||||
await expect(
|
||||
loadAgentFeedbackInstructions(
|
||||
async () => true,
|
||||
async () => '# Agent feedback protocol\n'
|
||||
)
|
||||
).resolves.toBe('# Agent feedback protocol\n')
|
||||
})
|
||||
|
||||
it('does not read the protocol when feedback is disabled', async () => {
|
||||
const readProtocol = jest.fn(async () => '# Agent feedback protocol\n')
|
||||
|
||||
await expect(
|
||||
loadAgentFeedbackInstructions(async () => false, readProtocol)
|
||||
).resolves.toBeNull()
|
||||
expect(readProtocol).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fails closed when the protocol cannot be read', async () => {
|
||||
await expect(
|
||||
loadAgentFeedbackInstructions(
|
||||
async () => true,
|
||||
async () => {
|
||||
throw new Error('protocol unavailable')
|
||||
}
|
||||
)
|
||||
).resolves.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import { readFile } from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { isAgentFeedbackEnabled } from './agent-feedback-status'
|
||||
|
||||
const AGENT_FEEDBACK_PROTOCOL_PATH = path.join(
|
||||
__dirname,
|
||||
'../../agent-feedback/protocol.md'
|
||||
)
|
||||
|
||||
type IsEnabled = () => Promise<boolean>
|
||||
type ReadProtocol = () => Promise<string>
|
||||
|
||||
export async function loadAgentFeedbackInstructions(
|
||||
isEnabled: IsEnabled = isAgentFeedbackEnabled,
|
||||
readProtocol: ReadProtocol = () =>
|
||||
readFile(AGENT_FEEDBACK_PROTOCOL_PATH, 'utf8')
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
if (!(await isEnabled())) {
|
||||
return null
|
||||
}
|
||||
|
||||
return await readProtocol()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function agentFeedbackInstructionsCli(): Promise<void> {
|
||||
const instructions = await loadAgentFeedbackInstructions()
|
||||
if (instructions) {
|
||||
process.stdout.write(instructions)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { isAgentFeedbackEnabled } from './agent-feedback-status'
|
||||
|
||||
describe('isAgentFeedbackEnabled', () => {
|
||||
it('returns true only for an exact successful true response', async () => {
|
||||
let requestInit: RequestInit | undefined
|
||||
const fetchImpl: typeof fetch = async (_input, init) => {
|
||||
requestInit = init
|
||||
return new Response('true')
|
||||
}
|
||||
|
||||
await expect(isAgentFeedbackEnabled(fetchImpl)).resolves.toBe(true)
|
||||
expect(requestInit).toEqual(expect.objectContaining({ cache: 'no-store' }))
|
||||
expect(requestInit?.signal).toBeInstanceOf(AbortSignal)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['disabled', new Response('false')],
|
||||
['unexpected body', new Response(' true ')],
|
||||
['unsuccessful response', new Response('true', { status: 500 })],
|
||||
])('returns false for an %s', async (_name, response) => {
|
||||
const fetchImpl: typeof fetch = async () => response
|
||||
|
||||
await expect(isAgentFeedbackEnabled(fetchImpl)).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('fails closed when the request rejects', async () => {
|
||||
const fetchImpl: typeof fetch = async () => {
|
||||
throw new Error('network unavailable')
|
||||
}
|
||||
|
||||
await expect(isAgentFeedbackEnabled(fetchImpl)).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('fails closed when the request times out', async () => {
|
||||
const fetchImpl: typeof fetch = (_input, init) => {
|
||||
return new Promise<Response>((_resolve, reject) => {
|
||||
init?.signal?.addEventListener('abort', () => {
|
||||
reject(new Error('aborted'))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
await expect(isAgentFeedbackEnabled(fetchImpl, 1)).resolves.toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
const AGENT_FEEDBACK_STATUS_URL =
|
||||
'https://next-agent-feedback-gate.playground-vercel.tools/api/enabled'
|
||||
|
||||
type Fetch = typeof fetch
|
||||
|
||||
export async function isAgentFeedbackEnabled(
|
||||
fetchImpl: Fetch = fetch,
|
||||
timeoutMs = 5_000
|
||||
): Promise<boolean> {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
||||
|
||||
try {
|
||||
const response = await fetchImpl(AGENT_FEEDBACK_STATUS_URL, {
|
||||
cache: 'no-store',
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
return response.ok && (await response.text()) === 'true'
|
||||
} catch {
|
||||
return false
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
@@ -195,6 +195,7 @@ export const experimentalSchema = {
|
||||
agenticAutoUpgrade: z
|
||||
.union([z.enum(['security', 'latest', 'future']), z.literal(false)])
|
||||
.optional(),
|
||||
agentFeedback: z.boolean().optional(),
|
||||
outputHashSalt: z.string().optional(),
|
||||
useSkewCookie: z.boolean().optional(),
|
||||
after: z.boolean().optional(),
|
||||
|
||||
@@ -491,6 +491,11 @@ export function resolveCssChunkingMode(
|
||||
export interface ExperimentalConfig {
|
||||
/** Nudge coding agents about security upgrades, stable releases, or Future Defaults. */
|
||||
agenticAutoUpgrade?: 'security' | 'latest' | 'future' | false
|
||||
/**
|
||||
* Adds managed instructions to AGENTS.md or CLAUDE.md that let AI coding
|
||||
* agents prepare anonymized Next.js feedback for user review.
|
||||
*/
|
||||
agentFeedback?: boolean
|
||||
/**
|
||||
* @deprecated Use the top-level `outputHashSalt` option instead.
|
||||
*/
|
||||
@@ -2317,6 +2322,7 @@ export const defaultConfig = Object.freeze({
|
||||
},
|
||||
adapterPath: process.env.NEXT_ADAPTER_PATH || undefined,
|
||||
experimental: {
|
||||
agentFeedback: false,
|
||||
coldCacheBadge: false,
|
||||
collapseAdapterRoutes: true,
|
||||
devValidationWorker: true,
|
||||
@@ -2493,6 +2499,7 @@ export interface NextConfigRuntime {
|
||||
experimental: Pick<
|
||||
NextConfigComplete['experimental'],
|
||||
| 'taint'
|
||||
| 'agentFeedback'
|
||||
| 'serverActions'
|
||||
| 'staleTimes'
|
||||
| 'dynamicOnHover'
|
||||
@@ -2564,6 +2571,7 @@ export function getNextConfigRuntime(
|
||||
|
||||
const experimental = {
|
||||
taint: ex.taint,
|
||||
agentFeedback: ex.agentFeedback,
|
||||
serverActions: ex.serverActions,
|
||||
staleTimes: ex.staleTimes,
|
||||
dynamicOnHover: ex.dynamicOnHover,
|
||||
|
||||
@@ -7,7 +7,11 @@ import { experimentalSchema } from '../config-schema'
|
||||
import { getAgentName } from '../../telemetry/agent-name'
|
||||
import { bundlerName, getBundlerFromEnv } from '../../lib/bundler'
|
||||
import {
|
||||
hasCurrentAgentFeedback,
|
||||
hasCurrentAgentRules,
|
||||
removeAgentFeedbackFiles,
|
||||
removeAgentRulesFiles,
|
||||
writeAgentFeedbackFiles,
|
||||
writeAgentFiles,
|
||||
type AgentFilesResult,
|
||||
} from './generate-agent-files'
|
||||
@@ -114,25 +118,37 @@ export function logExperimentalInfo({
|
||||
}
|
||||
|
||||
/**
|
||||
* When `next dev` detects an AI coding agent but the managed
|
||||
* agent-rules block is missing from AGENTS.md / CLAUDE.md — or an
|
||||
* outdated version of it is installed — auto-generate or refresh the
|
||||
* files so the agent has access to version-matched docs. Returns the
|
||||
* write result when files were touched, or `null` when no action was
|
||||
* needed.
|
||||
*
|
||||
* Callers gate this on `config.agentRules !== false` — opt-out is
|
||||
* declarative in next.config, not inside this function.
|
||||
* Keep the agent-rules block in sync with next.config. Enabling it still
|
||||
* requires a detected agent; disabling it removes only that managed block,
|
||||
* even when no agent is currently detected.
|
||||
*/
|
||||
export async function ensureAgentRulesForDev(
|
||||
dir: string
|
||||
export async function syncAgentRulesForDev(
|
||||
dir: string,
|
||||
enabled: boolean
|
||||
): Promise<AgentFilesResult | null> {
|
||||
if (!enabled) return removeAgentRulesFiles(dir)
|
||||
if ((await getAgentName()) === null) return null
|
||||
if (hasCurrentAgentRules(dir)) return null
|
||||
|
||||
return writeAgentFiles(dir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the opt-in agent-feedback block in sync with next.config. Enabling it
|
||||
* still requires a detected agent; disabling it removes only that managed
|
||||
* block, even when no agent is currently detected.
|
||||
*/
|
||||
export async function syncAgentFeedbackForDev(
|
||||
dir: string,
|
||||
enabled: boolean
|
||||
): Promise<AgentFilesResult | null> {
|
||||
if (!enabled) return removeAgentFeedbackFiles(dir)
|
||||
if ((await getAgentName()) === null) return null
|
||||
if (hasCurrentAgentFeedback(dir)) return null
|
||||
|
||||
return writeAgentFeedbackFiles(dir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets environment info for logging. Fast operation that doesn't require config.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import {
|
||||
AGENT_FEEDBACK_END_MARKER,
|
||||
AGENT_FEEDBACK_START_MARKER,
|
||||
removeAgentFeedbackFiles,
|
||||
writeAgentFeedbackFiles,
|
||||
} from './generate-agent-files'
|
||||
|
||||
const block = `${AGENT_FEEDBACK_START_MARKER}\nstale\n${AGENT_FEEDBACK_END_MARKER}`
|
||||
|
||||
describe('removeAgentFeedbackFiles', () => {
|
||||
let dir: string
|
||||
|
||||
beforeEach(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-files-'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function write(name: string, content: string): string {
|
||||
const filePath = path.join(dir, name)
|
||||
fs.writeFileSync(filePath, content)
|
||||
return filePath
|
||||
}
|
||||
|
||||
it('removes a block at the start of the file without a leading blank line', () => {
|
||||
const filePath = write('AGENTS.md', `${block}\n\n# Team rules\n`)
|
||||
|
||||
expect(removeAgentFeedbackFiles(dir).agentsMd).toBe('removed')
|
||||
expect(fs.readFileSync(filePath, 'utf-8')).toBe('# Team rules\n')
|
||||
})
|
||||
|
||||
it('removes a block in the middle and keeps one blank line between neighbors', () => {
|
||||
const filePath = write('AGENTS.md', `# Team rules\n\n${block}\n\n# More\n`)
|
||||
|
||||
removeAgentFeedbackFiles(dir)
|
||||
expect(fs.readFileSync(filePath, 'utf-8')).toBe('# Team rules\n\n# More\n')
|
||||
})
|
||||
|
||||
it('removes a block at the end without a trailing newline', () => {
|
||||
const filePath = write('AGENTS.md', `# Team rules\n\n${block}`)
|
||||
|
||||
removeAgentFeedbackFiles(dir)
|
||||
expect(fs.readFileSync(filePath, 'utf-8')).toBe('# Team rules\n')
|
||||
})
|
||||
|
||||
it('preserves CRLF line endings around the removed block', () => {
|
||||
const crlfBlock = block.split('\n').join('\r\n')
|
||||
const filePath = write(
|
||||
'CLAUDE.md',
|
||||
`# Team rules\r\n\r\n${crlfBlock}\r\n\r\nKeep this.\r\n`
|
||||
)
|
||||
|
||||
removeAgentFeedbackFiles(dir)
|
||||
expect(fs.readFileSync(filePath, 'utf-8')).toBe(
|
||||
'# Team rules\r\n\r\nKeep this.\r\n'
|
||||
)
|
||||
})
|
||||
|
||||
it('deletes a file that held nothing but the managed block', () => {
|
||||
const filePath = write('CLAUDE.md', `${block}\n`)
|
||||
|
||||
expect(removeAgentFeedbackFiles(dir).claudeMd).toBe('removed')
|
||||
expect(fs.existsSync(filePath)).toBe(false)
|
||||
})
|
||||
|
||||
it('leaves a file alone when the end marker is missing', () => {
|
||||
const content = `# Team rules\n\n${AGENT_FEEDBACK_START_MARKER}\nbroken\n`
|
||||
const filePath = write('AGENTS.md', content)
|
||||
|
||||
expect(removeAgentFeedbackFiles(dir).agentsMd).toBe('unchanged')
|
||||
expect(fs.readFileSync(filePath, 'utf-8')).toBe(content)
|
||||
})
|
||||
|
||||
it('reports skipped for files that do not exist', () => {
|
||||
expect(removeAgentFeedbackFiles(dir)).toEqual({
|
||||
agentsMd: 'skipped',
|
||||
claudeMd: 'skipped',
|
||||
})
|
||||
})
|
||||
|
||||
it('round-trips with writeAgentFeedbackFiles on an existing AGENTS.md', () => {
|
||||
const filePath = write('AGENTS.md', '# Team rules\n')
|
||||
|
||||
expect(writeAgentFeedbackFiles(dir).agentsMd).toBe('updated')
|
||||
expect(fs.readFileSync(filePath, 'utf-8')).toContain(
|
||||
AGENT_FEEDBACK_START_MARKER
|
||||
)
|
||||
expect(removeAgentFeedbackFiles(dir).agentsMd).toBe('removed')
|
||||
expect(fs.readFileSync(filePath, 'utf-8')).toBe('# Team rules\n')
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,9 @@ import path from 'path'
|
||||
|
||||
export const AGENT_RULES_START_MARKER = '<!-- BEGIN:nextjs-agent-rules -->'
|
||||
export const AGENT_RULES_END_MARKER = '<!-- END:nextjs-agent-rules -->'
|
||||
export const AGENT_FEEDBACK_START_MARKER =
|
||||
'<!-- BEGIN:nextjs-agent-feedback -->'
|
||||
export const AGENT_FEEDBACK_END_MARKER = '<!-- END:nextjs-agent-feedback -->'
|
||||
|
||||
/**
|
||||
* Markers written by the pre-bundled-docs version of `agents-md`.
|
||||
@@ -24,7 +27,7 @@ const LEGACY_AGENT_RULES_END_MARKER = '<!-- NEXT-AGENTS-MD-END -->'
|
||||
function buildAgentRulesBlock(): string {
|
||||
return `${AGENT_RULES_START_MARKER}
|
||||
|
||||
# This is NOT the Next.js you know
|
||||
## This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in \`node_modules/next/dist/docs/\` (resolved from this file's directory; in monorepos the \`next\` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||
|
||||
@@ -33,9 +36,39 @@ This block is written and re-added by \`next dev\` — verify at \`node_modules/
|
||||
${AGENT_RULES_END_MARKER}`
|
||||
}
|
||||
|
||||
/**
|
||||
* The block points agents at `next internal agent-feedback-instructions`.
|
||||
* That command is not covered by semver, which is acceptable here because
|
||||
* `next dev` rewrites this block whenever the installed version changes it,
|
||||
* so a project never keeps an instruction that its own `next` can't serve.
|
||||
*/
|
||||
function buildAgentFeedbackBlock(): string {
|
||||
return `${AGENT_FEEDBACK_START_MARKER}
|
||||
|
||||
## Report Next.js feedback
|
||||
|
||||
During the task, queue anonymized Next.js issues that cause a meaningful detour or are likely to recur, such as:
|
||||
|
||||
- Crashes, hangs, OOMs, or unexpected slowness in development, builds, or runtime
|
||||
- Incorrect behavior or output, misleading errors, or missing diagnostics
|
||||
- Missing, unclear, or conflicting documentation
|
||||
- Unexpected CLI, codemod, Skill, or upgrade behavior, repeated failures, or non-obvious workarounds
|
||||
|
||||
Continue the task without reporting immediately. Ignore routine debugging and issues caused by unverified changes. Never retain source code, logs, paths, URLs, secrets, personal information, or project-specific data.
|
||||
|
||||
At the final stopping point, from the directory containing this file, run \`node node_modules/next/dist/bin/next internal agent-feedback-instructions\` once per task and follow any instructions returned. If it returns nothing, continue without reporting.
|
||||
|
||||
${AGENT_FEEDBACK_END_MARKER}`
|
||||
}
|
||||
|
||||
const CLAUDE_MD_CONTENT = `@AGENTS.md\n`
|
||||
|
||||
export type AgentFileAction = 'created' | 'updated' | 'unchanged' | 'skipped'
|
||||
export type AgentFileAction =
|
||||
| 'created'
|
||||
| 'updated'
|
||||
| 'removed'
|
||||
| 'unchanged'
|
||||
| 'skipped'
|
||||
|
||||
export interface AgentFilesResult {
|
||||
agentsMd: AgentFileAction
|
||||
@@ -47,11 +80,23 @@ export interface AgentFilesResult {
|
||||
* `null` when the markers are absent or malformed.
|
||||
*/
|
||||
function extractAgentRulesBlock(content: string): string | null {
|
||||
const start = content.indexOf(AGENT_RULES_START_MARKER)
|
||||
return extractManagedBlock(
|
||||
content,
|
||||
AGENT_RULES_START_MARKER,
|
||||
AGENT_RULES_END_MARKER
|
||||
)
|
||||
}
|
||||
|
||||
function extractManagedBlock(
|
||||
content: string,
|
||||
startMarker: string,
|
||||
endMarker: string
|
||||
): string | null {
|
||||
const start = content.indexOf(startMarker)
|
||||
if (start === -1) return null
|
||||
const end = content.indexOf(AGENT_RULES_END_MARKER, start)
|
||||
const end = content.indexOf(endMarker, start)
|
||||
if (end === -1) return null
|
||||
return content.slice(start, end + AGENT_RULES_END_MARKER.length)
|
||||
return content.slice(start, end + endMarker.length)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +118,23 @@ export function hasCurrentAgentRules(dir: string): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
export function hasCurrentAgentFeedback(dir: string): boolean {
|
||||
const block = buildAgentFeedbackBlock()
|
||||
for (const file of ['AGENTS.md', 'CLAUDE.md']) {
|
||||
const content = tryReadFile(path.join(dir, file))
|
||||
if (!content) continue
|
||||
const installed = extractManagedBlock(
|
||||
content,
|
||||
AGENT_FEEDBACK_START_MARKER,
|
||||
AGENT_FEEDBACK_END_MARKER
|
||||
)
|
||||
if (installed !== null && normalizeEol(installed, '\n') === block) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the agent-rules block into `projectDir`, respecting whichever
|
||||
* file the user already uses:
|
||||
@@ -122,6 +184,91 @@ export function writeAgentFiles(projectDir: string): AgentFilesResult {
|
||||
return { agentsMd: 'created', claudeMd: 'created' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the opt-in agent-feedback block using the same managed-file convention
|
||||
* as the agent-rules block. If the rules already have a host file, keep both
|
||||
* Next.js blocks together.
|
||||
*/
|
||||
export function writeAgentFeedbackFiles(projectDir: string): AgentFilesResult {
|
||||
const agentsMdPath = path.join(projectDir, 'AGENTS.md')
|
||||
const claudeMdPath = path.join(projectDir, 'CLAUDE.md')
|
||||
const block = buildAgentFeedbackBlock()
|
||||
|
||||
const agentsContent = tryReadFile(agentsMdPath)
|
||||
const claudeContent = tryReadFile(claudeMdPath)
|
||||
const agentsMdExists = agentsContent !== null
|
||||
const claudeMdExists = claudeContent !== null
|
||||
|
||||
const agentsMdHostsFeedback =
|
||||
agentsContent?.includes(AGENT_FEEDBACK_START_MARKER) ?? false
|
||||
const claudeMdHostsFeedback =
|
||||
claudeContent?.includes(AGENT_FEEDBACK_START_MARKER) ?? false
|
||||
const agentsMdHostsRules =
|
||||
agentsContent?.includes(AGENT_RULES_START_MARKER) ?? false
|
||||
const claudeMdHostsRules =
|
||||
claudeContent?.includes(AGENT_RULES_START_MARKER) ?? false
|
||||
|
||||
if (
|
||||
agentsMdExists &&
|
||||
(agentsMdHostsFeedback ||
|
||||
(!claudeMdHostsFeedback && (agentsMdHostsRules || !claudeMdHostsRules)))
|
||||
) {
|
||||
return {
|
||||
agentsMd: upsertFeedbackFile(agentsMdPath, block),
|
||||
claudeMd: 'skipped',
|
||||
}
|
||||
}
|
||||
|
||||
if (claudeMdExists) {
|
||||
return {
|
||||
agentsMd: 'skipped',
|
||||
claudeMd: upsertFeedbackFile(claudeMdPath, block),
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(agentsMdPath, block + '\n', 'utf-8')
|
||||
fs.writeFileSync(claudeMdPath, CLAUDE_MD_CONTENT, 'utf-8')
|
||||
return { agentsMd: 'created', claudeMd: 'created' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove only the managed agent-feedback block, leaving all other content.
|
||||
* A file that held nothing but the block is deleted rather than left empty.
|
||||
*/
|
||||
export function removeAgentFeedbackFiles(projectDir: string): AgentFilesResult {
|
||||
return {
|
||||
agentsMd: removeManagedBlockFromFile(
|
||||
path.join(projectDir, 'AGENTS.md'),
|
||||
AGENT_FEEDBACK_START_MARKER,
|
||||
AGENT_FEEDBACK_END_MARKER
|
||||
),
|
||||
claudeMd: removeManagedBlockFromFile(
|
||||
path.join(projectDir, 'CLAUDE.md'),
|
||||
AGENT_FEEDBACK_START_MARKER,
|
||||
AGENT_FEEDBACK_END_MARKER
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove only the managed agent-rules block, leaving all other content.
|
||||
* A file that held nothing but the block is deleted rather than left empty.
|
||||
*/
|
||||
export function removeAgentRulesFiles(projectDir: string): AgentFilesResult {
|
||||
return {
|
||||
agentsMd: removeManagedBlockFromFile(
|
||||
path.join(projectDir, 'AGENTS.md'),
|
||||
AGENT_RULES_START_MARKER,
|
||||
AGENT_RULES_END_MARKER
|
||||
),
|
||||
claudeMd: removeManagedBlockFromFile(
|
||||
path.join(projectDir, 'CLAUDE.md'),
|
||||
AGENT_RULES_START_MARKER,
|
||||
AGENT_RULES_END_MARKER
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -142,6 +289,39 @@ function upsertFile(filePath: string, block: string): AgentFileAction {
|
||||
return 'updated'
|
||||
}
|
||||
|
||||
function upsertFeedbackFile(filePath: string, block: string): AgentFileAction {
|
||||
const existing = fs.readFileSync(filePath, 'utf-8')
|
||||
const updated = upsertManagedBlock(
|
||||
existing,
|
||||
block,
|
||||
AGENT_FEEDBACK_START_MARKER,
|
||||
AGENT_FEEDBACK_END_MARKER
|
||||
)
|
||||
if (updated === existing) return 'unchanged'
|
||||
fs.writeFileSync(filePath, updated, 'utf-8')
|
||||
return 'updated'
|
||||
}
|
||||
|
||||
function removeManagedBlockFromFile(
|
||||
filePath: string,
|
||||
startMarker: string,
|
||||
endMarker: string
|
||||
): AgentFileAction {
|
||||
const existing = tryReadFile(filePath)
|
||||
if (existing === null) return 'skipped'
|
||||
const updated = removeManagedBlock(existing, startMarker, endMarker)
|
||||
if (updated === existing) return 'unchanged'
|
||||
if (updated.trim() === '') {
|
||||
// Nothing but the managed block lived here, so Next.js effectively owned
|
||||
// the file. Leaving a zero-byte AGENTS.md or CLAUDE.md behind is more
|
||||
// confusing than removing it.
|
||||
fs.unlinkSync(filePath)
|
||||
} else {
|
||||
fs.writeFileSync(filePath, updated, 'utf-8')
|
||||
}
|
||||
return 'removed'
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the predominant line-ending style. Returns `'\r\n'` if any
|
||||
* CRLF is present, `'\n'` otherwise — avoids mixed EOLs on Windows.
|
||||
@@ -175,6 +355,72 @@ function upsertAgentRulesBlock(existing: string, block: string): string {
|
||||
return existing + separator + normalizedBlock + eol
|
||||
}
|
||||
|
||||
function upsertManagedBlock(
|
||||
existing: string,
|
||||
block: string,
|
||||
startMarker: string,
|
||||
endMarker: string
|
||||
): string {
|
||||
const eol = detectEol(existing)
|
||||
const normalizedBlock = normalizeEol(block, eol)
|
||||
const startIdx = existing.indexOf(startMarker)
|
||||
const endIdx = existing.indexOf(endMarker, startIdx)
|
||||
|
||||
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
||||
const before = existing.slice(0, startIdx)
|
||||
const after = existing.slice(endIdx + endMarker.length)
|
||||
return before + normalizedBlock + after
|
||||
}
|
||||
|
||||
const separator =
|
||||
existing.length === 0 || /\r?\n$/.test(existing) ? eol : eol + eol
|
||||
return existing + separator + normalizedBlock + eol
|
||||
}
|
||||
|
||||
function removeManagedBlock(
|
||||
existing: string,
|
||||
startMarker: string,
|
||||
endMarker: string
|
||||
): string {
|
||||
const startIdx = existing.indexOf(startMarker)
|
||||
if (startIdx === -1) return existing
|
||||
const endIdx = existing.indexOf(endMarker, startIdx)
|
||||
if (endIdx === -1) return existing
|
||||
|
||||
let cutStart = startIdx
|
||||
while (cutStart > 0 && /[\t ]/.test(existing[cutStart - 1])) cutStart--
|
||||
if (cutStart > 0 && existing[cutStart - 1] === '\n') {
|
||||
cutStart--
|
||||
if (cutStart > 0 && existing[cutStart - 1] === '\r') cutStart--
|
||||
}
|
||||
|
||||
let cutEnd = endIdx + endMarker.length
|
||||
while (cutEnd < existing.length && /[\t ]/.test(existing[cutEnd])) cutEnd++
|
||||
if (existing[cutEnd] === '\r') cutEnd++
|
||||
if (existing[cutEnd] === '\n') cutEnd++
|
||||
|
||||
// A block at the very top has no preceding newline to absorb, so also drop
|
||||
// the blank line that separated it from the content below. Otherwise the
|
||||
// file would start with an empty line.
|
||||
if (cutStart === 0) {
|
||||
if (existing[cutEnd] === '\r') cutEnd++
|
||||
if (existing[cutEnd] === '\n') cutEnd++
|
||||
}
|
||||
|
||||
const before = existing.slice(0, cutStart)
|
||||
const after = existing.slice(cutEnd)
|
||||
const separator =
|
||||
before.length > 0 &&
|
||||
after.length > 0 &&
|
||||
!before.endsWith('\n') &&
|
||||
!after.startsWith('\r') &&
|
||||
!after.startsWith('\n')
|
||||
? detectEol(existing)
|
||||
: ''
|
||||
|
||||
return before + separator + after
|
||||
}
|
||||
|
||||
function stripLegacyAgentRulesBlock(
|
||||
existing: string,
|
||||
eol: '\r\n' | '\n' = '\n'
|
||||
|
||||
@@ -26,6 +26,8 @@ export type ServerInitResult = {
|
||||
partialPrefetching?: boolean
|
||||
// Whether AGENTS.md / CLAUDE.md auto-generation is enabled (default true)
|
||||
agentRules?: boolean
|
||||
// Whether managed agent-feedback instructions are enabled (default false)
|
||||
agentFeedback?: boolean
|
||||
// Whether the development server memory threshold restart is enabled
|
||||
devMemoryThresholdRestart: boolean
|
||||
}
|
||||
|
||||
@@ -1113,6 +1113,7 @@ export async function initialize(opts: {
|
||||
cacheComponents: config.cacheComponents,
|
||||
partialPrefetching: config.partialPrefetching,
|
||||
agentRules: config.agentRules,
|
||||
agentFeedback: config.experimental.agentFeedback,
|
||||
devMemoryThresholdRestart,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,13 @@ import {
|
||||
PHASE_DEVELOPMENT_SERVER,
|
||||
} from '../../shared/lib/constants'
|
||||
import {
|
||||
ensureAgentRulesForDev,
|
||||
getEnvInfo,
|
||||
logExperimentalInfo,
|
||||
logStartInfo,
|
||||
syncAgentFeedbackForDev,
|
||||
syncAgentRulesForDev,
|
||||
} from './app-info-log'
|
||||
import type { AgentFilesResult } from './generate-agent-files'
|
||||
import { validateTurboNextConfig } from '../../lib/turbopack-warning'
|
||||
import {
|
||||
type Span,
|
||||
@@ -508,30 +510,24 @@ export async function startServer(
|
||||
partialPrefetching: initResult.partialPrefetching,
|
||||
})
|
||||
|
||||
// Auto-generate AGENTS.md / CLAUDE.md when an AI coding agent
|
||||
// is detected but the managed agent-rules block is missing.
|
||||
// Gated on `agentRules` in next.config (default true).
|
||||
if (initResult.agentRules !== false) {
|
||||
const result = await ensureAgentRulesForDev(dir)
|
||||
if (result) {
|
||||
const generated: string[] = []
|
||||
if (
|
||||
result.agentsMd === 'created' ||
|
||||
result.agentsMd === 'updated'
|
||||
)
|
||||
generated.push('AGENTS.md')
|
||||
if (
|
||||
result.claudeMd === 'created' ||
|
||||
result.claudeMd === 'updated'
|
||||
)
|
||||
generated.push('CLAUDE.md')
|
||||
if (generated.length > 0) {
|
||||
Log.event(
|
||||
`Generated ${generated.join(' and ')} for AI agents. Set \`agentRules: false\` in next.config to disable.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
logAgentFileSync(
|
||||
await syncAgentRulesForDev(dir, initResult.agentRules !== false),
|
||||
(files) =>
|
||||
`Generated ${files} for AI agents. Set \`agentRules: false\` in next.config to disable.`,
|
||||
(files) =>
|
||||
`Removed agent rules from ${files} because \`agentRules\` is disabled.`
|
||||
)
|
||||
|
||||
logAgentFileSync(
|
||||
await syncAgentFeedbackForDev(
|
||||
dir,
|
||||
initResult.agentFeedback === true
|
||||
),
|
||||
(files) =>
|
||||
`Generated agent feedback instructions in ${files}. Set \`experimental.agentFeedback: false\` in next.config to disable.`,
|
||||
(files) =>
|
||||
`Removed agent feedback instructions from ${files} because \`experimental.agentFeedback\` is disabled.`
|
||||
)
|
||||
}
|
||||
|
||||
handlersReady()
|
||||
@@ -676,3 +672,30 @@ if (process.env.NEXT_PRIVATE_WORKER && process.send) {
|
||||
})
|
||||
process.send({ nextWorkerReady: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Report which agent files a managed-block sync touched. Silent when the sync
|
||||
* was a no-op so every `next dev` start doesn't mention the files.
|
||||
*/
|
||||
function logAgentFileSync(
|
||||
result: AgentFilesResult | null,
|
||||
generatedMessage: (files: string) => string,
|
||||
removedMessage: (files: string) => string
|
||||
): void {
|
||||
if (!result) return
|
||||
|
||||
const generated: string[] = []
|
||||
const removed: string[] = []
|
||||
for (const [file, action] of [
|
||||
['AGENTS.md', result.agentsMd],
|
||||
['CLAUDE.md', result.claudeMd],
|
||||
] as const) {
|
||||
if (action === 'created' || action === 'updated') {
|
||||
generated.push(file)
|
||||
} else if (action === 'removed') {
|
||||
removed.push(file)
|
||||
}
|
||||
}
|
||||
if (generated.length > 0) Log.event(generatedMessage(generated.join(' and ')))
|
||||
if (removed.length > 0) Log.event(removedMessage(removed.join(' and ')))
|
||||
}
|
||||
|
||||
@@ -43,6 +43,13 @@ export async function copy_docs(task, opts) {
|
||||
}
|
||||
})
|
||||
.target('dist/docs')
|
||||
|
||||
// The agent-feedback protocol is intentionally kept out of dist/docs so
|
||||
// agents globbing the bundled docs don't read its instructions out of
|
||||
// context. `next internal agent-feedback-instructions` prints it on demand.
|
||||
await task
|
||||
.source(join(__dirname, 'src/agent-feedback/protocol.md'))
|
||||
.target('dist/agent-feedback')
|
||||
}
|
||||
|
||||
export async function copy_styled_jsx_assets(task, opts) {
|
||||
|
||||
@@ -185,3 +185,17 @@ Close the session with the same session and restore context:
|
||||
`agent-browser --session "$SESSION" --restore close`. `close` saves
|
||||
that session's cookies and storage so the next loop's `--restore` open
|
||||
keeps the user logged in. Leave `next dev` up for the next loop.
|
||||
|
||||
## Report Next.js friction
|
||||
|
||||
Only participate in agent feedback when managed Next.js feedback instructions
|
||||
are already loaded for the project. Their presence means the feature is
|
||||
enabled; their absence means it is disabled.
|
||||
|
||||
When enabled, add qualifying de-identified candidates found during verification
|
||||
to the shared friction queue in the current task context, then continue
|
||||
verification. Do not run the feedback command or open review forms during the
|
||||
loop or at this Skill's teardown.
|
||||
|
||||
The managed instructions own the single feedback pass at the final stopping
|
||||
point of the overall task. If they are absent, do not queue or report feedback.
|
||||
|
||||
+221
-18
@@ -5,6 +5,27 @@ import path from 'path'
|
||||
import { writeAgentFiles } from 'next/dist/server/lib/generate-agent-files'
|
||||
|
||||
const AGENT_RULES_MARKER = '<!-- BEGIN:nextjs-agent-rules -->'
|
||||
const AGENT_FEEDBACK_MARKER = '<!-- BEGIN:nextjs-agent-feedback -->'
|
||||
|
||||
/** Clears every variable `@vercel/detect-agent` inspects so no agent is detected. */
|
||||
const NO_AGENT_ENV = {
|
||||
AI_AGENT: '',
|
||||
CURSOR_TRACE_ID: '',
|
||||
CURSOR_AGENT: '',
|
||||
GEMINI_CLI: '',
|
||||
CODEX_SANDBOX: '',
|
||||
CODEX_CI: '',
|
||||
CODEX_THREAD_ID: '',
|
||||
ANTIGRAVITY_AGENT: '',
|
||||
AUGMENT_AGENT: '',
|
||||
OPENCODE_CLIENT: '',
|
||||
CLAUDECODE: '',
|
||||
CLAUDE_CODE: '',
|
||||
REPL_ID: '',
|
||||
COPILOT_MODEL: '',
|
||||
COPILOT_ALLOW_ALL: '',
|
||||
COPILOT_GITHUB_TOKEN: '',
|
||||
}
|
||||
|
||||
/**
|
||||
* The canonical block as the version under test generates it,
|
||||
@@ -17,6 +38,13 @@ function currentAgentRulesBlock(): string {
|
||||
return fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf-8').trimEnd()
|
||||
}
|
||||
|
||||
/** A feedback block from an older version, used to check refresh and removal. */
|
||||
function staleAgentFeedbackBlock(): string {
|
||||
return `${AGENT_FEEDBACK_MARKER}
|
||||
stale feedback instructions
|
||||
<!-- END:nextjs-agent-feedback -->`
|
||||
}
|
||||
|
||||
describe('agent-rules auto-generate on next dev (agent detected)', () => {
|
||||
const { next } = nextTestSetup({
|
||||
files: __dirname,
|
||||
@@ -35,6 +63,7 @@ describe('agent-rules auto-generate on next dev (agent detected)', () => {
|
||||
'utf-8'
|
||||
)
|
||||
expect(agentsContent).toContain(AGENT_RULES_MARKER)
|
||||
expect(agentsContent).toContain('\n## This is NOT the Next.js you know\n')
|
||||
expect(agentsContent).toContain('node_modules/next/dist/docs/')
|
||||
|
||||
const claudeContent = fs.readFileSync(
|
||||
@@ -51,24 +80,7 @@ describe('agent-rules auto-generate on next dev (no agent)', () => {
|
||||
// Explicitly clear every env var the agent detector inspects so the
|
||||
// test doesn't inherit one from the host shell (e.g. running it
|
||||
// inside Claude Code would otherwise trigger generation).
|
||||
env: {
|
||||
AI_AGENT: '',
|
||||
CURSOR_TRACE_ID: '',
|
||||
CURSOR_AGENT: '',
|
||||
GEMINI_CLI: '',
|
||||
CODEX_SANDBOX: '',
|
||||
CODEX_CI: '',
|
||||
CODEX_THREAD_ID: '',
|
||||
ANTIGRAVITY_AGENT: '',
|
||||
AUGMENT_AGENT: '',
|
||||
OPENCODE_CLIENT: '',
|
||||
CLAUDECODE: '',
|
||||
CLAUDE_CODE: '',
|
||||
REPL_ID: '',
|
||||
COPILOT_MODEL: '',
|
||||
COPILOT_ALLOW_ALL: '',
|
||||
COPILOT_GITHUB_TOKEN: '',
|
||||
},
|
||||
env: NO_AGENT_ENV,
|
||||
})
|
||||
|
||||
it('does not create AGENTS.md or CLAUDE.md when no agent is detected', async () => {
|
||||
@@ -197,3 +209,194 @@ describe('agent-rules auto-generate on next dev (CLAUDE.md exists, no AGENTS.md)
|
||||
expect(fs.existsSync(path.join(next.testDir, 'AGENTS.md'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('agent-feedback auto-generate on next dev (enabled)', () => {
|
||||
const { next } = nextTestSetup({
|
||||
files: __dirname,
|
||||
env: { CLAUDECODE: '1' },
|
||||
nextConfig: {
|
||||
experimental: {
|
||||
agentFeedback: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
it('creates a separate managed feedback block', async () => {
|
||||
await next.fetch('/')
|
||||
const content = fs.readFileSync(
|
||||
path.join(next.testDir, 'AGENTS.md'),
|
||||
'utf-8'
|
||||
)
|
||||
expect(content).toContain(AGENT_RULES_MARKER)
|
||||
expect(content).toContain(AGENT_FEEDBACK_MARKER)
|
||||
expect(content).toContain('\n## Report Next.js feedback\n')
|
||||
expect(content).toContain('meaningful detour or are likely to recur')
|
||||
expect(content).toContain('Crashes, hangs, OOMs, or unexpected slowness')
|
||||
expect(content).toContain('CLI, codemod, Skill, or upgrade behavior')
|
||||
expect(content).toContain(
|
||||
'node node_modules/next/dist/bin/next internal agent-feedback-instructions'
|
||||
)
|
||||
expect(content).toContain('once per task')
|
||||
expect(content).not.toContain('"schemaVersion":3')
|
||||
})
|
||||
|
||||
it('is idempotent across dev server restarts', async () => {
|
||||
await next.fetch('/')
|
||||
const before = fs.readFileSync(
|
||||
path.join(next.testDir, 'AGENTS.md'),
|
||||
'utf-8'
|
||||
)
|
||||
await next.stop()
|
||||
await next.start()
|
||||
await next.fetch('/')
|
||||
expect(fs.readFileSync(path.join(next.testDir, 'AGENTS.md'), 'utf-8')).toBe(
|
||||
before
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('agent-feedback auto-generate on next dev (agentRules: false)', () => {
|
||||
const { next } = nextTestSetup({
|
||||
files: __dirname,
|
||||
env: { CLAUDECODE: '1' },
|
||||
nextConfig: {
|
||||
agentRules: false,
|
||||
experimental: {
|
||||
agentFeedback: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
it('creates feedback instructions independently from agent rules', async () => {
|
||||
await next.fetch('/')
|
||||
const content = fs.readFileSync(
|
||||
path.join(next.testDir, 'AGENTS.md'),
|
||||
'utf-8'
|
||||
)
|
||||
expect(content).toContain(AGENT_FEEDBACK_MARKER)
|
||||
expect(content).not.toContain(AGENT_RULES_MARKER)
|
||||
expect(fs.readFileSync(path.join(next.testDir, 'CLAUDE.md'), 'utf-8')).toBe(
|
||||
'@AGENTS.md\n'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('agent-feedback auto-generate on next dev (stale CLAUDE.md block)', () => {
|
||||
const { next } = nextTestSetup({
|
||||
files: __dirname,
|
||||
env: { CLAUDECODE: '1' },
|
||||
nextConfig: {
|
||||
agentRules: false,
|
||||
experimental: {
|
||||
agentFeedback: true,
|
||||
},
|
||||
},
|
||||
skipStart: true,
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await next.patchFile(
|
||||
'CLAUDE.md',
|
||||
'# Team rules\r\n\r\n<!-- BEGIN:nextjs-agent-feedback -->\r\nstale\r\n<!-- END:nextjs-agent-feedback -->\r\n'
|
||||
)
|
||||
await next.start()
|
||||
})
|
||||
|
||||
it('refreshes CLAUDE.md in place and preserves CRLF line endings', async () => {
|
||||
await next.fetch('/')
|
||||
const content = fs.readFileSync(
|
||||
path.join(next.testDir, 'CLAUDE.md'),
|
||||
'utf-8'
|
||||
)
|
||||
expect(content).toContain('# Team rules\r\n')
|
||||
expect(content).toContain(
|
||||
'node node_modules/next/dist/bin/next internal agent-feedback-instructions'
|
||||
)
|
||||
expect(content).not.toContain('stale')
|
||||
expect(content).not.toMatch(/(?<!\r)\n/)
|
||||
expect(fs.existsSync(path.join(next.testDir, 'AGENTS.md'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('agent-feedback auto-generate on next dev (no agent)', () => {
|
||||
const { next } = nextTestSetup({
|
||||
files: __dirname,
|
||||
env: NO_AGENT_ENV,
|
||||
nextConfig: {
|
||||
agentRules: false,
|
||||
experimental: {
|
||||
agentFeedback: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
it('does not create feedback instructions without a detected agent', async () => {
|
||||
await next.fetch('/')
|
||||
expect(fs.existsSync(path.join(next.testDir, 'AGENTS.md'))).toBe(false)
|
||||
expect(fs.existsSync(path.join(next.testDir, 'CLAUDE.md'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('agent-feedback auto-generate on next dev (disabled)', () => {
|
||||
const { next } = nextTestSetup({
|
||||
files: __dirname,
|
||||
env: { CLAUDECODE: '1' },
|
||||
skipStart: true,
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await next.patchFile(
|
||||
'AGENTS.md',
|
||||
`# Team rules\n${staleAgentFeedbackBlock()}\n# More rules\n\n${currentAgentRulesBlock()}\n`
|
||||
)
|
||||
await next.start()
|
||||
})
|
||||
|
||||
it('removes only the managed feedback block', async () => {
|
||||
await next.fetch('/')
|
||||
const content = fs.readFileSync(
|
||||
path.join(next.testDir, 'AGENTS.md'),
|
||||
'utf-8'
|
||||
)
|
||||
expect(content).toContain('# Team rules\n# More rules')
|
||||
expect(content).toContain(AGENT_RULES_MARKER)
|
||||
expect(content).not.toContain(AGENT_FEEDBACK_MARKER)
|
||||
})
|
||||
})
|
||||
|
||||
describe('agent-rules auto-generate on next dev (disabled with existing blocks)', () => {
|
||||
const { next } = nextTestSetup({
|
||||
files: __dirname,
|
||||
env: NO_AGENT_ENV,
|
||||
nextConfig: {
|
||||
agentRules: false,
|
||||
experimental: {
|
||||
agentFeedback: true,
|
||||
},
|
||||
},
|
||||
skipStart: true,
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await next.patchFile(
|
||||
'AGENTS.md',
|
||||
`# Team rules\n\nKeep this content.\n\n${currentAgentRulesBlock()}\n\n${staleAgentFeedbackBlock()}\n`
|
||||
)
|
||||
await next.patchFile('CLAUDE.md', `${currentAgentRulesBlock()}\n`)
|
||||
await next.start()
|
||||
})
|
||||
|
||||
it('removes the managed rules block without a detected agent and drops an emptied file', async () => {
|
||||
await next.fetch('/')
|
||||
const content = fs.readFileSync(
|
||||
path.join(next.testDir, 'AGENTS.md'),
|
||||
'utf-8'
|
||||
)
|
||||
expect(content).toContain('Keep this content.')
|
||||
expect(content).not.toContain(AGENT_RULES_MARKER)
|
||||
expect(content).toContain(AGENT_FEEDBACK_MARKER)
|
||||
// CLAUDE.md held nothing but the managed block, so it is removed instead
|
||||
// of being left as an empty file.
|
||||
expect(fs.existsSync(path.join(next.testDir, 'CLAUDE.md'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user