fix(cli): retain bundled skill and bias setup wizard toward apply
Fix #399: when agent integration is selected but all agents are deselected, the bundled skill was silently dropped. The skill is now always included when agent integration is selected, regardless of client selection. Fix #398: the setup wizard defaulted to cancellation at every step despite explicit setup intent. The capability picker now pre-selects agent integration, the agent picker pre-selects all detected agents, the final confirmation defaults to Yes, and accessible prompts default to Yes for pre-selected items. Agent option hints now describe the agent instead of the file operation. Update tests/cliSetup.test.ts to cover the new skill-retention (#399) and default-toward-apply (#398) behavior, including accessible-mode opt-out. Update docs/installation.md to describe the new pre-selection and Yes-default confirmation.
This commit is contained in:
parent
eb27f01aee
commit
fec5c3ce7f
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<readonly string[] | undefined> => {
|
||||
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<readonly string[] | undefined> => {
|
||||
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<readonly string[] | undefined> => {
|
||||
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<readonly string[] | undefined> => {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue