Repository navigation
feat: run the Taskless CLI through a project launcher - #537
theCodeDrift wants to merge 15 commits into
Conversation
A project-local launcher per package (.taskless/bin/cli.mjs, cli-nightly.mjs) that runs the version recorded in .taskless/taskless.json, with launcher permission rules, a global trust entry that covers it and folds duplicates, and a ci recipe that runs it with Node supplied by the job.
Settings edits read as migrations: ALLOW green, REMOVE default, REPLACE yellow, SKIPPED red for an edit the user must make by hand. Records the Cursor, Codex, and OpenCode rule research, the Windows Git Bash confirmation, and that the skill runs the launcher from the directory containing .taskless/.
init writes .taskless/bin/cli.mjs (or cli-nightly.mjs), records each package's version under install.launchers, restores an edited launcher and says so, and renders the skill's invocation to the launcher. The skill now says to run Taskless commands from the directory containing .taskless/.
…n file Each harness gets a version-free rule for this build's launcher beside its pinned npx rule. Launcher paths count as Taskless rules of their package, so they are kept across upgrades, removed with their package, and a hand-added colon form is replaced. A deny on the launcher leaves the file alone.
…licates Each package's autoMode entry also allows its own launcher from the project root. More than one string under a build's label is stale even when one is current; the wizard keeps the first, replaced in place, and removes the rest. The outcome carries how many strings carry the label, and the warning says so.
ALLOW green, REMOVE in the default color, REPLACE yellow, SKIPPED red. Red is reserved for an edit the user has to make by hand. A missing or stale global trust entry after init now prints as a red SKIPPED (global) line. The init recipe documents the launcher field, the labelled count, and the new line.
A recipe fetched through a project launcher names that launcher, and tells the agent to run it from the directory containing .taskless/; any other TASKLESS_LAUNCHER value is ignored. The ci recipe runs node .taskless/bin/cli.mjs with Node supplied by the job, so a repository without package.json can run it, and its template now sets Node 22 to match the CLI's engine floor.
Adds the patch changeset. Two tasks stay open as follow-ups: confirming Cursor compares a path-valued Shell() base literally, and a nightly smoke test in an auto-mode session.
When the project launcher started the CLI, remedies and hints name ./.taskless/bin/cli.mjs instead of npx; any other TASKLESS_LAUNCHER value is ignored. The pending-migration and unreadable-manifest remedies now use the same choice, and .taskless/README.md lists the package's launcher first.
The hooks recipe now runs node .taskless/bin/cli.mjs in every hook tool, and no longer adds @taskless/cli as a dev dependency, which was a second pin to keep in step with .taskless/taskless.json.
The repo's .taskless/README.md is migration output from the pinned build that installed it, so a byte-for-byte comparison with HEAD's template forced either a hand edit of migration output or a dogfood reinstall whenever the template changed. The template is covered in scaffold-readme.test.ts and the stale-README migration by its fixture test. Restores the README the earlier commit hand-edited.
|
@claude /review |
|
Claude finished @theCodeDrift's task in 5m 24s —— View job Review completeReview mode: incremental — read 0 prior review thread(s) before reviewing.
Scope of this pass: this is a large, carefully-reasoned change (new project launcher, manifest schema for per-package launcher versions, permission-rule/global-trust-entry updates for every harness, settings-tone recoloring, recipe/doc rewrites). I traced the full execution paths for: the launcher template itself (version resolution, Windows cmd.exe quoting, exit/signal passthrough); the install-time launcher write/restore logic; the Claude/Cursor CLI/Codex/OpenCode permission-rule generation and denial-matching now covering both the pinned invocation and the launcher; the global-trust duplicate-entry folding and text-replacement-by-index logic (verified the reversed-order removal keeps earlier indices valid); and the One non-blocking finding (posted inline):
Things I specifically checked and found sound (noting since several are easy to get subtly wrong):
No findings were carried over from a previous review since none exists yet. |
Thanks. The one finding (no CI coverage for — AI Coding Agent |
Stack (root → tip):
initnow installs.taskless/bin/cli.mjs(cli-nightly.mjsfor a nightly), a small Node script that runs the CLI version recorded for it in.taskless/taskless.json. Agents, the skill, permissions, and CI call that one path instead ofnpx @taskless/cli@<version>.Why
Claude Code's auto-mode classifier refuses
npx @taskless/cli@<version>as downloaded code, even with a project allow rule. A manual probe on 2026-10-19 showed the same call made from a script in the repository,./.taskless/bin/cli.mjs, runs. It ran consistently, because the classifier treats files the user brought into the repository as theirs to run. With an allowance naming the launcher, it was reliable.node .taskless/bin/cli.mjswas refused as an external script, so agents use the./spelling.It also takes the version out of the installed skill, so the skill no longer changes on every release, and gives CI in any language one command whose version comes from the project.
What changes
packages/cli/assets/launcher/launcher.mjs): reads its package's version frominstall.launchers, falling back toinstall.cliVersiononly when that is its own package's version. It refuses anything that is not an exact version, runsnpx --yes <package>@<version>with the arguments unchanged, and passes the exit status and signals back. On Windows it quotes arguments for cmd the way cross-spawn does. Its comments describe what it does and nothing more, since an agent that reads a comment framing it as a way past a check gets refused.--jsonenvelope say so. The skill renders%(TASKLESS_CLI)sto the launcher, and says to run it from the directory that contains.taskless/. Stub recovery commands keep the npx form.autoMode.allowentry also covers its own launcher, so existing entries read as stale once. More than one string under a label is now stale too, and the wizard folds them into one under the same confirmation.--no-interactivestill writes nothing outside the project.ALLOWgreen,REMOVEuncolored,REPLACEyellow,SKIPPEDred. Red now only means "you have to do this yourself". A missing or stale global entry afterinitprints as a redSKIPPED (global):line.TASKLESS_LAUNCHERonly when it is exactly one of the two paths. Thecirecipe generatesnode .taskless/bin/cli.mjs checkwith Node supplied by the job, so a Python or Go repository can run it without apackage.json. Its template now sets Node 22, matching the CLI's engine floor (it said 20).hooksrecipe runsnode .taskless/bin/cli.mjsin every hook tool. It no longer adds@taskless/clias a dev dependency, which was a second pin to keep in step with.taskless/taskless.json../.taskless/bin/cli.mjsinstead ofnpx. This goes throughgetCliPrefix(), plus the two remedies that bypassed it (pending migration, unreadable manifest)..taskless/README.mdlists the package's launcher first, keeping thepnpm dlxandnpxlines for a checkout without it. This repository's own.taskless/README.mdis left as its installed CLI wrote it; the test that pinned it to HEAD's template is removed, since satisfying it meant hand-editing migration output or reinstalling the dogfood setup.Verification
pnpm typecheck,pnpm lint, and the full suite pass (134 files, 2435 tests).npx;init --no-interactivefrom the built CLI into a fresh project, then./.taskless/bin/cli.mjs infoand../.taskless/bin/cli.mjs checkfrom a subdirectory. Both ran the published 0.13.1. With the launcher variable set, the built CLI's no-rules hint names./.taskless/bin/cli.mjs rule create; with an unexpected value, it falls back to npx.codex execpolicy checkconfirmsprefix_rule(pattern = ["./.taskless/bin/cli.mjs"])matches the launcher with and without arguments. It does not matchcli.mjsx, the spelling without./, or the nightly's launcher.Shell(./.taskless/bin/cli.mjs). The launcher auto-ran with and without arguments../other/cli.mjs,./.taskless/bin/cli.mjsx,.taskless/bin/cli.mjsandnode .taskless/bin/cli.mjsall asked. Cursor compares the command's first word exactly as typed. The Cursor editor wasn't run; its documented word-bounded prefix match is taken to behave the same.Follow-ups (not blocking)
Delivery shape: single PR. The launcher, its permissions, and the invocation change are only useful together. Both OpenSpec changes,
pinned-cli-wrapperandlauncher-hints-and-hooks, are archived here.