From dab8ab7410999708a54d9c396a5c38d8dd0a7595 Mon Sep 17 00:00:00 2001 From: Mattietk Date: Wed, 8 Jul 2026 13:08:20 +0100 Subject: [PATCH 1/2] [wrangler] Add one-time --force notice and drop skipped telemetry in Pages delegation When an agent opts out of the Pages-to-Workers delegation with `--force`, Wrangler now prints an end-of-command notice (on success) explaining that `--force` only needs to be passed once, since the project then exists and subsequent commands are no longer delegated. Also stop emitting the `skipped` telemetry event: those are deterministic, expected non-cases (e.g. the account already has Pages projects) that carry no signal, and the count is derivable from the Pages command's own telemetry. --- .changeset/pages-delegation-force-notice.md | 7 ++ .../pages/delegate-to-workers.test.ts | 97 +++++++------------ .../wrangler/src/pages/delegate-to-workers.ts | 84 +++++++++++----- packages/wrangler/src/pages/deploy.ts | 11 ++- packages/wrangler/src/pages/projects.ts | 11 ++- 5 files changed, 121 insertions(+), 89 deletions(-) create mode 100644 .changeset/pages-delegation-force-notice.md diff --git a/.changeset/pages-delegation-force-notice.md b/.changeset/pages-delegation-force-notice.md new file mode 100644 index 0000000000..ddb658f751 --- /dev/null +++ b/.changeset/pages-delegation-force-notice.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Improve the agent-facing `--force` guidance for Pages-to-Workers delegation and stop emitting the low-signal `skipped` telemetry event + +When an AI agent opts out of the Pages-to-Workers delegation by passing `--force` to `wrangler pages deploy` or `wrangler pages project create`, Wrangler now prints a notice at the end of a successful command explaining that `--force` only needs to be passed once: the project then exists, so subsequent commands are no longer delegated. Wrangler also no longer emits a telemetry event for delegations that were skipped because the command was ineligible (for example the account already has Pages projects), since those are expected, deterministic cases that carry no signal. diff --git a/packages/wrangler/src/__tests__/pages/delegate-to-workers.test.ts b/packages/wrangler/src/__tests__/pages/delegate-to-workers.test.ts index d9a33771f3..bd8a594599 100644 --- a/packages/wrangler/src/__tests__/pages/delegate-to-workers.test.ts +++ b/packages/wrangler/src/__tests__/pages/delegate-to-workers.test.ts @@ -4,6 +4,7 @@ import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; import { beforeEach, describe, it, vi } from "vitest"; import { sendMetricsEvent } from "../../metrics"; import { + logPagesToWorkersForceOptOutNotice, maybeDelegatePagesToWorkers, recordPagesToWorkersDelegateFailure, } from "../../pages/delegate-to-workers"; @@ -47,7 +48,7 @@ describe("maybeDelegatePagesToWorkers", () => { }); for (const command of ["deploy", "create"] as const) { - it(`does not delegate when the account already has Pages projects (${command}), and records why`, async ({ + it(`does not delegate (or emit telemetry) when the account already has Pages projects (${command})`, async ({ expect, }) => { const result = await maybeDelegatePagesToWorkers({ @@ -57,14 +58,9 @@ describe("maybeDelegatePagesToWorkers", () => { }); expect(result).toEqual({ delegate: false }); - expect(sendMetricsEvent).toHaveBeenCalledWith( - "delegate pages to workers", - expect.objectContaining({ - result: "skipped", - reason: "account has pages projects", - }), - expect.anything() - ); + // Skips are deterministic, expected non-cases, so they are not sent to + // telemetry. + expect(sendMetricsEvent).not.toHaveBeenCalled(); }); } @@ -83,7 +79,7 @@ describe("maybeDelegatePagesToWorkers", () => { }); }); - it("skips delegation (and records why) when the account Pages projects lookup fails", async ({ + it("skips delegation (without emitting telemetry) when the account Pages projects lookup fails", async ({ expect, }) => { const result = await maybeDelegatePagesToWorkers({ @@ -95,14 +91,7 @@ describe("maybeDelegatePagesToWorkers", () => { }); expect(result).toEqual({ delegate: false }); - expect(sendMetricsEvent).toHaveBeenCalledWith( - "delegate pages to workers", - expect.objectContaining({ - result: "skipped", - reason: "account pages projects lookup failed", - }), - expect.anything() - ); + expect(sendMetricsEvent).not.toHaveBeenCalled(); }); it("does not query account Pages projects when a cheaper, local check already skips", async ({ @@ -121,14 +110,7 @@ describe("maybeDelegatePagesToWorkers", () => { // The functions/ directory is a local, no-cost skip reason, so the // account-listing API call must never be made. expect(accountHasPagesProjects).not.toHaveBeenCalled(); - expect(sendMetricsEvent).toHaveBeenCalledWith( - "delegate pages to workers", - expect.objectContaining({ - result: "skipped", - reason: "pages functions directory", - }), - expect.anything() - ); + expect(sendMetricsEvent).not.toHaveBeenCalled(); }); it("does not delegate when project has a functions directory", async ({ @@ -142,14 +124,7 @@ describe("maybeDelegatePagesToWorkers", () => { }); expect(result).toEqual({ delegate: false }); - expect(sendMetricsEvent).toHaveBeenCalledWith( - "delegate pages to workers", - expect.objectContaining({ - result: "skipped", - reason: "pages functions directory", - }), - expect.anything() - ); + expect(sendMetricsEvent).not.toHaveBeenCalled(); }); const unsupportedFileMarkers: [marker: string, reason: string][] = [ @@ -157,10 +132,8 @@ describe("maybeDelegatePagesToWorkers", () => { ["_routes.json", "_routes.json file"], ]; - for (const [marker, reason] of unsupportedFileMarkers) { - it(`does not delegate when project has a ${marker}, and records why`, async ({ - expect, - }) => { + for (const [marker] of unsupportedFileMarkers) { + it(`does not delegate when project has a ${marker}`, async ({ expect }) => { createFile(process.cwd(), marker); const result = await maybeDelegatePagesToWorkers({ @@ -169,11 +142,7 @@ describe("maybeDelegatePagesToWorkers", () => { }); expect(result).toEqual({ delegate: false }); - expect(sendMetricsEvent).toHaveBeenCalledWith( - "delegate pages to workers", - expect.objectContaining({ result: "skipped", reason }), - expect.anything() - ); + expect(sendMetricsEvent).not.toHaveBeenCalled(); }); } @@ -191,14 +160,7 @@ describe("maybeDelegatePagesToWorkers", () => { }); expect(result).toEqual({ delegate: false }); - expect(sendMetricsEvent).toHaveBeenCalledWith( - "delegate pages to workers", - expect.objectContaining({ - result: "skipped", - reason: "_routes.json file", - }), - expect.anything() - ); + expect(sendMetricsEvent).not.toHaveBeenCalled(); }); for (const marker of ["_redirects", "_headers"]) { @@ -252,14 +214,7 @@ describe("maybeDelegatePagesToWorkers", () => { }); expect(result).toEqual({ delegate: false }); - expect(sendMetricsEvent).toHaveBeenCalledWith( - "delegate pages to workers", - expect.objectContaining({ - result: "skipped", - reason: "unsupported args: --branch", - }), - expect.anything() - ); + expect(sendMetricsEvent).not.toHaveBeenCalled(); }); it("delegates a brand-new static deploy to Workers", async ({ expect }) => { @@ -354,7 +309,7 @@ describe("maybeDelegatePagesToWorkers", () => { }); }); - it("does not delegate when --force is set, and records the opt-out", async ({ + it("does not delegate when --force is set, records the opt-out, and flags forcedOptOut", async ({ expect, }) => { const result = await maybeDelegatePagesToWorkers({ @@ -363,7 +318,7 @@ describe("maybeDelegatePagesToWorkers", () => { force: true, }); - expect(result).toEqual({ delegate: false }); + expect(result).toEqual({ delegate: false, forcedOptOut: true }); expect(sendMetricsEvent).toHaveBeenCalledWith( "delegate pages to workers", expect.objectContaining({ command: "deploy", result: "forced" }), @@ -371,6 +326,26 @@ describe("maybeDelegatePagesToWorkers", () => { ); }); + it("emits a one-time, deploy-specific --force notice to stdout", ({ + expect, + }) => { + logPagesToWorkersForceOptOutNotice("deploy"); + + expect(std.out).toContain("deployed directly on Cloudflare Pages"); + expect(std.out).toContain("This is the only time you need --force"); + expect(std.out).toContain("this project now exists"); + expect(std.out).toContain("Do not pass --force on future commands"); + }); + + it("emits a one-time, create-specific --force notice to stdout", ({ + expect, + }) => { + logPagesToWorkersForceOptOutNotice("create"); + + expect(std.out).toContain("created directly on Cloudflare Pages"); + expect(std.out).toContain("This is the only time you need --force"); + }); + it("records failure and gives explicit, loop-safe --force guidance", async ({ expect, }) => { diff --git a/packages/wrangler/src/pages/delegate-to-workers.ts b/packages/wrangler/src/pages/delegate-to-workers.ts index 873711d842..219c0b301e 100644 --- a/packages/wrangler/src/pages/delegate-to-workers.ts +++ b/packages/wrangler/src/pages/delegate-to-workers.ts @@ -62,7 +62,15 @@ export interface PagesToWorkersDeployArgs { } export type PagesToWorkersDelegateResult = - | { delegate: false } + | { + delegate: false; + /** + * True when the caller passed `--force` to opt this command out of + * delegation. The caller uses it to emit the one-time `--force` notice + * on the command's success path. + */ + forcedOptOut?: boolean; + } | { delegate: true; command: PagesDelegateCommand; @@ -71,12 +79,7 @@ export type PagesToWorkersDelegateResult = }; /** The outcome recorded against the `delegate pages to workers` metrics event. */ -type DelegateResult = - | "delegated" - | "success" - | "failure" - | "skipped" - | "forced"; +type DelegateResult = "delegated" | "success" | "failure" | "forced"; /** * Status line emitted at the top of the deploy flow, before the Workers deploy @@ -117,6 +120,37 @@ function buildWorkersDeployFailedMessage( ); } +/** + * Agent-facing notice emitted at the end of a successful `--force` command. + * + * `--force` opts this one command out of delegation and runs it directly against + * Cloudflare Pages. Once it succeeds the project exists on the account, so every + * subsequent Pages command is no longer eligible for delegation and runs + * directly without `--force`. We say this explicitly so the agent stops + * appending `--force` to future commands. + */ +function buildForceOptOutNoticeMessage(command: PagesDelegateCommand): string { + const action = command === "deploy" ? "deployed" : "created"; + return ( + `Notice to agents: this project was ${action} directly on Cloudflare Pages because --force was set, bypassing delegation to Cloudflare Workers. ` + + "This is the only time you need --force: this project now exists, so subsequent commands will run directly against Cloudflare Pages and will not be delegated to Cloudflare Workers. " + + "Do not pass --force on future commands." + ); +} + +/** + * Emits the one-time `--force` opt-out notice to stdout so the agent sees it at + * the end of the command. + * + * Call this only from the command's success path: the "this project now exists" + * guidance is only true once the direct Pages command has actually succeeded. + */ +export function logPagesToWorkersForceOptOutNotice( + command: PagesDelegateCommand +): void { + logger.log(buildForceOptOutNoticeMessage(command)); +} + /** * Decides whether to delegate a Pages command to a Workers static-assets deploy * and, if so, returns the args for the caller to run as a Workers deploy. @@ -143,30 +177,28 @@ export async function maybeDelegatePagesToWorkers( // The agent explicitly opted out with `--force`. The only callers who should // reach for `--force` are agents we previously delegated, so this is a - // strong signal of dissatisfaction with the delegation — record it. + // strong signal of dissatisfaction with the delegation — record it. We flag + // `forcedOptOut` so the caller emits the one-time `--force` notice once the + // direct Pages command succeeds (see `logPagesToWorkersForceOptOutNotice`). if (options.force) { recordDelegate("forced", options, agent.id); logger.debug("Pages-to-Workers delegation skipped: --force opt-out"); - return { delegate: false }; + return { delegate: false, forcedOptOut: true }; } if (options.unsupportedArgs && options.unsupportedArgs.length > 0) { - skipDelegate( - `unsupported args: ${options.unsupportedArgs.join(", ")}`, - options, - agent.id - ); + skipDelegate(`unsupported args: ${options.unsupportedArgs.join(", ")}`); return { delegate: false }; } - // Bail (and record why) if the project uses any Pages feature we can't carry + // Bail (and log why) if the project uses any Pages feature we can't carry // across to a Workers static-assets deploy. const unsupportedFeature = findUnsupportedPagesFeature( options.projectPath, options.assetsDirectory ); if (unsupportedFeature) { - skipDelegate(unsupportedFeature, options, agent.id); + skipDelegate(unsupportedFeature); return { delegate: false }; } @@ -185,11 +217,11 @@ export async function maybeDelegatePagesToWorkers( e instanceof Error ? e.message : String(e) })` ); - skipDelegate("account pages projects lookup failed", options, agent.id); + skipDelegate("account pages projects lookup failed"); return { delegate: false }; } if (hasPagesProjects) { - skipDelegate("account has pages projects", options, agent.id); + skipDelegate("account has pages projects"); return { delegate: false }; } } @@ -264,16 +296,16 @@ function buildWorkersDeployArgs( } /** - * Records a `skipped` delegation: logs the reason (so we can see why delegations - * don't happen) and sends a metrics event carrying that reason. + * Logs (at debug level, for local visibility) why a delegation was skipped. + * + * Skips are deliberately not sent to telemetry: they are deterministic, expected + * non-cases (not an agent's brand-new static project — e.g. the account already + * has Pages projects, or the project uses an unsupported Pages feature), so the + * volume carries no signal. The number of skipped commands is derivable from the + * Pages command's own telemetry, so a dedicated event is not needed. */ -function skipDelegate( - reason: string, - options: MaybeDelegatePagesToWorkersOptions, - agentId: string | null -): void { +function skipDelegate(reason: string): void { logger.debug(`Pages-to-Workers delegation skipped: ${reason}`); - recordDelegate("skipped", options, agentId, { reason }); } /** Sends a `delegate pages to workers` metrics event for the given outcome. */ diff --git a/packages/wrangler/src/pages/deploy.ts b/packages/wrangler/src/pages/deploy.ts index 5fce026c0d..d975fb155d 100644 --- a/packages/wrangler/src/pages/deploy.ts +++ b/packages/wrangler/src/pages/deploy.ts @@ -26,7 +26,10 @@ import { MAX_DEPLOYMENT_STATUS_ATTEMPTS, PAGES_CONFIG_CACHE_FILENAME, } from "./constants"; -import { maybeDelegatePagesToWorkers } from "./delegate-to-workers"; +import { + logPagesToWorkersForceOptOutNotice, + maybeDelegatePagesToWorkers, +} from "./delegate-to-workers"; import { EXIT_CODE_INVALID_PAGES_CONFIG } from "./errors"; import { listProjects } from "./projects"; import { promptSelectProject } from "./prompt-select-project"; @@ -634,6 +637,12 @@ export const pagesDeployCommand = createCommand({ }); metrics.sendMetricsEvent("create pages deployment"); + + // If the agent opted this deploy out of delegation with `--force`, tell it + // (at the end, on success) that `--force` is a one-time action. + if (delegation.forcedOptOut) { + logPagesToWorkersForceOptOutNotice("deploy"); + } }, }); diff --git a/packages/wrangler/src/pages/projects.ts b/packages/wrangler/src/pages/projects.ts index 2195519fae..415fd2cda4 100644 --- a/packages/wrangler/src/pages/projects.ts +++ b/packages/wrangler/src/pages/projects.ts @@ -12,7 +12,10 @@ import { logger } from "../logger"; import * as metrics from "../metrics"; import { requireAuth } from "../user"; import { PAGES_CONFIG_CACHE_FILENAME } from "./constants"; -import { maybeDelegatePagesToWorkers } from "./delegate-to-workers"; +import { + logPagesToWorkersForceOptOutNotice, + maybeDelegatePagesToWorkers, +} from "./delegate-to-workers"; import { runPagesToWorkersDeploy } from "./run-workers-deploy"; import type { PagesConfigCache, Project } from "./types"; @@ -264,6 +267,12 @@ export const pagesProjectCreateCommand = createCommand({ `To deploy a folder of assets, run 'wrangler pages deploy [directory]'.` ); metrics.sendMetricsEvent("create pages project"); + + // If the agent opted this create out of delegation with `--force`, tell it + // (at the end, on success) that `--force` is a one-time action. + if (delegation.forcedOptOut) { + logPagesToWorkersForceOptOutNotice("create"); + } }, }); From e7f9936d10a0762c002bc9a9caac9bda2b1055ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=20=E2=80=98TK=E2=80=99=20Taylor?= Date: Wed, 8 Jul 2026 15:26:35 +0100 Subject: [PATCH 2/2] Update .changeset/pages-delegation-force-notice.md Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .changeset/pages-delegation-force-notice.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/pages-delegation-force-notice.md b/.changeset/pages-delegation-force-notice.md index ddb658f751..1b0a4db376 100644 --- a/.changeset/pages-delegation-force-notice.md +++ b/.changeset/pages-delegation-force-notice.md @@ -2,6 +2,6 @@ "wrangler": patch --- -Improve the agent-facing `--force` guidance for Pages-to-Workers delegation and stop emitting the low-signal `skipped` telemetry event +Improve the agent-facing `--force` guidance for Pages-to-Workers delegation -When an AI agent opts out of the Pages-to-Workers delegation by passing `--force` to `wrangler pages deploy` or `wrangler pages project create`, Wrangler now prints a notice at the end of a successful command explaining that `--force` only needs to be passed once: the project then exists, so subsequent commands are no longer delegated. Wrangler also no longer emits a telemetry event for delegations that were skipped because the command was ineligible (for example the account already has Pages projects), since those are expected, deterministic cases that carry no signal. +When an AI agent opts out of the Pages-to-Workers delegation by passing `--force` to `wrangler pages deploy` or `wrangler pages project create`, Wrangler now prints a notice at the end of a successful command explaining that `--force` only needs to be passed once: the project then exists, so subsequent commands are no longer delegated and do not need the flag.