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
11 changes: 11 additions & 0 deletions .changeset/uninstall-unchecked-locations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@taskless/cli": patch
---

Make the wizard's tool-selection step manifest-aware so unchecking a location removes Taskless from it.

- **Manifest-driven pre-check**: the `taskless init` tool-selection multiselect now pre-checks the union of directories recorded in the install manifest and detected tool directories — previously it pre-checked detected tools only. A location Taskless already installed into (notably `.agents/`, which has no detection signal of its own) now shows checked, so it can be unchecked to remove the stubs. The install engine already performed manifest-diffed, target-scoped removal; this only surfaces it in the UI.
- **Three-state hint**: each entry is hinted by origin — `installed` (recorded in the manifest, takes precedence), `detected` (tool present), or `not detected`.
- **Itemized removal confirmation**: when unchecking a location triggers removals, the confirm prompt now names each target and its stub count (e.g. `Remove Taskless from .claude/ (2 stubs)?`) instead of a generic message.

The non-interactive `init --no-interactive` / `update` paths are unchanged, and the canonical `.taskless/` store is never removed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-18
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
## Context

Taskless installs the consolidated `taskless` skill once into a canonical `.taskless/` store, then writes thin reference stubs into each selected tool directory (`.claude/`, `.cursor/`, `.opencode/`, `.agents/`). The install manifest at `.taskless/taskless.json` records, per target, what was written.

`applyInstallPlan` already performs manifest-driven, target-scoped removal: any target present in the previous manifest but absent from the current plan produces an all-removals diff entry, and the apply loop `rm`s those stub files. `renderSummaryAndConfirm` already gates any diff with removals behind a `confirm()`.

The gap is purely in the wizard's selection step. `locationChoices(detected)` derives the multiselect's pre-checked set (`initialValues`) from `detectTools()` — filesystem detection of whether a tool is present. Detection answers "where might you want Taskless?"; it does not answer "where is Taskless installed?". That second answer lives only in the manifest. Because the manifest never reaches the multiselect, a location Taskless installed into but that is not independently detected (notably `.agents/`, which has no detection signal of its own) renders unchecked — so it cannot be meaningfully unchecked, and the removal path the engine already supports is unreachable from the UI.

## Goals / Non-Goals

**Goals:**

- Make the wizard's pre-checked tool set reflect where Taskless is actually installed, so unchecking a location is a real, discoverable action.
- Give each multiselect entry an accurate origin hint: `installed`, `detected`, or `not detected`.
- Itemize the removal confirmation per target so the user sees exactly which locations lose Taskless.
- Keep `locationChoices` a pure, unit-testable function.

**Non-Goals:**

- Full uninstall — removing the canonical `.taskless/` store or the manifest. The at-least-one-tool selection rule stays.
- Pruning empty `skills/` or `commands/tskl/` directory shells left after stub removal.
- Any change to the removal engine (`applyInstallPlan`, `computeInstallDiff`) — it already does the work.
- Any change to the non-interactive `init --no-interactive` / `update` paths — they stay detection-driven and additive.

## Decisions

### Pre-checked set = manifest targets ∪ detected tool directories

`locationChoices` gains a second parameter: the directories recorded in the install manifest. `initialValues` becomes the union of (a) manifest target directories that match a `SHIM_TARGETS` entry and (b) detected tool install directories. The canonical `.taskless/` store is filtered out — it is never a `SHIM_TARGETS` entry, so it is naturally excluded.

The existing "`.agents/` when nothing is detected" fallback still applies, but only when **both** inputs are empty (no manifest, no detection) — i.e. a genuine first run.

_Alternative considered:_ pre-check from the manifest alone. Rejected — a first run has no manifest, and a user who adds a new tool after install should still see it offered as `detected`. The union covers both the install case and the discovery case.

### Three-state hint derived from origin

The per-entry hint is computed from set membership:

```
in manifest?
┌──────┬──────┐
│ yes │ no │
┌─────────────┼──────┼──────┤
detected│ yes │ inst │ det │
? │ no │ inst │ none │
└─────────────┴──────┴──────┘
inst = "installed" det = "detected"
none = "not detected" (".agents/" first-run default keeps its
"generic agent skills" hint)
```

"installed" takes precedence over "detected" — if Taskless is there, that is the more relevant fact for an uninstall decision.

### `locationChoices` stays pure

The manifest is read by `promptLocations` (which already does I/O) via the existing `readInstallState`, and the resulting target directory list is passed _into_ `locationChoices` as an argument. `locationChoices` performs no filesystem access. This preserves the property the spec already calls out — the detection-and-manifest-to-choices mapping is fully unit-testable without a TTY or a filesystem fixture.

### Itemized per-target removal confirmation

`renderSummaryAndConfirm` already iterates `diff.entries`. When `diff.hasRemovals`, instead of the generic single-line prompt, it builds the confirmation message from the entries that carry removals — listing each target directory and its removed stub count: e.g. `Remove Taskless from .claude/ (2 stubs), .cursor/ (1 stub)?`. The diff summary block above the prompt is unchanged. No-removal installs still skip the extra confirm.

