--help on a subcommand runs the subcommand
taskless check --help does not print help. It runs check. The same is true of every subcommand — --help is not recognized as a help request anywhere below the root, so it is parsed as an unknown flag and the command body executes.
This was found while quoting real --help output for the README (#130): asking check for help migrated the repo's own .taskless/ scaffold as a side effect.
Mechanism
packages/cli/src/index.ts:125 calls citty's runCommand. citty implements --help only inside runMain (citty@0.1.6/dist/index.mjs:435); runCommand has no help branch at all.
The root command appears to work only because it hand-rolls the behavior — index.ts:88-91 calls showUsage(cmd) when the flags present are not init flags. That is the root reimplementing for itself what runMain would have given the whole tree.
Why this is a defect and not a design choice
citty appends this line to the usage of any command that has subcommands (dist/index.mjs:425):
Use `taskless <command> --help` for more information about a command.
So taskless --help — which does work — instructs the reader to use a flag the CLI does not implement. The CLI advertises its own broken path.
Worth separating two things that are easy to conflate:
- The agent-facing path is fine and is not what this is about.
taskless agent <topic> is the documented way an agent discovers how to run a command, and it works. This is a human-facing gap.
- The
check case is the mild one. The migration it triggers (packages/cli/src/commands/check.ts:324) is deliberate, documented, idempotent, and gated on .taskless/ already existing. Not a bug on its own.
The reason it matters is that the dispatch path is identical for every subcommand, so taskless init --help should launch the installer rather than describe it. (Not verified by running it, for the obvious reason.)
Fix: intercept before dispatch, render with citty's own showUsage
Do not switch to runMain. It is a package deal, and both its exit paths call process.exit() — process.exit(0) on help (dist/index.mjs:439) and process.exit(1) in its own catch (:456). process.exit does not run finally blocks, so adopting it silently deletes the telemetry wrapper at index.ts:160-175 — the block whose comment calls cli_run "the per-invocation denominator: emitted exactly once here, on both success and failure, so no command has to remember to." Every failed invocation would stop reporting. It would also swallow the CLIError / reported handling that exists so a throw site which did not print still produces output and a non-zero exit.
Trading the telemetry denominator for a help flag is not a good trade, and it is the kind of regression no test would catch.
Instead, keep runCommand and intercept --help / -h before dispatch, handing off to citty's exported showUsage:
showUsage and renderUsage are public (export { createMain, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage }). No help text is hand-written — citty still does the formatting.
- The only piece that is not public is
resolveSubCommand, so resolution has to walk positionals through subCommands locally.
- Return normally rather than exiting, so the
finally block still emits cli_run.
Two details the walk has to get right:
- It must hop more than once.
auth (commands/auth.ts:158) and rules (commands/rules.ts:681) define their own subCommands, so taskless auth login --help needs to descend twice. Walk positionals while each one resolves to a child, and render the deepest command that resolved, with its parent.
- A flag value is not a positional. Every command in the CLI aliases
--dir to -d, so the walk must skip the token after -d/--dir — the same rule index.ts:68-73 already applies in hasPositional. Reuse that logic rather than writing a second copy that can drift.
-h is safe to claim: no command defines it. The only alias in the CLI is d (the sole other "h" in the tree is a URL parameter in rules/rule-hash.ts:96, unrelated to argv).
Also
Once --help works at every depth, citty's Use \taskless --help`` line becomes true and can stay. If for any reason this is closed without fixing, that line should be suppressed instead — advertising a flag that runs the command is worse than not having the flag.
Tests
--help and -h on a leaf subcommand print usage and run nothing (assert the command's side effects did not happen, not just that output appeared)
--help on a nested subcommand (auth login) renders that subcommand's usage, not auth's
taskless -d <path> check --help resolves check, not <path>
- the root's existing
--help behavior is unchanged
cli_run telemetry is still emitted for a help invocation
--helpon a subcommand runs the subcommandtaskless check --helpdoes not print help. It runscheck. The same is true of every subcommand —--helpis not recognized as a help request anywhere below the root, so it is parsed as an unknown flag and the command body executes.This was found while quoting real
--helpoutput for the README (#130): askingcheckfor help migrated the repo's own.taskless/scaffold as a side effect.Mechanism
packages/cli/src/index.ts:125calls citty'srunCommand. citty implements--helponly insiderunMain(citty@0.1.6/dist/index.mjs:435);runCommandhas no help branch at all.The root command appears to work only because it hand-rolls the behavior —
index.ts:88-91callsshowUsage(cmd)when the flags present are not init flags. That is the root reimplementing for itself whatrunMainwould have given the whole tree.Why this is a defect and not a design choice
citty appends this line to the usage of any command that has subcommands (
dist/index.mjs:425):So
taskless --help— which does work — instructs the reader to use a flag the CLI does not implement. The CLI advertises its own broken path.Worth separating two things that are easy to conflate:
taskless agent <topic>is the documented way an agent discovers how to run a command, and it works. This is a human-facing gap.checkcase is the mild one. The migration it triggers (packages/cli/src/commands/check.ts:324) is deliberate, documented, idempotent, and gated on.taskless/already existing. Not a bug on its own.The reason it matters is that the dispatch path is identical for every subcommand, so
taskless init --helpshould launch the installer rather than describe it. (Not verified by running it, for the obvious reason.)Fix: intercept before dispatch, render with citty's own
showUsageDo not switch to
runMain. It is a package deal, and both its exit paths callprocess.exit()—process.exit(0)on help (dist/index.mjs:439) andprocess.exit(1)in its own catch (:456).process.exitdoes not runfinallyblocks, so adopting it silently deletes the telemetry wrapper atindex.ts:160-175— the block whose comment callscli_run"the per-invocation denominator: emitted exactly once here, on both success and failure, so no command has to remember to." Every failed invocation would stop reporting. It would also swallow theCLIError/reportedhandling that exists so a throw site which did not print still produces output and a non-zero exit.Trading the telemetry denominator for a help flag is not a good trade, and it is the kind of regression no test would catch.
Instead, keep
runCommandand intercept--help/-hbefore dispatch, handing off to citty's exportedshowUsage:showUsageandrenderUsageare public (export { createMain, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage }). No help text is hand-written — citty still does the formatting.resolveSubCommand, so resolution has to walk positionals throughsubCommandslocally.finallyblock still emitscli_run.Two details the walk has to get right:
auth(commands/auth.ts:158) andrules(commands/rules.ts:681) define their ownsubCommands, sotaskless auth login --helpneeds to descend twice. Walk positionals while each one resolves to a child, and render the deepest command that resolved, with its parent.--dirto-d, so the walk must skip the token after-d/--dir— the same ruleindex.ts:68-73already applies inhasPositional. Reuse that logic rather than writing a second copy that can drift.-his safe to claim: no command defines it. The only alias in the CLI isd(the sole other"h"in the tree is a URL parameter inrules/rule-hash.ts:96, unrelated to argv).Also
Once
--helpworks at every depth, citty'sUse \taskless --help`` line becomes true and can stay. If for any reason this is closed without fixing, that line should be suppressed instead — advertising a flag that runs the command is worse than not having the flag.Tests
--helpand-hon a leaf subcommand print usage and run nothing (assert the command's side effects did not happen, not just that output appeared)--helpon a nested subcommand (auth login) renders that subcommand's usage, notauth'staskless -d <path> check --helpresolvescheck, not<path>--helpbehavior is unchangedcli_runtelemetry is still emitted for a help invocation