[GH-ISSUE #5856] [Bug] Interactive CLI-only skill selection is dropped when only Claude Code is detected #3550

Closed
opened 2026-10-31 00:41:46 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5856

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Setup, packaging, or upgrade

Observed behavior

Interactive setup can discard an accepted CLI-only skill installation and report ready without installing it. With Claude Code as the only detected client, leaving the agent selection empty and accepting “Install the guided REA skill for CLI use?” returns the action ID install_skill:claude_code. The setup workflow drops that choice: its final plan and applied actions are empty, the installer is never called, and the final readiness scope has skill: false.

This was executed through the production interactive confirmation adapter and setup workflow using simulated prompt answers and the repository's recording setup host. A real terminal session and filesystem installation were not exercised.

Reproduction

The focused test below ran once at the stated revision; both cases passed their assertions of the observed behavior. It uses the existing FakeSetupHost, supplies an empty multi-select and affirmative confirmations, and connects confirmInteractiveSetup directly to runSetup, as the CLI adapter does. The synthetic Hopper path prevents an unrelated installation offer; no native provider is invoked.

To rerun, save the test as tests/process-global/skillOnlySelection.test.ts in the checkout with locked dependencies installed, then run:

node node_modules/vitest/vitest.mjs run tests/process-global/skillOnlySelection.test.ts --maxWorkers=1
import { expect, it, vi } from "vitest";
import { runSetup } from "../../src/application/Setup.js";
import { FakeSetupHost, options } from "../../src/application/Setup.fixture.js";
import { confirmInteractiveSetup } from "../../src/cli/interactiveSetup.js";

const prompts = vi.hoisted(() => ({
  confirm: vi.fn(async () => true),
  multiselect: vi.fn(async () => []),
}));
vi.mock("@clack/prompts", () => ({
  ...prompts,
  cancel: vi.fn(), intro: vi.fn(), outro: vi.fn(),
  isCancel: () => false,
}));

it.each(["claude_code", "codex"] as const)(
  "accepts the CLI-only skill prompt with %s as the only detected client",
  async (client) => {
    prompts.confirm.mockClear();
    prompts.multiselect.mockClear();
    const host = new FakeSetupHost();
    host.hopper = "/Applications/Hopper";
    host.clients = [{ name: client, configPath: "/fixture/config" }];
    const offered: string[][] = [];
    const selected: unknown[] = [];
    const result = await runSetup(
      { ...options(false), structured: false }, host,
      async (actions, context) => {
        if (context?.stage === "select") offered.push(actions.map(a => a.id));
        const decision = await confirmInteractiveSetup(actions, false, context);
        selected.push(decision);
        return decision;
      },
    );
    expect(prompts.multiselect).toHaveBeenCalledTimes(1);
    expect(prompts.confirm).toHaveBeenCalledWith(expect.objectContaining({
      message: "Install the guided REA skill for CLI use?",
    }));
    const actionId = client === "codex" ? "install_skill" : "install_skill:claude_code";
    expect(offered).toEqual([[actionId]]);
    expect(selected[0]).toEqual({approved: false, selectedActionIds: [actionId]});
    expect(result.status).toBe("ready");
    expect(host.configurations).toBe(0);
    if (client === "claude_code") {
      expect(result.plannedActions).toEqual([]);
      expect(result.appliedActions).toEqual([]);
      expect(host.skillInstalls).toBe(0);
      expect(result.doctor.scope.skill).toBe(false);
    } else {
      expect(result.plannedActions.map(a => a.id)).toEqual(["install_skill"]);
      expect(result.appliedActions).toEqual(["installed_skill"]);
      expect(host.skillInstalls).toBe(1);
      expect(host.skillDirectoryValidations).toEqual([[], []]);
    }
    console.log(JSON.stringify({client, offered, selected, status: result.status,
      planned: result.plannedActions.map(a => a.id), applied: result.appliedActions,
      installs: host.skillInstalls, scope: result.doctor.scope}));
  },
);

The Claude case asserts the defect as observed. The codex case is a positive control whose skill destination resolves to the shared root; it installs once despite the same empty agent selection. Prompt rendering, real terminal keystrokes, and real disk writes are outside this test's coverage.

Expected behavior

Accepting the separate CLI-only skill offer should retain the requested installation, show the shared destination for approval, install there after approval, and include that skill in final readiness checks.

The setup guide describes the separate offer with no agent selected. Its skill destination contract says this selection uses ~/.agents/skills.

Impact and scope

A supported setup choice silently omits the guided workflow and reports success. The executed case requires Claude Code detection, no selected registrations, and an offered skill installation. The shared-root control succeeds. Other combinations of client-specific destinations were not executed.

This is a bounded setup failure (P2). Explicit shared skill selection is a documented alternative: rea setup --skill-client shared --yes --json. That command was not executed in this reproduction.

REA version

rea-agents 6.7.0 source revision 52415975942a4b9c53a817915b313ccc404f280e.

Runtime and platform

macOS 27.0.1 arm64, Node.js v26.12.0, locked Vitest 5.0.3. Recording setup host and simulated prompt answers; no real analysis provider used.

Target and artifact context

Not applicable; setup selection for the bundled skill requires no target binary.

Interface

CLI

Earliest failure stage

Setup or provider admission

Relevant output or evidence

The focused test logged these workflow results:

{"client":"claude_code","offered":[["install_skill:claude_code"]],"selected":[{"approved":false,"selectedActionIds":["install_skill:claude_code"]}],"status":"ready","planned":[],"applied":[],"installs":0,"scope":{"mode":"explicit","clients":[],"providers":[],"skill":false,"target":null}}
{"client":"codex","offered":[["install_skill"]],"selected":[{"approved":false,"selectedActionIds":["install_skill"]},{"approved":true,"selectedActionIds":["install_skill"]}],"status":"ready","planned":["install_skill"],"applied":["installed_skill"],"installs":1,"scope":{"mode":"explicit","clients":[],"skill_clients":[],"providers":[],"skill":true,"target":null}}

Result: one focused test file, two tests passed. The zero-install case and its positive control were observed; actual installed-file absence is inferred from the lack of any installer call in the recording host.

The fetched origin/main revision f801b2fbfd28f4e3713adefddc7bd73db44c284c has no differences from the tested revision in Setup.ts, SetupPlan.ts, SupportedClients.ts, or interactiveSetup.ts.

Typed outcome or recovery expected

The accepted skill choice should produce an install_skill action for the shared root and, after approval and successful installation, report installed_skill with skill readiness enabled. An installation blocker should produce an explicit non-ready outcome rather than silently removing the choice.

Analysis or suspected cause

Source tracing matches the executed result:

  • The selection plan uses detected/existing clients for skill proposals even though registration selection is empty.
  • Action construction assigns install_skill:claude_code to the personal destination.
  • The no-client prompt returns that ID unchanged on Yes.
  • Selection consumption recognizes only exact install_skill when no clients were selected. Consequently skillSelected is false and the final plan omits the accepted action.

The public CLI wiring uses the same confirmation adapter and workflow connected by this test. No repair has been tested.

Additional context

Related #1711 and PR #1744 concern final readiness checking an unselected shared copy after explicit client-only installation. Their fix is already an ancestor of the tested revision. Here the selected action is discarded before installation, with no skill included in readiness, so that earlier fix does not cover this behavior. No linked directories or failed Hopper installation are needed.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5856 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Setup, packaging, or upgrade ### Observed behavior Interactive setup can discard an accepted CLI-only skill installation and report `ready` without installing it. With Claude Code as the only detected client, leaving the agent selection empty and accepting “Install the guided REA skill for CLI use?” returns the action ID `install_skill:claude_code`. The setup workflow drops that choice: its final plan and applied actions are empty, the installer is never called, and the final readiness scope has `skill: false`. This was executed through the production interactive confirmation adapter and setup workflow using simulated prompt answers and the repository's recording setup host. A real terminal session and filesystem installation were not exercised. ### Reproduction The focused test below ran once at the stated revision; both cases passed their assertions of the observed behavior. It uses the existing `FakeSetupHost`, supplies an empty multi-select and affirmative confirmations, and connects `confirmInteractiveSetup` directly to `runSetup`, as the CLI adapter does. The synthetic Hopper path prevents an unrelated installation offer; no native provider is invoked. To rerun, save the test as `tests/process-global/skillOnlySelection.test.ts` in the checkout with locked dependencies installed, then run: ```sh node node_modules/vitest/vitest.mjs run tests/process-global/skillOnlySelection.test.ts --maxWorkers=1 ``` ```typescript import { expect, it, vi } from "vitest"; import { runSetup } from "../../src/application/Setup.js"; import { FakeSetupHost, options } from "../../src/application/Setup.fixture.js"; import { confirmInteractiveSetup } from "../../src/cli/interactiveSetup.js"; const prompts = vi.hoisted(() => ({ confirm: vi.fn(async () => true), multiselect: vi.fn(async () => []), })); vi.mock("@clack/prompts", () => ({ ...prompts, cancel: vi.fn(), intro: vi.fn(), outro: vi.fn(), isCancel: () => false, })); it.each(["claude_code", "codex"] as const)( "accepts the CLI-only skill prompt with %s as the only detected client", async (client) => { prompts.confirm.mockClear(); prompts.multiselect.mockClear(); const host = new FakeSetupHost(); host.hopper = "/Applications/Hopper"; host.clients = [{ name: client, configPath: "/fixture/config" }]; const offered: string[][] = []; const selected: unknown[] = []; const result = await runSetup( { ...options(false), structured: false }, host, async (actions, context) => { if (context?.stage === "select") offered.push(actions.map(a => a.id)); const decision = await confirmInteractiveSetup(actions, false, context); selected.push(decision); return decision; }, ); expect(prompts.multiselect).toHaveBeenCalledTimes(1); expect(prompts.confirm).toHaveBeenCalledWith(expect.objectContaining({ message: "Install the guided REA skill for CLI use?", })); const actionId = client === "codex" ? "install_skill" : "install_skill:claude_code"; expect(offered).toEqual([[actionId]]); expect(selected[0]).toEqual({approved: false, selectedActionIds: [actionId]}); expect(result.status).toBe("ready"); expect(host.configurations).toBe(0); if (client === "claude_code") { expect(result.plannedActions).toEqual([]); expect(result.appliedActions).toEqual([]); expect(host.skillInstalls).toBe(0); expect(result.doctor.scope.skill).toBe(false); } else { expect(result.plannedActions.map(a => a.id)).toEqual(["install_skill"]); expect(result.appliedActions).toEqual(["installed_skill"]); expect(host.skillInstalls).toBe(1); expect(host.skillDirectoryValidations).toEqual([[], []]); } console.log(JSON.stringify({client, offered, selected, status: result.status, planned: result.plannedActions.map(a => a.id), applied: result.appliedActions, installs: host.skillInstalls, scope: result.doctor.scope})); }, ); ``` The Claude case asserts the defect as observed. The `codex` case is a positive control whose skill destination resolves to the shared root; it installs once despite the same empty agent selection. Prompt rendering, real terminal keystrokes, and real disk writes are outside this test's coverage. ### Expected behavior Accepting the separate CLI-only skill offer should retain the requested installation, show the shared destination for approval, install there after approval, and include that skill in final readiness checks. The [setup guide](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/installation.md#L33-L46) describes the separate offer with no agent selected. Its [skill destination contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/installation.md#L132-L138) says this selection uses `~/.agents/skills`. ### Impact and scope A supported setup choice silently omits the guided workflow and reports success. The executed case requires Claude Code detection, no selected registrations, and an offered skill installation. The shared-root control succeeds. Other combinations of client-specific destinations were not executed. This is a bounded setup failure (P2). Explicit shared skill selection is a documented alternative: `rea setup --skill-client shared --yes --json`. That command was not executed in this reproduction. ### REA version rea-agents 6.7.0 source revision `52415975942a4b9c53a817915b313ccc404f280e`. ### Runtime and platform macOS 27.0.1 arm64, Node.js v26.12.0, locked Vitest 5.0.3. Recording setup host and simulated prompt answers; no real analysis provider used. ### Target and artifact context Not applicable; setup selection for the bundled skill requires no target binary. ### Interface CLI ### Earliest failure stage Setup or provider admission ### Relevant output or evidence The focused test logged these workflow results: ```json {"client":"claude_code","offered":[["install_skill:claude_code"]],"selected":[{"approved":false,"selectedActionIds":["install_skill:claude_code"]}],"status":"ready","planned":[],"applied":[],"installs":0,"scope":{"mode":"explicit","clients":[],"providers":[],"skill":false,"target":null}} {"client":"codex","offered":[["install_skill"]],"selected":[{"approved":false,"selectedActionIds":["install_skill"]},{"approved":true,"selectedActionIds":["install_skill"]}],"status":"ready","planned":["install_skill"],"applied":["installed_skill"],"installs":1,"scope":{"mode":"explicit","clients":[],"skill_clients":[],"providers":[],"skill":true,"target":null}} ``` Result: one focused test file, two tests passed. The zero-install case and its positive control were observed; actual installed-file absence is inferred from the lack of any installer call in the recording host. The fetched `origin/main` revision `f801b2fbfd28f4e3713adefddc7bd73db44c284c` has no differences from the tested revision in `Setup.ts`, `SetupPlan.ts`, `SupportedClients.ts`, or `interactiveSetup.ts`. ### Typed outcome or recovery expected The accepted skill choice should produce an `install_skill` action for the shared root and, after approval and successful installation, report `installed_skill` with skill readiness enabled. An installation blocker should produce an explicit non-ready outcome rather than silently removing the choice. ### Analysis or suspected cause Source tracing matches the executed result: - [The selection plan](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/Setup.ts#L151-L171) uses detected/existing clients for skill proposals even though registration selection is empty. - [Action construction](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/SetupPlan.ts#L336-L351) assigns `install_skill:claude_code` to the personal destination. - [The no-client prompt](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/cli/interactiveSetup.ts#L135-L147) returns that ID unchanged on Yes. - [Selection consumption](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/Setup.ts#L197-L216) recognizes only exact `install_skill` when no clients were selected. Consequently `skillSelected` is false and the final plan omits the accepted action. The [public CLI wiring](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/cli/setupCommands.ts#L207-L230) uses the same confirmation adapter and workflow connected by this test. No repair has been tested. ### Additional context Related #1711 and PR #1744 concern final readiness checking an unselected shared copy after explicit client-only installation. Their [fix](https://github.com/morluto/rea/blob/bb508134efbba392a404b651d6ec05be8b7eac1e/src/application/Setup.ts#L523-L537) is already an ancestor of the tested revision. Here the selected action is discarded before installation, with no skill included in readiness, so that earlier fix does not cover this behavior. No linked directories or failed Hopper installation are needed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3550
No description provided.