diff --git a/openspec/specs/cli-init/spec.md b/openspec/specs/cli-init/spec.md index 09e1aade..f9923940 100644 --- a/openspec/specs/cli-init/spec.md +++ b/openspec/specs/cli-init/spec.md @@ -371,7 +371,7 @@ The wizard SHALL begin by rendering an ASCII rendition of the Taskless wordmark ### Requirement: Wizard prompts the user to choose install locations -The wizard's location step SHALL be presented as a tool-selection step: "which tools do you want to enable Taskless for?". It SHALL offer one row per directory: `Claude Code` (`.claude/`), `Cursor` (`.cursor/`), `OpenCode` (`.opencode/`), and a row for `.agents/`. A user who arrived from a harness that reads `.agents/` SHALL NOT have to know that before recognising their harness in the list, so the `.agents/` row SHALL be labelled with harness names: the detected harnesses whose own documentation says they read `.agents/skills` (OpenCode, Codex, Gemini CLI, Crush), in catalog order, followed by `and other .agents harnesses`, for example `OpenCode, Codex, and other .agents harnesses (.agents/)`. When none of them is detected, the row SHALL name all of them, so a harness is still recognisable on a machine where nothing is detected. +The wizard's location step SHALL be presented as a tool-selection step: "which tools do you want to enable Taskless for?". It SHALL offer one row per directory: `Claude Code` (`.claude/`), `Cursor` (`.cursor/`), `OpenCode` (`.opencode/`), and a row for `.agents/`. A user who arrived from a harness that reads `.agents/` SHALL NOT have to know that before recognising their harness in the list, so the `.agents/` row SHALL be labelled with harness names: the detected harnesses whose own documentation says they read `.agents/skills` (OpenCode, Codex, Gemini CLI, Crush), in catalog order, followed by `and other .agents harnesses`, for example `OpenCode, Codex, and other .agents harnesses (.agents/)`. The row SHALL name only detected harnesses: an undetected harness SHALL NOT be named, since the harnesses that read `.agents/skills` outnumber what fits on one row. When none of them is detected, the row SHALL be labelled `Agent Skills (.agents/)`. Every list of directories derived from the catalog SHALL name each directory at most once, however many detected tools install into it. The pre-checked set SHALL be the union of (a) every directory recorded as a target in the install manifest (`install.targets`) that matches an offered row, and (b) every detected tool's install directory, deduplicated by directory. When the manifest records no targets AND no tools are detected, `.agents/` SHALL be pre-checked as the first-run default. The canonical `.taskless/` store SHALL NOT appear as a selectable entry and SHALL NOT be pre-checked — it is always written and is never a manifest tool-directory target. @@ -383,10 +383,16 @@ Each selected directory SHALL produce exactly one `reference` stub target; the r #### Scenario: Codex is named in the tool list -- **WHEN** the wizard renders the tool-selection multiselect with Codex detected, or with no `.agents` harness detected +- **WHEN** the wizard renders the tool-selection multiselect with Codex detected - **THEN** the `.agents/` row's label SHALL name `Codex` - **AND** exactly one entry SHALL be offered for `.agents/` +#### Scenario: Undetected .agents harnesses are not named + +- **WHEN** the wizard renders the tool-selection multiselect with no `.agents` harness detected +- **THEN** the `.agents/` row SHALL be labelled `Agent Skills (.agents/)` +- **AND** the label SHALL NOT name OpenCode, Codex, Gemini CLI, or Crush + #### Scenario: Detected .agents harnesses are named in catalog order - **WHEN** Codex and OpenCode are detected diff --git a/packages/cli/src/wizard/steps/locations.ts b/packages/cli/src/wizard/steps/locations.ts index 33f05c29..b9867d81 100644 --- a/packages/cli/src/wizard/steps/locations.ts +++ b/packages/cli/src/wizard/steps/locations.ts @@ -23,15 +23,21 @@ export interface LocationChoice { * `.agents/skills`, in catalog order, then "and other .agents harnesses", * since many more read it than Taskless detects. * - * With none detected it names every harness Taskless knows reads it, so a - * Codex or Gemini CLI user still finds their harness on a fresh machine. + * Only detected harnesses are named. Listing every harness that reads + * `.agents/skills` grows with the catalog and buries the row, so with none + * detected the row keeps its catalog label, `fallback`. */ -export function agentsRowLabel(detected: readonly ToolDescriptor[]): string { +export function agentsRowLabel( + detected: readonly ToolDescriptor[], + fallback: string +): string { const detectedNames = new Set(detected.map((tool) => tool.name)); - const readers = TOOLS.filter((tool) => tool.readsAgentSkills === true); - const named = readers.filter((tool) => detectedNames.has(tool.name)); - const names = (named.length > 0 ? named : readers).map((tool) => tool.name); - return `${names.join(", ")}, and other .agents harnesses`; + const names = TOOLS.filter( + (tool) => tool.readsAgentSkills === true && detectedNames.has(tool.name) + ).map((tool) => tool.name); + if (names.length === 0) return fallback; + const list = names.length === 1 ? `${names[0]} ` : `${names.join(", ")}, `; + return `${list}and other .agents harnesses`; } /** @@ -76,7 +82,7 @@ export function locationChoices( const options = SHIM_TARGETS.map((shim) => ({ value: shim.dir, - label: `${shim.generic === true ? agentsRowLabel(detected) : shim.label} (${shim.dir}/)`, + label: `${shim.generic === true ? agentsRowLabel(detected, shim.label) : shim.label} (${shim.dir}/)`, hint: installedDirectories.has(shim.dir) ? "installed" : detectedDirectories.has(shim.dir) diff --git a/packages/cli/test/wizard-steps.test.ts b/packages/cli/test/wizard-steps.test.ts index ed0401d0..cd80e029 100644 --- a/packages/cli/test/wizard-steps.test.ts +++ b/packages/cli/test/wizard-steps.test.ts @@ -55,17 +55,18 @@ describe("locationChoices", () => { "OpenCode, Codex, and other .agents harnesses (.agents/)" ); expect(named(["Crush"])).toBe( - "Crush, and other .agents harnesses (.agents/)" + "Crush and other .agents harnesses (.agents/)" ); }); - // On a fresh machine nothing is detected, and a Codex user scanning for - // "Codex" still has to find it. - it("names every known .agents harness when none is detected", async () => { - const { agentsRowLabel } = await import("../src/wizard/steps/locations"); - expect(agentsRowLabel([])).toBe( - "OpenCode, Codex, Gemini CLI, Crush, and other .agents harnesses" - ); + // Naming every harness that reads `.agents/skills` grows with the catalog, + // so an undetected harness is never named. + it("names no harness on the .agents/ row when none is detected", async () => { + const { locationChoices } = await import("../src/wizard/steps/locations"); + const label = locationChoices([]).options.find( + (o) => o.value === ".agents" + )?.label; + expect(label).toBe("Agent Skills (.agents/)"); }); // OpenCode reads `.agents/skills`, so it is named, but it already gets @@ -77,7 +78,7 @@ describe("locationChoices", () => { const { options, initialValues } = locationChoices([opencode]); const agents = options.find((o) => o.value === ".agents"); - expect(agents?.label).toMatch(/^OpenCode, and other/); + expect(agents?.label).toMatch(/^OpenCode and other/); expect(agents?.hint).toBe("not detected"); expect(initialValues).toEqual([".opencode"]); });