From f86b4d5e700eb5dd8bd4927b3238e4bcb45a467d Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Sun, 27 Sep 2026 09:53:16 -0700 Subject: [PATCH 1/2] feat(wizard): drop the login prompt from setup The setup wizard no longer stops to ask "Log in to taskless.io now?". Setup needs no account, since local sg and vale rules run without one, and taskless auth login plus the auth-required remedies are unchanged. Removes the wizard's auth step and the unread authPromptShown / authCompleted fields, and archives the remove-wizard-login-prompt OpenSpec change. --- .changeset/remove-wizard-login-prompt.md | 5 + .../proposal.md | 53 +++++++++ .../specs/cli-init/spec.md | 61 +++++++++++ .../tasks.md | 25 +++++ openspec/specs/cli-init/spec.md | 103 +++++++----------- packages/cli/src/auth/login-interactive.ts | 8 +- packages/cli/src/wizard/index.ts | 10 -- packages/cli/src/wizard/steps/auth.ts | 48 -------- packages/cli/test/wizard-integration.test.ts | 24 +++- 9 files changed, 207 insertions(+), 130 deletions(-) create mode 100644 .changeset/remove-wizard-login-prompt.md create mode 100644 openspec/changes/archive/2026-09-27-remove-wizard-login-prompt/proposal.md create mode 100644 openspec/changes/archive/2026-09-27-remove-wizard-login-prompt/specs/cli-init/spec.md create mode 100644 openspec/changes/archive/2026-09-27-remove-wizard-login-prompt/tasks.md delete mode 100644 packages/cli/src/wizard/steps/auth.ts 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; From 1f9a7d4a4f00e5fb992913f7a269841502bfad5a Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Sun, 27 Sep 2026 10:08:01 -0700 Subject: [PATCH 2/2] test(wizard): name a real step in the ask() cancellation test The auth step is gone, so "auth" no longer names anything the wizard runs. Use "summary", which it does. --- packages/cli/test/wizard-steps.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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" + ); } }); });