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
5 changes: 5 additions & 0 deletions .changeset/install-trailer-skill-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

The line `taskless init` prints after a successful install now points only at the Taskless skill: ask your AI tool to onboard with Taskless. It no longer mentions `/tskl onboard` or a terminal `onboard` command.
28 changes: 8 additions & 20 deletions openspec/specs/cli-init/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,31 +513,19 @@ A re-install SHALL compute a diff against the previous manifest. With v0.7.0 the

### Requirement: Init prints a post-install onboarding trailer

After a successful install (both wizard and `--no-interactive` paths), `taskless init` SHALL print a single one-line trailer pointing the user at the new onboarding flow. The trailer SHALL be printed AFTER the install summary (the lines that report what was written and any obsolete files removed) and SHALL be the final line of output before the process exits. The trailer SHALL NOT be gated on the value of `install.onboarded` in the manifest — it is informational, printed every successful install. The trailer's wording SHALL adapt to the install plan: when at least one installed target received the `tskl` slash command (Claude Code or Cursor), the trailer SHALL mention `/tskl onboard`, the Taskless skill, AND `taskless onboard` (since command-receiving tools also get the skill, both AI-tool entry points are surfaced); when no installed target received commands (OpenCode, Codex, or the `.agents/` fallback), the trailer SHALL mention only the Taskless skill and `taskless onboard`, and SHALL NOT mention `/tskl onboard`. The trailer SHALL be suppressed when `taskless init` exits non-zero (cancelled wizard, install failure, etc.) or when the install was a no-op (no targets selected, no files to write).
After a successful install (both wizard and `--no-interactive` paths), `taskless init` SHALL print a single one-line trailer pointing the user at the new onboarding flow. The trailer SHALL be printed AFTER the install summary (the lines that report what was written and any obsolete files removed) and SHALL be the final line of output before the process exits. The trailer SHALL NOT be gated on the value of `install.onboarded` in the manifest — it is informational, printed every successful install. The trailer's wording SHALL be the same for every install plan: every installed target receives the Taskless skill, so the trailer SHALL tell the user to ask their AI tool to onboard with Taskless, naming the Taskless skill, and SHALL NOT mention `/tskl onboard` or a terminal `taskless onboard` command. The trailer SHALL be suppressed when `taskless init` exits non-zero (cancelled wizard, install failure, etc.) or when the install was a no-op (no targets selected, no files to write).

#### Scenario: Wizard install with commands mentions slash command, skill, and CLI
#### Scenario: Wizard install points at the skill only

- **WHEN** a user runs `taskless init` in an interactive terminal and the wizard completes successfully
- **AND** at least one selected target received the `tskl` slash command (Claude Code or Cursor)
- **THEN** the final line of output SHALL be a single-line trailer that mentions `/tskl onboard`, the Taskless skill, AND `taskless onboard`
- **THEN** the final line of output SHALL be a single-line trailer telling the user to ask their AI tool to onboard with the Taskless skill
- **AND** the trailer SHALL NOT mention `/tskl onboard` or `taskless onboard`, whether or not a selected target received the `tskl` slash command

#### Scenario: Wizard install without commands mentions skill and CLI only
#### Scenario: Non-interactive install points at the skill only

- **WHEN** a user runs `taskless init` in an interactive terminal and the wizard completes successfully
- **AND** no selected target received commands (e.g. only OpenCode and/or Codex were chosen)
- **THEN** the final line of output SHALL be a single-line trailer instructing the user to invoke the Taskless skill via natural language and mentioning `taskless onboard` as a terminal fallback
- **AND** the trailer SHALL NOT mention `/tskl onboard`

#### Scenario: Non-interactive install with commands mentions slash command, skill, and CLI

- **WHEN** a user runs `taskless init --no-interactive` against a project where Claude Code or Cursor is detected
- **THEN** the final line of output SHALL mention `/tskl onboard`, the Taskless skill, AND `taskless onboard`

#### Scenario: Non-interactive install with no commands mentions skill and CLI only

- **WHEN** a user runs `taskless init --no-interactive` against a project where no command-receiving tool is detected (the install uses the `.agents/` fallback or only OpenCode/Codex)
- **THEN** the final line of output SHALL mention the Taskless skill and `taskless onboard`
- **AND** SHALL NOT mention `/tskl onboard`
- **WHEN** a user runs `taskless init --no-interactive`, whether or not a command-receiving tool (Claude Code or Cursor) is detected
- **THEN** the final line of output SHALL be the same skill-only trailer
- **AND** SHALL NOT mention `/tskl onboard` or `taskless onboard`

#### Scenario: Cancelled wizard suppresses the trailer

