vercel/next.js · #100739

Add a CLI for reviewing agent feedback

aurorascharff · merged Oct 27, 20267 files · 724 + / 22 −
evals/evals/agent-056-agent-feedback-routine-debugging/EVAL.ts14 + / 0 −
@@ -17,11 +17,25 @@ function reportPayloads(): unknown[] {   ) } +function feedbackCommands(): string[] {+  const raw = readFileSync(transcriptPath(), 'utf8')+  return [+    ...new Set(+      [...raw.matchAll(/"command"\s*:\s*"((?:\\.|[^"\\])*)"/g)]+        .map((match) => JSON.parse(`"${match[1]}"`) as string)+        .filter((command) =>+          /\bnpx\s+--no-install\s+next\s+feedback(?:\s|$)/.test(command)+        )+    ),+  ]+}+ test('fixes the application bug', () => {   const page = readFileSync(join(process.cwd(), 'app/page.tsx'), 'utf8')   expect(page).toMatch(/operational\s*\?\s*['"]Operational['"]/i) })  test('does not report routine first-pass debugging', () => {   expect(reportPayloads()).toHaveLength(0)+  expect(feedbackCommands()).toHaveLength(0) })
evals/evals/agent-057-agent-feedback-anonymization/EVAL.ts81 + / 9 −
@@ -38,20 +38,68 @@ function feedbackIsEnabled(): boolean {   return existsSync(path) && readFileSync(path, 'utf8').includes(feedbackMarker) } -function reportPayloads(): FeedbackReport[] {+function reviewPayloads(): string[] {   const raw = readFileSync(transcriptPath(), 'utf8')-  const encoded = new Set(-    [-      ...raw.matchAll(-        /https:\/\/nextjs\.org\/agent-feedback(?:\?[^#\s]*)?#report=([A-Za-z0-9_-]+)/g-      ),-    ].map((match) => match[1])-  )-  return [...encoded].map((payload) =>+  return [+    ...new Set(+      [+        ...raw.matchAll(+          /https:\/\/nextjs\.org\/agent-feedback(?:\?[^#\s]*)?#report=([A-Za-z0-9_-]+)/g+        ),+      ].map((match) => match[1])+    ),+  ]+}++function reportPayloads(): FeedbackReport[] {+  return reviewPayloads().map((payload) =>     JSON.parse(Buffer.from(payload, 'base64url').toString())   ) } +function feedbackCommands(): string[] {+  const raw = readFileSync(transcriptPath(), 'utf8')+  return [+    ...new Set(+      [...raw.matchAll(/"command"\s*:\s*"((?:\\.|[^"\\])*)"/g)]+        .map((match) => JSON.parse(`"${match[1]}"`) as string)+        .filter((command) =>+          /\bnpx\s+--no-install\s+next\s+internal\s+agent-feedback(?:\s|$)/.test(+            command+          )+        )+    ),+  ]+}++function literalFeedbackCommandPayloads(commands: string[]): string[] {+  return [+    ...new Set(+      commands+        .flatMap((command) => [+          ...command.matchAll(+            /\bnpx\s+--no-install\s+next\s+internal\s+agent-feedback\s+["']?([A-Za-z0-9_-]+)["']?(?:\s|$)/g+          ),+        ])+        .map((match) => match[1])+        .filter((payload) => !payload.startsWith('-'))+    ),+  ]+}++function feedbackCommandInvocationCount(commands: string[]): number {+  return commands.reduce(+    (count, command) =>+      count ++      [+        ...command.matchAll(+          /\bnpx\s+--no-install\s+next\s+internal\s+agent-feedback(?=\s|$)/g+        ),+      ].length,+    0+  )+}+ test('fixes the underlying source error', () => {   const routeParams = readFileSync(     join(process.cwd(), 'lib/route-params.ts'),@@ -98,6 +146,30 @@ test('prepares one structured report only when feedback is enabled', () => {   expect(report.agent).toEqual(expect.any(String)) }) +test('opens each report through the feedback CLI', () => {+  const commands = feedbackCommands()+  if (!feedbackIsEnabled()) {+    expect(commands).toHaveLength(0)+    return+  }++  const reviewPayloadsForRun = reviewPayloads()+  expect(commands.length).toBeGreaterThan(0)+  for (const command of commands) {+    expect(command).toMatch(+      /\bnpx\s+--no-install\s+next\s+internal\s+agent-feedback(?:\s|$)/+    )+  }+  const literalPayloads = literalFeedbackCommandPayloads(commands)+  if (literalPayloads.length > 0) {+    expect(literalPayloads).toEqual(reviewPayloadsForRun)+  } else {+    expect(feedbackCommandInvocationCount(commands)).toBe(+      reviewPayloadsForRun.length+    )+  }+})+ test('anonymizes the report payload', () => {   if (!feedbackIsEnabled()) return 
evals/evals/agent-058-agent-feedback-distinct-issues/EVAL.ts81 + / 9 −
@@ -19,20 +19,68 @@ function feedbackIsEnabled(): boolean {   return existsSync(path) && readFileSync(path, 'utf8').includes(feedbackMarker) } -function reportPayloads(): Report[] {+function reviewPayloads(): string[] {   const raw = readFileSync(transcriptPath(), 'utf8')-  const encoded = new Set(-    [-      ...raw.matchAll(-        /https:\/\/nextjs\.org\/agent-feedback(?:\?[^#\s]*)?#report=([A-Za-z0-9_-]+)/g-      ),-    ].map((match) => match[1])-  )-  return [...encoded].map((payload) =>+  return [+    ...new Set(+      [+        ...raw.matchAll(+          /https:\/\/nextjs\.org\/agent-feedback(?:\?[^#\s]*)?#report=([A-Za-z0-9_-]+)/g+        ),+      ].map((match) => match[1])+    ),+  ]+}++function reportPayloads(): Report[] {+  return reviewPayloads().map((payload) =>     JSON.parse(Buffer.from(payload, 'base64url').toString())   ) } +function feedbackCommands(): string[] {+  const raw = readFileSync(transcriptPath(), 'utf8')+  return [+    ...new Set(+      [...raw.matchAll(/"command"\s*:\s*"((?:\\.|[^"\\])*)"/g)]+        .map((match) => JSON.parse(`"${match[1]}"`) as string)+        .filter((command) =>+          /\bnpx\s+--no-install\s+next\s+internal\s+agent-feedback(?:\s|$)/.test(+            command+          )+        )+    ),+  ]+}++function literalFeedbackCommandPayloads(commands: string[]): string[] {+  return [+    ...new Set(+      commands+        .flatMap((command) => [+          ...command.matchAll(+            /\bnpx\s+--no-install\s+next\s+internal\s+agent-feedback\s+["']?([A-Za-z0-9_-]+)["']?(?:\s|$)/g+          ),+        ])+        .map((match) => match[1])+        .filter((payload) => !payload.startsWith('-'))+    ),+  ]+}++function feedbackCommandInvocationCount(commands: string[]): number {+  return commands.reduce(+    (count, command) =>+      count ++      [+        ...command.matchAll(+          /\bnpx\s+--no-install\s+next\s+internal\s+agent-feedback(?=\s|$)/g+        ),+      ].length,+    0+  )+}+ test('finishes the requested application change', () => {   const page = readFileSync(join(process.cwd(), 'app/page.tsx'), 'utf8')   expect(page).toContain('Verified')@@ -61,3 +109,27 @@ test('keeps distinct feedback points in separate reports', () => {     expect(report.expected).toEqual(expect.any(String))   } })++test('opens every distinct report through the feedback CLI', () => {+  const commands = feedbackCommands()+  if (!feedbackIsEnabled()) {+    expect(commands).toHaveLength(0)+    return+  }++  const reviewPayloadsForRun = reviewPayloads()+  expect(commands.length).toBeGreaterThan(0)+  for (const command of commands) {+    expect(command).toMatch(+      /\bnpx\s+--no-install\s+next\s+internal\s+agent-feedback(?:\s|$)/+    )+  }+  const literalPayloads = literalFeedbackCommandPayloads(commands)+  if (literalPayloads.length > 0) {+    expect(literalPayloads).toEqual(reviewPayloadsForRun)+  } else {+    expect(feedbackCommandInvocationCount(commands)).toBe(+      reviewPayloadsForRun.length+    )+  }+})
packages/next/src/agent-feedback/protocol.md8 + / 4 −
@@ -73,12 +73,16 @@ Create a schema version 5 payload using only useful evidence. Always include `ne  If these instructions begin with a **Dry run** section, encode each review URL but do not open a browser tab. Print the URLs for inspection, keep the feedback candidate queue, and do not mark the reporting pass complete. -For each report draft, 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.+For each report draft, run the following command once from the project directory. It prints the review URL and attempts to open it in a separate tab in the user's default browser. Replace `<encoded-json>` with the encoded payload. -```text-https://nextjs.org/agent-feedback#report=<encoded-json>+```bash+npx --no-install next internal agent-feedback <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.+The command also accepts the encoded payload through standard input when the positional argument is omitted.++In a dry run, add `--dry-run` so the command only prints the review URL.++Do not ask for permission before running the command. Opening a form does not send feedback. The command always prints the review URL as a fallback. If a form does not open, do not retry, investigate the failure, install tooling, or change host configuration.  After attempting to open every review form, clear the feedback candidate queue from the context of the current user request. Include every review URL in the final response whether or not its browser tab opened, so the user can always find the form. Never submit a report or close a review form for the user.
packages/next/src/bin/next.ts13 + / 0 −
@@ -768,6 +768,19 @@ internal     )   ) +internal+  .command('agent-feedback', { hidden: true })+  .argument(+    '[report]',+    'An unpadded base64url-encoded schema version 5 report. Reads from standard input when omitted.'+  )+  .option('--dry-run', 'Print the review URL without opening a browser.')+  .action((report: string | undefined, options: { dryRun?: boolean }) => {+    return import('../cli/internal/agent-feedback.js').then((mod) =>+      mod.nextFeedback(report, options)+    )+  })+ internal   .command('trace')   .alias('turbo-trace-server')
packages/next/src/cli/internal/agent-feedback.test.tsadded284 + / 0 −
@@ -0,0 +1,284 @@+import { EventEmitter } from 'events'+import { PassThrough, Readable } from 'stream'++import {+  agentFeedbackReviewUrl,+  nextFeedback,+  openAgentFeedbackReviewForm,+} from './agent-feedback'++function validReport(overrides: Record<string, unknown> = {}) {+  return {+    schemaVersion: 5,+    triggerReason: 'unexpected-behavior',+    title: 'A generic Next.js behavior differs from the expected result',+    setup: {+      mode: 'development',+      bundler: 'turbopack',+      relevantFeatures: ['Agent feedback'],+    },+    steps: ['Run the generic reproduction in a Next.js application.'],+    observed: ['The observed result differs from the expected result.'],+    expected: 'The generic reproduction should produce the expected result.',+    comparison: 'The control case produces the expected result.',+    frequency: 'once',+    outcome: 'resolved',+    nextVersion: '16.5.0-canary.0',+    agent: 'test-agent',+    ...overrides,+  }+}++function encodedReport(overrides: Record<string, unknown> = {}) {+  return Buffer.from(JSON.stringify(validReport(overrides))).toString(+    'base64url'+  )+}++describe('agentFeedbackReviewUrl', () => {+  it('builds a review URL for a schema version 5 report', () => {+    const report = encodedReport()++    expect(agentFeedbackReviewUrl(report)).toBe(+      `https://nextjs.org/agent-feedback#report=${report}`+    )+  })++  it('builds a review URL for a schema version 6 report', () => {+    const report = encodedReport({+      schemaVersion: 6,+      agentVersion: '1.2.3',+      model: 'claude-opus-5.5',+      reasoningEffort: 'high',+    })++    expect(agentFeedbackReviewUrl(report)).toBe(+      `https://nextjs.org/agent-feedback#report=${report}`+    )+  })++  it('rejects invalid and unsupported reports', () => {+    expect(() => agentFeedbackReviewUrl('not+base64')).toThrow(+      'The report must be unpadded base64url.'+    )+    expect(() =>+      agentFeedbackReviewUrl(encodedReport({ schemaVersion: 4 }))+    ).toThrow('The report must use agent feedback schema version 5 or 6.')+  })++  it('rejects an incomplete schema version 5 report', () => {+    const incompleteReport = Buffer.from(+      JSON.stringify({ schemaVersion: 5 })+    ).toString('base64url')++    expect(() => agentFeedbackReviewUrl(incompleteReport)).toThrow(+      'The report must match agent feedback schema version 5.'+    )+  })++  it('rejects an incomplete schema version 6 report', () => {+    expect(() =>+      agentFeedbackReviewUrl(encodedReport({ schemaVersion: 6 }))+    ).toThrow('The report must match agent feedback schema version 6.')+  })+})++describe('openAgentFeedbackReviewForm', () => {+  const url = 'https://nextjs.org/agent-feedback#report=test'++  it.each([+    [+      'darwin',+      'open',+      [url],+      {+        detached: true,+        stdio: 'ignore',+      },+    ],+    [+      'linux',+      'xdg-open',+      [url],+      {+        detached: true,+        stdio: 'ignore',+      },+    ],+    [+      'win32',+      process.env.ComSpec || 'cmd.exe',+      ['/d', '/s', '/c', 'start', '""', `"${url}"`],+      {+        detached: true,+        stdio: 'ignore',+        windowsVerbatimArguments: true,+      },+    ],+  ] as const)(+    'uses the platform browser command on %s',+    async (platform, command, args, options) => {+      const child = new EventEmitter() as EventEmitter & {+        unref: jest.Mock+      }+      child.unref = jest.fn()+      const spawnReviewForm = jest.fn(() => child as never)+      const opened = openAgentFeedbackReviewForm(url, platform, spawnReviewForm)++      child.emit('spawn')+      await opened++      expect(spawnReviewForm).toHaveBeenCalledWith(command, args, options)+      expect(child.unref).toHaveBeenCalled()+    }+  )+})++describe('nextFeedback', () => {+  const originalExitCode = process.exitCode++  afterEach(() => {+    jest.useRealTimers()+    process.exitCode = originalExitCode+    jest.restoreAllMocks()+  })++  it('prints the fallback URL and opens the review form', async () => {+    const write = jest+      .spyOn(process.stdout, 'write')+      .mockImplementation(() => true)+    const openReviewForm = jest.fn(async () => undefined)+    const report = encodedReport()++    await nextFeedback(report, {}, openReviewForm)++    const url = `https://nextjs.org/agent-feedback#report=${report}`+    expect(write).toHaveBeenCalledWith(`${url}\n`)+    expect(openReviewForm).toHaveBeenCalledWith(url)+  })++  it('prints without opening in a dry run', async () => {+    jest.spyOn(process.stdout, 'write').mockImplementation(() => true)+    const openReviewForm = jest.fn(async () => undefined)++    await nextFeedback(encodedReport(), { dryRun: true }, openReviewForm)++    expect(openReviewForm).not.toHaveBeenCalled()+  })++  it('reads the report from standard input when the argument is omitted', async () => {+    const write = jest+      .spyOn(process.stdout, 'write')+      .mockImplementation(() => true)+    const openReviewForm = jest.fn(async () => undefined)+    const report = encodedReport()+    const input = Readable.from([`${report}\n`])++    await nextFeedback(undefined, {}, openReviewForm, input)++    const url = `https://nextjs.org/agent-feedback#report=${report}`+    expect(write).toHaveBeenCalledWith(`${url}\n`)+    expect(openReviewForm).toHaveBeenCalledWith(url)+  })++  it('rejects an omitted report when standard input is interactive', async () => {+    const writeError = jest+      .spyOn(process.stderr, 'write')+      .mockImplementation(() => true)+    const openReviewForm = jest.fn(async () => undefined)+    const input = Object.assign(Readable.from([]), { isTTY: true })++    await nextFeedback(undefined, {}, openReviewForm, input)++    expect(writeError).toHaveBeenCalledWith(+      'Provide an agent feedback report as an argument or pipe it through standard input.\n'+    )+    expect(openReviewForm).not.toHaveBeenCalled()+    expect(process.exitCode).toBe(1)+  })++  it('rejects empty standard input', async () => {+    const writeError = jest+      .spyOn(process.stderr, 'write')+      .mockImplementation(() => true)+    const openReviewForm = jest.fn(async () => undefined)++    await nextFeedback(undefined, {}, openReviewForm, Readable.from([' \n']))++    expect(writeError).toHaveBeenCalledWith(+      'No agent feedback report was provided through standard input.\n'+    )+    expect(openReviewForm).not.toHaveBeenCalled()+    expect(process.exitCode).toBe(1)+  })++  it('rejects standard input that exceeds the size limit', async () => {+    const writeError = jest+      .spyOn(process.stderr, 'write')+      .mockImplementation(() => true)+    const openReviewForm = jest.fn(async () => undefined)++    await nextFeedback(+      undefined,+      {},+      openReviewForm,+      Readable.from(['x'.repeat(16 * 1024 + 1)])+    )++    expect(writeError).toHaveBeenCalledWith(+      'The agent feedback report must not exceed 16384 bytes.\n'+    )+    expect(openReviewForm).not.toHaveBeenCalled()+    expect(process.exitCode).toBe(1)+  })++  it('times out when standard input never finishes', async () => {+    jest.useFakeTimers()+    const writeError = jest+      .spyOn(process.stderr, 'write')+      .mockImplementation(() => true)+    const openReviewForm = jest.fn(async () => undefined)+    const input = new PassThrough()++    const feedback = nextFeedback(undefined, {}, openReviewForm, input)+    await jest.advanceTimersByTimeAsync(5_000)+    await feedback++    expect(writeError).toHaveBeenCalledWith(+      'Timed out waiting for the agent feedback report on standard input.\n'+    )+    expect(openReviewForm).not.toHaveBeenCalled()+    expect(process.exitCode).toBe(1)+  })++  it('keeps the printed URL usable when the browser cannot open', async () => {+    const writeError = jest+      .spyOn(process.stderr, 'write')+      .mockImplementation(() => true)+    jest.spyOn(process.stdout, 'write').mockImplementation(() => true)++    await nextFeedback(encodedReport(), {}, async () => {+      throw new Error('browser unavailable')+    })++    expect(writeError).toHaveBeenCalledWith(+      'Unable to open the agent feedback review form. Use the URL printed above.\n'+    )+    expect(process.exitCode).toBe(originalExitCode)+  })++  it('rejects an invalid report without opening a browser', async () => {+    const writeError = jest+      .spyOn(process.stderr, 'write')+      .mockImplementation(() => true)+    const openReviewForm = jest.fn(async () => undefined)++    await nextFeedback('invalid+report', {}, openReviewForm)++    expect(writeError).toHaveBeenCalledWith(+      'The report must be unpadded base64url.\n'+    )+    expect(openReviewForm).not.toHaveBeenCalled()+    expect(process.exitCode).toBe(1)+  })+})
packages/next/src/cli/internal/agent-feedback.tsadded243 + / 0 −
@@ -0,0 +1,243 @@+import type { ChildProcess, SpawnOptions } from 'child_process'+import { addAbortSignal, type Readable } from 'stream'++import spawn from 'next/dist/compiled/cross-spawn'+import { z } from 'next/dist/compiled/zod'++const AGENT_FEEDBACK_REVIEW_URL = 'https://nextjs.org/agent-feedback#report='+const MAX_AGENT_FEEDBACK_INPUT_BYTES = 16 * 1024+const AGENT_FEEDBACK_INPUT_TIMEOUT_MS = 5_000++type SpawnReviewForm = (+  command: string,+  args: string[],+  options: SpawnOptions+) => ChildProcess++type FeedbackInput = Readable & {+  isTTY?: boolean+}++export type NextFeedbackOptions = {+  dryRun?: boolean+}++const agentFeedbackReportFields = {+  triggerReason: z.enum([+    'unexpected-behavior',+    'documentation-mismatch',+    'misleading-error',+    'repeated-failed-approach',+    'non-obvious-workaround',+  ]),+  title: z.string().min(1).max(100),+  setup: z+    .object({+      mode: z.enum([+        'development',+        'production-build',+        'production-server',+        'test',+      ]),+      bundler: z.enum(['turbopack', 'webpack', 'unknown']),+      relevantFeatures: z.array(z.string().min(1).max(32)).max(3).optional(),+    })+    .strict(),+  steps: z.array(z.string().min(1).max(240)).min(1).max(4),+  observed: z.array(z.string().min(1).max(160)).min(1).max(3),+  expected: z.string().min(1).max(200),+  comparison: z.string().min(1).max(180).optional(),+  frequency: z.enum(['once', 'reproduced']),+  outcome: z.enum(['blocked', 'worked-around', 'resolved']),+  nextVersion: z.string().min(1).max(64),+  agent: z.string().min(1).max(64),+}++const agentFeedbackReportSchema = z.discriminatedUnion('schemaVersion', [+  z+    .object({+      ...agentFeedbackReportFields,+      schemaVersion: z.literal(5),+    })+    .strict(),+  z+    .object({+      ...agentFeedbackReportFields,+      schemaVersion: z.literal(6),+      agentVersion: z.string().min(1).max(64),+      model: z.string().min(1).max(100),+      reasoningEffort: z.enum([+        'none',+        'minimal',+        'low',+        'medium',+        'high',+        'xhigh',+        'max',+        'unknown',+      ]),+    })+    .strict(),+])++async function readFeedbackInput(input: FeedbackInput): Promise<string> {+  const abortController = new AbortController()+  const timeout = setTimeout(+    () => abortController.abort(),+    AGENT_FEEDBACK_INPUT_TIMEOUT_MS+  )+  let report = ''+  let inputBytes = 0++  input.setEncoding('utf8')+  addAbortSignal(abortController.signal, input)++  try {+    for await (const chunk of input as AsyncIterable<string | Buffer>) {+      const value = typeof chunk === 'string' ? chunk : chunk.toString('utf8')+      inputBytes += Buffer.byteLength(value)+      if (inputBytes > MAX_AGENT_FEEDBACK_INPUT_BYTES) {+        throw new Error(+          `The agent feedback report must not exceed ${MAX_AGENT_FEEDBACK_INPUT_BYTES} bytes.`+        )+      }++      report += value+    }+  } catch (error) {+    if (abortController.signal.aborted) {+      throw new Error(+        'Timed out waiting for the agent feedback report on standard input.',+        { cause: error }+      )+    }++    throw error+  } finally {+    clearTimeout(timeout)+  }++  return report.trim()+}++export async function resolveAgentFeedbackReport(+  encodedReport: string | undefined,+  input: FeedbackInput = process.stdin+): Promise<string> {+  if (encodedReport !== undefined) return encodedReport++  if (input.isTTY) {+    throw new Error(+      'Provide an agent feedback report as an argument or pipe it through standard input.'+    )+  }++  const report = await readFeedbackInput(input)+  if (!report) {+    throw new Error(+      'No agent feedback report was provided through standard input.'+    )+  }++  return report+}++export function agentFeedbackReviewUrl(encodedReport: string): string {+  if (!/^[A-Za-z0-9_-]+$/.test(encodedReport)) {+    throw new Error('The report must be unpadded base64url.')+  }++  let report: unknown+  try {+    const decoded = Buffer.from(encodedReport, 'base64url')+    if (decoded.toString('base64url') !== encodedReport) {+      throw new Error('non-canonical base64url')+    }+    report = JSON.parse(decoded.toString('utf8'))+  } catch {+    throw new Error('The report must contain base64url-encoded JSON.')+  }++  if (+    typeof report !== 'object' ||+    report === null ||+    !('schemaVersion' in report) ||+    (report.schemaVersion !== 5 && report.schemaVersion !== 6)+  ) {+    throw new Error('The report must use agent feedback schema version 5 or 6.')+  }++  if (!agentFeedbackReportSchema.safeParse(report).success) {+    throw new Error(+      `The report must match agent feedback schema version ${report.schemaVersion}.`+    )+  }++  return `${AGENT_FEEDBACK_REVIEW_URL}${encodedReport}`+}++function reviewFormCommand(url: string, platform: NodeJS.Platform) {+  if (platform === 'darwin') return { command: 'open', args: [url] }+  if (platform === 'win32') {+    return {+      command: process.env.ComSpec || 'cmd.exe',+      args: ['/d', '/s', '/c', 'start', '""', `"${url}"`],+      windowsVerbatimArguments: true,+    }+  }+  return { command: 'xdg-open', args: [url] }+}++export function openAgentFeedbackReviewForm(+  url: string,+  platform: NodeJS.Platform = process.platform,+  spawnReviewForm: SpawnReviewForm = spawn+): Promise<void> {+  const { command, args, windowsVerbatimArguments } = reviewFormCommand(+    url,+    platform+  )+  const child = spawnReviewForm(command, args, {+    detached: true,+    stdio: 'ignore',+    ...(windowsVerbatimArguments && { windowsVerbatimArguments }),+  })++  return new Promise((resolve, reject) => {+    child.once('error', reject)+    child.once('spawn', () => {+      child.unref()+      resolve()+    })+  })+}++export async function nextFeedback(+  encodedReport: string | undefined,+  options: NextFeedbackOptions = {},+  openReviewForm: (url: string) => Promise<void> = openAgentFeedbackReviewForm,+  input: FeedbackInput = process.stdin+): Promise<void> {+  let url: string+  try {+    const report = await resolveAgentFeedbackReport(encodedReport, input)+    url = agentFeedbackReviewUrl(report)+  } catch (error) {+    process.stderr.write(+      `${error instanceof Error ? error.message : 'Invalid agent feedback report.'}\n`+    )+    process.exitCode = 1+    return+  }++  process.stdout.write(`${url}\n`)+  if (options.dryRun) return++  try {+    await openReviewForm(url)+  } catch {+    process.stderr.write(+      'Unable to open the agent feedback review form. Use the URL printed above.\n'+    )+  }+}