Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions openspec/specs/cli-init/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down
22 changes: 14 additions & 8 deletions packages/cli/src/wizard/steps/locations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
}

/**
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 10 additions & 9 deletions packages/cli/test/wizard-steps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]);
});
Expand Down
Loading