diff --git a/doc/plans/2026-10-12-claude-runner-default.md b/doc/plans/2026-10-12-claude-runner-default.md index 1aa4863c0bd..b43f2fadd0a 100644 --- a/doc/plans/2026-10-12-claude-runner-default.md +++ b/doc/plans/2026-10-12-claude-runner-default.md @@ -614,3 +614,38 @@ assertions remain unchanged. The full fake-transport suite passes 72 with one existing skip; runner noemit and maps pass. Fresh exact-head runner CI remains required after this test-only correction. No package campaign, paid provider call, local native build, Docker build or upstream prerequisite edit. + +## Guided creation with refreshed settings — 2026-10-12 + +Review 4253594222 on behavior head `4440df2e44e346080308e40ec3287c94c7f776a2` +identified stale cached environment selection during a background instance-settings +read. The mounted reproduction actually calls setup against cached SSH/Legacy +before the pending current settings select local/native, rather than merely +observing a loading label. Guided Claude/Codex setup now requires current completed +environment, settings, execution policy, experimental policy and matching Auto +runner discovery reads before testing or hiring. Existing connection controls stay +mounted inside a disabled fieldset while refreshing, preserving an API-key draft. +Both the cached SSH-to-local case and same-environment discovery refresh pass; +all 65 guided setup controls, UI noemit, token gates and structural maps pass. +Logs: `/private/tmp/claude-guided-current-settings-before.log`, +`/private/tmp/claude-guided-current-settings-draft-controls.log`, and +`/private/tmp/claude-guided-current-settings-final-{types,tokens,maps}.log`. +This repair is prepared locally while the source owner integrates the common +transport correction; published behavior head remains the literal 4440 revision. +No paid turns, local native builds or Docker builds were performed. + +Platform head `173a73ec95386d7b9eac50d87699a16ec27da3b3` failed a credential +fixture during its two-port reservation (EADDRINUSE before the production staging +call). The existing bounded three-port fixture reservation helper is extended to +hold two candidates, retrying fixture preparation only and never a production +staging call. A foreign-collision control verifies that foreign ownership survives, +the auth sentinel stays unchanged, and preparation stops within eight attempts. +All 42 credential controls and runner noemit pass locally after supplying the +worktree's declared tsx test loader. Logs: +`/private/tmp/claude-quorum-fixture-complete-controls.log` and +`/private/tmp/claude-quorum-fixture-final-types.log`. This test-only correction +is prepared in the protocol worktree and has not been published during the parent +integration hold. The independent serialized issue-route timeout remains under +phase diagnosis; a subsequent full mocked suite passes 36 controls but does not +waive the cloud failure or prove its latency cause. Final source composition, +required CI and the provider-free cloud packaging campaign remain pending. diff --git a/ui/src/components/new-agent/NewAgentSetup.tsx b/ui/src/components/new-agent/NewAgentSetup.tsx index 56ae564fb24..d6b3618eca4 100644 --- a/ui/src/components/new-agent/NewAgentSetup.tsx +++ b/ui/src/components/new-agent/NewAgentSetup.tsx @@ -340,7 +340,7 @@ function Setup({ !adapter.disabled && !getAdapterDisplay(adapterType).comingSoon, ) && (requestAdapterType !== "paperclip_runner" || adapterType === "paperclip_runner" || adapterInventory.data?.some(adapter => adapter.type === "paperclip_runner" && adapter.loaded && !adapter.disabled)); - const ready = Boolean( + const connectionSettingsReady = Boolean( available && (!["codex_local", "claude_local"].includes(adapterType) || runner !== "auto" || Boolean(adapters.data)) && !environmentError && @@ -358,6 +358,14 @@ function Setup({ !caps.error && (!(managedOnly || forced.forced) || environmentId), ); + const needsCurrentSetup = ["codex_local", "claude_local"].includes(adapterType); + const ready = connectionSettingsReady && (!needsCurrentSetup || ( + envs.isSuccess && !envs.isFetching + && settings.isSuccess && !settings.isFetching + && experimental.isSuccess && !experimental.isFetching + && general.isSuccess && !general.isFetching + && (runner !== "auto" || (adapters.isSuccess && !adapters.isFetching)) + )); const busy = testState === "running" || saving; function buildConfig( @@ -762,7 +770,7 @@ function Setup({ center /> - {ready ? :

