Skip to content

CLI: documented flags are unbindable or silently ignored, and no error path exits non-zero #684

Description

@EnmaJim

What happened

A sweep of every verb across three contexts (non-git directory, git repo without .bot, initialised
project — 168 invocations) found three distinct flag-handling failure modes. All of them exit 0.

Mode 1 — real flags that fail with a raw PowerShell error.

ConvertTo-SplatArg (bin\dotbot.ps1:182-190) strips leading dashes and uses the flag text
verbatim as the splat key, so --older-than becomes the key older-than. Binding then requires a
parameter or alias literally named older-than. Whether one exists is per-script and inconsistent —
in the same param block:

# prune-branches.ps1:22-28
[string]$OlderThan = '30d',                        # <-- no alias  => --older-than FAILS
[ValidateSet('workflow','task','all')]
[string]$Match = 'all',                            #     no hyphen => --match works
[Alias('dry-run')][switch]$DryRun,                 # <-- alias     => --dry-run works
[Alias('include-remote')][switch]$IncludeRemote,   # <-- alias     => works

Confirmed broken, each reproduced twice:

Command Error Exit
dotbot prune-branches --older-than 90d --dry-run A parameter cannot be found that matches parameter name 'older-than'. 0
dotbot logs --poll-interval-ms 500 --tail 2 A parameter cannot be found that matches parameter name 'poll-interval-ms'. 0
dotbot runtime-status --json A parameter cannot be found that matches parameter name 'json'. 0