Expand Down
4 changes: 1 addition & 3 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,7 @@ export const initCommand = defineCommand({
if (result.reloadNotice !== undefined) {
console.log(result.reloadNotice);
}
console.log(
getOnboardTrailer({ commandsInstalled: result.commandsInstalled })
);
console.log(getOnboardTrailer());
}
// Concrete state event: skills/commands were installed (non-interactive).
telemetry.capture("cli_installed");
Expand Down
18 changes: 5 additions & 13 deletions packages/cli/src/commands/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,12 @@ import {
* successful install. Lives here so the install paths share the same
* wording with the onboard subcommand they point at.
*
* Branches on whether any installed target received the `tskl` slash
* command. Tools that get commands (Claude Code, Cursor) also get the
* Taskless skill, so the with-commands trailer mentions both AI-tool
* paths; tools without commands (OpenCode, Codex, `.agents` fallback)
* still get the skill, so the no-commands trailer points at the skill.
* Both trailers also mention `taskless onboard` as the bare CLI fallback.
* Every install target receives the Taskless skill, so the trailer points
* at the skill in plain language and nothing else: no slash command, no
* terminal fallback.
*/
export function getOnboardTrailer(args: {
commandsInstalled: boolean;
}): string {
if (args.commandsInstalled) {
return `Next: in your AI tool, run /tskl onboard or ask it to use the Taskless skill (or run \`${getCliPrefix()} onboard\` from your terminal) to set up starter rules from your codebase and see a report of what they catch.`;
}
return `Next: in your AI tool, ask it to use the Taskless skill (or run \`${getCliPrefix()} onboard\` from your terminal) to set up starter rules from your codebase and see a report of what they catch.`;
export function getOnboardTrailer(): string {
return "In your AI tool, you can use the Taskless skill now - just ask to onboard with Taskless to set up starter rules for your codebase and get a report of what Taskless can catch.";
}

export const onboardCommand = defineCommand({
Expand Down
5 changes: 1 addition & 4 deletions packages/cli/src/wizard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,6 @@ export async function runWizard(
{ migrated }
);
if (pinnedNotice !== undefined) console.log(pinnedNotice);
const commandsInstalled = plan.targets.some(
(t) => t.mode === "reference" && t.commands.length > 0
);
// `previousState` was read before the plan was applied, so it still holds
// the version this project was on when the session that is watching this
// install started.
Expand All @@ -94,7 +91,7 @@ export async function runWizard(
cliVersion,
});
if (reloadNotice !== undefined) console.log(reloadNotice);
console.log(getOnboardTrailer({ commandsInstalled }));
console.log(getOnboardTrailer());
return finish({ status: "completed" });
} catch (error) {
if (error instanceof WizardCancelled) {
Expand Down
29 changes: 14 additions & 15 deletions packages/cli/test/init-no-interactive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ describe("taskless init (the batch install)", () => {
expect(manifest.install).toBeDefined();
});

it("prints a trailer mentioning /tskl onboard when commands were installed", async () => {
// Claude Code receives commands, so the trailer should mention the
// slash command form and the skill (both work) plus the bare CLI.
it("prints a skill-only trailer when commands were installed", async () => {
// Claude Code receives commands too, but the trailer points only at the
// skill: no slash command, no terminal fallback.
await mkdir(join(cwd, ".claude"), { recursive: true });

const { stdout } = await execFileAsync("node", [
Expand All @@ -198,24 +198,23 @@ describe("taskless init (the batch install)", () => {
cwd,
]);

expect(stdout).toMatch(/Next:.*\/tskl onboard/);
expect(stdout).toMatch(/Taskless skill/);
expect(stdout).toMatch(/`(?:npx|pnpm dlx) @taskless\/cli@latest onboard`/);
expect(stdout).toMatch(/^In your AI tool, .*Taskless skill/m);
expect(stdout).not.toContain("/tskl onboard");
expect(stdout).not.toMatch(/@taskless\/cli@latest onboard/);
});

it("prints a skill-only trailer when no commands were installed", async () => {
// .agents/ fallback receives no commands. The trailer should NOT mention
// /tskl onboard but SHOULD mention the skill and the bare CLI.
it("prints the same skill-only trailer when no commands were installed", async () => {
// .agents/ fallback receives no commands; the trailer is the same.
const { stdout } = await execFileAsync("node", [
binPath,
"init",
"-d",
cwd,
]);

expect(stdout).toMatch(/^In your AI tool, .*Taskless skill/m);
expect(stdout).not.toContain("/tskl onboard");
expect(stdout).toMatch(/Taskless skill/);
expect(stdout).toMatch(/`(?:npx|pnpm dlx) @taskless\/cli@latest onboard`/);
expect(stdout).not.toMatch(/@taskless\/cli@latest onboard/);
});

it("prints an upgrade trailer naming the changed directories, before the onboarding trailer", async () => {
Expand Down Expand Up @@ -248,7 +247,7 @@ describe("taskless init (the batch install)", () => {
const reloadAt = lines.findIndex((line) => line.includes("Reload skills"));
expect(trailerAt).toBeGreaterThan(-1);
expect(reloadAt).toBeGreaterThan(trailerAt);
expect(lines.at(-1)).toMatch(/^Next:/);
expect(lines.at(-1)).toMatch(/^In your AI tool, /);
});

it("omits the update pointer when the version did not move", async () => {
Expand Down Expand Up @@ -324,7 +323,7 @@ describe("taskless init (the batch install)", () => {
// trailer.
expect(trailerAt).toBeGreaterThan(-1);
expect(pinAt).toBeGreaterThan(trailerAt);
expect(lines.at(-1)).toMatch(/^Next:/);
expect(lines.at(-1)).toMatch(/^In your AI tool, /);
});

it("says CI will break when the same run migrated .taskless/", async () => {
Expand Down Expand Up @@ -446,7 +445,7 @@ describe("taskless init (the batch install)", () => {
cwd,
]);

expect(stdout).not.toMatch(/Next:.*onboard/);
expect(stdout).not.toMatch(/ask to onboard with Taskless/);
expect(stdout).not.toContain("/tskl onboard");
});

Expand All @@ -471,7 +470,7 @@ describe("taskless init (the batch install)", () => {
cwd,
]);

expect(stdout).toMatch(/Next:.*onboard/);
expect(stdout).toMatch(/ask to onboard with Taskless/);

const manifest = JSON.parse(
await readFile(join(cwd, ".taskless", "taskless.json"), "utf8")
Expand Down
Loading