* feat: wrap cloud VM sessions in tmux for session persistence
- Ctrl+C exits the agent → user lands at a shell prompt (can run CLI commands)
- SSH disconnect → tmux session persists, `spawn last` reattaches
- Install tmux automatically during env setup if not present
- Reconnect flow (`spawn last`, `spawn enter`) also uses tmux attach
- Replaces the restart loop — tmux gives users control over restarts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: auto-tunnel gateway dashboard port over SSH
Forward port 18789 (OpenClaw gateway dashboard) to localhost so users
can access http://localhost:18789 from their browser during SSH sessions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review — command injection, port forwarding, tmux install order
1. wrapWithTmux: escape backslashes, $, and backticks in addition to
double quotes to prevent command injection via tmux send-keys
2. SSH port forwarding: remove unconditional -L 18789 tunnel from
SSH_INTERACTIVE_OPTS; export SSH_TUNNEL_OPTS for agent-specific use
3. tmux install: try sudo apt-get first (most cloud VMs need it on AWS)
Co-Authored-By: Claude Opus 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>
Ubuntu 24.04 replaced chromium-browser with a snap redirect that fails
on cloud VMs without snapd. Playwright's bundled Chromium is
self-contained (~170MB), works headless, and has no snap dependency.
Installed as a non-fatal post-install step — if it fails, the agent
still works but without browser capabilities.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The startup script temp file was cleaned up immediately after the first
gcloud call, but the billing retry path re-used the same args array
referencing that file. This meant billing retries always failed with a
file-not-found error. Move cleanup to a try/finally block that runs
after all retry paths. Also add randomness and mode 0o600 to the temp
file path.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Previously .spawnrc only exported env vars (API keys). The PATH entries
for agent binaries (~/.npm-global/bin, ~/.bun/bin, etc.) were only set
in per-agent launch commands, so reconnecting via SSH left users with
"command not found" errors.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Move the PkgVersionSchema (v.object({ version: v.string() })) from its
duplicate definitions in commands/shared.ts and update-check.ts into the
shared parse module. Both consumers now import from the single source.
Bump CLI version to 0.15.22.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
- Remove unused `getBillingUrl()` and `getSetupSteps()` from billing-guidance.ts
(only called by their own tests, never by production code)
- Remove unused `validateModelId()` from ui.ts (same — test-only, no callers)
- Remove stale daytona entries from billing-guidance data structures
(daytona is not in manifest.json and has no cloud module)
- Update tests README with 3 undocumented test files
- Remove corresponding dead test cases
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace 4 inline `err instanceof Error ? err.message : String(err)`
patterns in aws.ts, digitalocean.ts, and hetzner.ts with the shared
getErrorMessage() helper. The shared helper uses duck-typing which is
more robust across realms/prototypes than instanceof checks.
Export OAUTH_CSS from shared/oauth.ts and import it in
digitalocean/digitalocean.ts instead of duplicating the 250+ char
CSS string.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
hasMessage was exported from shared/type-guards.ts but never imported
outside of its own test file. getErrorMessage already covers the
message-extraction use case. Remove the dead function and its tests.
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Moves getErrorMessage to zero-dep shared module, eliminating 13 inline
copies and 2 hasMessage variant sites across the codebase.
Fixes#2341
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
PR #2335 fixed this bug in digitalocean.ts, gcp.ts, and aws.ts but
missed hetzner.ts. The billing retry block assigned serverId/serverIp
to undefined local variables (hetznerServerId, hetznerServerIp) instead
of _state.serverId / _state.serverIp, so the retry always threw
"Server creation failed" even when the API call succeeded. This also
adds the missing saveVmConnection() call in the retry success path so
the VM is recorded in spawn history.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Five exported, production-used functions had zero direct test coverage:
- generateEnvConfig (security-critical env var validation/escaping)
- toRecord, toObjectArray, hasStatus, hasMessage (type narrowing)
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Five undefined variable references across three cloud modules caused
billing retry paths to silently fail:
- digitalocean: doToken, doDropletId, doServerIp → _state.token/dropletId/serverIp
- gcp: gcpProject → _state.project
- aws: instanceName → _state.instanceName
These caused checkAccountStatus() and checkBillingEnabled() to always
return early, and billing retry saves to use wrong/undefined values.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Remove `export` from `verifyOpenrouterKey` in shared/oauth.ts (only used internally)
- Remove `export` from `tcpCheck` in shared/ssh.ts (only used internally)
- Fix stale comment in commands/index.ts referencing non-existent `./commands.js`
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Tests were failing because getActiveServers() found real history
records in ~/.spawn/history.json, causing an extra p.select() call
that shifted the mock prompt index and made manifest.agents[agent]
resolve to undefined.
Set SPAWN_HOME to an isolated directory in beforeEach so tests
always see an empty history regardless of host state.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: update cloud picker prompt to "Pick your cloud"
The previous "Where should your agent run?" was vague. Simplify to
"Pick your cloud (type to filter)" for clarity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use "Select a cloud" for cloud picker prompt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cherry-picks UX improvements from #2321: simplifies cloud descriptions
to plain language, adds account/payment requirements upfront so users
know what they need before starting.
Fixes#2323
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: reorder auth flow and persist OpenRouter API key across retries
Two onboarding issues reported by users:
1. After DigitalOcean OAuth, the message said "OpenRouter authentication
in 5s..." but then a GitHub CLI prompt appeared first. Fix: move API
key acquisition immediately after cloud auth, before preProvision
hooks (which include the GitHub prompt). Remove the misleading 5s
delay message.
2. On retry after billing failure, DigitalOcean token was remembered but
the OpenRouter API key was lost (only stored in process.env). Fix:
persist the key to ~/.config/spawn/openrouter.json and load it on
subsequent runs, matching how cloud tokens are already persisted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add mode 0o700 to config dir and await saveOpenRouterKey
- Add mode: 0o700 to mkdirSync in saveOpenRouterKey to match other cloud
modules (aws, hetzner, digitalocean) and prevent directory permission leak
- Add missing await on saveOpenRouterKey(manualKey) to ensure manual API
keys persist to disk before the function returns
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
The runServerCapture function was defined in aws, hetzner, gcp, and
digitalocean modules but never called anywhere in the codebase. All
cloud modules use runServer (which streams to stderr) and the
CloudRunner interface only requires runServer, not runServerCapture.
Bump CLI version 0.15.14 → 0.15.15.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
New users don't know which SSH key to pick. Just use all discovered
keys silently (ed25519 sorted first). If none exist, generate one.
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
New users don't know what LLM models are — prompting them to pick one
with no context is confusing and openrouter/auto can route to weak
models. Remove the interactive model prompt entirely; agents use their
modelDefault silently (or MODEL_ID env var for power users).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Detect billing-related server creation errors, open the cloud's billing
page in the browser, and prompt the user to retry after adding a payment
method. Adds pre-flight account checks for DigitalOcean (account status)
and GCP (billing enabled).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Auto-detect GitHub credentials (GITHUB_TOKEN env var or `gh auth token`)
instead of interactively asking users. Rename promptGithubAuth → detectGithubAuth.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(cli): show connect-or-create menu when existing spawns are present
When the user runs `spawn` with no arguments and has active servers in
history, display a top-level menu before jumping into the create flow:
What would you like to do?
❯ Connect to existing server
Create a new server
Selecting "Connect to existing server" opens the same interactive picker
as `spawn list` (activeServerPicker). Selecting "Create a new server" or
having no existing spawns continues with the current create flow, so
there is no behaviour change for first-time users.
Fixes#2308
Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* chore(cli): bump version to 0.15.14
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>
Remove tests that verify JavaScript language semantics rather than
application logic. These tests would pass even if the source code
were deleted:
- 18 isValidManifest tests (JS truthiness of null, 0, false, "", [])
- 7 matrixStatus edge cases (Object property lookup with hyphens,
underscores, empty strings, long keys)
- 5 agentKeys/cloudKeys ordering tests (Object.keys insertion order,
an ES2015 spec guarantee)
- 3 countImplemented tests (for-loop over 1000 items, single entry,
non-standard statuses)
Kept 17 tests that exercise real application behavior: cache corruption
recovery, HTTP error fallback, in-memory cache, fallback chains, and
countImplemented case-sensitivity.
Closes#2315
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Interactive picker: add blank separator line between entries so label
and subtitle are visually grouped (not blending into adjacent entries)
- Non-interactive table: wrap subtitle in pc.dim() for better contrast
with the bold entry name
- Update pickerHeight to account for added separator lines
Fixes#2309
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Three distinct E2E bugs fixed:
1. SSH key generation race condition: When multiple agents provision in
parallel, concurrent processes all call generateSshKey() and race to
create ~/.ssh/id_ed25519. ssh-keygen won't overwrite an existing file
(prompts on stdin which is "ignore"), causing zeroclaw/codex to fail
with "SSH key generation failed". Fix: check if key already exists
before generating, and re-check after a failed generation attempt.
2. Hetzner SSH key 409 uniqueness_error: The Hetzner API returns HTTP 409
with "SSH key not unique" when the same key content is registered under
a different name. The hetznerApi() function throws on non-2xx before
the error-parsing code runs, and the regex /already/ didn't match
"not unique". Fix: catch 409 in ensureSshKey() and match against
uniqueness_error/not unique/already patterns.
3. Hermes binary not found: The hermes install script (uv tool) creates
the actual binary + venv at ~/.hermes/hermes-agent/venv/ with a symlink
at ~/.local/bin/hermes. The tarball capture script only captured the
symlink + ~/.local/share/, leaving a dangling symlink. Fix: include
~/.hermes/ in capture paths, add venv/bin to verify.sh PATH check,
and update hermes launchCmd to include the venv PATH.
Fixes#2304
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Tests for getScriptFailureGuidance were failing when cloud credential
env vars (HCLOUD_TOKEN, DO_API_TOKEN) were set in the environment.
The tests expected these vars to appear as "missing" in the output,
but only unset OPENROUTER_API_KEY. Now both the cloud-specific var
and OPENROUTER_API_KEY are saved/unset before each test.
Bump CLI version to 0.15.11.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
The Phase 2 SSH handshake loop in waitForSsh spawns SSH processes
without a per-process timeout. ConnectTimeout=10 only covers TCP
connect — if sshd accepts the connection but stalls during key
exchange or authentication, the process hangs indefinitely. This
causes the entire spawn command to freeze with no way to recover.
Add a 30s killWithTimeout guard to each probe, matching the pattern
already used in every cloud-specific runServer/uploadFile function.
-- refactor/code-health
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
All four SSH-based uploadFile functions (Hetzner, DO, AWS, GCP) used
`await proc.exited` on SCP subprocesses without any timeout guard.
If SCP hangs due to a network issue, the CLI hangs indefinitely.
This adds the same killWithTimeout pattern already used by runServer
and runServerCapture in these same files: a 120-second timeout that
kills the SCP process if it stalls.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
De-export interfaces, types, and constants that are only used within
their own module files. These were exported but never imported by any
other module or test file, unnecessarily widening the public API surface.
Affected symbols:
- aws: AwsState, Region, REGIONS, AGENT_BUNDLE_DEFAULTS
- digitalocean: DigitalOceanState, DropletSize, DROPLET_SIZES, DoRegion, DO_REGIONS
- gcp: GcpState, MachineTypeTier, MACHINE_TYPES, ZoneOption, ZONES
- hetzner: HetznerState, ServerTypeTier, SERVER_TYPES, LocationOption, LOCATIONS
- sprite: SpriteState
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
The PKCE migration TODO referenced closed issue #2041. The TODO
itself is still valid (DigitalOcean still doesn't support PKCE),
so keep the migration checklist but drop the issue number.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
* refactor: remove commands.ts compatibility shim and fix stale references
- Delete packages/cli/src/commands.ts shim file (only re-exported commands/index.ts)
- Update index.ts to import directly from ./commands/index.js
- Update 24 test files to import from ../commands/index.js
- Fix stale CLAUDE.md reference to commands.ts
- Fix stale QA prompt references to commands.ts and wrong line numbers
- Bump CLI version to 0.15.8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: remove stale references to deleted commands.ts compatibility shim
---------
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>
The v0 fallback path in loadHistory() returned raw parsed JSON array
directly without validating individual elements. This could cause
TypeErrors (e.g. r.agent.toLowerCase() on undefined) in callers like
getActiveServers and filterHistory when corrupted entries exist.
Now filters each element through v.safeParse(SpawnRecordSchema, el),
matching the validation the v1 path already performs.
Fixes#2277
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: claude snapshot build — remove npm fallback from install command
The native install (curl | bash) succeeds but exits non-zero due to a
PATH warning. The || fallback then tries `npm install` which doesn't
exist on the "minimal" tier → exit 127.
Fix: replace npm fallback with binary existence check (same pattern
as hermes agent). If install exits non-zero but ~/.local/bin/claude
exists, the build succeeds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: snapshot cleanup and lookup — use name prefix instead of tags
DO Packer builder `tags` only apply to the temporary build droplet,
not the resulting snapshot image. Both the workflow cleanup step and
the CLI's findSpawnSnapshot() were querying by `tag_name` which
returned nothing — old snapshots piled up and the CLI couldn't find
existing snapshots.
Fix: filter by snapshot name prefix (`spawn-{agent}-`) instead of
tags, in both the workflow and the CLI. Remove misleading `tags`
from the Packer template. Add test cases for name-prefix filtering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: remove packages/shared, deduplicate with packages/cli/src/shared
packages/shared duplicated packages/cli/src/shared (parse.ts, result.ts,
type-guards.ts) with the CLI never importing from the shared package.
The only consumer was .claude/skills/setup-spa, which now imports directly
from packages/cli/src/shared via relative paths.
- Delete packages/shared entirely
- Update setup-spa imports to use relative paths to CLI shared
- Remove @openrouter/spawn-shared workspace dependency from setup-spa
- Update CLAUDE.md and type-safety.md references
Agent: complexity-hunter
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: remove packages/shared from lint workflow, fix import sorting
The Biome Lint CI step referenced packages/shared/src/ which no longer
exists after this PR removes the package. Also fix import ordering in
setup-spa files to satisfy Biome's organizeImports rule.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: address Devin review — update stale packages/shared references
- Update type-safety.md line 67: packages/shared/src/parse.ts → packages/cli/src/shared/parse.ts
- Update install.ps1 sparse-checkout: remove packages/shared reference
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>
Remove 5 unused reset*State() exports (aws, hetzner, gcp, digitalocean,
sprite) that were never called anywhere in the codebase. Convert their
associated _state variables from let to const since they are no longer
reassigned.
Remove stale Daytona references in status.ts (comment and IP check)
left over after Daytona cloud provider removal in #2261.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Restores the nightly Packer snapshot build pipeline (reverted in #2205)
that pre-bakes agent images as DigitalOcean snapshots. When a snapshot
exists on the user's account, droplet boot skips cloud-init and tarball
install entirely — cutting provisioning from ~10min to ~2min.
- Add `packer/digitalocean.pkr.hcl` HCL2 template with multi-region
distribution, apt-lock wait, and snapshot marker
- Add `.github/workflows/packer-snapshots.yml` nightly build with
matrix strategy, auto-cleanup of old snapshots, and injection-safe
env var handling
- Add `findSpawnSnapshot()` to query DO API for pre-built snapshots
- Add `waitForSshOnly()` for snapshot boots (skip cloud-init wait)
- Modify `createServer()` to accept optional `snapshotId` param
- Wire snapshot detection in DO `main.ts` orchestrator
- Add `skipAgentInstall` to `CloudOrchestrator` interface to skip
tarball + install steps when booting from snapshot
- Add 5 unit tests for snapshot lookup (happy path, empty, error,
invalid ID, network failure)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The status command (PR #2254) added --prune and --json flags but did not
register them in KNOWN_FLAGS. This caused the CLI to reject them with
"Unknown flag" errors before the command could even dispatch.
Bump CLI version 0.15.4 -> 0.15.5.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Simplify the cloud matrix by removing Daytona. All Daytona-specific code,
scripts, tests, and configuration have been removed. Daytona has been moved
to "Previously Considered" in the Cloud Provider Wishlist (#1183) and can
be revived on community demand.
Closes#2260
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes#2249
The overly broad `>>? word` pattern and generic doubled-operator check
were blocking legitimate natural-language developer prompts like:
- "Fix the merge conflict >> registration flow"
- "Run tests && deploy if they pass"
Root cause: `validatePrompt` is called before the prompt is set as the
`SPAWN_PROMPT` env var. Inside double-quoted shell arguments, `>>` and
`&&` are not interpreted as shell operators, so blocking them provided
no real security benefit while creating confusing UX rejections.
Changes:
- Remove `/>>?\s*[a-zA-Z_]\w{2,}/` pattern (false-positive on >> in English)
- Remove generic `hasDoubledOperators` check (false-positive on && in English)
- Keep all targeted patterns: $(cmd), backticks, ${var}, | bash/sh,
; rm -rf, fd redirections, heredoc, process substitution, path redirects
- Update tests: split broad && / || tests into "commands" vs "natural language"
- Add tests asserting all issue #2249 example prompts are now accepted
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#2252
history.json now uses a versioned envelope:
{ "version": 1, "records": [...] }
This creates a migration escape hatch for future SpawnRecord shape changes.
loadHistory() transparently reads both v0 (bare array) and v1 formats,
automatically migrating v0 files on next write. All write operations now
use writeHistory() to stamp the current schema version consistently.
Validation uses valibot schemas (VMConnectionSchema, SpawnRecordSchema,
HistoryFileV1Schema) so the structure is verified and typed without `as`
casts. Updated all affected tests to check data.records instead of data.
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the `spawn status` command requested in #2253. The command:
- Reads active (non-deleted) cloud servers from history
- Queries Hetzner and DigitalOcean REST APIs in parallel using saved tokens
- Shows a live-state table: ID, Agent, Cloud, IP, State, Since
- States: running (green), stopped (yellow), gone (dim), unknown (dim)
- --prune flag marks gone servers as deleted in history
- --json flag outputs machine-readable JSON for scripting
- `spawn ps` is an alias for `spawn status`
Other clouds (AWS, GCP, Sprite, Daytona) require CLI auth flows that cannot
run non-interactively; they report "unknown" with a helpful hint.
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Each cloud module (aws, daytona, digitalocean, gcp, hetzner, sprite) previously
stored per-operation state in bare module-level `let` variables, making them
process-global singletons. This is safe for single-cloud CLI invocations today
but creates latent bugs for multi-cloud orchestration and test isolation.
Replace scattered `let` globals with a single typed `_state` object per module:
- `AwsState` / `resetAwsState()` — 8 fields including `selectedBundle`
- `DaytonaState` / `resetDaytonaState()` — 5 fields
- `DigitalOceanState` / `resetDigitalOceanState()` — 3 fields
- `GcpState` / `resetGcpState()` — 5 fields
- `HetznerState` / `resetHetznerState()` — 3 fields
- `SpriteState` / `resetSpriteState()` — 2 fields
Each module exports a `resetXxxState()` function for test isolation. No function
signatures or existing exports were changed.
Fixes#2251
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: ARM tarball builds + arch-aware download
- Add ARM64 matrix entries for native binary agents (zeroclaw, opencode,
hermes, claude) in agent-tarballs.yml workflow
- Update agent-tarball.ts to detect remote VM arch via uname -m and
download the correct tarball (x86_64 or arm64)
- Change release strategy to support multiple arch assets per tag
- Document ARM build requirements in discovery.md for future agents
- Bump CLI version to 0.15.2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use sudo for tarball extraction on non-root SSH clouds
On AWS Lightsail, SSH connects as 'ubuntu' (not root), but tarballs
extract to /root/. Without sudo, tar fails with "Permission denied".
Conditionally use sudo when not running as root (id -u != 0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Docker delivery is superseded by the tarball approach (#2232) which is
faster (curl|tar ~5-15s vs Docker install ~30s + pull ~60s) and works
on every cloud without Docker as a dependency.
- Remove tryInstallFromDocker, withDockerInstall, DOCKER_IMAGE_PREFIX
- Remove dockerImage and slowInstall from AgentConfig
- Remove Docker cloud-init from DigitalOcean
- Unwrap openclaw and zeroclaw to direct install (tarball is tried
first in orchestrate.ts, these are the fallback)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Remove parseJsonRaw from packages/shared — exported but never imported
- Remove dead re-exports from agent-setup.ts (AgentConfig type, generateEnvConfig)
that no consumer imports (all callers use the original modules directly)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
- Replace require() calls with ESM import in history-spawn-id.test.ts
(require() violates ESM-only rule per shell-scripts.md)
- Fix stale parseJsonRaw reference in test README (cli parse.ts does
not export parseJsonRaw; only packages/shared does)
- Add 5 missing test file entries to test README
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Remove two tests from "sequential saves at the boundary" that were
exact duplicates of tests in the "MAX_HISTORY_ENTRIES trimming" section:
- "99 to 100 entries" duplicated "should keep all entries when at exactly 100"
- "100 to 101 entries" duplicated "should trim to 100 when adding entry that exceeds the limit"
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>