Add 6 undocumented test files to the test index README:
- do-payment-warning.test.ts (Cloud-specific)
- sprite-keep-alive.test.ts (Cloud-specific)
- history-corruption.test.ts (Infrastructure)
- paths.test.ts (Infrastructure)
- fs-sandbox.test.ts (Infrastructure)
- picker.test.ts (Parsing and type utilities)
Also remove duplicate manifest-cache-lifecycle.test.ts entry
that appeared in both Core manifest and Infrastructure sections.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The 'create a spawn first' message was shown even when active servers
existed but none matched the filter. Now shows 'Run spawn delete without
filters to see all servers.' for the unmatched-filter case and reserves
the create hint for when no servers exist at all.
Fixes#2454
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Security: the manifest-derived fallback path in connect.ts bypassed the
validateLaunchCmd() allowlist that guards history-derived commands. A
malicious or modified manifest.json cache could inject arbitrary commands
executed on the remote VM via SSH.
Fixes#2453
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Instead of telling users to pipe through `spawn list | cat` to view their
spawn history, render the history table inline when no active connections
exist. The | cat workaround was needed because non-interactive mode skips
the picker; now interactive mode falls through to renderListTable directly,
consistent with what `spawn list | cat` was already doing.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
OpenClaw runs a web dashboard on port 18791 of the remote VM. This
change SSH-tunnels that port to localhost and auto-opens the browser,
giving users a web UI with zero CLI knowledge needed.
- Add TunnelConfig to AgentConfig interface (agents.ts)
- Add startSshTunnel function with port-finding logic (ssh.ts)
- Capture gateway token in closure so the same token is used for both
the remote config and the browser URL (agent-setup.ts)
- Wire tunnel into orchestration pipeline between preLaunch and
interactiveSession (orchestrate.ts)
- Add getConnectionInfo to CloudOrchestrator interface and implement
in all SSH-based clouds (DO, Hetzner, AWS, GCP)
- Local: opens browser directly at localhost:18791
- Sprite: gracefully skipped (no standard SSH)
- Add USER.md bootstrap to guide OpenClaw users to web dashboard
Closes#2449
Supersedes #2418
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
AWS and GCP both include $HOME/.npm-global/bin and $HOME/.claude/local/bin in the
PATH exported before running remote commands. Hetzner and DO were missing these two
entries, causing "command not found" errors for Claude Code and npm-global packages
on those clouds.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
All four SSH-based cloud drivers (aws, digitalocean, gcp, hetzner)
passed the command string directly as an SSH argument, which gets
interpreted by the remote shell. While current callers pass trusted
E2E test code, this creates a security footgun for future changes.
Fix: base64-encode the command locally and decode it on the remote
side before piping to bash. The encoded string contains only safe
characters [A-Za-z0-9+/=], eliminating any injection vector. Stdin
is preserved for callers that pipe data into cloud_exec.
Closes#2432, closes#2433, closes#2434, closes#2435
Agent: complexity-hunter
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
- Replace word-split _sprite_org_flags() call sites with _sprite_cmd()
helper that uses a proper bash array for the -o flag, eliminating
injection risk from org names with spaces or shell metacharacters
- Validate _SPRITE_ORG against [A-Za-z0-9_-]+ in _sprite_validate_env
- Use grep -qF (fixed-string) instead of grep -q for app name matching
to prevent regex metacharacters in names from causing false matches
- Use mktemp for _stderr_tmp in _sprite_exec instead of predictable
PID-based path (/tmp/sprite-exec-err.$$) to prevent symlink attacks
Closes#2436
Agent: complexity-hunter
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
- Validate app_name at function entry (alphanumeric, dots, hyphens, underscores
only) before it's used in file paths or passed to cloud_exec
- Add trap-based cleanup for the temp file used during .spawnrc fallback creation
- Add security comments documenting the three-layer defense model: printf %q
quoting, base64 encoding, and stdin piping (no interpolation into command
strings)
The core vulnerability (env_b64 interpolated into the cloud_exec command string)
was already fixed in a prior commit that switched to stdin piping. This change
adds defense-in-depth and documentation.
Fixes#2437, #2441
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
install.sh: Replace color variable interpolation in printf format strings
with %b arguments to prevent format string injection (fixes#2443).
common.sh: Use %b for color escapes in logging functions. Document that
BASH_SOURCE and source usage in load_cloud_driver is intentional since
e2e scripts are filesystem-only, not curl|bash (fixes#2438).
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add defense-in-depth validation across all e2e cloud driver scripts:
- Validate IP addresses match IPv4 format before use in SSH commands
(aws, digitalocean, gcp, hetzner)
- Validate SSH username contains only safe characters (gcp)
- Validate resource IDs are numeric before interpolating into API URLs
(digitalocean droplet IDs, hetzner server IDs)
- URL-encode app name in Hetzner API query parameter to prevent
query parameter injection
- Validate numeric env vars (INPUT_TEST_TIMEOUT, PROVISION_TIMEOUT,
INSTALL_WAIT) that get interpolated into remote command strings
Fixes#2432, #2433, #2434, #2435, #2442
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
status.ts passed server_id from history directly into Hetzner/DO API
URLs without calling validateServerIdentifier(). Both delete.ts and
connect.ts validate first; status.ts was the only gap. A tampered
~/.spawn/history.json could craft a server_id with path traversal
characters (e.g. "../v2/account") causing the Bearer token to be
sent to an unintended API endpoint (SSRF via URL path manipulation).
Fix: call validateServerIdentifier() after extracting serverId,
returning "unknown" gracefully on failure.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The validate-file.ts hook previously only blocked `set -u` when
`set -eo pipefail` was absent from the file. This allowed scripts
with both `set -eo pipefail` and `set -u` to pass validation,
contradicting the shell rules that unconditionally ban nounset.
Fix the regex to always reject `set -u` variants on actual set
invocation lines (not comments or strings), and update the error
message to recommend `${VAR:-}` instead.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
These path-utility tests were duplicated between history.test.ts and
paths.test.ts. Consolidate into paths.test.ts (the canonical location)
and move 4 unique test cases (dot-relative path, .. resolution, outside
home rejection, home-as-SPAWN_HOME) that only existed in history.test.ts.
Removes 64 lines of duplicate test code with zero coverage loss.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Without per-process timeouts, if the user's network drops during
cloud-init polling, the CLI hangs forever while billing continues.
Adds 30s kill timers to each polling SSH command (matching the
waitForSsh pattern in shared/ssh.ts) and 330s to DO's streaming SSH.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds sprite-keep-running support so sprites stay alive during long
agent sessions instead of shutting down due to inactivity.
- Add installSpriteKeepAlive() to sprite/sprite.ts: downloads and
installs the sprite-keep-running script (~/.local/bin) on the sprite
during setup. Non-fatal: logs a warning if download fails so
deployment still proceeds.
- Modify interactiveSession() to wrap the session command in a temp
script (base64-encoded to handle multi-line restart loops) and exec
it via sprite-keep-running if available, with plain bash fallback.
- Call installSpriteKeepAlive() in sprite/main.ts createServer() step
after setupShellEnvironment(), applying to all Sprite agents.
- Add sprite-keep-alive.test.ts: 11 unit tests covering download URL,
install path, error resilience, session script structure, and
keep-alive wrapper inclusion.
Fixes#2424
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: set SPAWN_HOME in preload and add fs-sandbox guardrail test
The test preload now sets SPAWN_HOME to the sandbox directory by default,
so tests that call cmdRun/saveSpawnRecord without explicitly setting
SPAWN_HOME no longer write to the real ~/.spawn/history.json.
Add fs-sandbox.test.ts that verifies the sandbox is correctly configured
(HOME, SPAWN_HOME, XDG vars all point to temp). Update testing.md with
mandatory filesystem isolation rules.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: add root bunfig.toml and fix biome formatting
Add root-level bunfig.toml with test preload so `bun test` works from
the repo root. Fix biome formatting in orchestrate.test.ts afterEach.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
Move all filesystem path helpers (getUserHome, getSpawnDir, getHistoryPath,
getSpawnCloudConfigPath, getCacheDir, getCacheFile, getUpdateFailedPath,
getSshDir, getTmpDir) into a single shared/paths.ts module. This eliminates
scattered homedir()/process.env.HOME patterns across 8+ files and provides
a single import source for all path resolution.
- Create packages/cli/src/shared/paths.ts with 9 exported functions
- Update 17 source files to import from paths.ts
- Add re-exports in ui.ts and history.ts for backward compatibility
- Remove direct homedir() imports from gcp, sprite, local, ssh-keys, etc.
- Add comprehensive unit tests in paths.test.ts
- Bump CLI version to 0.15.34
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The manifest was updated to moonshotai/kimi-k2.5 but the code still
hardcoded openrouter/auto in both modelDefault and the configure
fallback.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Bun's os.homedir() reads from getpwuid() and ignores runtime changes to
process.env.HOME. Named imports capture the native function binding, so
patching os.homedir on the default export doesn't propagate. This caused
all test files using homedir() to write .spawn-test-* dirs to the real
home directory instead of the preload sandbox.
Add getUserHome() helper to shared/ui.ts that prefers process.env.HOME,
replace all direct homedir() calls in production and test code.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add @commitlint/cli and @commitlint/config-conventional at repo root
- Configure commitlint with project-specific types (security, etc.)
- Set up Husky v9 with commit-msg hook running commitlint
- Add pre-commit hook running biome check on CLI source
Fixes#2406
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The "recovers from corrupted existing history file and creates backup"
test was a subset of the more thorough coverage in
history-corruption.test.ts. Removed the duplicate and its unused
readdirSync import.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Consolidates duplicate server naming logic from 5 cloud modules into shared utilities in src/shared/ui.ts. No behavioral changes - purely structural refactor.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* security: escape pkill regex metacharacters in app_name
Fixes#2409 - escape regex metacharacters (., [, \, *, ^, $) in
app_name before using in pkill -f pattern to prevent unintended
process termination. Even though app_name is validated against a
safe character whitelist, . and - are regex metacharacters that
could match broader patterns than intended.
Note: #2410 (unquoted regex in bash conditional) was already fixed
by a prior commit that refactored the code to use sed instead of
[[ =~ BASH_REMATCH ]].
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: remove dead exec_long functions reintroduced from pre-#2407 code
Remove cloud_exec_long dispatcher and all _*_exec_long() functions
from common.sh and cloud driver files (aws, digitalocean, gcp,
hetzner, sprite). These were explicitly removed as dead code in
PR #2407 (commit c4ae1684) and must not be reintroduced.
Issue #2410 (unquoted regex in bash conditional) is already resolved:
the [[ =~ ]] pattern was previously replaced with case/sed parsing.
Fixes#2409Fixes#2410
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The worktree path regex in pre-merge-check.ts used [^\s/]+ which only
matched a single path segment after /tmp/spawn-worktrees/. This blocked
PR merges from nested worktrees like refactor/fix/issue-N used by the
automated refactoring service.
Fix both the TypeScript regex ([^\s/]+ -> [^\s]+) and the inline bash
grep pattern in settings.json ([a-zA-Z0-9._-]+ -> [a-zA-Z0-9._/-]+).
Closes#2401
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The cloud_exec_long dispatcher in common.sh and all five cloud-specific
_exec_long implementations (aws, digitalocean, gcp, hetzner, sprite)
were defined but never called by any code in the e2e test suite.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Before creating symlinks in /usr/local/bin, verify that any existing
symlink points to a safe location ($HOME/.local/*, $HOME/.bun/*,
/usr/local/*, $HOME/.npm-global/*). If a symlink points to an
unexpected location, warn the user and skip to prevent malicious
symlink persistence through reinstalls.
Uses portable `readlink` (without -f) for macOS bash 3.2 compatibility.
Fixes#2402
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Add logDebug() function gated on SPAWN_DEBUG=1 for surfacing error
details without cluttering normal output. Refactor 6 silent/overly-broad
catch blocks:
- agent-tarball.ts: split 70-line try into fetch+parse and remote exec
- update-check.ts: remove outer try, wrap only performAutoUpdate
- history.ts: add warnings to swallowed tryCatch results
- oauth.ts: warn when API key save fails
- orchestrate.ts: warn on checkAccountReady and preProvision failures
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: restore @openrouter/spawn-shared workspace package
Restore packages/shared/ as canonical location for parse.ts, result.ts,
and type-guards.ts. CLI shared files become thin re-exports, preserving
all existing import paths. SPA imports switch from fragile relative paths
to the workspace package.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sort exports in shared package barrel to satisfy biome
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sort SPA imports to satisfy biome organizeImports
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show a proactive warning before the OAuth/token entry flow when the user
has no saved DigitalOcean config and no DO_API_TOKEN env var. This prevents
new users from completing the full setup flow only to fail at provisioning
because their account has no payment method on file.
Warning is shown only once per first-time setup — returning users (who have
a saved token, even if expired or invalid) skip the reminder.
Closes#2395
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
After SSH reconnect, agent commands (openclaw, codex, kilocode, junie) were
not found because PATH was only written to ~/.bashrc, which is not sourced
by login shells. Login shells (used by SSH) source ~/.profile or
~/.bash_profile instead.
Changes:
- Write .spawnrc sourcing to ~/.profile and ~/.bash_profile in addition
to ~/.bashrc and ~/.zshrc (orchestrate.ts)
- Write npm-global PATH export to ~/.profile and ~/.bash_profile for all
npm-installed agents: OpenClaw, Codex, Kilo Code, Junie (agent-setup.ts)
- Write Claude Code PATH to ~/.profile and ~/.bash_profile (agent-setup.ts)
- Write OpenCode PATH to ~/.profile and ~/.bash_profile (agent-setup.ts)
- Extract NPM_GLOBAL_PATH_PERSIST constant to DRY up repeated shell snippets
- Fix e2e provision.sh to also write .spawnrc sourcing to login shell configs
- Bump CLI version to 0.15.32
Fixes#2394
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Reword preflight OpenRouter credential message to not imply it happens
immediately (cloud auth runs first in the orchestration pipeline)
- Clarify GitHub CLI setup messages to specify "remote server" instead of
leaving ambiguous "this machine" context for cloud users
Fixes#2396
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The orchestrate test suite called runOrchestration (which internally
calls saveSpawnRecord) without setting SPAWN_HOME to a temp directory.
Every test run wrote ~20 fake records into the user's real history,
eventually filling it with 100 connectionless "testagent" entries
and wiping all real spawn history.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: graceful recovery from corrupted history.json
- Atomic writes (write to .tmp, rename into place) to prevent corruption
- Backup corrupted files with .corrupt suffix before discarding
- Per-record salvaging: if some v1 records are malformed, keep the valid ones
- Archive recovery: when history.json is corrupted, try loading from archives
- Stderr warnings when corruption is detected or records are recovered
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: replace try/catch with Result tryCatch wrapper in history.ts
Add tryCatch() to shared/result.ts and use it throughout history.ts to
eliminate all 7 try/catch blocks. Errors are now handled via Result
pattern matching instead of exception control flow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
When both server_id and server_name are missing from a connection record,
serverId falls back to "". Passing "" to fetchHetznerStatus/fetchDoStatus
constructs URLs like /v1/servers/ (list all), wasting rate-limit quota and
sending auth tokens to the wrong endpoint. Early-return "unknown" instead.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The two-phase save architecture was fundamentally broken: saveVmConnection()
was called inside createServer() BEFORE saveSpawnRecord() created the record,
so the merge-by-spawnId silently failed every time — resulting in records
with no connection data and `spawn ls` showing nothing.
Replace with atomic single-save: createServer() now returns VMConnection,
and the orchestrator calls saveSpawnRecord() once with connection data
included. Removes saveVmConnection(), getConnectionPath(),
mergeLastConnection(), and last-connection.json entirely.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
The TTY key loop treated explicit user cancellation (ESC/Ctrl-C) the same
as a TTY failure — both called fallback() which renders a numbered-list
picker. Now the key loop distinguishes between the two: cancel() exits
cleanly, fallback() is only used when /dev/tty is unavailable.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace 30+ individual it() blocks that each tested a single typo input
with data-driven loops using arrays of test cases. Same coverage, less
boilerplate. Reduces check-entity.test.ts from 401 to 330 lines.
Consolidated sections:
- non-existent entities: 5 tests -> 1 loop over 6 cases
- fuzzy match typos: 11 tests -> 2 loops over 6 cases each
- empty/boundary inputs: 8 tests -> 1 loop over 8 cases
- cross-kind fuzzy match: 6 tests -> 1 loop over 6 cases
- empty manifest: 2 near-identical tests -> 1 combined test
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
DO default was s-2vcpu-4gb which isn't available in nyc3, causing 422
errors. Changed to s-2vcpu-2gb to match manifest.json. Also aligned
Hetzner default location from nbg1 to fsn1 to match manifest.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Merge security-edge-cases.test.ts and security-encoding.test.ts into
security.test.ts. Move stripDangerousKeys tests to manifest.test.ts
(where the function is defined). All 1447 tests pass, zero regressions.
-- qa/dedup-scanner
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
- Remove unused multiPickToTTY function, MultiPickOption interface, and
MultiPickConfig interface from picker.ts (never called anywhere)
- Remove export keyword from 7 internal-only functions in commands/shared.ts
that are used within the file but never imported externally:
getEntityCollection, getEntityKeys, formatAuthVarLine,
hasCloudConfigCredentials, getCredentialGuidance,
checkAllCredentialsReady, printAuthVariableStatus
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
On headless VMs there's no Chrome extension to attach to. Setting
defaultProfile to "openclaw" tells OpenClaw to launch and manage
the browser itself via CDP instead of waiting for an extension relay.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
`spawn status` silently ignored -a and -c flags, showing all servers
regardless. This is inconsistent with `spawn list` and `spawn delete`
which both support these filters.
- Update `cmdStatus` to accept `agentFilter`/`cloudFilter` options and
pass them to `filterHistory()`
- Update `dispatchStatusCommand` to parse filter flags using the shared
`parseListFilters` helper (same as list/delete)
- Document filter flags in help text for `spawn status`
- Bump version to 0.15.27
Fixes#2377
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The `_maxAttempts` parameter in both Hetzner and DigitalOcean's
`waitForCloudInit()` was silently ignored — loop bounds and early-exit
checks were hardcoded. Rename to `maxAttempts` and use it consistently,
matching the AWS/GCP implementations.
Fixes#2378
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>