Loading connection settings…

} + /> :

Loading connection settings…

} {["codex_local", "claude_local"].includes(adapterType) &&
Advanced { container.remove(); }); describe("New agent setup", () => { + it("waits for the current instance default instead of testing cached SSH discovery", async () => { + let finish!: (value: object) => void; + settings.get.mockReturnValue(new Promise(resolve => { finish = resolve; })); + const environments = [ + { id: "local-1", name: "Local", driver: "local", config: {} }, + { id: "linux-ssh", name: "SSH", driver: "ssh", config: {} }, + ]; + envApi.list.mockResolvedValue(environments); + cache.setQueryData(queryKeys.environments.list("company-1"), environments); + cache.setQueryData(queryKeys.instance.settings, { defaultEnvironmentId: "linux-ssh" }); + adapterApi.list.mockImplementation(async (context?: { environmentId?: string | null }) => state.adapters.map((adapter: any) => adapter.type === "claude_local" + ? { ...adapter, supportedRunners: ["paperclip", "legacy"], defaultRunner: context?.environmentId === "local-1" ? "paperclip" : "legacy" } : adapter)); + await render("claude_local"); + await connect("Claude"); + expect(api.testEnvironment).not.toHaveBeenCalled(); + expect(api.hire).not.toHaveBeenCalled(); + await act(async () => finish({ defaultEnvironmentId: "local-1" })); + await settle(); + await connect("Claude"); + await click("Finish setup"); + expect(api.testEnvironment).toHaveBeenCalledTimes(1); + expect(api.testEnvironment.mock.calls[0][2].environmentId).toBe("local-1"); + expect(api.hire.mock.calls[0][1].adapterConfig).not.toHaveProperty("maxTurnsPerRun"); + }); + + it("keeps the API key draft while waiting for refreshed discovery", async () => { + state.adapters = state.adapters.map((adapter: any) => adapter.type === "claude_local" + ? { ...adapter, defaultRunner: "paperclip", supportedRunners: ["paperclip", "legacy"] } : adapter); + await render("claude_local"); + await click("ClaudeAPI key"); + await fill("API key", "example-test-secret"); + const draft = container.querySelector('[aria-label="API key"]') as HTMLInputElement; + let finish!: (value: object[]) => void; + adapterApi.list.mockReturnValue(new Promise(resolve => { finish = resolve; })); + let refresh!: Promise; + await act(async () => { + refresh = cache.invalidateQueries({ queryKey: queryKeys.adapters.availability("company-1", "local-1") }); + }); + await settle(); + expect(container.querySelector('[aria-label="API key"]')).toBe(draft); + expect(draft.value).toBe("example-test-secret"); + await click("Connect"); + expect(managedApi.create).not.toHaveBeenCalled(); + expect(api.testEnvironment).not.toHaveBeenCalled(); + await act(async () => { finish(state.adapters); await refresh; }); + await settle(); + expect(container.querySelector('[aria-label="API key"]')).toBe(draft); + expect(draft.value).toBe("example-test-secret"); + await click("Connect"); + await click("Finish setup"); + expect(api.testEnvironment).toHaveBeenCalledTimes(1); + expect(api.hire).toHaveBeenCalledTimes(1); + expect(api.hire.mock.calls[0][1].adapterConfig).not.toHaveProperty("maxTurnsPerRun"); + }); + it.each(["paperclip", "legacy", undefined] as const)("uses Claude's discovered %s default for Auto setup and hire", async (defaultRunner) => { settings.getExperimental.mockResolvedValue({ enableNativeRunner: false }); state.adapters = (state.adapters as any[]).map(adapter => adapter.type === "claude_local"