diff --git a/docs/installation.md b/docs/installation.md index 06a7a9d00..3d074e50c 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -51,20 +51,24 @@ mechanics: - use the bundled skill for a repeatable investigation workflow. REA then summarizes the detected clients and asks which capabilities to set up. -The capability picker starts empty and presents `Agent integration` (the MCP -registration and matching guided workflow together) plus the optional Hopper -provider. Selecting agent integration opens a second empty checklist for the -exact detected agents that should receive a registration. Detection provides -context; it does not preselect or authorize a configuration write. +The capability picker pre-selects `Agent integration` (the MCP registration and +matching guided workflow together) when detected agents exist, and presents the +optional Hopper provider alongside it. Selecting agent integration opens a second +checklist for the exact detected agents that should receive a registration; all +detected agents are pre-selected. Detection provides context and pre-selection; +it does not authorize a configuration write until the user confirms. -Choosing no capabilities exits without changes. Selecting the provider does not -implicitly select an agent, and selecting agent integration does not include -every detected agent. The picker keeps its navigation, selection, confirmation, -and cancellation keys visible instead of relying on a transient hint. +Because the user has explicitly invoked `rea setup`, the wizard biases toward the +happy path: capabilities and agents are pre-selected, and the final approval +defaults to **Yes**. The user can still deselect any item or cancel at any prompt. +Choosing no capabilities exits without changes. Selecting agent integration +always includes the bundled skill, even if all agents are deselected. The picker +keeps its navigation, selection, confirmation, and cancellation keys visible +instead of relying on a transient hint. Every selected path converges on the same exact preflight. REA validates the current state, prints the proposed effects, and asks for final approval with -**No** as the default. Selection alone never authorizes a mutation. The plan +**Yes** as the default. Selection alone never authorizes a mutation. The plan identifies: - an existing Hopper installation, a validated bring-your-own Ghidra environment, or the official Hopper package it proposes to install; diff --git a/src/cliSetup.ts b/src/cliSetup.ts index 69b925643..0cd3ea434 100644 --- a/src/cliSetup.ts +++ b/src/cliSetup.ts @@ -61,7 +61,7 @@ export const confirmInteractiveSetup = async ( selectedActions.length === 1 ? "Apply this change?" : `Apply these ${String(selectedActions.length)} changes?`, - initialValue: false, + initialValue: true, active: "Yes, apply", inactive: "No, cancel", }); @@ -131,9 +131,10 @@ const selectSetupActions = async ( ({ kind }) => kind !== "configure_client", ); const capabilities = setupCapabilities(clientActions, componentActions); + const preselectedCapabilityIds = defaultCapabilityIds(capabilities); const selectedCapabilities = accessible - ? await selectCapabilitiesAccessibly(capabilities) - : await selectCapabilities(capabilities); + ? await selectCapabilitiesAccessibly(capabilities, preselectedCapabilityIds) + : await selectCapabilities(capabilities, preselectedCapabilityIds); if (selectedCapabilities === undefined) return undefined; const selectedCapabilityIds = new Set(selectedCapabilities); const bundledSkillAction = componentActions.find( @@ -147,23 +148,35 @@ const selectSetupActions = async ( .map(({ id }) => id); if (!selectedCapabilityIds.has(agentIntegrationCapabilityId)) return selectedActionIds; + const bundledSkillId = + bundledSkillAction === undefined ? undefined : bundledSkillAction.id; if (clientActions.length === 0) return [ - ...(bundledSkillAction === undefined ? [] : [bundledSkillAction.id]), + ...(bundledSkillId === undefined ? [] : [bundledSkillId]), ...selectedActionIds, ]; + const preselectedClientIds = clientActions.map(({ id }) => id); const selectedClients = accessible - ? await selectClientsAccessibly(clientActions) - : await selectClients(clientActions); + ? await selectClientsAccessibly(clientActions, preselectedClientIds) + : await selectClients(clientActions, preselectedClientIds); if (selectedClients === undefined) return undefined; - if (selectedClients.length === 0) return selectedActionIds; return [ ...selectedClients, - ...(bundledSkillAction === undefined ? [] : [bundledSkillAction.id]), + ...(bundledSkillId === undefined ? [] : [bundledSkillId]), ...selectedActionIds, ]; }; +const defaultCapabilityIds = ( + capabilities: readonly SetupCapability[], +): readonly string[] => { + const hasAgentIntegration = capabilities.some( + ({ id }) => id === agentIntegrationCapabilityId, + ); + if (!hasAgentIntegration) return []; + return [agentIntegrationCapabilityId]; +}; + const setupCapabilities = ( clientActions: readonly SetupAction[], componentActions: readonly SetupAction[], @@ -192,6 +205,7 @@ const setupCapabilities = ( const selectCapabilities = async ( capabilities: readonly SetupCapability[], + initialValues: readonly string[], ): Promise => { const selection = await multiselect({ ...promptStreams, @@ -201,6 +215,7 @@ const selectCapabilities = async ( label: capability.label, hint: capability.hint, })), + initialValues: [...initialValues], required: false, maxItems: Math.max( 3, @@ -213,13 +228,15 @@ const selectCapabilities = async ( const selectCapabilitiesAccessibly = async ( capabilities: readonly SetupCapability[], + preselectedIds: readonly string[], ): Promise => { + const preselected = new Set(preselectedIds); const selected: string[] = []; for (const capability of capabilities) { const included = await confirm({ ...promptStreams, message: `Set up ${capability.label}? ${capability.hint}`, - initialValue: false, + initialValue: preselected.has(capability.id), vertical: true, }); if (isCancel(included)) return undefined; @@ -230,6 +247,7 @@ const selectCapabilitiesAccessibly = async ( const selectClients = async ( actions: readonly SetupAction[], + initialValues: readonly string[], ): Promise => { const selection = await multiselect({ ...promptStreams, @@ -237,8 +255,9 @@ const selectClients = async ( options: actions.map((action) => ({ value: action.id, label: `${action.label} (detected)`, - hint: `${action.operation} ยท ${action.target}`, + hint: `Configure the REA MCP server for ${action.label}`, })), + initialValues: [...initialValues], required: false, maxItems: Math.max( 3, @@ -251,13 +270,15 @@ const selectClients = async ( const selectClientsAccessibly = async ( actions: readonly SetupAction[], + initialValues: readonly string[], ): Promise => { + const preselected = new Set(initialValues); const selected: string[] = []; for (const action of actions) { const included = await confirm({ ...promptStreams, message: `Configure ${action.label}? ${action.target}`, - initialValue: false, + initialValue: preselected.has(action.id), vertical: true, }); if (isCancel(included)) return undefined; diff --git a/tests/cliSetup.test.ts b/tests/cliSetup.test.ts index fbc58a332..5928acce4 100644 --- a/tests/cliSetup.test.ts +++ b/tests/cliSetup.test.ts @@ -64,8 +64,8 @@ describe("interactive setup journey", () => { expect(result.decision.approved).toBe(false); }); - it("does not turn available capabilities into selected setup actions", async () => { - const result = await runJourney([step("What should REA set up?", "\r")]); + it("allows deselecting the pre-selected agent integration to opt out", async () => { + const result = await runJourney([step("What should REA set up?", " \r")]); expect(result.output).toContain( "Agent integration (MCP + guided workflow)", @@ -82,9 +82,20 @@ describe("interactive setup journey", () => { }); }); + it("pre-selects agent integration and proceeds to the agent picker on Enter", async () => { + const result = await runJourney([ + step("What should REA set up?", "\r"), + step("Which agents should use REA?", "\u0003"), + ]); + + expect(result.output).toContain("Codex (detected)"); + expect(result.output).toContain("Setup cancelled. No changes were made."); + expect(result.decision.approved).toBe(false); + }); + it("offers one bundled repair when MCP is aligned but the skill is missing", async () => { const result = await runJourney( - [step("What should REA set up?", "\r")], + [step("What should REA set up?", "\r"), step("Apply this change?", "\r")], false, actions.filter(({ kind }) => kind !== "configure_client"), ); @@ -96,13 +107,16 @@ describe("interactive setup journey", () => { expect(result.output).not.toContain( "REA reverse-engineering skill (skill)", ); - expect(result.decision.selectedActionIds).toEqual([]); + expect(result.decision).toEqual({ + approved: true, + selectedActionIds: ["install_skill"], + }); }); - it("shows multiple detected agents without preselecting either one", async () => { + it("pre-selects all detected agents in the agent picker", async () => { const result = await runJourney( [ - step("What should REA set up?", " \r"), + step("What should REA set up?", "\r"), step("Which agents should use REA?", "\u0003"), ], false, @@ -123,21 +137,26 @@ describe("interactive setup journey", () => { expect(result.decision.approved).toBe(false); }); - it("asks for agent targets only after agent integration is selected", async () => { + it("retains the bundled skill even when all agents are deselected (#399)", async () => { const result = await runJourney([ - step("What should REA set up?", " \r"), - step("Which agents should use REA?", "\u0003"), + step("What should REA set up?", "\r"), + step("Which agents should use REA?", " \r"), + step("Apply this change?", "\r"), ]); - expect(result.output).toContain("Codex (detected)"); - expect(result.output).toContain("Setup cancelled. No changes were made."); - expect(result.decision.approved).toBe(false); + expect(result.output).toContain("Ready to review"); + expect(result.output).toContain("INSTALL REA reverse-engineering skill"); + expect(result.output).not.toContain("CREATE Codex"); + expect(result.decision).toEqual({ + approved: true, + selectedActionIds: ["install_skill"], + }); }); - it("reviews only explicitly selected actions with default-No consent", async () => { + it("defaults the final confirmation to Yes, apply", async () => { const result = await runJourney([ - step("What should REA set up?", " \r"), - step("Which agents should use REA?", " \r"), + step("What should REA set up?", "\r"), + step("Which agents should use REA?", "\r"), step("Apply these 2 changes?", "\r"), ]); @@ -148,7 +167,21 @@ describe("interactive setup journey", () => { expect(result.output).not.toContain( "INSTALL Hopper deep-analysis provider", ); - expect(result.output).toContain("No, cancel"); + expect(result.output).toContain("Yes, apply"); + expect(result.decision).toEqual({ + approved: true, + selectedActionIds: ["configure_client:codex", "install_skill"], + }); + }); + + it("can still be cancelled at the final confirmation", async () => { + const result = await runJourney([ + step("What should REA set up?", "\r"), + step("Which agents should use REA?", "\r"), + step("Apply these 2 changes?", "\u001b[D\r"), + ]); + + expect(result.output).toContain("Ready to review"); expect(result.output).toContain("Setup cancelled. No changes were made."); expect(result.decision).toEqual({ approved: false, @@ -158,10 +191,7 @@ describe("interactive setup journey", () => { it("repairs an aligned agent integration through the bundled option", async () => { const result = await runJourney( - [ - step("What should REA set up?", " \r"), - step("Apply this change?", "\r"), - ], + [step("What should REA set up?", "\r"), step("Apply this change?", "\r")], false, actions.filter(({ kind }) => kind !== "configure_client"), ); @@ -175,21 +205,40 @@ describe("interactive setup journey", () => { "INSTALL Hopper deep-analysis provider", ); expect(result.decision).toEqual({ - approved: false, + approved: true, selectedActionIds: ["install_skill"], }); }); - it("keeps every accessible capability prompt defaulted to skip", async () => { + it("defaults accessible agent integration to Yes and others to No", async () => { const result = await runJourney( [ step("Set up Agent integration (MCP + guided workflow)?", "\r"), step("Set up Hopper deep-analysis provider (provider)?", "\r"), + step("Apply this change?", "\r"), + ], + true, + actions.filter(({ kind }) => kind !== "configure_client"), + ); + + expect(result.output).toContain("Ready to review"); + expect(result.output).toContain("INSTALL REA reverse-engineering skill"); + expect(result.decision).toEqual({ + approved: true, + selectedActionIds: ["install_skill"], + }); + }); + + it("allows accessible opt-out of the pre-selected agent integration", async () => { + const result = await runJourney( + [ + step("Set up Agent integration (MCP + guided workflow)?", "\u001b[B\r"), + step("Set up Hopper deep-analysis provider (provider)?", "\r"), ], true, ); - expect(result.output).not.toContain("Ready to review"); + expect(result.output).toContain("Nothing selected. No changes were made."); expect(result.decision.selectedActionIds).toEqual([]); });