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
- 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-than → OlderThan once, centrally.
- An unrecognised flag should be rejected, never silently dropped —
doctor.ps1 should be an
advanced script like its siblings.
- 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.
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
What happened
A sweep of every verb across three contexts (non-git directory, git repo without
.bot, initialisedproject — 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 textverbatim as the splat key, so
--older-thanbecomes the keyolder-than. Binding then requires aparameter or alias literally named
older-than. Whether one exists is per-script and inconsistent —in the same param block:
Confirmed broken, each reproduced twice:
dotbot prune-branches --older-than 90d --dry-runA parameter cannot be found that matches parameter name 'older-than'.dotbot logs --poll-interval-ms 500 --tail 2A parameter cannot be found that matches parameter name 'poll-interval-ms'.dotbot runtime-status --jsonA parameter cannot be found that matches parameter name 'json'.--older-thanmatters beyond cosmetics: it is the only control overprune-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:
Mode 2 — flags silently ignored, producing a false result.
doctor.ps1:14declares a bareparam()with no[CmdletBinding()]and no[Parameter()]attribute, making it a simple script: unrecognised named arguments land in
$argsand are discardedrather than rejected. So the kebab-case form of its own documented parameter is dropped, and doctor
scans the default
./.botwhile reporting on a path the operator never named:A health check that reports PASS for a directory it did not examine is worse than one that errors.
doctor --jsonis swallowed the same way, so a caller asking for JSON receives ANSI-decorated humantext with exit 0. (
--jsonin fact exists only onstatus.ps1.)Mode 3 — no error path sets a non-zero exit code. Every one of these exits 0:
dotbot bogus-verb✗ Unknown command: bogus-verbdotbot run(no name)⚠ Usage: dotbot run <workflow-name> …dotbot workflow run(no name)dotbot run smoke-test --wathc(typo)A parameter cannot be found that matches parameter name 'wathc'.dotbot run smoke-test --watch --poll-interval-ms 100ValidateRangeviolation (min 250)dotbot logs --tail 0ValidateRangeviolation (min 1)dotbot prune-branches --match bogusValidateSetviolationdotbot install contentbin\dotbot.ps1:753-758printsUnknown commandand falls out of theswitchwith noexit 1;Invoke-Run:539and theworkflow runpath do the same for a missing name. Onlydoctor(:740-742)and
runwith a valid workflow (:535-537) propagate a child exit code. Consequentlydotbot prune-branches --older-than 7d && echo okprintsokhaving pruned nothing, and no CI stepcan detect a mistyped dotbot command.
Also confirmed in the sweep:
dotbot helpadvertisesinstall content, whichInvoke-Install:505does not accept (it takesruntime | agent(s) | prompt(s) | skill(s)). Typing it prints usage, exit 0.dotbot helpomitsworkflow scaffold, which exists and works — and whose usage line advertises--Forcein PascalCase, unlike every other documented flag.dotbot tasks runsilently drops all flags:Invoke-Tasks:546-549calls& $scriptwith nosplat at all, so
dotbot tasks run --jsonis accepted and ignored.dotbot studiois unreachable::702resolves$DotbotBase\studio-uibut the directory issrc\studio-ui, so it always prints✗ Studio not found.— and its remedy,⚠ Run 'dotbot update' to install the studio, cannot help, becausedotbot updateonly printsgit pull.profilesis an undocumented alias forlist.What you expected
[Alias()]on every multi-word parameter(
OlderThan,PollIntervalMs,BotRoot, …) or normaliseConvertTo-SplatArgto map--older-than→OlderThanonce, centrally.doctor.ps1should be anadvanced script like its siblings.
failure, validation failure. Errors should also be reported through the theme helpers rather than
surfacing raw PowerShell stack frames.
dotbot helpshould match reality: dropinstall content, addworkflow scaffold.Steps to reproduce
Environment
Severity
medium
Logs / screenshots
Sweep totals, identical in the non-git and no-
.botcontexts and near-identical in a live project:Usage:Unknown command.botFull per-invocation records (args, exit code, duration, output) are in the attached CSVs; complete
untruncated output per invocation is in the matching
.txtfiles.Every multi-word CLI parameter and whether it carries the kebab alias the dispatcher needs: