diff --git a/.changeset/remove-wizard-login-prompt.md b/.changeset/remove-wizard-login-prompt.md new file mode 100644 index 00000000..762969ca --- /dev/null +++ b/.changeset/remove-wizard-login-prompt.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +The setup wizard no longer asks "Log in to taskless.io now?". Setup needs no account, since local `sg` and `vale` rules run without one. `taskless auth login` is unchanged, and commands that need an account still say so. diff --git a/openspec/changes/archive/2026-09-27-remove-wizard-login-prompt/proposal.md b/openspec/changes/archive/2026-09-27-remove-wizard-login-prompt/proposal.md new file mode 100644 index 00000000..5a27309b --- /dev/null +++ b/openspec/changes/archive/2026-09-27-remove-wizard-login-prompt/proposal.md @@ -0,0 +1,53 @@ +## Why + +The setup wizard (bare `taskless` in a terminal) stops between choosing tools +and installing to ask **"Log in to taskless.io now?"**, after an +"Authentication" note about conversation history. Issue #402 asks for it to go: + +- Every question in first-run setup is friction between a person and a working + `check`. +- The free tier is local-only. Authoring and running `sg` and `vale` rules needs + no account, so asking a free user to log in during setup sells them something + they do not need yet. +- Login stays reachable where it is needed. `taskless auth login` is unchanged, + and the commands that need an account already say so (`create-remote-rule` + "(login)", the `AUTH_REQUIRED` errors and their `taskless auth login` remedy). + +`taskless init` already runs without prompts and without an auth step, so the +wizard is the only place the prompt lives. + +## What Changes + +- **`cli-init`**: the requirement that the wizard explains the auth tradeoff and + offers to log in is removed, and a requirement that the wizard does not offer + to log in replaces it. The shared login routine requirement and the wizard + cancellation requirement each carry one scenario about the auth step. A + MODIFIED block cannot drop a scenario, so both are removed and re-added + under new titles with every other scenario intact: "A single interactive + login routine serves auth login" and "Cancelling the wizard writes nothing". +- The wizard's auth step (`wizard/steps/auth.ts`) is deleted. +- `WizardResult` drops `authPromptShown` and `authCompleted`. Nothing read + them, and no telemetry event carried them, so no event changes shape. The + per-run `cli_run` event keeps reporting `loggedIn`, resolved from the token, + which is unaffected. + +## Out of scope + +How paid accounts authenticate for remote generation or runtime rules. That +path, `taskless auth login`, and every "run `taskless auth login`" remedy are +unchanged. + +## Delivery shape + +**Single PR.** The change is a deletion of roughly twenty lines of code plus +its spec, and each half is only correct with the other. It lands and archives +together. + +## Impact + +- `packages/cli/src/wizard/index.ts`, `packages/cli/src/wizard/steps/auth.ts` + (deleted), `packages/cli/src/auth/login-interactive.ts` (comment only) +- `packages/cli/test/wizard-integration.test.ts`: the clack mock no longer + routes a "log in" confirm, and a new test runs the wizard with no token and + asserts nothing offers to log in +- `openspec/specs/cli-init/spec.md` diff --git a/openspec/changes/archive/2026-09-27-remove-wizard-login-prompt/specs/cli-init/spec.md b/openspec/changes/archive/2026-09-27-remove-wizard-login-prompt/specs/cli-init/spec.md new file mode 100644 index 00000000..0f5d2d8b --- /dev/null +++ b/openspec/changes/archive/2026-09-27-remove-wizard-login-prompt/specs/cli-init/spec.md @@ -0,0 +1,61 @@ +## ADDED Requirements + +### Requirement: Wizard does not offer to log in + +The wizard SHALL NOT prompt the user to log in, and SHALL NOT display an authentication explanation, at any step. It SHALL proceed from the tool-selection step directly to the install summary whether or not a token is available. Setup requires no account: authoring and running local rules needs none, and `taskless auth login` remains available, as do the "run `taskless auth login`" remedies on commands that require authentication. + +#### Scenario: Wizard advances from tools to summary without a login prompt + +- **WHEN** the wizard completes the tool-selection step and no valid token is resolvable +- **THEN** the wizard SHALL NOT display an authentication note or a login prompt +- **AND** SHALL advance to the install summary + +#### Scenario: Wizard does not start the login flow + +- **WHEN** the wizard runs to completion +- **THEN** it SHALL NOT call `loginInteractive()` + +### Requirement: A single interactive login routine serves auth login + +The CLI SHALL expose a single `loginInteractive()` function that performs the device-code login flow and returns once the token is stored or cancelled. The `auth login` subcommand SHALL call this function. No duplicate login implementation SHALL exist. + +#### Scenario: Auth login uses the shared routine + +- **WHEN** a user runs `taskless auth login` +- **THEN** the command handler SHALL call `loginInteractive()` + +### Requirement: Cancelling the wizard writes nothing + +If the user cancels the wizard at any step (Ctrl-C, Esc, or equivalent clack cancel signal) before the install step completes, the CLI SHALL NOT write any skill files, command files, or manifest updates. The CLI SHALL exit with a non-zero exit code and print a short message indicating how to resume (`taskless init`). + +#### Scenario: Cancel at locations step + +- **WHEN** the user cancels the wizard during the locations step +- **THEN** no files SHALL be written +- **AND** the CLI SHALL exit non-zero + +#### Scenario: Cancel at summary confirm + +- **WHEN** the user declines the summary confirm +- **THEN** no files SHALL be written +- **AND** the CLI SHALL exit non-zero + +## REMOVED Requirements + +### Requirement: Wizard explains the auth tradeoff and offers to log in + +**Reason**: Every first-run question is friction, and the free tier is local-only, so asking during setup sells an account the user does not need yet (#402). + +**Migration**: Run `taskless auth login` when an account is wanted. Commands that require authentication already report that and name the command. + +### Requirement: Shared interactive login routine + +**Reason**: Its "Wizard uses the shared routine" scenario describes the removed auth step. A MODIFIED block cannot drop a scenario, so the requirement is restated without it as "A single interactive login routine serves auth login". + +**Migration**: None. `auth login` still calls `loginInteractive()`, and it is still the only login implementation. + +### Requirement: Wizard cancellation aborts without filesystem writes + +**Reason**: Its "Cancel at auth step" scenario describes a step that no longer exists. The requirement is restated without it as "Cancelling the wizard writes nothing". + +**Migration**: None. Cancellation behavior at the remaining steps is unchanged. diff --git a/openspec/changes/archive/2026-09-27-remove-wizard-login-prompt/tasks.md b/openspec/changes/archive/2026-09-27-remove-wizard-login-prompt/tasks.md new file mode 100644 index 00000000..17020bb6 --- /dev/null +++ b/openspec/changes/archive/2026-09-27-remove-wizard-login-prompt/tasks.md @@ -0,0 +1,25 @@ +## 1. Spec + +- [x] 1.1 Remove "Wizard explains the auth tradeoff and offers to log in" and + add "Wizard does not offer to log in" in its place. +- [x] 1.2 Retitle "Shared interactive login routine" and "Wizard cancellation + aborts without filesystem writes" as REMOVED plus ADDED, carrying every + scenario except the one naming the wizard's auth step. +- [x] 1.3 Dry-run `openspec archive` and confirm every other scenario in + `cli-init` survives. + +## 2. Code + +- [x] 2.1 Delete `wizard/steps/auth.ts` and its call in `runWizard`. +- [x] 2.2 Drop `authPromptShown` / `authCompleted` from `WizardResult`. +- [x] 2.3 Update the `loginInteractive` comments that named the wizard. +- [x] 2.4 Remove the "log in" branch from the wizard test's clack mock, so a + reintroduced login confirm would be answered as the summary confirm and + fail the tests' expectations on its message. +- [x] 2.5 Add a wizard test that runs with no token and asserts no confirm + mentions logging in. Checked by inserting a login confirm into the + wizard: the test fails, and passes once it is removed. + +## 3. Release + +- [x] 3.1 Add a `patch` changeset. diff --git a/openspec/specs/cli-init/spec.md b/openspec/specs/cli-init/spec.md index 98aab139..58af4ab9 100644 --- a/openspec/specs/cli-init/spec.md +++ b/openspec/specs/cli-init/spec.md @@ -464,47 +464,6 @@ Each selected directory SHALL produce exactly one `reference` stub target, even - **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 explains the auth tradeoff and offers to log in - -The wizard SHALL present a short informational screen describing the tradeoff between anonymous and authenticated use: authenticated rules retain conversation history across teammates, enabling rule provenance (answering "why do we have this rule?"). The screen SHALL be followed by a yes/no prompt asking the user whether they want to log in now. If the user accepts, the wizard SHALL invoke the shared interactive login routine (the same routine used by `taskless auth login`) and SHALL block until the login flow completes or is cancelled. If the user declines, the wizard SHALL print a one-line hint that they can run `taskless auth login` later and proceed to the install step. If a valid token is already present, the wizard SHALL skip this step entirely. - -#### Scenario: Auth step is shown when not logged in - -- **WHEN** the wizard reaches the auth step and no valid token is resolvable -- **THEN** the wizard SHALL display the tradeoff explanation -- **AND** SHALL prompt the user to log in - -#### Scenario: Accepting login blocks until completion - -- **WHEN** the user accepts the login prompt -- **THEN** the wizard SHALL invoke the shared interactive login routine -- **AND** SHALL NOT advance to the install step until the login routine resolves - -#### Scenario: Declining login advances with a hint - -- **WHEN** the user declines the login prompt -- **THEN** the wizard SHALL print a hint mentioning `taskless auth login` -- **AND** SHALL advance to the install step - -#### Scenario: Auth step is skipped when already logged in - -- **WHEN** the wizard reaches the auth step and a valid token already exists for the working directory -- **THEN** the wizard SHALL skip the auth explanation and prompt entirely - -### Requirement: Shared interactive login routine - -The CLI SHALL expose a single `loginInteractive()` function that performs the device-code login flow and returns once the token is stored or cancelled. Both the `auth login` subcommand and the wizard's auth step SHALL call this function. No duplicate login implementation SHALL exist. - -#### Scenario: Auth login uses the shared routine - -- **WHEN** a user runs `taskless auth login` -- **THEN** the command handler SHALL call `loginInteractive()` - -#### Scenario: Wizard uses the shared routine - -- **WHEN** the wizard user accepts the login prompt -- **THEN** the wizard SHALL call `loginInteractive()` - ### 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: @@ -532,28 +491,6 @@ If the summary contains any removals, the wizard SHALL require an explicit `conf - **WHEN** the summary contains only additions and unchanged entries - **THEN** the wizard MAY proceed directly to writes without an extra confirm -### Requirement: Wizard cancellation aborts without filesystem writes - -If the user cancels the wizard at any step (Ctrl-C, Esc, or equivalent clack cancel signal) before the install step completes, the CLI SHALL NOT write any skill files, command files, or manifest updates. The CLI SHALL exit with a non-zero exit code and print a short message indicating how to resume (`taskless init`). - -#### Scenario: Cancel at locations step - -- **WHEN** the user cancels the wizard during the locations step -- **THEN** no files SHALL be written -- **AND** the CLI SHALL exit non-zero - -#### Scenario: Cancel at auth step - -- **WHEN** the user cancels the wizard during the auth step -- **THEN** no skill files or manifest updates SHALL be written -- **AND** the CLI SHALL exit non-zero - -#### Scenario: Cancel at summary confirm - -- **WHEN** the user declines the summary confirm -- **THEN** no files SHALL be written -- **AND** the CLI SHALL exit non-zero - ### Requirement: Install manifest records what was installed per target The install manifest in `.taskless/taskless.json` continues to record what was written per target. Each target entry SHALL additionally record a `mode` field (`canonical` or `reference`) as defined by the per-target install mode requirement. The `.taskless` target records the canonical store; tool-directory targets record the stubs written for that directory. @@ -825,3 +762,43 @@ The `migrated` field is unchanged: present with the migration report when a migr - **WHEN** `taskless init --json` runs - **THEN** the envelope SHALL contain `cliVersion.previous` (a string or `null`), `cliVersion.installed`, a `targets` array with one entry per install target, and a boolean `changed` - **AND** `changed` SHALL be `true` exactly when `migrated` is present, any target's written or removed list is non-empty, or `cliVersion.previous` is non-null and differs from `cliVersion.installed` + +### Requirement: Wizard does not offer to log in + +The wizard SHALL NOT prompt the user to log in, and SHALL NOT display an authentication explanation, at any step. It SHALL proceed from the tool-selection step directly to the install summary whether or not a token is available. Setup requires no account: authoring and running local rules needs none, and `taskless auth login` remains available, as do the "run `taskless auth login`" remedies on commands that require authentication. + +#### Scenario: Wizard advances from tools to summary without a login prompt + +- **WHEN** the wizard completes the tool-selection step and no valid token is resolvable +- **THEN** the wizard SHALL NOT display an authentication note or a login prompt +- **AND** SHALL advance to the install summary + +#### Scenario: Wizard does not start the login flow + +- **WHEN** the wizard runs to completion +- **THEN** it SHALL NOT call `loginInteractive()` + +### Requirement: A single interactive login routine serves auth login + +The CLI SHALL expose a single `loginInteractive()` function that performs the device-code login flow and returns once the token is stored or cancelled. The `auth login` subcommand SHALL call this function. No duplicate login implementation SHALL exist. + +#### Scenario: Auth login uses the shared routine + +- **WHEN** a user runs `taskless auth login` +- **THEN** the command handler SHALL call `loginInteractive()` + +### Requirement: Cancelling the wizard writes nothing + +If the user cancels the wizard at any step (Ctrl-C, Esc, or equivalent clack cancel signal) before the install step completes, the CLI SHALL NOT write any skill files, command files, or manifest updates. The CLI SHALL exit with a non-zero exit code and print a short message indicating how to resume (`taskless init`). + +#### Scenario: Cancel at locations step + +- **WHEN** the user cancels the wizard during the locations step +- **THEN** no files SHALL be written +- **AND** the CLI SHALL exit non-zero + +#### Scenario: Cancel at summary confirm + +- **WHEN** the user declines the summary confirm +- **THEN** no files SHALL be written +- **AND** the CLI SHALL exit non-zero diff --git a/packages/cli/src/auth/login-interactive.ts b/packages/cli/src/auth/login-interactive.ts index 3e85744f..e2d609cc 100644 --- a/packages/cli/src/auth/login-interactive.ts +++ b/packages/cli/src/auth/login-interactive.ts @@ -26,7 +26,7 @@ export interface LoginInteractiveOptions { * polls the Taskless auth endpoint, persists the token on success, and * returns a tagged result the caller can surface however it wants. * - * Does NOT emit telemetry — the caller (wizard or `auth login` command) is + * Does NOT emit telemetry — the caller (the `auth login` command) is * responsible for its own telemetry events so the events stay scoped to * their originating flow. */ @@ -37,9 +37,9 @@ export async function loginInteractive( const out = options.out ?? ((line) => console.log(line)); const error_ = options.err ?? ((line) => console.error(line)); - // Silent: we're inside an interactive (clack) flow — stderr warnings from - // legacy/tracked-token checks would corrupt the UI. Pre-checks happen in - // promptAuth() before we get here. + // Silent: this is only an "already logged in?" probe. Stderr warnings from + // legacy/tracked-token checks belong to the commands that use the token, + // not to the login flow that is about to replace it. const existing = await getToken(cwd, { silent: true }); if (existing) { return { status: "already_logged_in" }; diff --git a/packages/cli/src/wizard/index.ts b/packages/cli/src/wizard/index.ts index 40f35877..bee4db5a 100644 --- a/packages/cli/src/wizard/index.ts +++ b/packages/cli/src/wizard/index.ts @@ -17,7 +17,6 @@ import { CLIError } from "../util/cli-error"; import { WizardCancelled } from "./ask"; import { getCliVersion, renderIntro } from "./intro"; import { promptLocations } from "./steps/locations"; -import { promptAuth } from "./steps/auth"; import { renderSummaryAndConfirm } from "./steps/summary"; export interface RunWizardOptions { @@ -28,8 +27,6 @@ export interface WizardResult { status: "completed" | "cancelled"; locations: string[]; optionalSkills: string[]; - authPromptShown: boolean; - authCompleted: boolean; durationMs: number; cancelledStep?: string; } @@ -44,17 +41,12 @@ export async function runWizard( let locations: string[] = []; // Optional skills no longer exist post-consolidation — always empty. const optionalSkills: string[] = []; - let authPromptShown = false; - let authCompleted = false; console.error(renderIntro()); intro(" Taskless setup "); try { locations = await promptLocations(options.cwd); - const authResult = await promptAuth(options.cwd); - authPromptShown = authResult.prompted; - authCompleted = authResult.loggedIn; // Catalog has one entry now (`taskless`); install all embedded skills. const plan = buildInstallPlan( @@ -129,8 +121,6 @@ export async function runWizard( status: args.status, locations, optionalSkills, - authPromptShown, - authCompleted, durationMs, cancelledStep, }; diff --git a/packages/cli/src/wizard/steps/auth.ts b/packages/cli/src/wizard/steps/auth.ts deleted file mode 100644 index 782b7d9a..00000000 --- a/packages/cli/src/wizard/steps/auth.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { confirm, log, note } from "@clack/prompts"; - -import { getToken } from "../../auth/token"; -import { loginInteractive } from "../../auth/login-interactive"; -import { ask } from "../ask"; - -export interface AuthStepResult { - /** True when the step was actually shown (user was not already logged in). */ - prompted: boolean; - /** True when the user ended the step with a valid token. */ - loggedIn: boolean; -} - -export async function promptAuth(cwd: string): Promise { - const existing = await getToken(cwd, { silent: true }); - if (existing) { - return { prompted: false, loggedIn: true }; - } - - note( - [ - "Taskless can work without an account, but authenticated rules retain", - "conversation history across teammates — great for rule provenance", - '("why do we have this rule?").', - ].join("\n"), - "Authentication" - ); - - const wantsLogin = await ask("auth", () => - confirm({ - message: "Log in to taskless.io now?", - initialValue: false, - }) - ); - - if (!wantsLogin) { - log.info( - "You can run `taskless auth login` at any time to authenticate later." - ); - return { prompted: true, loggedIn: false }; - } - - const result = await loginInteractive({ cwd }); - return { - prompted: true, - loggedIn: result.status === "ok" || result.status === "already_logged_in", - }; -} diff --git a/packages/cli/test/wizard-integration.test.ts b/packages/cli/test/wizard-integration.test.ts index 0f9a0ba7..a31f24f3 100644 --- a/packages/cli/test/wizard-integration.test.ts +++ b/packages/cli/test/wizard-integration.test.ts @@ -26,7 +26,6 @@ const cancelSpy = vi.fn(); // Clack mock responses are set per-test via these mutable refs. const clackResponses: { locations?: string[] | symbol; - auth?: boolean | symbol; summary?: boolean | symbol; } = {}; @@ -49,9 +48,6 @@ vi.mock("@clack/prompts", () => ({ isCancel: (value: unknown) => value === fakeCancelSymbol, multiselect: vi.fn(() => Promise.resolve(clackResponses.locations)), confirm: vi.fn(({ message }: { message: string }) => { - if (message.toLowerCase().includes("log in")) { - return Promise.resolve(clackResponses.auth); - } summaryConfirmMessage = message; return Promise.resolve(clackResponses.summary); }), @@ -74,7 +70,6 @@ beforeEach(async () => { captureSpy.mockClear(); cancelSpy.mockClear(); clackResponses.locations = undefined; - clackResponses.auth = undefined; clackResponses.summary = undefined; summaryConfirmMessage = undefined; vi.stubEnv("TASKLESS_TOKEN", "stub-token"); @@ -113,6 +108,25 @@ describe("runWizard end-to-end", () => { expect(captureSpy).toHaveBeenCalledWith("cli_installed"); }); + it("completes without a token and never offers to log in", async () => { + vi.stubEnv("TASKLESS_TOKEN", ""); + clackResponses.locations = [".claude"]; + clackResponses.summary = true; + + const clack = await import("@clack/prompts"); + const confirmMock = vi.mocked(clack.confirm); + confirmMock.mockClear(); + + const { runWizard } = await import("../src/wizard"); + const result = await runWizard({ cwd }); + + expect(result.status).toBe("completed"); + const messages = confirmMock.mock.calls.map(([options]) => + String(options.message) + ); + expect(messages.filter((m) => /log\s*in/i.test(m))).toEqual([]); + }); + it("re-running with the same location is idempotent", async () => { clackResponses.locations = [".claude"]; clackResponses.summary = true; diff --git a/packages/cli/test/wizard-steps.test.ts b/packages/cli/test/wizard-steps.test.ts index 24f970c9..ef0b53b9 100644 --- a/packages/cli/test/wizard-steps.test.ts +++ b/packages/cli/test/wizard-steps.test.ts @@ -23,11 +23,13 @@ describe("ask wrapper", () => { it("the thrown error carries the step name", async () => { const { ask, WizardCancelled } = await import("../src/wizard/ask"); try { - await ask("auth", () => Promise.resolve(fakeCancelSymbol)); + await ask("summary", () => Promise.resolve(fakeCancelSymbol)); expect.fail("expected WizardCancelled"); } catch (error) { expect(error).toBeInstanceOf(WizardCancelled); - expect((error as InstanceType).step).toBe("auth"); + expect((error as InstanceType).step).toBe( + "summary" + ); } }); });