## Risks / Trade-offs

- **A detected-but-never-installed tool now appears pre-checked as `detected`.** This matches today's behavior (detection already pre-checks), so no regression — the union only ever adds the manifest set on top.
- **Manifest lists a directory no longer in `SHIM_TARGETS`** (e.g. a removed/renamed tool) → it is filtered out of `initialValues` and not offered. The stub, if any, is then neither shown nor removed. Acceptable: out-of-catalog directories are outside this change's scope, and the existing convergence logic is unaffected.
- **User unchecks every previously-installed tool but the at-least-one rule forces one selection** → no full uninstall. This is intended (full uninstall is a Non-Goal); the user keeps at least one stub location plus the canonical store.

## Migration Plan

No data migration. The manifest schema is unchanged — this change only _reads_ `install.targets`. Behavior change is confined to interactive wizard runs; the non-interactive path is byte-for-byte unchanged. Rollback is a straight revert of the wizard files.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Why

The install engine already removes Taskless stubs from any tool directory dropped from the install plan, but the wizard's tool-selection step pre-checks entries from **tool detection** alone — not from where Taskless is actually installed. A user who wants to consolidate onto `.agents/` and drop the `.claude/` and `.cursor/` shims has no way to "uncheck" those locations, because the manifest's record of them never reaches the multiselect's pre-checked state.

## What Changes

- The wizard's tool-selection step SHALL pre-check the **union of** directories recorded in the install manifest (`install.targets`, excluding the canonical `.taskless/` store) **and** detected tool directories — so a location Taskless already installed into shows checked and can be unchecked.
- Each multiselect entry SHALL carry a three-state hint reflecting its origin: `installed` (in the manifest), `detected` (tool present, not yet installed), or `not detected`.
- Unchecking a manifest-recorded location SHALL remove Taskless's reference stubs from that directory on the next install, via the existing manifest-diff removal path — no new removal engine.
- The removal confirmation in the wizard summary SHALL be itemized per target (e.g. "Remove Taskless from `.claude/` (2 stubs), `.cursor/` (2 stubs)?") instead of the current generic single-line prompt.
- The at-least-one-tool selection rule is unchanged; full uninstall (removing the canonical `.taskless/` store) is explicitly out of scope.
- Empty-directory pruning is out of scope: only the stub files Taskless wrote are removed; `skills/` and `commands/tskl/` directory shells are left in place (current behavior).
- The non-interactive `init --no-interactive` / `update` paths are unchanged: they remain detection-driven, additive, and never surprise-delete a still-present tool's stubs.

## Capabilities

### New Capabilities

<!-- None — this modifies existing wizard behavior. -->

### Modified Capabilities

- `cli-init`: The "Wizard prompts the user to choose install locations" requirement changes — the pre-checked set becomes the union of manifest-recorded and detected directories, with a three-state per-entry hint. The "Wizard shows a diff-style summary before writing" requirement changes — the removal confirmation becomes itemized per target.

## Impact

- `packages/cli/src/wizard/steps/locations.ts` — `locationChoices` gains a manifest-targets parameter (kept pure); `promptLocations` reads install state and passes both manifest targets and detected tools down.
- `packages/cli/src/wizard/steps/summary.ts` — `renderSummaryAndConfirm` builds an itemized per-target removal confirmation message.
- `packages/cli/src/install/install.ts` / `state.ts` — read-only consumption of existing `readInstallState`; no removal-engine changes.
- Tests: `locationChoices` mapping unit tests extended for manifest-driven pre-check and the three-state hint; summary-confirmation copy assertions updated.
- No changes to the non-interactive install path, the manifest schema, or the canonical-store handling.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
## MODIFIED Requirements

### 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 a fixed multiselect of `.claude/`, `.cursor/`, `.opencode/`, and `.agents/`. The pre-checked set SHALL be the union of (a) every directory recorded as a target in the install manifest (`install.targets`) that matches one of the four offered entries, and (b) every detected tool's install 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.

Each offered entry SHALL carry an origin hint: `installed` when the entry's directory is recorded in the install manifest; otherwise `detected` when the entry's tool is detected on the filesystem; otherwise `not detected` (the `.agents/` first-run default MAY instead carry a hint describing it as the generic agent-skills location). The `installed` hint SHALL take precedence over `detected` when both apply.

Unchecking a pre-checked, manifest-recorded entry SHALL cause the resulting install plan to omit that target, so the existing manifest-diff removal path removes Taskless's reference stubs from that directory. The at-least-one-tool selection rule is unchanged: the wizard SHALL require at least one checked entry.

Each checked entry SHALL produce one `reference` stub target; the resulting install plan always contains the single `taskless` skill (and, for `.claude/` and `.cursor/`, the `tskl` command). The function that maps detected tools and manifest targets to multiselect choices SHALL be pure — it SHALL receive both the detected tools and the manifest target list as arguments and SHALL perform no filesystem access — so the mapping is unit-testable.

#### Scenario: Detected tools are pre-checked

