diff --git a/.changeset/agent-ok-preflight.md b/.changeset/agent-ok-preflight.md new file mode 100644 index 00000000..0fb4b673 --- /dev/null +++ b/.changeset/agent-ok-preflight.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +New `taskless agent ok` topic: it prints one line confirming the CLI can run, through the invocation it was launched with, and does nothing else. The Taskless skill and the `/tskl` command now run it once per session before any other Taskless command, so a harness that blocks download-and-run commands (Claude Code's auto mode, for one) stops the agent before it starts work rather than partway through. On a blocked command the agent now names the exact command to the user, offers to have them approve it, run it themselves with `!`, or allow exactly that invocation, and never proposes an allow rule that wildcards the package name or version. diff --git a/commands/tskl/tskl.md b/commands/tskl/tskl.md index 57da7526..85ee2a6b 100644 --- a/commands/tskl/tskl.md +++ b/commands/tskl/tskl.md @@ -19,6 +19,15 @@ with Taskless before proceeding. Otherwise, follow the same flow as the `taskless` skill: +0. Preflight, once per session, before any other Taskless command: run + `%(TASKLESS_CLI)s agent ok` and proceed once it prints its confirmation + line. If the harness or the user blocks it, or blocks any later Taskless + command, stop and do not try to get around the block. Tell the user the + exact command and that it downloads and runs a package from npm, then + offer: they approve that exact command in their reply, they run it + themselves as `! `, or you add an allow rule for exactly + the invocation `%(TASKLESS_CLI)s`. Never propose a rule wider than that + invocation, such as a wildcard over the package name or its version. 1. Identify the topic from `$ARGUMENTS` using the table below. 2. Fetch the canonical recipe with `%(TASKLESS_CLI)s agent ` (or `%(TASKLESS_CLI)s agent --anonymous` if the user is offline or diff --git a/openspec/changes/archive/2026-10-15-agent-ok-preflight/.openspec.yaml b/openspec/changes/archive/2026-10-15-agent-ok-preflight/.openspec.yaml new file mode 100644 index 00000000..49ea85b3 --- /dev/null +++ b/openspec/changes/archive/2026-10-15-agent-ok-preflight/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-14 diff --git a/openspec/changes/archive/2026-10-15-agent-ok-preflight/design.md b/openspec/changes/archive/2026-10-15-agent-ok-preflight/design.md new file mode 100644 index 00000000..d193486c --- /dev/null +++ b/openspec/changes/archive/2026-10-15-agent-ok-preflight/design.md @@ -0,0 +1,35 @@ +## Context + +The `taskless` skill is a router: every action starts by running `%(TASKLESS_CLI)s agent `, which on a release is `npx @taskless/cli …` and on a nightly `npx @taskless/cli-nightly@ …`. Harnesses increasingly gate download-and-run commands. Claude Code's auto mode classifier blocked one in a real session, and its docs say a block can be cleared by a user message that names the exact action, or by a narrow `permissions.allow` rule. Broad rules (blanket `Bash(*)`, wildcarded interpreters, package-manager run commands) are dropped on entering auto mode. + +The session that hit the block had a vague request ("let's use taskless"), ran two read-only commands, then hit the block on `agent onboard`. Onboard's consent question was never reached, because it is in the recipe. + +## Goals / Non-Goals + +**Goals:** + +- An agent learns whether it may run the CLI before it starts any task, at the cost of one trivial command. +- When it may not, the agent asks once, names the exact command, and suggests only remedies scoped to that command. + +**Non-Goals:** + +- Writing harness permission files (`.claude/settings.json` and others) from the installer. Whether a pinned `npx` allow rule survives auto mode is undocumented, and is a follow-up to test by hand before anything depends on it. +- Changing onboard's own consent question. + +## Decisions + +**`ok` is built into `agent`, not a `.md` recipe.** A recipe must carry the `# Topic:` header, Goal/Preconditions/Steps/Errors/See Also, and the served copy gains the fetch-time directive ("fetch again next task"). All of that is noise around one line, and the directive is wrong here: a confirmation is not a recipe to refetch. The branch sits before recipe lookup in `agent.ts`, and the index lists it beside the commands. Alternative considered: a minimal `ok.md` exempted from the template requirement. Rejected, since an exemption in the format test is a second rule to maintain for one line. + +**The line names the detected invocation, via `detectCliInvocation`.** That is the value recipes are rendered with, so the preflight and every later recipe agree on spelling. When it returns `undefined` the line says "via the command you just ran" instead of falling back to `npx` the way `getCliPrefix` does, because a guessed launcher in text an agent copies is worse than none. + +**No project side effects.** `ok` does not bootstrap `.taskless/`, read the manifest, or check auth. It must be safe to run in any directory, first, and a preflight that writes project files would be one an agent cannot run speculatively. It does emit `cli_agent` with `topic: "ok"`, like every other topic: preflight volume against task volume is the measurement for how often agents stop at the gate. That capture carries telemetry's usual costs, which this change does not alter: unless telemetry is disabled, it writes the anonymous ID under the user's config directory and runs a `git` subprocess for repository context. Making `ok` skip telemetry entirely would lose the measurement, so it is left as every other topic has it. + +**The remedies are scoped to the exact invocation.** The skill names three: the user approves the exact command in their reply (Claude Code reads an approval that names the specific action as clearing the block), the user runs `! `, or an allow rule for exactly `%(TASKLESS_CLI)s`. It forbids widening the rule, because the agent in the incident improvised `npx @taskless/cli-nightly:*`, which approves every future nightly rather than the one installed, and which auto mode may drop anyway. + +**Once per session, not per task.** Permission does not change between tasks in a session; recipes do. The skill's existing "fetch the recipe for every task" rule is unchanged. + +## Risks / Trade-offs + +- [The classifier allows `agent ok` but blocks a later command] → An approval or classifier judgement can be per-action. The skill's blocked-command guidance is written for any Taskless command, not only the preflight, so a later block gets the same handling. +- [The download block cannot be cleared by chat approval at all] → Then the `!` route and the allow rule remain. A hand test under auto mode, run after this change and outside it, decides whether a follow-up should make the skill lead with one of those instead. +- [One more command before every session's first task] → It is cached by npx after the first run and prints one line; the first-run latency note in the skill already covers the cold fetch. diff --git a/openspec/changes/archive/2026-10-15-agent-ok-preflight/proposal.md b/openspec/changes/archive/2026-10-15-agent-ok-preflight/proposal.md new file mode 100644 index 00000000..87bfcd45 --- /dev/null +++ b/openspec/changes/archive/2026-10-15-agent-ok-preflight/proposal.md @@ -0,0 +1,35 @@ +## Why + +A harness can refuse the CLI before Taskless says a word. In a real session, Claude Code's auto mode blocked `npx @taskless/cli-nightly@ agent onboard` because it downloads and runs a package from npm. The block is correct, but it landed on the first command that does work, and the one consent question onboarding asks lives inside that recipe, behind the call that was refused. The agent stopped, improvised an allow rule (`npx @taskless/cli-nightly:*`) broader than the pin it was installed with, and asked its question after the fact. + +Nothing tells an agent, before it starts a task, whether it is allowed to run Taskless at all. That is cheap to find out and expensive to discover halfway through. + +## What Changes + +- Add `taskless agent ok`, a built-in topic that does nothing but print one line: `This message confirms you can run Taskless via .` It reads no project state, needs no install, and exits 0. Reaching it at all is the signal. +- The `taskless` skill and the `tskl` command run `agent ok` once per session, before any other Taskless command. +- When the harness blocks that preflight, the skill tells the agent to stop before doing any work and ask the user, naming the exact command and that it downloads a package from npm. It offers three ways forward: the user approves that exact command in their reply, the user runs `! ` themselves, or the agent adds an allow rule for exactly the invocation the skill names, never a wildcard over the package name or version. +- `ok` appears in the `taskless agent` index. + +## Delivery shape + +**Single PR.** One built-in topic, two skill-body sections, and their tests fit one reviewable diff well under the size guidance, and none of it is useful split: the topic without the skill instruction is never called, and the instruction without the topic names a command that does not exist. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `cli-agent`: adds the `ok` preflight topic. +- `skill-taskless`: adds the preflight step and the blocked-preflight guidance to the skill and `tskl` command bodies. + +## Impact + +- `packages/cli/src/commands/agent.ts`: the `ok` branch and its index row. +- `skills/taskless/SKILL.md`, `commands/tskl/tskl.md`: the preflight section. The skill body stays within its 150-line cap. +- Tests for the topic output, the index row, telemetry, and the skill body. +- A `patch` changeset (pre-1.0, additive). +- Out of scope: writing harness permission settings from the installer. Whether a narrow `npx` allow rule survives Claude Code's auto mode is undocumented, and is a follow-up to test by hand before anything depends on it. diff --git a/openspec/changes/archive/2026-10-15-agent-ok-preflight/specs/cli-agent/spec.md b/openspec/changes/archive/2026-10-15-agent-ok-preflight/specs/cli-agent/spec.md new file mode 100644 index 00000000..3b9223b5 --- /dev/null +++ b/openspec/changes/archive/2026-10-15-agent-ok-preflight/specs/cli-agent/spec.md @@ -0,0 +1,39 @@ +## ADDED Requirements + +### Requirement: ok topic confirms the CLI can run + +`taskless agent ok` SHALL print exactly one line, `This message confirms you can run Taskless via .`, and exit 0. `` SHALL be the invocation detected for this process. When the launcher cannot be determined, the line SHALL read `This message confirms you can run Taskless via the command you just ran.` rather than guessing a launcher. + +The topic is built into the `agent` command, not a recipe file. It SHALL NOT carry the `# Topic:` header, the fetch-time directive, or a survey invite, and it SHALL NOT read or create `.taskless/`. 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 +- **THEN** stdout SHALL be a single line beginning `This message confirms you can run Taskless via` +- **AND** the CLI SHALL exit with code 0 + +#### Scenario: ok names the detected invocation + +- **WHEN** `taskless agent ok` is run by a launcher the CLI detects +- **THEN** the line SHALL name that launcher and the pinned package specifier + +#### Scenario: ok does not guess an unknown launcher + +- **WHEN** `taskless agent ok` is run and the launcher cannot be determined +- **THEN** the line SHALL say `via the command you just ran` and SHALL NOT name a launcher + +#### Scenario: ok runs outside a Taskless project + +- **WHEN** `taskless agent ok` is run in a directory with no `.taskless/` +- **THEN** it SHALL print the confirmation line and exit 0 +- **AND** SHALL NOT create `.taskless/` + +#### 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-10-15-agent-ok-preflight/specs/skill-taskless/spec.md b/openspec/changes/archive/2026-10-15-agent-ok-preflight/specs/skill-taskless/spec.md new file mode 100644 index 00000000..80a77620 --- /dev/null +++ b/openspec/changes/archive/2026-10-15-agent-ok-preflight/specs/skill-taskless/spec.md @@ -0,0 +1,24 @@ +## ADDED Requirements + +### Requirement: Skill preflights the CLI before the first Taskless command + +The skill body and the `tskl` command body SHALL instruct the agent to run `%(TASKLESS_CLI)s agent ok` once per session, before any other Taskless command, and to proceed only once it prints its confirmation line. + +When the preflight is blocked by the harness or the user, the bodies SHALL instruct the agent to: + +1. Stop before doing any Taskless work, and not attempt to work around the block. +2. Tell the user the exact command, and that it downloads and runs a package from npm. +3. Offer the user ways forward: approve that exact command in their reply, run `! ` themselves, or have the agent add an allow rule for exactly the invocation the skill names. +4. Never propose an allow rule that widens the invocation, such as a wildcard over the package name or its version. + +#### Scenario: Skill body runs the preflight first + +- **WHEN** the skill body or the `tskl` command body is read from source +- **THEN** it SHALL instruct the agent to run `%(TASKLESS_CLI)s agent ok` before any other Taskless command in the session + +#### Scenario: Skill body handles a blocked preflight + +- **WHEN** the skill body or the `tskl` command body is read from source +- **THEN** it SHALL instruct the agent, on a blocked preflight, to stop and name the exact command to the user +- **AND** SHALL offer both the `!` route and an allow rule for the exact invocation +- **AND** SHALL forbid an allow rule that wildcards the package name or version diff --git a/openspec/changes/archive/2026-10-15-agent-ok-preflight/tasks.md b/openspec/changes/archive/2026-10-15-agent-ok-preflight/tasks.md new file mode 100644 index 00000000..7cb67a5d --- /dev/null +++ b/openspec/changes/archive/2026-10-15-agent-ok-preflight/tasks.md @@ -0,0 +1,16 @@ +## 1. The `ok` topic + +- [x] 1.1 Add the `ok` branch to `packages/cli/src/commands/agent.ts`, before recipe lookup: print the confirmation line from `detectCliInvocation`, with the "command you just ran" fallback, and capture `cli_agent` with `topic: "ok"` +- [x] 1.2 List `ok` in the `taskless agent` index +- [x] 1.3 Tests: one-line output and exit 0, detected and unknown launcher, no `.taskless/` created, index row, telemetry topic + +## 2. Skill and command bodies + +- [x] 2.1 Add the preflight section to `skills/taskless/SKILL.md`: run `%(TASKLESS_CLI)s agent ok` once per session first; on a block, stop, name the exact command, offer chat approval, `!`, or an exact-invocation allow rule, and forbid widening it. Keep the body within 150 lines +- [x] 2.2 Add the same step to `commands/tskl/tskl.md` +- [x] 2.3 Tests asserting both bodies carry the preflight and the blocked-preflight guidance + +## 3. Release + +- [x] 3.1 Add a `patch` changeset +- [x] 3.2 Run `pnpm typecheck` and `pnpm lint` diff --git a/openspec/specs/cli-agent/spec.md b/openspec/specs/cli-agent/spec.md index fa5ff7f2..5668a094 100644 --- a/openspec/specs/cli-agent/spec.md +++ b/openspec/specs/cli-agent/spec.md @@ -394,3 +394,41 @@ An agent topic `report` SHALL be registered. The CLI SHALL embed `packages/cli/s - **WHEN** an agent runs `taskless agent report` - **THEN** PostHog SHALL receive a `cli_agent` event whose `topic` property is `report` + +### Requirement: ok topic confirms the CLI can run + +`taskless agent ok` SHALL print exactly one line, `This message confirms you can run Taskless via .`, and exit 0. `` SHALL be the invocation detected for this process. When the launcher cannot be determined, the line SHALL read `This message confirms you can run Taskless via the command you just ran.` rather than guessing a launcher. + +The topic is built into the `agent` command, not a recipe file. It SHALL NOT carry the `# Topic:` header, the fetch-time directive, or a survey invite, and it SHALL NOT read or create `.taskless/`. 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 +- **THEN** stdout SHALL be a single line beginning `This message confirms you can run Taskless via` +- **AND** the CLI SHALL exit with code 0 + +#### Scenario: ok names the detected invocation + +- **WHEN** `taskless agent ok` is run by a launcher the CLI detects +- **THEN** the line SHALL name that launcher and the pinned package specifier + +#### Scenario: ok does not guess an unknown launcher + +- **WHEN** `taskless agent ok` is run and the launcher cannot be determined +- **THEN** the line SHALL say `via the command you just ran` and SHALL NOT name a launcher + +#### Scenario: ok runs outside a Taskless project + +- **WHEN** `taskless agent ok` is run in a directory with no `.taskless/` +- **THEN** it SHALL print the confirmation line and exit 0 +- **AND** SHALL NOT create `.taskless/` + +#### 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/specs/skill-taskless/spec.md b/openspec/specs/skill-taskless/spec.md index baa6c4b3..37a15c39 100644 --- a/openspec/specs/skill-taskless/spec.md +++ b/openspec/specs/skill-taskless/spec.md @@ -182,3 +182,26 @@ The skill body's topic table SHALL map a request to make, publish, regenerate, o - **WHEN** the skill body is read - **THEN** it SHALL NOT describe how to assemble, publish, or delete a report + +### Requirement: Skill preflights the CLI before the first Taskless command + +The skill body and the `tskl` command body SHALL instruct the agent to run `%(TASKLESS_CLI)s agent ok` once per session, before any other Taskless command, and to proceed only once it prints its confirmation line. + +When the preflight is blocked by the harness or the user, the bodies SHALL instruct the agent to: + +1. Stop before doing any Taskless work, and not attempt to work around the block. +2. Tell the user the exact command, and that it downloads and runs a package from npm. +3. Offer the user ways forward: approve that exact command in their reply, run `! ` themselves, or have the agent add an allow rule for exactly the invocation the skill names. +4. Never propose an allow rule that widens the invocation, such as a wildcard over the package name or its version. + +#### Scenario: Skill body runs the preflight first + +- **WHEN** the skill body or the `tskl` command body is read from source +- **THEN** it SHALL instruct the agent to run `%(TASKLESS_CLI)s agent ok` before any other Taskless command in the session + +#### Scenario: Skill body handles a blocked preflight + +- **WHEN** the skill body or the `tskl` command body is read from source +- **THEN** it SHALL instruct the agent, on a blocked preflight, to stop and name the exact command to the user +- **AND** SHALL offer both the `!` route and an allow rule for the exact invocation +- **AND** SHALL forbid an allow rule that wildcards the package name or version diff --git a/packages/cli/src/commands/agent.ts b/packages/cli/src/commands/agent.ts index 8cc012e3..b9736f29 100644 --- a/packages/cli/src/commands/agent.ts +++ b/packages/cli/src/commands/agent.ts @@ -49,6 +49,25 @@ const FEEDBACK_TOPICS: ReadonlyArray<[string, string]> = [ ["bug-report", "Report a Taskless bug (user asks; no GitHub account)"], ]; +/** + * The preflight topic. Built into this command rather than kept as a recipe + * file, because it is a confirmation and not a recipe: the recipe template's + * sections and the fetch-time directive ("fetch again next task") would wrap + * one line in instructions that do not apply to it. + * + * Reaching it at all is the signal. A harness that refuses to run the CLI + * (a permission check that blocks download-and-run commands, say) refuses + * this first, before an agent has started any work, rather than halfway into + * a task whose consent question sits inside a recipe it never received. So it + * touches no project state (no `.taskless/`, manifest, or auth) and is safe to + * run anywhere, first. Its one side effect is the intent event every topic + * captures, with telemetry's usual anonymous-id write. + */ +const PREFLIGHT_TOPIC: [string, string] = [ + "ok", + "Confirm you can run Taskless here (run once per session, first)", +]; + async function unwrap(resolvable: Resolvable): Promise { if (typeof resolvable === "function") { return (resolvable as () => T | Promise)(); @@ -151,7 +170,6 @@ export function createAgentCommand(subCommands: SubCommandsDef) { ) ); console.log("then ask your coding agent to do the work.\n"); - console.log("Topics:"); const entries: Array<[string, string]> = []; for (const [name, cmd] of Object.entries(subCommands)) { @@ -160,13 +178,21 @@ export function createAgentCommand(subCommands: SubCommandsDef) { entries.push([name, description]); } - // Pad commands and recipe topics against a shared width so the two - // sections line up. + // Pad the preflight, commands, and recipe topics against a shared + // width so the sections line up. const maxLength = Math.max( + PREFLIGHT_TOPIC[0].length, ...entries.map(([name]) => name.length), ...RECIPE_TOPICS.map(([name]) => name.length), ...FEEDBACK_TOPICS.map(([name]) => name.length) ); + const [preflightName, preflightDescription] = PREFLIGHT_TOPIC; + console.log("Preflight:"); + console.log( + ` ${preflightName.padEnd(maxLength + 2)}${preflightDescription}` + ); + + console.log("\nTopics:"); for (const [name, description] of entries) { console.log(` ${name.padEnd(maxLength + 2)}${description}`); } @@ -209,6 +235,20 @@ export function createAgentCommand(subCommands: SubCommandsDef) { const key = positionals[0]!; + if (key === PREFLIGHT_TOPIC[0]) { + telemetry.capture("cli_agent", { topic: key }); + // The detected invocation, the same value recipes are rendered with, + // so the preflight and every later recipe spell the CLI alike. When + // the launcher is unknown, say so rather than fall back to `npx` as + // `getCliPrefix()` would: an agent copies this text, and a guessed + // launcher is worse than none. + const invocation = detectCliInvocation(processLauncherContext()); + console.log( + `This message confirms you can run Taskless via ${invocation ?? "the command you just ran"}.` + ); + return; + } + // Anonymous variant lookup: prefer .anonymous.md when // --anonymous is set, fall back to the canonical recipe. The lookup and // the render both live in the shared prompts module, so `agent` and the diff --git a/packages/cli/test/agent-ok.test.ts b/packages/cli/test/agent-ok.test.ts new file mode 100644 index 00000000..5957dc13 --- /dev/null +++ b/packages/cli/test/agent-ok.test.ts @@ -0,0 +1,103 @@ +import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const capture = vi.fn(); +vi.mock("../src/telemetry", () => ({ + getTelemetry: vi.fn(() => + Promise.resolve({ capture, shutdown: () => Promise.resolve() }) + ), + shutdownTelemetry: () => Promise.resolve(), + isTelemetryEnabled: () => false, +})); + +const { createAgentCommand } = await import("../src/commands/agent"); + +interface RunnableCommand { + run: (context: { + args: { dir: string; anonymous: boolean }; + rawArgs: string[]; + }) => Promise; +} + +async function runAgent( + directory: string, + rawArguments: string[] +): Promise { + const command = createAgentCommand({}) as unknown as RunnableCommand; + await command.run({ + args: { dir: directory, anonymous: false }, + rawArgs: rawArguments, + }); +} + +describe("agent ok", () => { + let directory: string; + let logSpy: ReturnType; + let errorSpy: ReturnType; + + beforeEach(() => { + capture.mockClear(); + process.exitCode = undefined; + directory = mkdtempSync(join(tmpdir(), "taskless-agent-ok-")); + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + errorSpy.mockRestore(); + vi.unstubAllEnvs(); + rmSync(directory, { recursive: true, force: true }); + }); + + function printed(): string[] { + return logSpy.mock.calls.map((call) => String(call[0])); + } + + it("prints one confirmation line and succeeds", async () => { + await runAgent(directory, ["agent", "ok"]); + expect(printed()).toHaveLength(1); + expect(printed()[0]).toMatch( + /^This message confirms you can run Taskless via .+\.$/ + ); + expect(errorSpy).not.toHaveBeenCalled(); + expect(process.exitCode).toBeUndefined(); + }); + + it("names the detected launcher and package", async () => { + vi.stubEnv("npm_command", "exec"); + vi.stubEnv("npm_lifecycle_event", "npx"); + await runAgent(directory, ["agent", "ok"]); + expect(printed()[0]).toMatch( + /^This message confirms you can run Taskless via npx @taskless\/cli\S*@\S+\.$/ + ); + }); + + it("does not guess a launcher it cannot detect", async () => { + vi.stubEnv("npm_command", ""); + vi.stubEnv("npm_lifecycle_event", ""); + vi.stubEnv("npm_config_user_agent", ""); + await runAgent(directory, ["agent", "ok"]); + expect(printed()[0]).toBe( + "This message confirms you can run Taskless via the command you just ran." + ); + }); + + it("does not create .taskless/", async () => { + await runAgent(directory, ["agent", "ok"]); + expect(existsSync(join(directory, ".taskless"))).toBe(false); + }); + + it("captures cli_agent with the ok topic", async () => { + await runAgent(directory, ["agent", "ok"]); + expect(capture).toHaveBeenCalledWith("cli_agent", { topic: "ok" }); + }); + + it("is listed in the agent index", async () => { + await runAgent(directory, ["agent"]); + expect(printed()).toContainEqual(expect.stringMatching(/^ {2}ok\s+\S/)); + }); +}); diff --git a/packages/cli/test/installed-documentation.test.ts b/packages/cli/test/installed-documentation.test.ts index 1d478d1c..0f59bbcb 100644 --- a/packages/cli/test/installed-documentation.test.ts +++ b/packages/cli/test/installed-documentation.test.ts @@ -186,6 +186,37 @@ describe("this repository's own installed Taskless docs", () => { } }); + it("preflights the CLI and scopes a blocked one to the exact invocation", async () => { + // A harness may refuse a download-and-run command. Without a preflight + // the refusal lands on the first command that does work, after any + // consent question that lives inside a recipe. And an agent left to pick + // its own remedy proposed `npx @taskless/cli-nightly:*`, approving every + // future nightly rather than the one installed. Both always-loaded + // surfaces carry the step, since a tool may install one without the other. + const skill = await readFile( + resolve(repositoryRoot, "skills", "taskless", "SKILL.md"), + "utf8" + ); + const command = await readFile( + resolve(repositoryRoot, "commands", "tskl", "tskl.md"), + "utf8" + ); + for (const [name, body] of [ + ["SKILL.md", skill], + ["tskl.md", command], + ] as const) { + // Compared as prose, so where a line happens to wrap does not matter. + const prose = body.replaceAll(/\s+/g, " "); + expect(prose, name).toContain("%(TASKLESS_CLI)s agent ok"); + expect(prose, name).toContain("before any other Taskless command"); + expect(prose, name).toContain("! "); + expect(prose, name).toContain( + "allow rule for exactly the invocation `%(TASKLESS_CLI)s`" + ); + expect(prose, name).toContain("wildcard over the package name"); + } + }); + it("keeps the skill body within its 150-line cap", async () => { // `skill-taskless` caps the body at 150 lines of markdown. The cap guards // against unbounded growth, not against guidance the router needs: when diff --git a/skills/taskless/SKILL.md b/skills/taskless/SKILL.md index fa1dfe69..21d125f3 100644 --- a/skills/taskless/SKILL.md +++ b/skills/taskless/SKILL.md @@ -34,6 +34,25 @@ canonical recipes live behind `%(TASKLESS_CLI)s agent `. Always fetch the recipe first; do not improvise from prior knowledge — recipes change with each CLI version. +## Preflight: confirm you can run Taskless + +Once per session, before any other Taskless command, run +`%(TASKLESS_CLI)s agent ok`. It touches no project files, and prints one line +confirming the invocation works. Proceed once you see it. + +If the harness or the user blocks it, or blocks any later Taskless command, +stop before doing any Taskless work and do not try to get around the block. +Tell the user the exact command you ran, and that it downloads and runs a +package from npm. Then offer the ways forward: + +- They approve that exact command in their reply. +- They run it themselves as `! `, so its output lands here. +- You add an allow rule for exactly the invocation `%(TASKLESS_CLI)s`. + +Never propose a rule wider than that invocation, such as a wildcard over the +package name or its version: it would approve a CLI this project never +installed. + ## Authoring a rule: always start at route For any request to add/write/create a rule — whether or not the user names a