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/environment-steps-run-init.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

Fix the upgrade steps `agent ok` and `check` print. They told you to run the CLI with no arguments, which only installs from a terminal: anywhere else it printed the topic index and changed nothing. They now name `init`, which works in both.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-11-04
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Context

`environmentSteps` writes the fixing commands for both `agent ok` and `check`. A bare invocation reaches the wizard only when stdin and stdout are TTYs and `CI` is unset. Otherwise it prints a preamble and the topic index.

## Decisions

**Name `init`, the batch install, in both steps.** It behaves the same with or without a terminal, and it is exempt from the version gate, so the newer CLI can run it in a project that records an older one. The alternative, making a bare non-interactive run install, would turn every piped or agent-run bare invocation into a write.

## Risks / Trade-offs

- [`init` skips the wizard's confirmations, including the global trust entry] → It reports that entry as a `SKIPPED (global):` line and says how to add it, as any scripted install does.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## Why

`agent ok` and `check` tell the reader to fix a stale CLI or skill by running the CLI with no arguments. That only installs from a terminal, where it opens the wizard. Anywhere else it prints the topic index and exits 0 with nothing changed, so a reader following the step ends up where they started, with no error to say so.

## What Changes

- The cli step's command becomes `<launcher> <package>@<latest> init`.
- The skill step's command becomes `<invocation> init`.
- The step text says `init` rather than "with no arguments".

## Capabilities

### New Capabilities

None.

### Modified Capabilities

- `cli-agent`: the fixing commands for the cli and skill parts of `agent ok` name `init`.

## Impact

- `packages/cli/src/install/freshness.ts` and its tests. `check`'s notice takes the same steps, so it changes with it.
- Delivery shape: **single PR**. The spec, the fix, and the archive land together, because it is a two-line behavior change.
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
## MODIFIED Requirements

### Requirement: ok topic confirms the CLI can run

`taskless agent ok` SHALL serve the recipe at `packages/cli/src/agent/ok.md` through the same render path as every other topic, and exit 0. Serving it at all confirms the CLI can run. The rendered recipe SHALL carry the `# Topic:` header and the one-command-per-call paragraph. It SHALL NOT carry the fetch-time directive, because the preflight runs once per session rather than once per task, and SHALL NOT carry a survey invite.

The recipe's body SHALL be one of two passages, chosen from the environment state the CLI measured when the recipe was fetched:

- When nothing is out of date: `Your Taskless environment is up to date and you can proceed.`
- When one or more parts are out of date: `Your Taskless environment is out of date, the following steps are needed in order for commands to work reliably:`, followed by one numbered step per out-of-date part, in the order cli, skill, rules. Each step SHALL name what is out of date, say why it matters, and give the exact command that fixes it, spelled with the detected invocation. After the steps, the passage SHALL tell the agent to give the steps to the user to run themselves, rather than running them, and to run `<invocation> agent ok` again once the user says they are done.

The three parts SHALL be decided as follows:

- **cli**: a newer version of the package this build is (`@taskless/cli` for a release build, `@taskless/cli-nightly` for a nightly) is published as `latest` than the running version, by semver precedence. The latest version SHALL be asked of the npm registry only, at most once per run, with a timeout of at most one second. The answer SHALL be used only when it is a strict semantic version (`MAJOR.MINOR.PATCH` with optional prerelease and build identifiers, and nothing else), because it is written into a command the user runs. A lookup that fails, times out, or returns a missing or invalid version SHALL leave this part out, and SHALL NOT change the output's form or the exit code. The lookup SHALL be skipped, and the part left out, in a CI environment, when `NO_UPDATE_NOTIFIER` is set to a non-empty value (the opt-out convention many CLIs honor), and on a build that is not a published package. The fixing command SHALL run `init` with that package at exactly the newer version.
- **skill**: the Taskless skill installed in the project is not the version this CLI carries, by the same comparison `taskless info` reports. The fixing command SHALL be `<invocation> init`. When the cli part is also present, the skill step SHALL say that the cli step's command also refreshes the skill and SHALL give no separate command.
- **rules**: the project's rules are not reconciled to this CLI, by the same status `taskless info` and `taskless init` report. The fixing command SHALL be `<invocation> update`.

In a directory with no `.taskless/`, the skill and rules parts SHALL be left out. The command MAY read `.taskless/`, and SHALL NOT create or write it.

When the recipe is rendered by a caller that supplies no measured state, as the `@taskless/cli/prompts` export does, the body SHALL say that the environment was not checked, and SHALL claim neither outcome.

It SHALL emit the command's `cli_agent` intent event with `topic: "ok"`. The topic SHALL appear in the `taskless agent` index.

#### Scenario: ok prints one confirmation line

- **WHEN** `taskless agent ok` is run in a project where nothing is out of date
- **THEN** stdout SHALL begin with the `# Topic: ok` header line
- **AND** SHALL contain `Your Taskless environment is up to date and you can proceed.`
- **AND** the CLI SHALL exit with code 0

#### Scenario: ok carries no fetch-time directive

- **WHEN** `taskless agent ok` is run
- **THEN** its output SHALL NOT contain the fetch-time directive

#### Scenario: ok names the detected invocation

- **WHEN** `taskless agent ok` is run by a launcher the CLI detects, and reports an out-of-date part
- **THEN** each fixing command SHALL be spelled with that launcher

#### Scenario: ok does not guess an unknown launcher

- **WHEN** `taskless agent ok` is run, the launcher cannot be determined, and an out-of-date part is reported
- **THEN** each fixing command SHALL be spelled with the same agent-fill marker every recipe uses for an unknown invocation, and SHALL NOT name a guessed launcher

#### Scenario: ok runs outside a Taskless project

- **WHEN** `taskless agent ok` is run in a directory with no `.taskless/`
- **THEN** it SHALL serve the recipe and exit 0
- **AND** SHALL report neither the skill nor the rules part
- **AND** SHALL NOT create `.taskless/`

#### Scenario: ok reports a newer published CLI

- **WHEN** `taskless agent ok` is run and the published `latest` for this build's package is newer than the running version
- **THEN** the output SHALL contain the out-of-date passage
- **AND** a step SHALL name the running version and the newer one
- **AND** SHALL give a command that runs `init` with that package at exactly the newer version

#### Scenario: The out-of-date passage hands the steps to the user

- **WHEN** `taskless agent ok` reports any out-of-date part
- **THEN** the output SHALL tell the agent to have the user run the steps
- **AND** SHALL tell the agent to run `<invocation> agent ok` again once the user is done

#### Scenario: A failed registry lookup is silent

- **WHEN** `taskless agent ok` is run, nothing local is out of date, and the registry lookup fails or exceeds its timeout
- **THEN** the output SHALL contain the up-to-date passage
- **AND** the CLI SHALL exit with code 0

#### Scenario: An invalid version is ignored

- **WHEN** `taskless agent ok` is run and the registry returns a `latest` that is not a strict semantic version
- **THEN** the cli part SHALL be left out
- **AND** the output SHALL NOT contain that string

#### Scenario: The registry is not asked in CI

- **WHEN** `taskless agent ok` is run in a CI environment, or with `NO_UPDATE_NOTIFIER` set
- **THEN** the CLI SHALL NOT contact the registry

#### Scenario: ok reports an out-of-date skill

- **WHEN** `taskless agent ok` is run in a project whose installed Taskless skill is not the version this CLI carries, and no newer CLI is reported
- **THEN** the output SHALL contain the out-of-date passage
- **AND** a step SHALL give `<invocation> init`

#### Scenario: A newer CLI subsumes the skill refresh

- **WHEN** `taskless agent ok` reports both the cli and skill parts
- **THEN** the skill step SHALL point to the cli step's command and SHALL give no command of its own

#### Scenario: ok reports rules owing a reconcile

- **WHEN** `taskless agent ok` is run in a project whose rules are not reconciled to this CLI
- **THEN** a step SHALL give `<invocation> update`

#### Scenario: ok does not write the project

- **WHEN** `taskless agent ok` is run in a project with a `.taskless/` directory
- **THEN** no file under `.taskless/` SHALL be created or modified

#### Scenario: The prompts export claims no outcome

- **WHEN** the `ok` recipe is rendered with no measured state
- **THEN** it SHALL say the environment was not checked
- **AND** SHALL contain neither the up-to-date nor the out-of-date passage

#### Scenario: ok is in the index

- **WHEN** `taskless agent` is run with no arguments
- **THEN** the index SHALL include a row for `ok`

#### Scenario: ok captures its topic

- **WHEN** `taskless agent ok` is run
- **THEN** PostHog SHALL receive a `cli_agent` event whose `topic` property is `ok`
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## 1. Fixing commands

- [x] 1.1 Name `init` in the cli and skill steps of `environmentSteps`
- [x] 1.2 Update the freshness tests

## 2. Ship

- [x] 2.1 Add the changeset
- [x] 2.2 Confirm every prior scenario survives the archive
8 changes: 4 additions & 4 deletions openspec/specs/cli-agent/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,8 +436,8 @@ The recipe's body SHALL be one of two passages, chosen from the environment stat

The three parts SHALL be decided as follows:

- **cli**: a newer version of the package this build is (`@taskless/cli` for a release build, `@taskless/cli-nightly` for a nightly) is published as `latest` than the running version, by semver precedence. The latest version SHALL be asked of the npm registry only, at most once per run, with a timeout of at most one second. The answer SHALL be used only when it is a strict semantic version (`MAJOR.MINOR.PATCH` with optional prerelease and build identifiers, and nothing else), because it is written into a command the user runs. A lookup that fails, times out, or returns a missing or invalid version SHALL leave this part out, and SHALL NOT change the output's form or the exit code. The lookup SHALL be skipped, and the part left out, in a CI environment, when `NO_UPDATE_NOTIFIER` is set to a non-empty value (the opt-out convention many CLIs honor), and on a build that is not a published package. The fixing command SHALL run that package at exactly the newer version with no arguments.
- **skill**: the Taskless skill installed in the project is not the version this CLI carries, by the same comparison `taskless info` reports. The fixing command SHALL be the invocation run with no arguments. When the cli part is also present, the skill step SHALL say that the cli step's command also refreshes the skill and SHALL give no separate command.
- **cli**: a newer version of the package this build is (`@taskless/cli` for a release build, `@taskless/cli-nightly` for a nightly) is published as `latest` than the running version, by semver precedence. The latest version SHALL be asked of the npm registry only, at most once per run, with a timeout of at most one second. The answer SHALL be used only when it is a strict semantic version (`MAJOR.MINOR.PATCH` with optional prerelease and build identifiers, and nothing else), because it is written into a command the user runs. A lookup that fails, times out, or returns a missing or invalid version SHALL leave this part out, and SHALL NOT change the output's form or the exit code. The lookup SHALL be skipped, and the part left out, in a CI environment, when `NO_UPDATE_NOTIFIER` is set to a non-empty value (the opt-out convention many CLIs honor), and on a build that is not a published package. The fixing command SHALL run `init` with that package at exactly the newer version.
- **skill**: the Taskless skill installed in the project is not the version this CLI carries, by the same comparison `taskless info` reports. The fixing command SHALL be `<invocation> init`. When the cli part is also present, the skill step SHALL say that the cli step's command also refreshes the skill and SHALL give no separate command.
- **rules**: the project's rules are not reconciled to this CLI, by the same status `taskless info` and `taskless init` report. The fixing command SHALL be `<invocation> update`.

In a directory with no `.taskless/`, the skill and rules parts SHALL be left out. The command MAY read `.taskless/`, and SHALL NOT create or write it.
Expand Down Expand Up @@ -480,7 +480,7 @@ It SHALL emit the command's `cli_agent` intent event with `topic: "ok"`. The top
- **WHEN** `taskless agent ok` is run and the published `latest` for this build's package is newer than the running version
- **THEN** the output SHALL contain the out-of-date passage
- **AND** a step SHALL name the running version and the newer one
- **AND** SHALL give a command that runs that package at exactly the newer version with no arguments
- **AND** SHALL give a command that runs `init` with that package at exactly the newer version

#### Scenario: The out-of-date passage hands the steps to the user

Expand Down Expand Up @@ -509,7 +509,7 @@ It SHALL emit the command's `cli_agent` intent event with `topic: "ok"`. The top

- **WHEN** `taskless agent ok` is run in a project whose installed Taskless skill is not the version this CLI carries, and no newer CLI is reported
- **THEN** the output SHALL contain the out-of-date passage
- **AND** a step SHALL give the invocation run with no arguments
- **AND** a step SHALL give `<invocation> init`

#### Scenario: A newer CLI subsumes the skill refresh

Expand Down
12 changes: 8 additions & 4 deletions packages/cli/src/install/freshness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ export async function measureEnvironment(
* `invocation` spells the current CLI. `launcher` is the command word that
* runs a package (`npx`, `pnpm dlx`), used for the newer CLI, which the
* pinned invocation cannot reach.
*
* Both install steps name `init` rather than the bare invocation. A bare run
* only installs from a terminal, through the wizard. Anywhere else it prints
* the topic index and exits 0 having changed nothing.
*/
export function environmentSteps(
measurement: EnvironmentMeasurement,
Expand All @@ -147,8 +151,8 @@ export function environmentSteps(
part: "cli",
text:
`A newer Taskless CLI is published: ${cli.packageName} ${cli.latest}, and this project runs ${cli.running}. ` +
"Running the newer version with no arguments moves the project to it and refreshes the installed skills.",
command: `${spelling.launcher} ${cli.packageName}@${cli.latest}`,
"Running `init` with the newer version moves the project to it and refreshes the installed skills.",
command: `${spelling.launcher} ${cli.packageName}@${cli.latest} init`,
});
}
if (skill !== undefined) {
Expand All @@ -164,8 +168,8 @@ export function environmentSteps(
part: "skill",
text:
`The installed Taskless skill is not this CLI's (${listed}), so its instructions may not match the commands. ` +
"Running the CLI with no arguments reinstalls it.",
command: spelling.invocation,
"Running `init` reinstalls it.",
command: `${spelling.invocation} init`,
}
: {
part: "skill",
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/test/agent-ok.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe("agent ok", () => {
expect(output()).toContain(
"Your Taskless environment is out of date, the following steps are needed in order for commands to work reliably:"
);
expect(output()).toContain("npx @taskless/cli@999.0.0");
expect(output()).toContain("npx @taskless/cli@999.0.0 init");
expect(output()).toContain("Do not run these steps yourself.");
expect(output()).toMatch(/run `npx @taskless\/cli@\S+ agent ok` again/);
});
Expand All @@ -139,7 +139,9 @@ describe("agent ok", () => {
vi.stubEnv("npm_config_user_agent", "");
stubRegistry("999.0.0");
await runAgent(directory, ["agent", "ok"]);
expect(output()).toContain("<package-manager-dlx> @taskless/cli@999.0.0");
expect(output()).toContain(
"<package-manager-dlx> @taskless/cli@999.0.0 init"
);
});

it("is silent when the registry lookup fails", async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/check-environment-notice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe("check out-of-date notice", () => {
notice.includes("out of date")
);
expect(added).toHaveLength(1);
expect(added[0]).toContain("@taskless/cli@9.0.0");
expect(added[0]).toContain("@taskless/cli@9.0.0 init");
});

it("prints the notice as a marked line in text output", async () => {
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/test/freshness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ describe("measureEnvironment", () => {
});
const [step] = environmentSteps(measured, SPELLING);
expect(step?.part).toBe("cli");
expect(step?.command).toBe("npx @taskless/cli@1.1.0");
expect(step?.command).toBe("npx @taskless/cli@1.1.0 init");
expect(step?.text).toContain("1.0.0");
});

Expand All @@ -131,15 +131,15 @@ describe("measureEnvironment", () => {
expect(measured.cli).toBeUndefined();
});

it("reports a stale skill with the bare invocation", async () => {
it("reports a stale skill with init", async () => {
project();
checkStaleness.mockResolvedValue(STALE_SKILLS);
const measured = await measure(registry("1.0.0"));
const steps = environmentSteps(measured, SPELLING);
expect(steps).toEqual([
expect.objectContaining({
part: "skill",
command: "npx @taskless/cli@1.0.0",
command: "npx @taskless/cli@1.0.0 init",
}),
]);
});
Expand All @@ -165,7 +165,7 @@ describe("measureEnvironment", () => {
expect(steps[1]?.command).toBeUndefined();
const notice = environmentNotice(steps);
expect(notice).toContain("(1) ");
expect(notice).toContain("Run: npx @taskless/cli@2.0.0");
expect(notice).toContain("Run: npx @taskless/cli@2.0.0 init");
expect(notice).not.toContain("\n");
});

Expand Down
Loading