- **WHEN** the wizard reaches the tool-selection step and `.claude/` is detected
- **THEN** `.claude/` SHALL be pre-checked in the multiselect
- **AND** `.claude/` SHALL carry the `detected` hint when it is not recorded in the install manifest

#### Scenario: Manifest-recorded locations are pre-checked

- **WHEN** the wizard reaches the tool-selection step and the install manifest records `.agents/` as a target
- **THEN** `.agents/` SHALL be pre-checked in the multiselect
- **AND** `.agents/` SHALL carry the `installed` hint

#### Scenario: Installed hint takes precedence over detected

- **WHEN** the wizard reaches the tool-selection step and `.claude/` is both detected on the filesystem and recorded in the install manifest
- **THEN** `.claude/` SHALL be pre-checked
- **AND** `.claude/` SHALL carry the `installed` hint, not the `detected` hint

#### Scenario: Unchecking an installed location removes its stubs

- **WHEN** the install manifest records `.claude/` and `.agents/` as targets and the user unchecks `.claude/` while leaving `.agents/` checked
- **THEN** the resulting install plan SHALL omit the `.claude/` target
- **AND** the wizard summary SHALL list the `.claude/` reference stubs as removals

#### Scenario: Agents is the default when nothing is detected or installed

- **WHEN** the wizard reaches the tool-selection step, no tools are detected, and the install manifest records no tool-directory targets
- **THEN** `.agents/` SHALL be pre-checked

#### Scenario: Canonical store is not a selectable entry

- **WHEN** the wizard renders the tool-selection multiselect
- **THEN** `.taskless/` SHALL NOT appear as a selectable option
- **AND** `.taskless/` SHALL NOT be pre-checked even though the manifest records it as a target

### Requirement: Wizard shows a diff-style summary before writing

Before any filesystem writes, the wizard SHALL display a summary of planned actions grouped by target location. The summary SHALL include:

- Additions: skills that will be newly written to a target
- Removals: skills previously recorded in the install manifest but not selected in the current session
- Unchanged: skills that will be overwritten with identical content (may be collapsed to a count)

If the summary contains any removals, the wizard SHALL require an explicit `confirm()` before proceeding. The confirmation prompt SHALL be itemized per target: it SHALL name each target directory losing content and the count of stubs being removed from it (for example, "Remove Taskless from `.claude/` (2 stubs), `.cursor/` (1 stub)?"). If the summary contains no removals, the wizard MAY proceed without an extra confirm.

#### Scenario: Additions are shown in the summary

- **WHEN** the wizard reaches the summary step and the user has selected a location that was not in the previous install manifest
- **THEN** the summary SHALL list every skill to be added under that location

#### Scenario: Removals trigger an itemized confirm

- **WHEN** the summary contains at least one skill or location to be removed
- **THEN** the wizard SHALL display a confirm prompt before writing
- **AND** the confirm prompt SHALL name each target directory losing content and the count of stubs removed from it
- **AND** declining the confirm SHALL abort without writes

#### Scenario: No-removal summaries skip the extra confirm

- **WHEN** the summary contains only additions and unchanged entries
- **THEN** the wizard MAY proceed directly to writes without an extra confirm
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## 1. Manifest-aware location choices

- [x] 1.1 Extend `locationChoices` in `packages/cli/src/wizard/steps/locations.ts` to accept a second argument: the list of install-manifest target directories. Keep the function pure (no filesystem access).
- [x] 1.2 Compute `initialValues` as the union of manifest target directories matching a `SHIM_TARGETS` entry and detected tools' install directories; keep the `.agents/` first-run default only when both inputs are empty.
- [x] 1.3 Compute each entry's hint with `installed` (in manifest) taking precedence over `detected`, falling back to `not detected`, preserving the `.agents/` "generic agent skills" default-hint case.
- [x] 1.4 Update `promptLocations` to read install state via `readInstallState`, derive the manifest target directory list (excluding `.taskless/`), and pass it into `locationChoices`.

## 2. Itemized removal confirmation

- [x] 2.1 In `packages/cli/src/wizard/steps/summary.ts`, build the removal confirmation message from `diff.entries` that carry removals — naming each target directory and its removed stub count.
- [x] 2.2 Keep the diff summary block and the no-removal fast-path (`!diff.hasRemovals` returns `true`) unchanged.

## 3. Tests

- [x] 3.1 Extend the `locationChoices` unit tests: manifest-only pre-check, manifest ∪ detected union, `installed`-over-`detected` precedence, and the both-empty `.agents/` default.
- [x] 3.2 Add a test that an entry recorded in the manifest carries the `installed` hint and `.taskless/` is never an offered or pre-checked entry.
- [x] 3.3 Update summary-confirmation tests to assert the itemized per-target message (target names and stub counts) and that no-removal summaries skip the confirm. Added end-to-end uninstall tests in `wizard-integration.test.ts` covering stub removal on uncheck and decline-keeps-stubs.

## 4. Verification

- [x] 4.1 Run `pnpm typecheck` and `pnpm lint`; fix any failures.
- [x] 4.2 Run the CLI test suite (`pnpm --filter @taskless/cli test`) and confirm all tests pass.
Loading
Loading