diff --git a/.changeset/environment-steps-run-init.md b/.changeset/environment-steps-run-init.md index 10316cc8..69559ce8 100644 --- a/.changeset/environment-steps-run-init.md +++ b/.changeset/environment-steps-run-init.md @@ -3,3 +3,7 @@ --- 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. + +Your agent now finishes the job after you upgrade. Moving the CLI stays yours to do, and once you have, the agent applies the rules instructions itself instead of handing them back. On a project launcher run, the upgrade step now names `npx` rather than a placeholder. + +`agent ok` also offers the upgrade the way your harness runs it: with the `!` prefix in the Claude Code CLI, OpenCode, Codex, Gemini CLI, and Devin, and with `/shell` in Cursor. Another terminal is always offered, and it's the only offer when the harness isn't recognized. diff --git a/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/.openspec.yaml b/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/.openspec.yaml new file mode 100644 index 00000000..4ccb2b41 --- /dev/null +++ b/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-11-04 diff --git a/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/design.md b/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/design.md new file mode 100644 index 00000000..d33f2f9d --- /dev/null +++ b/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/design.md @@ -0,0 +1,18 @@ +## Context + +`environmentStatusBlock` renders one instruction for every step: give them to the user. A stub-based `upgrade` command that an agent could run was drafted first and dropped. It would have let an allowed path run newly published code with nobody in the loop, and `npx @ init` already moves the pin, repairs a broken one, and rewrites the launcher. + +## Decisions + +**The split follows what each step runs.** The cli and skill steps change what the project runs or what its harnesses read, so they stay the user's. `update` runs the recorded version and fetches nothing new. The rules recipe can rewrite rules, which is the kind of change an agent already makes and a person reviews in the diff. + +**The rules step is the agent's on any invocation, not only a launcher run.** The agent just ran `agent ok` through that same invocation, so the harness that allowed it will normally allow `update`. A refusal falls back to the user. + +**A rules step beside an upgrade waits.** The confirming `agent ok` reports what is owed after the move, which may be different. The agent never runs the step the first report listed. + +**`npx` on a launcher run.** The launcher cannot reveal how the CLI was first installed, and `npx` is the documented upgrade spelling. Printing the agent-fill marker asked the agent to fill in a command it is not going to run. + +## Risks / Trade-offs + +- [A user declines the upgrade while the rules are owed on this CLI] → Declining is fine, and the passage says what follows. The upgrade stays listed, and the agent applies the rules step to the current CLI, because `check` and `verify` refuse until it is recorded. +- [The rules recipe can rewrite rules with no one watching] → That is what the agent already does when a person hands it the step, and the result is a reviewable diff. diff --git a/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/proposal.md b/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/proposal.md new file mode 100644 index 00000000..8398d45d --- /dev/null +++ b/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/proposal.md @@ -0,0 +1,27 @@ +## Why + +`agent ok` tells the agent not to run any of its steps. That is right for moving the CLI, which is the user's decision and runs newly published code. It is more than the rules step needs. Once the user has upgraded, the rules step runs the version the project already records, and walking that recipe is ordinary agent work. Today the agent hands that step back too, so an upgrade stops halfway unless the user also does the reconcile. + +## What Changes + +- **The user upgrades.** The cli and skill steps stay the user's. They name `init`, and say that in a terminal the same command without `init` opens the installer and its options. +- **Then the agent applies the rules step.** On its own, the rules step is the agent's: it runs the command and follows the recipe to the last step, and hands it to the user only if the command is refused. Beside a cli or skill step, it waits for the confirming `agent ok`, because a newer CLI can owe a different reconcile. +- **A launcher run names `npx` for the upgrade.** The cli step printed an agent-fill marker there, because the launcher is not a package runner. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `cli-agent`: `agent ok` says who runs each step, and names `npx` for the upgrade on a launcher run. +- `skill-taskless`: the skill leaves upgrades to the user and applies the rules step. + +## Impact + +- `packages/cli/src/install/freshness.ts`, `src/prompts/recipes.ts`, `src/commands/agent.ts`, `src/util/package-manager.ts`. +- `skills/taskless/SKILL.md`. +- Replaces the "the agent never runs the steps" decision in `agent-ok-update-notice` for the rules step only. +- Delivery shape: **single PR**. It sits on #608, which changes the same spec requirement. diff --git a/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/specs/cli-agent/spec.md b/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/specs/cli-agent/spec.md new file mode 100644 index 00000000..ca615828 --- /dev/null +++ b/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/specs/cli-agent/spec.md @@ -0,0 +1,140 @@ +## 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 say who runs them. The cli and skill steps belong to the user: when either is present, the passage SHALL tell the agent to give them to the user to run themselves, rather than running them, and to run ` agent ok` again once the user says they are done. The rules step belongs to the agent: when it is the only step, the passage SHALL tell the agent to run its command and follow the recipe it prints to its last step, and to give the command to the user if it is refused. When it appears beside a cli or skill step, the passage SHALL tell the agent to leave it until the confirming `agent ok`, and to apply it the same way if that run still reports it. When the cli part is present, the passage SHALL say the user can decline the upgrade and what follows: the project stays on this CLI and the report keeps listing the upgrade, and a reported rules step is still owed on this CLI, so the agent applies it. + +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, and the step SHALL say that in a terminal the same command without `init` opens the installer. A project launcher hides how the CLI was first installed, so when one started the CLI this command SHALL be spelled with `npx`. +- **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 ` init`, and the step SHALL say that in a terminal the same command without `init` opens the installer. 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 ` 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 the cli or skill part +- **THEN** the output SHALL tell the agent to have the user run those steps +- **AND** SHALL tell the agent to run ` agent ok` again once the user is done + +#### Scenario: The agent applies a rules step on its own + +- **WHEN** `taskless agent ok` reports only the rules part +- **THEN** the output SHALL tell the agent to run the step's command and follow its recipe to the last step +- **AND** SHALL NOT tell the agent to have the user run it, unless the command is refused + +#### Scenario: A rules step waits for the upgrade + +- **WHEN** `taskless agent ok` reports the rules part beside the cli part +- **THEN** the output SHALL tell the agent to leave the rules step until the confirming `agent ok` + +#### Scenario: Declining an upgrade is explained + +- **WHEN** `taskless agent ok` reports the cli part +- **THEN** the output SHALL say the user can decline, and that the project stays on this CLI and the report keeps listing the upgrade +- **AND** when the rules part is also reported, SHALL say the rules are still owed on this CLI and tell the agent to apply the rules step once the user declines + +#### Scenario: A launcher run spells the upgrade with npx + +- **WHEN** `./.taskless/bin/cli.mjs agent ok` is run and a newer CLI is published +- **THEN** the cli step's command SHALL be `npx @ init` + +#### 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 ` 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 ` 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` diff --git a/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/specs/skill-taskless/spec.md b/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/specs/skill-taskless/spec.md new file mode 100644 index 00000000..7db79fcc --- /dev/null +++ b/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/specs/skill-taskless/spec.md @@ -0,0 +1,41 @@ +## MODIFIED Requirements + +### Requirement: Skill acts on the preflight's environment report + +The skill body SHALL tell the agent that `%(TASKLESS_CLI)s agent ok` returns a recipe whose body reports whether the Taskless environment is up to date. When it reports the environment is up to date, the body SHALL instruct the agent to proceed with the user's request. + +When it reports the environment is out of date, the body SHALL instruct the agent to give the user the steps that move the CLI or reinstall the skill, with their commands, once, for the user to run themselves, and SHALL NOT instruct the agent to run those steps. The body SHALL name the ways to run them: in Claude Code, typing the command with the `!` prefix, which runs it in the session's shell; or in another terminal. The reason is that moving the CLI is the user's decision. The body SHALL instruct the agent to carry on with the user's request, and, when the user says they ran the steps, to run `%(TASKLESS_CLI)s agent ok` again to confirm. + +When a step says the rules owe a reconcile and no CLI or skill step is left, the body SHALL instruct the agent to run its command and follow the recipe it prints to its last step, and to give the command to the user if it is refused. The body SHALL say that when the user declines an upgrade, the agent tells them what that means, and applies a reported rules step to this CLI. + +That confirming run is the only time the body SHALL instruct the agent to run the preflight again in a session once it has passed. + +The `!` prefix offered here is distinct from the blocked-preflight guidance, which does not offer it. There, `!` would clear only the one preflight call and leave every later Taskless command blocked. Here, the steps are one-off commands, and running them once is the whole job. + +#### Scenario: Skill body explains both outcomes + +- **WHEN** the skill body is read from source +- **THEN** it SHALL describe the up-to-date and the out-of-date outcomes of `agent ok` + +#### Scenario: Skill body hands the steps to the user + +- **WHEN** the skill body is read from source +- **THEN** it SHALL instruct the agent, on an out-of-date report, to give the user the steps that move the CLI or reinstall the skill once, to run themselves +- **AND** SHALL name the `!` prefix in Claude Code and another terminal as the ways to run them +- **AND** SHALL NOT instruct the agent to run those steps itself +- **AND** SHALL instruct the agent to carry on with the user's request + +#### Scenario: Skill body applies the rules step + +- **WHEN** the skill body is read from source +- **THEN** it SHALL instruct the agent to run a rules step's command and follow its recipe once no CLI or skill step is left + +#### Scenario: Skill body re-checks after the user updates + +- **WHEN** the skill body is read from source +- **THEN** it SHALL instruct the agent, once the user says they ran the steps, to run `agent ok` again + +#### Scenario: Skill body runs the preflight once otherwise + +- **WHEN** the skill body is read from source +- **THEN** it SHALL NOT instruct the agent to run `agent ok` again in a session after it has passed, except to confirm the user's update diff --git a/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/tasks.md b/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/tasks.md new file mode 100644 index 00000000..f13b0f6c --- /dev/null +++ b/openspec/changes/archive/2026-11-04-agent-applies-rules-after-upgrade/tasks.md @@ -0,0 +1,12 @@ +## 1. Steps + +- [x] 1.1 Say in the cli and skill steps that the command without `init` opens the installer in a terminal +- [x] 1.2 Render who runs each step in the `ok` passage +- [x] 1.3 Spell the cli step with `npx` on a launcher run +- [x] 1.4 Update the skill's preflight section + +## 2. Ship + +- [x] 2.1 Update the tests +- [x] 2.2 Grow the changeset +- [x] 2.3 Confirm every prior scenario survives the archive diff --git a/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/.openspec.yaml b/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/.openspec.yaml new file mode 100644 index 00000000..4ccb2b41 --- /dev/null +++ b/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-11-04 diff --git a/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/design.md b/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/design.md new file mode 100644 index 00000000..0b61ee36 --- /dev/null +++ b/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/design.md @@ -0,0 +1,19 @@ +## Context + +No code in the CLI asks which harness started it. A wrong answer here costs little: the output is a suggestion, and another terminal is always offered. + +## Decisions + +**Vendor variables from a fixed table, for wording only.** Variables are not a shared standard: `AI_AGENT` is an open proposal, and its values differ by product. So each row names a variable that product sets and a prefix it documents, and nothing from the environment reaches the output. Any process can set these, so they never decide what is allowed. + +**Only harnesses Taskless installs for, and only documented syntax.** Crush installs but documents no such prefix, so it gets a terminal. Copilot CLI, Aider, and Amp have prefixes and are not in the installer. + +**`!` only on Claude Code's CLI entrypoint.** The VS Code extension sets `CLAUDECODE` and has no `!` shortcut, so `CLAUDE_CODE_ENTRYPOINT` narrows it. + +**Cursor states its limit.** `/shell` stops at 30 seconds and takes no prompts, and a first `npx` of a version can take longer, so the passage names another terminal for that case. + +## Risks / Trade-offs + +- [`CURSOR_AGENT` was reported unset by Cursor's CLI in 2025, and `CODEX_THREAD_ID` is undocumented] → A miss falls back to another terminal, which works. +- [Harness variables are inherited, so a nested agent can read as its parent] → The prefix may name the outer harness. Another terminal is still offered. +- [Prefixes change] → Each row cites its source, and a stale row costs one wrong suggestion. diff --git a/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/proposal.md b/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/proposal.md new file mode 100644 index 00000000..47a1a64d --- /dev/null +++ b/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/proposal.md @@ -0,0 +1,27 @@ +## Why + +The `agent ok` passage tells every agent to offer Claude Code's `!` prefix, whichever harness it is running in. Cursor users need `/shell`, and a harness with no prefix gets advice that does nothing. The CLI can usually tell where it is running, so it can give the instruction that works there. + +## What Changes + +- `agent ok` recognizes the harness from environment variables and shows the upgrade command with that harness's prefix. +- `!` for the Claude Code CLI, OpenCode, Codex, Gemini CLI, and Devin. `/shell ` for Cursor, with its 30-second limit stated. +- Another terminal is always offered, and is the only offer when the harness is not recognized. +- The skill points at the report rather than naming Claude Code's prefix. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `cli-agent`: the `ok` passage names the harness's prefix. +- `skill-taskless`: the skill defers to the report for how a person runs a step. + +## Impact + +- New `packages/cli/src/detect/harness-shell.ts`. `src/commands/agent.ts`, `src/prompts/recipes.ts`, `skills/taskless/SKILL.md`. +- `check`'s notice is unchanged: it prints commands for whoever reads the output, with no agent passage to carry the wording. +- Delivery shape: **single PR**, stacked on #608 with the rule-step change it extends. diff --git a/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/specs/cli-agent/spec.md b/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/specs/cli-agent/spec.md new file mode 100644 index 00000000..654b91aa --- /dev/null +++ b/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/specs/cli-agent/spec.md @@ -0,0 +1,158 @@ +## 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 say who runs them. The cli and skill steps belong to the user: when either is present, the passage SHALL tell the agent to give them to the user to run themselves, rather than running them, and to run ` agent ok` again once the user says they are done. The passage SHALL always offer another terminal. When the CLI recognizes the harness that started it, the passage SHALL also name that harness and show the first command with the prefix that harness documents for a person's one-off command, and SHALL state any documented limit of that route: `!` in the Claude Code CLI, OpenCode, Codex, Gemini CLI, and Devin, and `/shell ` in Cursor with its 30-second limit. A harness the CLI does not recognize, or a surface with no such prefix, SHALL get only another terminal. Recognition SHALL read environment variables against a fixed table, SHALL shape this wording only, and SHALL NOT place any environment value in the output. The rules step belongs to the agent: when it is the only step, the passage SHALL tell the agent to run its command and follow the recipe it prints to its last step, and to give the command to the user if it is refused. When it appears beside a cli or skill step, the passage SHALL tell the agent to leave it until the confirming `agent ok`, and to apply it the same way if that run still reports it. When the cli part is present, the passage SHALL say the user can decline the upgrade and what follows: the project stays on this CLI and the report keeps listing the upgrade, and a reported rules step is still owed on this CLI, so the agent applies it. + +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, and the step SHALL say that in a terminal the same command without `init` opens the installer. A project launcher hides how the CLI was first installed, so when one started the CLI this command SHALL be spelled with `npx`. +- **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 ` init`, and the step SHALL say that in a terminal the same command without `init` opens the installer. 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 ` 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 the cli or skill part +- **THEN** the output SHALL tell the agent to have the user run those steps +- **AND** SHALL tell the agent to run ` agent ok` again once the user is done + +#### Scenario: The passage offers the harness's prefix + +- **WHEN** `taskless agent ok` reports the cli part, run by the Claude Code CLI +- **THEN** the output SHALL show the command with the `!` prefix +- **AND** SHALL also offer another terminal + +#### Scenario: Cursor's limit is stated + +- **WHEN** `taskless agent ok` reports the cli part, with `CURSOR_AGENT` set +- **THEN** the output SHALL show the command with the `/shell ` prefix +- **AND** SHALL say it stops after 30 seconds, and offer another terminal for longer runs + +#### Scenario: An unrecognized harness gets a terminal + +- **WHEN** `taskless agent ok` reports the cli part and no known harness variable is set +- **THEN** the output SHALL offer another terminal +- **AND** SHALL NOT name a prefix + +#### Scenario: The agent applies a rules step on its own + +- **WHEN** `taskless agent ok` reports only the rules part +- **THEN** the output SHALL tell the agent to run the step's command and follow its recipe to the last step +- **AND** SHALL NOT tell the agent to have the user run it, unless the command is refused + +#### Scenario: A rules step waits for the upgrade + +- **WHEN** `taskless agent ok` reports the rules part beside the cli part +- **THEN** the output SHALL tell the agent to leave the rules step until the confirming `agent ok` + +#### Scenario: Declining an upgrade is explained + +- **WHEN** `taskless agent ok` reports the cli part +- **THEN** the output SHALL say the user can decline, and that the project stays on this CLI and the report keeps listing the upgrade +- **AND** when the rules part is also reported, SHALL say the rules are still owed on this CLI and tell the agent to apply the rules step once the user declines + +#### Scenario: A launcher run spells the upgrade with npx + +- **WHEN** `./.taskless/bin/cli.mjs agent ok` is run and a newer CLI is published +- **THEN** the cli step's command SHALL be `npx @ init` + +#### 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 ` 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 ` 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` diff --git a/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/specs/skill-taskless/spec.md b/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/specs/skill-taskless/spec.md new file mode 100644 index 00000000..ed8db268 --- /dev/null +++ b/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/specs/skill-taskless/spec.md @@ -0,0 +1,41 @@ +## MODIFIED Requirements + +### Requirement: Skill acts on the preflight's environment report + +The skill body SHALL tell the agent that `%(TASKLESS_CLI)s agent ok` returns a recipe whose body reports whether the Taskless environment is up to date. When it reports the environment is up to date, the body SHALL instruct the agent to proceed with the user's request. + +When it reports the environment is out of date, the body SHALL instruct the agent to give the user the steps that move the CLI or reinstall the skill, with their commands, once, for the user to run themselves, and SHALL NOT instruct the agent to run those steps. The body SHALL name the ways to run them: as the report says, with the harness's prefix when it names one, or in another terminal. The reason is that moving the CLI is the user's decision. The body SHALL instruct the agent to carry on with the user's request, and, when the user says they ran the steps, to run `%(TASKLESS_CLI)s agent ok` again to confirm. + +When a step says the rules owe a reconcile and no CLI or skill step is left, the body SHALL instruct the agent to run its command and follow the recipe it prints to its last step, and to give the command to the user if it is refused. The body SHALL say that when the user declines an upgrade, the agent tells them what that means, and applies a reported rules step to this CLI. + +That confirming run is the only time the body SHALL instruct the agent to run the preflight again in a session once it has passed. + +A prefix offered here is distinct from the blocked-preflight guidance, which does not offer it. There, a prefix would clear only the one preflight call and leave every later Taskless command blocked. Here, the steps are one-off commands, and running them once is the whole job. + +#### Scenario: Skill body explains both outcomes + +- **WHEN** the skill body is read from source +- **THEN** it SHALL describe the up-to-date and the out-of-date outcomes of `agent ok` + +#### Scenario: Skill body hands the steps to the user + +- **WHEN** the skill body is read from source +- **THEN** it SHALL instruct the agent, on an out-of-date report, to give the user the steps that move the CLI or reinstall the skill once, to run themselves +- **AND** SHALL name the harness's prefix from the report and another terminal as the ways to run them +- **AND** SHALL NOT instruct the agent to run those steps itself +- **AND** SHALL instruct the agent to carry on with the user's request + +#### Scenario: Skill body applies the rules step + +- **WHEN** the skill body is read from source +- **THEN** it SHALL instruct the agent to run a rules step's command and follow its recipe once no CLI or skill step is left + +#### Scenario: Skill body re-checks after the user updates + +- **WHEN** the skill body is read from source +- **THEN** it SHALL instruct the agent, once the user says they ran the steps, to run `agent ok` again + +#### Scenario: Skill body runs the preflight once otherwise + +- **WHEN** the skill body is read from source +- **THEN** it SHALL NOT instruct the agent to run `agent ok` again in a session after it has passed, except to confirm the user's update diff --git a/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/tasks.md b/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/tasks.md new file mode 100644 index 00000000..e9d4f491 --- /dev/null +++ b/openspec/changes/archive/2026-11-04-ok-names-the-harness-prefix/tasks.md @@ -0,0 +1,10 @@ +## 1. Harness prefix + +- [x] 1.1 Add the harness table and detection, with tests +- [x] 1.2 Pass it through `agent ok`, and word the passage from it +- [x] 1.3 Point the skill at the report + +## 2. Ship + +- [x] 2.1 Grow the changeset +- [x] 2.2 Confirm every prior scenario survives the archive diff --git a/openspec/specs/cli-agent/spec.md b/openspec/specs/cli-agent/spec.md index 94707a41..8ca9f4d1 100644 --- a/openspec/specs/cli-agent/spec.md +++ b/openspec/specs/cli-agent/spec.md @@ -432,12 +432,12 @@ An agent topic `report` SHALL be registered. The CLI SHALL embed `packages/cli/s 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 ` agent ok` again once the user says they are done. +- 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 say who runs them. The cli and skill steps belong to the user: when either is present, the passage SHALL tell the agent to give them to the user to run themselves, rather than running them, and to run ` agent ok` again once the user says they are done. The passage SHALL always offer another terminal. When the CLI recognizes the harness that started it, the passage SHALL also name that harness and show the first command with the prefix that harness documents for a person's one-off command, and SHALL state any documented limit of that route: `!` in the Claude Code CLI, OpenCode, Codex, Gemini CLI, and Devin, and `/shell ` in Cursor with its 30-second limit. A harness the CLI does not recognize, or a surface with no such prefix, SHALL get only another terminal. Recognition SHALL read environment variables against a fixed table, SHALL shape this wording only, and SHALL NOT place any environment value in the output. The rules step belongs to the agent: when it is the only step, the passage SHALL tell the agent to run its command and follow the recipe it prints to its last step, and to give the command to the user if it is refused. When it appears beside a cli or skill step, the passage SHALL tell the agent to leave it until the confirming `agent ok`, and to apply it the same way if that run still reports it. When the cli part is present, the passage SHALL say the user can decline the upgrade and what follows: the project stays on this CLI and the report keeps listing the upgrade, and a reported rules step is still owed on this CLI, so the agent applies it. 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 ` 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. +- **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, and the step SHALL say that in a terminal the same command without `init` opens the installer. A project launcher hides how the CLI was first installed, so when one started the CLI this command SHALL be spelled with `npx`. +- **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 ` init`, and the step SHALL say that in a terminal the same command without `init` opens the installer. 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 ` 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. @@ -484,10 +484,50 @@ It SHALL emit the command's `cli_agent` intent event with `topic: "ok"`. The top #### 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 +- **WHEN** `taskless agent ok` reports the cli or skill part +- **THEN** the output SHALL tell the agent to have the user run those steps - **AND** SHALL tell the agent to run ` agent ok` again once the user is done +#### Scenario: The passage offers the harness's prefix + +- **WHEN** `taskless agent ok` reports the cli part, run by the Claude Code CLI +- **THEN** the output SHALL show the command with the `!` prefix +- **AND** SHALL also offer another terminal + +#### Scenario: Cursor's limit is stated + +- **WHEN** `taskless agent ok` reports the cli part, with `CURSOR_AGENT` set +- **THEN** the output SHALL show the command with the `/shell ` prefix +- **AND** SHALL say it stops after 30 seconds, and offer another terminal for longer runs + +#### Scenario: An unrecognized harness gets a terminal + +- **WHEN** `taskless agent ok` reports the cli part and no known harness variable is set +- **THEN** the output SHALL offer another terminal +- **AND** SHALL NOT name a prefix + +#### Scenario: The agent applies a rules step on its own + +- **WHEN** `taskless agent ok` reports only the rules part +- **THEN** the output SHALL tell the agent to run the step's command and follow its recipe to the last step +- **AND** SHALL NOT tell the agent to have the user run it, unless the command is refused + +#### Scenario: A rules step waits for the upgrade + +- **WHEN** `taskless agent ok` reports the rules part beside the cli part +- **THEN** the output SHALL tell the agent to leave the rules step until the confirming `agent ok` + +#### Scenario: Declining an upgrade is explained + +- **WHEN** `taskless agent ok` reports the cli part +- **THEN** the output SHALL say the user can decline, and that the project stays on this CLI and the report keeps listing the upgrade +- **AND** when the rules part is also reported, SHALL say the rules are still owed on this CLI and tell the agent to apply the rules step once the user declines + +#### Scenario: A launcher run spells the upgrade with npx + +- **WHEN** `./.taskless/bin/cli.mjs agent ok` is run and a newer CLI is published +- **THEN** the cli step's command SHALL be `npx @ init` + #### 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 diff --git a/openspec/specs/skill-taskless/spec.md b/openspec/specs/skill-taskless/spec.md index e41dee66..05a17dc5 100644 --- a/openspec/specs/skill-taskless/spec.md +++ b/openspec/specs/skill-taskless/spec.md @@ -260,11 +260,13 @@ The skill SHALL say that a "No such file or directory" error from the launcher m The skill body SHALL tell the agent that `%(TASKLESS_CLI)s agent ok` returns a recipe whose body reports whether the Taskless environment is up to date. When it reports the environment is up to date, the body SHALL instruct the agent to proceed with the user's request. -When it reports the environment is out of date, the body SHALL instruct the agent to give the user the listed steps and their commands once, for the user to run themselves, and SHALL NOT instruct the agent to run them. The body SHALL name the ways to run them: in Claude Code, typing the command with the `!` prefix, which runs it in the session's shell; or in another terminal. The reason is that a harness whose permissions block Taskless commands usually blocks these steps too. The body SHALL instruct the agent to carry on with the user's request, and, when the user says they ran the steps, to run `%(TASKLESS_CLI)s agent ok` again to confirm. +When it reports the environment is out of date, the body SHALL instruct the agent to give the user the steps that move the CLI or reinstall the skill, with their commands, once, for the user to run themselves, and SHALL NOT instruct the agent to run those steps. The body SHALL name the ways to run them: as the report says, with the harness's prefix when it names one, or in another terminal. The reason is that moving the CLI is the user's decision. The body SHALL instruct the agent to carry on with the user's request, and, when the user says they ran the steps, to run `%(TASKLESS_CLI)s agent ok` again to confirm. + +When a step says the rules owe a reconcile and no CLI or skill step is left, the body SHALL instruct the agent to run its command and follow the recipe it prints to its last step, and to give the command to the user if it is refused. The body SHALL say that when the user declines an upgrade, the agent tells them what that means, and applies a reported rules step to this CLI. That confirming run is the only time the body SHALL instruct the agent to run the preflight again in a session once it has passed. -The `!` prefix offered here is distinct from the blocked-preflight guidance, which does not offer it. There, `!` would clear only the one preflight call and leave every later Taskless command blocked. Here, the steps are one-off commands, and running them once is the whole job. +A prefix offered here is distinct from the blocked-preflight guidance, which does not offer it. There, a prefix would clear only the one preflight call and leave every later Taskless command blocked. Here, the steps are one-off commands, and running them once is the whole job. #### Scenario: Skill body explains both outcomes @@ -274,11 +276,16 @@ The `!` prefix offered here is distinct from the blocked-preflight guidance, whi #### Scenario: Skill body hands the steps to the user - **WHEN** the skill body is read from source -- **THEN** it SHALL instruct the agent, on an out-of-date report, to give the user the listed steps once, to run themselves -- **AND** SHALL name the `!` prefix in Claude Code and another terminal as the ways to run them -- **AND** SHALL NOT instruct the agent to run the steps itself +- **THEN** it SHALL instruct the agent, on an out-of-date report, to give the user the steps that move the CLI or reinstall the skill once, to run themselves +- **AND** SHALL name the harness's prefix from the report and another terminal as the ways to run them +- **AND** SHALL NOT instruct the agent to run those steps itself - **AND** SHALL instruct the agent to carry on with the user's request +#### Scenario: Skill body applies the rules step + +- **WHEN** the skill body is read from source +- **THEN** it SHALL instruct the agent to run a rules step's command and follow its recipe once no CLI or skill step is left + #### Scenario: Skill body re-checks after the user updates - **WHEN** the skill body is read from source diff --git a/packages/cli/src/commands/agent.ts b/packages/cli/src/commands/agent.ts index e59a5ada..4b144f83 100644 --- a/packages/cli/src/commands/agent.ts +++ b/packages/cli/src/commands/agent.ts @@ -11,6 +11,7 @@ import { import { getTelemetry } from "../telemetry"; import { detectHostTools } from "../detect/host-tools"; import { detectRepository } from "../detect/scan"; +import { detectHarnessShell } from "../detect/harness-shell"; import { environmentSteps, measureEnvironment } from "../install/freshness"; import { ENVIRONMENT_VARIABLES, @@ -29,6 +30,7 @@ import { detectLauncherCommand, getCliPrefix, processLauncherContext, + projectLauncher, } from "../util/package-manager"; // Recipe-only topics (no backing subcommand) that should still be @@ -319,9 +321,14 @@ export function createAgentCommand(subCommands: SubCommandsDef) { invocation: resolveInvocation({ invocation }), launcher: detectLauncherCommand(launcherContext) ?? - PACKAGE_MANAGER_DLX_MARKER, + // A project launcher hides how the CLI was first installed, + // and npx is the spelling for moving to a new version. + (projectLauncher(launcherContext) === undefined + ? PACKAGE_MANAGER_DLX_MARKER + : "npx"), } ), + shell: detectHarnessShell(process.env), } : undefined; const recipe = getRecipe(key, { diff --git a/packages/cli/src/detect/harness-shell.ts b/packages/cli/src/detect/harness-shell.ts new file mode 100644 index 00000000..df66f3e2 --- /dev/null +++ b/packages/cli/src/detect/harness-shell.ts @@ -0,0 +1,71 @@ +import type { HarnessShell } from "../prompts/recipes"; + +/** + * How a person runs a one-off command from inside the harness that started + * this CLI, for wording an instruction. Only ever a suggestion: a harness that + * is not recognized, or is recognized wrongly, still gets "another terminal". + * + * Read from a fixed table so that nothing from the environment reaches the + * text an agent reads. Never use this to authorize anything: any process can + * set these variables. + */ +const BANG = "!"; + +const HARNESS_SHELLS: ReadonlyArray< + HarnessShell & { + matches: (env: Record) => boolean; + } +> = [ + { + // code.claude.com/docs/en/env-vars sets `CLAUDECODE`, and + // code.claude.com/docs/en/vs-code lists no `!` shortcut in the extension, + // so only the CLI entrypoint is offered it. + name: "Claude Code", + prefix: BANG, + matches: (env) => + env.CLAUDECODE !== undefined && + (env.CLAUDE_CODE_ENTRYPOINT ?? "cli") === "cli", + }, + { + // cursor.com/docs/cli/shell-mode: 30 seconds, no interactive prompts. + // cursor.com/docs/agent/terminal: `CURSOR_AGENT` marks an agent's shell. + name: "Cursor", + prefix: "/shell ", + caveat: "`/shell` stops a command after 30 seconds", + matches: (env) => env.CURSOR_AGENT !== undefined, + }, + { + // opencode.ai/docs/tui/, and `OPENCODE=1` from anomalyco/opencode#3843. + name: "OpenCode", + prefix: BANG, + matches: (env) => env.OPENCODE !== undefined, + }, + { + // developers.openai.com/codex/cli/reference; the variable is set by + // codex-rs/core/src/exec_env.rs and is not documented. + name: "Codex", + prefix: BANG, + matches: (env) => env.CODEX_THREAD_ID !== undefined, + }, + { + // geminicli.com/docs/reference/commands/ + name: "Gemini CLI", + prefix: BANG, + matches: (env) => env.GEMINI_CLI !== undefined, + }, + { + // docs.devin.ai/cli/changelog/stable: `AI_AGENT=devin__agent`. + name: "Devin", + prefix: BANG, + matches: (env) => env.AI_AGENT?.startsWith("devin_") === true, + }, +]; + +export function detectHarnessShell( + env: Record +): HarnessShell | undefined { + const match = HARNESS_SHELLS.find((harness) => harness.matches(env)); + if (match === undefined) return undefined; + const { name, prefix, caveat } = match; + return { name, prefix, caveat }; +} diff --git a/packages/cli/src/install/freshness.ts b/packages/cli/src/install/freshness.ts index c0a3cf03..9a106279 100644 --- a/packages/cli/src/install/freshness.ts +++ b/packages/cli/src/install/freshness.ts @@ -127,6 +127,10 @@ export async function measureEnvironment( return { cli, skill, rules }; } +/** What a person gets by leaving `init` off, which only works in a terminal. */ +const INSTALLER_NOTE = + "In a terminal, the same command without `init` opens the installer and its options."; + /** * The steps that bring the environment up to date, in the order they are to * be run: cli, skill, rules. `update` has to run with the CLI the project @@ -151,7 +155,7 @@ export function environmentSteps( part: "cli", text: `A newer Taskless CLI is published: ${cli.packageName} ${cli.latest}, and this project runs ${cli.running}. ` + - "Running `init` with the newer version moves the project to it and refreshes the installed skills.", + `Running \`init\` with the newer version moves the project to it and refreshes the installed skills. ${INSTALLER_NOTE}`, command: `${spelling.launcher} ${cli.packageName}@${cli.latest} init`, }); } @@ -168,7 +172,7 @@ 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 `init` reinstalls it.", + `Running \`init\` reinstalls it. ${INSTALLER_NOTE}`, command: `${spelling.invocation} init`, } : { diff --git a/packages/cli/src/prompts/recipes.ts b/packages/cli/src/prompts/recipes.ts index 1bbbe371..b8bf03d9 100644 --- a/packages/cli/src/prompts/recipes.ts +++ b/packages/cli/src/prompts/recipes.ts @@ -175,6 +175,20 @@ export interface HostTool { reason?: string; } +/** + * How a person runs a one-off command from inside the harness running this. + * Detected by the CLI (`src/detect/harness-shell.ts`), since reading the + * environment is something this module may not do. + */ +export interface HarnessShell { + /** The harness's name, as its users know it. */ + name: string; + /** What goes in front of the command at the harness's prompt. */ + prefix: string; + /** A limit of this route worth saying out loud, if it has one. */ + caveat?: string; +} + /** * One step that brings the Taskless environment up to date, as the CLI * measured and worded it (`src/install/freshness.ts`). Pre-rendered on the CLI @@ -291,7 +305,11 @@ export interface RecipeOptions { * renders a passage that says the environment was not checked, never one * that claims either outcome. */ - environment?: { steps: EnvironmentStep[] }; + environment?: { + steps: EnvironmentStep[]; + /** How the harness running this runs a command for a person, if known. */ + shell?: HarnessShell; + }; /** * What the host has on its `PATH`, for the passages that condition on it. * @@ -558,6 +576,9 @@ rendered, so it says nothing about whether Taskless is up to date.`; if (environment.steps.length === 0) { return "Your Taskless environment is up to date and you can proceed."; } + const rulesOwed = environment.steps.some((step) => step.part === "rules"); + const personOwed = environment.steps.some((step) => step.part !== "rules"); + const upgradeOwed = environment.steps.some((step) => step.part === "cli"); const steps = environment.steps .map((step, index) => { const body = `${String(index + 1)}. ${step.text}`; @@ -566,16 +587,46 @@ rendered, so it says nothing about whether Taskless is up to date.`; : `${body}\n \`\`\`\n ${step.command}\n \`\`\``; }) .join("\n\n"); - return `Your Taskless environment is out of date, the following steps are needed in order for commands to work reliably: + const header = `Your Taskless environment is out of date, the following steps are needed in order for commands to work reliably: -${steps} +${steps}`; + // Moving the CLI is the user's decision, and it runs newly published code. + // Reconciling the rules runs the version the project already records, so it + // is ordinary agent work once the CLI has stopped moving. + const firstCommand = environment.steps.find( + (step) => step.part !== "rules" && step.command !== undefined + )?.command; + const shell = environment.shell; + const howToRun = + shell === undefined || firstCommand === undefined + ? "in another terminal" + : `in ${shell.name} by typing the command with its \`${shell.prefix.trim()}\` prefix, as \`${shell.prefix}${firstCommand}\`, which runs it in this session's shell, or in another terminal${ + shell.caveat === undefined + ? "" + : ` if it takes longer, since ${shell.caveat}` + }`; + const applyRules = `run its command and follow the recipe it prints to its last step. If that command is refused, give it to the user to run instead.`; + if (!personOwed) { + return `${header} + +Apply this step yourself: ${applyRules}`; + } + const afterUser = rulesOwed + ? ` Leave the rules step until then, because a newer CLI can owe a different reconcile. If that run still reports it, ${applyRules}` + : ""; + const declined = upgradeOwed + ? `\n\nThe user can decline the upgrade. Tell them what that means: the project stays on this CLI and keeps working, and this report keeps listing the upgrade until they move.${ + rulesOwed + ? ` The rules are still owed on this CLI, and \`check\` and \`verify\` refuse to run them until that is recorded, so apply the rules step now: ${applyRules}` + : "" + }` + : ""; + return `${header} -Do not run these steps yourself. Give them to the user, once, to run -themselves: in Claude Code by typing each command with the \`!\` prefix, -which runs it in this session's shell, or in another terminal. A -harness that blocks Taskless commands usually blocks these too. Then -carry on with the user's request. When the user says they ran the -steps, run \`${invocation} agent ok\` again to confirm.`; +Moving the CLI and reinstalling the skill are the user's to do, so do not +run those steps yourself. Give them to the user, once, to run themselves: +${howToRun}. Then carry on with the user's request. When the user says they ran the steps, +run \`${invocation} agent ok\` again to confirm.${afterUser}${declined}`; } /** diff --git a/packages/cli/src/util/package-manager.ts b/packages/cli/src/util/package-manager.ts index 48745890..938760b6 100644 --- a/packages/cli/src/util/package-manager.ts +++ b/packages/cli/src/util/package-manager.ts @@ -223,7 +223,7 @@ const PROJECT_LAUNCHERS = new Set([ ]); /** The project launcher that started this process, if one did. */ -function projectLauncher(context: LauncherContext): string | undefined { +export function projectLauncher(context: LauncherContext): string | undefined { const named = context.env.TASKLESS_LAUNCHER; return named !== undefined && PROJECT_LAUNCHERS.has(named) ? named diff --git a/packages/cli/test/agent-ok.test.ts b/packages/cli/test/agent-ok.test.ts index 571cd124..4b947225 100644 --- a/packages/cli/test/agent-ok.test.ts +++ b/packages/cli/test/agent-ok.test.ts @@ -129,10 +129,32 @@ describe("agent ok", () => { "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 init"); - expect(output()).toContain("Do not run these steps yourself."); + expect(output()).toContain("do not\nrun those steps yourself"); expect(output()).toMatch(/run `npx @taskless\/cli@\S+ agent ok` again/); }); + it("spells the upgrade with npx on a project launcher run", async () => { + vi.stubEnv("npm_command", ""); + vi.stubEnv("npm_lifecycle_event", ""); + vi.stubEnv("npm_config_user_agent", ""); + vi.stubEnv("TASKLESS_LAUNCHER", "./.taskless/bin/cli.mjs"); + stubRegistry("999.0.0"); + await runAgent(directory, ["agent", "ok"]); + expect(output()).toContain("npx @taskless/cli@999.0.0 init"); + expect(output()).toContain("./.taskless/bin/cli.mjs agent ok"); + }); + + it("offers the prefix of the harness running it", async () => { + vi.stubEnv("npm_command", "exec"); + vi.stubEnv("npm_lifecycle_event", "npx"); + // eslint-disable-next-line unicorn/no-useless-undefined -- stubEnv requires the value, and undefined deletes the variable so this holds when the suite runs inside Claude Code + vi.stubEnv("CLAUDECODE", undefined); + vi.stubEnv("CURSOR_AGENT", "1"); + stubRegistry("999.0.0"); + await runAgent(directory, ["agent", "ok"]); + expect(output()).toContain("`/shell npx @taskless/cli@999.0.0 init`"); + }); + it("does not guess a launcher it cannot detect", async () => { vi.stubEnv("npm_command", ""); vi.stubEnv("npm_lifecycle_event", ""); diff --git a/packages/cli/test/freshness.test.ts b/packages/cli/test/freshness.test.ts index f2f4026e..d99c6d21 100644 --- a/packages/cli/test/freshness.test.ts +++ b/packages/cli/test/freshness.test.ts @@ -124,6 +124,7 @@ describe("measureEnvironment", () => { expect(step?.part).toBe("cli"); expect(step?.command).toBe("npx @taskless/cli@1.1.0 init"); expect(step?.text).toContain("1.0.0"); + expect(step?.text).toContain("without `init` opens the installer"); }); it("does not report an older published version", async () => { diff --git a/packages/cli/test/harness-shell.test.ts b/packages/cli/test/harness-shell.test.ts new file mode 100644 index 00000000..a1367226 --- /dev/null +++ b/packages/cli/test/harness-shell.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; + +import { detectHarnessShell } from "../src/detect/harness-shell"; + +describe("detectHarnessShell", () => { + it.each([ + [{ CLAUDECODE: "1", CLAUDE_CODE_ENTRYPOINT: "cli" }, "Claude Code", "!"], + [{ CLAUDECODE: "1" }, "Claude Code", "!"], + [{ CURSOR_AGENT: "1" }, "Cursor", "/shell "], + [{ OPENCODE: "1" }, "OpenCode", "!"], + [{ CODEX_THREAD_ID: "abc" }, "Codex", "!"], + [{ GEMINI_CLI: "1" }, "Gemini CLI", "!"], + [{ AI_AGENT: "devin_3000_agent" }, "Devin", "!"], + ])("recognizes %j", (env, name, prefix) => { + expect(detectHarnessShell(env)).toMatchObject({ name, prefix }); + }); + + it("does not offer `!` in a Claude Code surface without it", () => { + expect( + detectHarnessShell({ + CLAUDECODE: "1", + CLAUDE_CODE_ENTRYPOINT: "claude-vscode", + }) + ).toBeUndefined(); + }); + + it("says nothing about an agent it has no syntax for", () => { + expect(detectHarnessShell({ AI_AGENT: "crush" })).toBeUndefined(); + expect(detectHarnessShell({})).toBeUndefined(); + }); + + it("names Cursor's time limit", () => { + expect(detectHarnessShell({ CURSOR_AGENT: "1" })?.caveat).toContain( + "30 seconds" + ); + }); +}); diff --git a/packages/cli/test/installed-documentation.test.ts b/packages/cli/test/installed-documentation.test.ts index 6de8d684..c99a1aa5 100644 --- a/packages/cli/test/installed-documentation.test.ts +++ b/packages/cli/test/installed-documentation.test.ts @@ -194,8 +194,10 @@ describe("this repository's own installed Taskless docs", () => { // the user rather than run, and the one confirming rerun. expect(prose).toContain("the environment is up to date"); expect(prose).toContain("the environment is out of date"); - expect(prose).toContain("Do not run them yourself"); - expect(prose).toContain("with the `!` prefix"); + expect(prose).toContain("do not run those steps yourself"); + expect(prose).toContain("follow the recipe it prints to its last step"); + expect(prose).toContain("If the user declines an upgrade"); + expect(prose).toContain("with this harness's prefix"); expect(prose).toContain("another terminal"); expect(prose).toContain("Then carry on with the user's request"); expect(prose).toContain( diff --git a/packages/cli/test/prompts.test.ts b/packages/cli/test/prompts.test.ts index e27b9652..ec624a4c 100644 --- a/packages/cli/test/prompts.test.ts +++ b/packages/cli/test/prompts.test.ts @@ -964,5 +964,69 @@ describe("environment status", () => { ); expect(text).toContain("2. Refreshed by the step above.\n\n3."); expect(text).toContain(`run \`${invocation} agent ok\` again`); + expect(text).toContain("Leave the rules step until then"); + expect(text).toContain("The user can decline the upgrade."); + expect(text).toContain("so apply the rules step now"); + }); + + it("gives the CLI step to the user and keeps the rules out of it", () => { + const text = + getRecipe("ok", { + invocation, + environment: { + steps: [{ part: "cli", text: "Newer CLI.", command: "npx x@2.0.0" }], + }, + }) ?? ""; + expect(text).toContain("user's to do"); + expect(text).not.toContain("Leave the rules step"); + expect(text).toContain("The user can decline the upgrade."); + expect(text).not.toContain("so apply the rules step now"); + expect(text).not.toContain("Apply this step yourself"); + }); + + it("offers the harness's prefix for the first command it can run", () => { + const text = + getRecipe("ok", { + invocation, + environment: { + shell: { name: "Cursor", prefix: "/shell ", caveat: "it is limited" }, + steps: [{ part: "cli", text: "Newer CLI.", command: "npx x@2.0.0" }], + }, + }) ?? ""; + expect(text).toContain("in Cursor by typing"); + expect(text).toContain("`/shell npx x@2.0.0`"); + expect(text).toContain("or in another terminal if it takes longer"); + expect(text).toContain("since it is limited"); + }); + + it("offers only another terminal for an unknown harness", () => { + const text = + getRecipe("ok", { + invocation, + environment: { + steps: [{ part: "cli", text: "Newer CLI.", command: "npx x@2.0.0" }], + }, + }) ?? ""; + expect(text).toContain("to run themselves:\nin another terminal."); + expect(text).not.toContain("prefix"); + }); + + it("has the agent apply a rules step when nothing else is owed", () => { + const text = + getRecipe("ok", { + invocation, + environment: { + steps: [ + { + part: "rules", + text: "Rules owe a reconcile.", + command: `${invocation} update`, + }, + ], + }, + }) ?? ""; + expect(text).toContain("Apply this step yourself"); + expect(text).toContain("to its last step"); + expect(text).not.toContain("user's to do"); }); }); diff --git a/skills/taskless/SKILL.md b/skills/taskless/SKILL.md index 0a904d62..7bd93159 100644 --- a/skills/taskless/SKILL.md +++ b/skills/taskless/SKILL.md @@ -46,13 +46,18 @@ If it says the environment is up to date, go straight to the user's request: no survey of the install, and no menu of what Taskless can do. If it says the environment is out of date, it lists numbered steps with their -commands. Do not run them yourself, since a harness that blocks Taskless -commands usually blocks these too. Give them to the user once, to run -themselves: in Claude Code by typing each command with the `!` prefix, which -runs it in this session's shell, or in another terminal. Then carry on with -the user's request. When the user says they ran the steps, run -`%(TASKLESS_CLI)s agent ok` again to confirm. Apart from that confirmation, -do not run the preflight again in the session once it has passed. +commands. Moving the CLI and reinstalling the skill are the user's to do, so +do not run those steps yourself. Give them to the user once, to run +themselves, the way the report says: with this harness's prefix when it names +one, or in another terminal. Then carry on with the user's request. When the +user says they ran the steps, run `%(TASKLESS_CLI)s agent ok` again to confirm. + +A step that says the rules owe a reconcile is yours once no CLI or skill step +is left: run its command and follow the recipe it prints to its last step. If +that command is refused, give it to the user instead. If the user declines an +upgrade, tell them what the report says that means, and apply a rules step to +this CLI. Apart from confirming those steps, do not run the preflight again in +the session once it has passed. Run every Taskless command on a line of its own that begins with `%(TASKLESS_CLI)s`, with nothing around it: no variable assignment, `cd`,