--older-than matters beyond cosmetics: it is the only control over prune-branches' 30-day window,
so run debris cannot be cleaned through the documented CLI at all (see #683).

What the operator sees is a stack frame inside dotbot's own dispatcher:

prune-branches.ps1: C:\projects\dotbot\dotbot\bin\dotbot.ps1:747
Line |
 747 |  … anches" { & (Join-Path $ScriptsDir 'prune-branches.ps1') @SplatArgs }
     |                                                             ~~~~~~~~~~
     | A parameter cannot be found that matches parameter name 'older-than'.

Mode 2 — flags silently ignored, producing a false result.

doctor.ps1:14 declares a bare param() with no [CmdletBinding()] and no [Parameter()]
attribute, making it a simple script: unrecognised named arguments land in $args and are discarded
rather than rejected. So the kebab-case form of its own documented parameter is dropped, and doctor
scans the default ./.bot while reporting on a path the operator never named:

> dotbot doctor --bot-root C:\does\not\exist
    Project: tanda                     <- scanned the wrong directory
  PASS: 8    WARN: 0    FAIL: 0
  exit=0

> dotbot doctor -BotRoot C:\does\not\exist
    Project: not
  ✗ .bot directory not found at: C:\does\not\exist
  exit=2                               <- correct, via the PowerShell-native spelling

A health check that reports PASS for a directory it did not examine is worse than one that errors.
doctor --json is swallowed the same way, so a caller asking for JSON receives ANSI-decorated human
text with exit 0. (--json in fact exists only on status.ps1.)

Mode 3 — no error path sets a non-zero exit code. Every one of these exits 0:

Invocation Output
dotbot bogus-verb ✗ Unknown command: bogus-verb
dotbot run (no name) ⚠ Usage: dotbot run <workflow-name> …
dotbot workflow run (no name) usage
dotbot run smoke-test --wathc (typo) A parameter cannot be found that matches parameter name 'wathc'.
dotbot run smoke-test --watch --poll-interval-ms 100 ValidateRange violation (min 250)
dotbot logs --tail 0 ValidateRange violation (min 1)
dotbot prune-branches --match bogus ValidateSet violation
dotbot install content usage

bin\dotbot.ps1:753-758 prints Unknown command and falls out of the switch with no exit 1;
Invoke-Run:539 and the workflow run path do the same for a missing name. Only doctor (:740-742)
and run with a valid workflow (:535-537) propagate a child exit code. Consequently
dotbot prune-branches --older-than 7d && echo ok prints ok having pruned nothing, and no CI step
can detect a mistyped dotbot command.

Also confirmed in the sweep:

  • dotbot help advertises install content, which Invoke-Install:505 does not accept (it takes
    runtime | agent(s) | prompt(s) | skill(s)). Typing it prints usage, exit 0.
  • dotbot help omits workflow scaffold, which exists and works — and whose usage line advertises
    --Force in PascalCase, unlike every other documented flag.
  • dotbot tasks run silently drops all flags: Invoke-Tasks:546-549 calls & $script with no
    splat at all, so dotbot tasks run --json is accepted and ignored.
  • dotbot studio is unreachable: :702 resolves $DotbotBase\studio-ui but the directory is
    src\studio-ui, so it always prints ✗ Studio not found. — and its remedy, ⚠ Run 'dotbot update' to install the studio, cannot help, because dotbot update only prints git pull.
  • profiles is an undocumented alias for list.

What you expected

  1. A documented flag should bind. Either add the kebab-case [Alias()] on every multi-word parameter
    (OlderThan, PollIntervalMs, BotRoot, …) or normalise ConvertTo-SplatArg to map
    --older-thanOlderThan once, centrally.
  2. An unrecognised flag should be rejected, never silently dropped — doctor.ps1 should be an
    advanced script like its siblings.
  3. Every error path should exit non-zero: unknown verb, missing required argument, parameter-binding
    failure, validation failure. Errors should also be reported through the theme helpers rather than
    surfacing raw PowerShell stack frames.
  4. dotbot help should match reality: drop install content, add workflow scaffold.

Steps to reproduce

$env:DOTBOT_HOME = '<dotbot checkout>'
$db = "$env:DOTBOT_HOME\bin\dotbot.ps1"
cd <any initialised dotbot project>

# Mode 1 — unbindable documented flags, all exit 0
& $db prune-branches --older-than 90d --dry-run ; "exit=$LASTEXITCODE"
& $db logs --poll-interval-ms 500 --tail 2      ; "exit=$LASTEXITCODE"
& $db runtime-status --json                     ; "exit=$LASTEXITCODE"

# Mode 2 — silently ignored, false PASS on a directory that does not exist
& $db doctor --bot-root C:\does\not\exist       ; "exit=$LASTEXITCODE"   # PASS 8, exit 0
& $db doctor -BotRoot   C:\does\not\exist       ; "exit=$LASTEXITCODE"   # correct failure, exit 2

# Mode 3 — no non-zero exit anywhere
& $db bogus-verb                                ; "exit=$LASTEXITCODE"
& $db run                                       ; "exit=$LASTEXITCODE"
& $db run smoke-test --wathc                    ; "exit=$LASTEXITCODE"
& $db logs --tail 0                             ; "exit=$LASTEXITCODE"
& $db install content                           ; "exit=$LASTEXITCODE"
& $db studio                                    ; "exit=$LASTEXITCODE"

Environment

OS: Windows 11 Pro 10.0.26200 | dotbot v4.0.2 (main @ 7c95b466)
pwsh 7.6.4 | git 2.54.0.windows.1
DOTBOT_HOME set explicitly; %APPDATA%\dotbot\user-settings.json absent
Contexts swept: non-git directory; git repo without .bot; initialised project (1129-file repo)

Severity

medium

Logs / screenshots

Sweep totals, identical in the non-git and no-.bot contexts and near-identical in a live project:

Context Invocations exit≠0 Raw PS errors exit 0 with Usage: exit 0 with Unknown command
non-git 57 4 8 13 2
git, no .bot 57 4 8 13 2
initialised project 54 1 8 13 2

Full per-invocation records (args, exit code, duration, output) are in the attached CSVs; complete
untruncated output per invocation is in the matching .txt files.

Every multi-word CLI parameter and whether it carries the kebab alias the dispatcher needs:

MISSING the alias (--flag hard-fails):
  prune-branches.ps1  OlderThan        --older-than
  logs.ps1            PollIntervalMs   --poll-interval-ms
  doctor.ps1          BotRoot          --bot-root          (silently ignored instead: lax param())
  serve.ps1           MothershipApiKey --mothership-api-key   (has 'mothership-key' only)
  install-content.ps1 GlobalInstall    --global-install       (has 'global' only)

HAS the alias (works):
  init-project.ps1    CopyRuntime      --copy-runtime
  init-project.ps1    DryRun           --dry-run
  prune-branches.ps1  DryRun           --dry-run
  prune-branches.ps1  IncludeRemote    --include-remote

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status
    Inbox

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions