this function has no callers in production code but is intentionally
used in unit tests (custom-flag.test.ts) for state introspection.
adding documentation prevents it from being incorrectly identified
as dead code in future code quality scans.
code quality scan results:
- dead code: none found
- stale references: none found
- python usage: none found
- duplicate utilities: getCloudInitUserdata has per-cloud variants
with intentional differences (not mergeable)
- stale comments: none found
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Consolidate repetitive per-field test iterations in manifest-type-contracts.test.ts
into data-driven loops, eliminating ~15 near-identical it() blocks. Share a single
startGateway() invocation across all 3 gateway-resilience tests via beforeEach.
Remove redundant toBeDefined() check in junie-agent.test.ts that was immediately
superseded by a stronger assertion on the same value.
-- qa/dedup-scanner
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Each `openclaw config set` call does a read-modify-write that can drop
fields like channels and gateway auth. After all config set calls,
re-download the config, deep-merge our configObj on top, and re-upload
to restore any dropped fields.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- security.test.ts: remove "should handle prompt with only whitespace"
(line 614) — fully covered by "should reject empty prompts" (line 363)
which already tests validatePrompt(" ") and validatePrompt("\n\t")
- script-failure-guidance.test.ts: consolidate three separate "returns
simple command" tests (no-arg, undefined, empty string) into one.
All three called buildRetryCommand with absent/falsy prompt and
asserted identical output — the input variation is not a meaningful
behavioral distinction.
net: 3 tests removed. 1410 pass, 0 fail. biome lint clean.
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 three dead functions that were defined but never called:
- verify_setup_github — checked GitHub CLI auth status
- verify_setup_browser — checked Chrome browser install
- verify_setup_telegram — checked openclaw Telegram config
These were orphaned helpers (never called from verify_agent or anywhere
else). All agent-specific checks go through verify_agent() which dispatches
to the per-agent verify_*() functions, none of which called these helpers.
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>
* fix: increase packer snapshot transfer timeout to 60m
The default 30m timeout is too short for transferring snapshots to
distant DO regions (blr1, sgp1, syd1). This caused zeroclaw and
kilocode builds to fail despite successful provisioning.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* revert: remove batch splitting from packer workflow
DO droplet cap is no longer an issue — revert to single parallel build
job for all agents.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- commands-error-paths.test.ts: consolidate 4 groups of repetitive tests
into data-driven loops: 7 identifier validation tests, 6 prompt
validation tests, 5 cmdAgentInfo invalid-input tests, and 3 empty-input
tests — each group had identical structure (rejects.toThrow + exit(1))
with only the input varying. net: 21 separate tests → 4 compact loops
covering the same cases, reducing 41 lines of boilerplate.
- commands-cloud-info.test.ts: consolidate 8 separate "should reject cloud
with X" tests (invalid identifier describe block) into a single
data-driven loop, reducing 24 lines.
All 1413 tests still pass. biome lint clean.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Splits the 8 agents into 2 sequential batches of 4 so we stay under
DigitalOcean's concurrent droplet creation limit. Batch 2 waits for
batch 1 to finish before starting. Single-agent builds are unaffected.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
- aws.test.ts: remove "all bundles have required fields" test that used
toBeTruthy() on id/label — fully redundant with the more specific
"bundle IDs follow naming convention" (/_3_0$/) and "labels include
pricing info" ($, /mo) tests below it.
- commands-cloud-info.test.ts: consolidate 3 separate tests for
"cloud with no implemented agents" that each fetched the same manifest,
called cmdCloudInfo("emptycloud"), and checked different assertions on
identical output into a single test.
- credential-hints.test.ts: merge "reports credentials appear set..."
and "lists the env var names when all are set" — identical setup (same
env vars, same function call) with overlapping assertions split across
two tests for no good reason.
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 junie.Dockerfile was added in PR #2601 but the docker.yml workflow
matrix was not updated, so no Docker image for junie was ever being built.
Add junie to the agent list so ghcr.io/openrouterteam/spawn-junie gets
built alongside all other agents.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
collectMissingCredentials() was incorrectly reporting saved credentials as
missing in two ways:
1. It only checked process.env.OPENROUTER_API_KEY, ignoring keys saved via
OAuth flow to ~/.config/spawn/openrouter.json
2. When hasCloudConfigCredentials() returned true, it filtered to keep
OPENROUTER_API_KEY in the missing list instead of returning []
Fix: also call hasSavedOpenRouterKey() before marking OPENROUTER_API_KEY as
missing, and return [] (not a filtered list) when cloud config exists.
Fixes#2639
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: offer delete or remap when server is gone from cloud provider
When a user tries to connect to a server that no longer exists, instead
of silently marking it as deleted, present an interactive picker that
lets them remap the history entry to an existing instance on the same
cloud or explicitly remove it from history.
- Add listServers() to Hetzner, DigitalOcean, AWS, and GCP providers
- Add updateRecordConnection() to history for remapping server details
- Add handleGoneServer() interactive flow in list.ts
- Fall back to silent deletion in non-interactive mode (SPAWN_NON_INTERACTIVE)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: move InstancesListSchema to module level
Declare valibot schema at module top level per project convention,
not inside the listServers() function body.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: extract shared CloudInstance type from duplicated inline types
The { id, name, ip, status } shape was declared inline 9 times across
5 files. Extract it as a shared CloudInstance interface in history.ts
and import it in all cloud providers and list.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add "Custom model" option to setup menu for OpenClaw
Adds a "Custom model" entry to the setup options multiselect. When
selected, prompts the user for an OpenRouter model ID (e.g.
anthropic/claude-sonnet-4) with validation. The model ID is passed
through via MODEL_ID env var to the orchestration pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: simplify custom model prompt text
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>
When DO API calls fail (billing issues, locked account, droplet creation
errors), users may be logged into the wrong account. Now shows email/team/
status and offers to re-authenticate before giving up.
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 AWS Lightsail's internal HTTP retry fires after a successful
create but dropped response, the NameExists error now checks if the
instance is in pending/running state and reuses it instead of failing.
Fixes#2630
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes Connection reset by peer failures on spotty networks by doubling
delay on each retry (10s→20s→40s→80s) and giving installAgent and
uploadConfigFile 4 attempts instead of 2.
Fixes#2631
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sort agent picker by github_stars descending so most popular agents
appear first. Add update-stars.sh script to QA quality sweep to keep
star counts fresh.
Security fixes from PR #2629 review:
- Validate repo format (owner/name pattern) before gh api calls
- Validate and canonicalize REPO_ROOT with realpath
Supersedes #2629.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
* feat: add downloadFile to CloudRunner + local OpenClaw config merge
Add `downloadFile(remotePath, localPath)` to the CloudRunner interface
and implement it across all 6 cloud providers (Hetzner, AWS, GCP,
DigitalOcean, Sprite, Local) — mirroring the existing `uploadFile` with
reversed SCP direction.
Replace the OpenClaw config write with a download → deep-merge → upload
flow so config merging happens in our own linted TypeScript instead of
a remote script.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: move isPlainObject and deepMerge to shared utils
Extract `isPlainObject` to `shared/type-guards.ts` and `deepMerge` to
`shared/parse.ts` so they're reusable across the codebase.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: promote isPlainObject to shared package, use across codebase
Move `isPlainObject` from cli/type-guards.ts into
@openrouter/spawn-shared so it can be used everywhere. Replace
inline `val !== null && typeof val === "object" && !Array.isArray(val)`
checks in:
- shared/type-guards.ts (toRecord, toObjectArray)
- shared/parse.ts (parseJsonObj)
- cli/manifest.ts (isValidManifest)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: remove type-guards re-export, import directly from spawn-shared
Delete `packages/cli/src/shared/type-guards.ts` (was just a re-export
barrel). All 35 consuming files now import `getErrorMessage`, `isString`,
`isNumber`, `isPlainObject`, `toRecord`, etc. directly from
`@openrouter/spawn-shared`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
doApi() throws on any non-2xx response before the isBillingError() check
at the call site could execute, making billing error detection dead code.
Wrap the POST /droplets call in asyncTryCatch so the thrown error message
(which includes the response body) is checked with isBillingError(). If it
matches a billing pattern, handleBillingError() is shown with the billing
page link and retry prompt — same UX as the proactive first-run warning.
Also adds a test asserting isBillingError() matches errors in the format
doApi throws (regression guard for #2395).
Fixes#2395
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
WhatsApp setup is too complex for normal users (QR scan + separate
device + pairing). Remove it from the setup options entirely.
Also change multiselect defaults to nothing pre-selected — let users
opt in to what they want instead of pre-selecting for them.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#2624
When reconnecting to an existing server via `spawn ls` or `spawn last`,
the CLI now queries the cloud provider API for the server's current IP
before attempting SSH. This prevents silent SSH timeouts when a server's
IP changes (e.g., after a restart or elastic IP reallocation).
Changes:
- Add `getServerIp()` to DigitalOcean, Hetzner, AWS, and GCP modules
- Add `updateRecordIp()` to history.ts to persist IP changes
- Add `refreshConnectionIp()` in list.ts that authenticates with the
cloud provider and refreshes the IP before enter/reconnect/fix actions
- If the server no longer exists, mark it deleted and inform the user
- If refresh fails (e.g., no credentials), fall back to cached IP
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add "Open Dashboard" as its own menu item for agents with tunnel
metadata (e.g., OpenClaw). Establishes an SSH tunnel, opens the
browser with the auth token, and waits for Enter to close.
The menu now shows both options for dashboard agents:
- Enter OpenClaw (launches TUI via SSH)
- Open Dashboard (opens web UI in browser)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When an agent has an SSH tunnel (e.g., OpenClaw dashboard), store the
tunnel remote port and browser URL template in connection.metadata at
spawn time. On reconnect via `spawn ls` → "Enter agent", re-establish
the SSH tunnel and open the dashboard automatically.
- Add saveMetadata() to history.ts for merging key-value pairs into records
- Store tunnel_remote_port and tunnel_browser_url_template in orchestrate.ts
- Re-establish tunnel in cmdEnterAgent (connect.ts) when metadata is present
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The OpenClaw dashboard (Control UI) is served by the Gateway on port
18789, which also handles WebSocket connections for agent communication.
Port 18791 is the internal Control Service — not the user-facing dashboard.
We were tunneling 18791, so the browser connected to the wrong service
and showed "Unauthorized" because the Control Service doesn't accept
token-based dashboard auth.
Fix: tunnel port 18789 (Gateway) and update all USER.md references.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OpenClaw 2026.3.7+ requires an explicit `gateway.auth.mode: "token"` field
when `gateway.auth.token` is set. Without it the gateway rejects auth and the
dashboard shows "Unauthorized".
Additionally, pass the token via URL fragment (`#token=`) instead of query
parameter (`?token=`) to match the updated auth flow and avoid leaking the
token in server logs / Referer headers (GHSA-rchv-x836-w7xp).
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ensureSshKeys tests had two identical tests covering the same code
path: "uses all keys in non-interactive mode when multiple exist" and
"uses all keys when multiselect is unavailable". Both created the same
two fake key pairs, used the same spawnSync mock, and made the identical
assertion (toHaveLength(2)).
The first test set SPAWN_NON_INTERACTIVE=1 which ensureSshKeys does not
check — stale logic from a removed interactive multiselect flow. The
second test referenced unavailable @clack/prompts multiselect which also
no longer exists in the implementation.
Consolidated into one deterministic test that also validates key ordering
(ed25519 sorts before rsa).
-- qa/dedup-scanner
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
The `sprite create` API call in `createSprite()` had no timeout, so when
the Sprite API blocked for certain agents (kilocode, opencode), the
process hung indefinitely. The bash-level timeout in provision.sh wraps
the outer subshell but the deeply-nested `sprite create` subprocess
could survive signal propagation.
Add a 300s (configurable via SPRITE_CREATE_TIMEOUT) timeout to the
`sprite create` subprocess using the existing killWithTimeout +
asyncTryCatch pattern already used by runSprite() and destroyServer().
Fixes#2612
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The @jetbrains/junie-cli postinstall script may download the actual
binary to non-standard locations that verify_junie() wasn't checking.
Add ~/.junie/bin, /usr/local/bin, and dynamic npm global bin resolution
to the PATH search in the binary check.
Fixes#2611
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The prompt referenced `sh/test/fixtures/{cloud}/_env.sh` for loading
cloud credentials, but that path does not exist. Cloud credentials are
actually stored in `~/.config/spawn/{cloud}.json` via key-request.sh.
Updated Steps 1-2 to reference the correct credential mechanism and
list the actual env vars needed per cloud (HCLOUD_TOKEN, DO_API_TOKEN,
AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY).
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
* fix: messaging UX — silence doctor, fix groupPolicy, remove early WhatsApp pairing
- Set groupPolicy to "open" for both Telegram and WhatsApp (was
"allowlist" with empty allowFrom, causing doctor warnings)
- Suppress doctor warning spam by redirecting openclaw config set
stdout to /dev/null
- Remove WhatsApp pairing prompt (appeared immediately after QR scan
before user could message the bot — now just tells them the command)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: improve Telegram/WhatsApp pairing instructions
Add step-by-step instructions for Telegram pairing so users know to
search for their bot in Telegram and message it. Improve WhatsApp
post-link instructions to explain how contacts pair.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: pre-select Telegram in setup options as recommended channel
Telegram has the smoothest setup UX (bot token + pairing code) compared
to WhatsApp (QR scan + separate device). Pre-select it alongside Chrome
in the multiselect and label it as "recommended" in the hint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Telegram is a built-in channel, not a plugin. Replace broken
`openclaw plugins enable telegram` (OOM) and `openclaw channels add`
(doesn't exist) with proper setup:
- Write channel config (botToken, dmPolicy: pairing, groups) directly
into the atomic JSON config file during setup
- After gateway starts, prompt user to pair via
`openclaw pairing approve <channel> <CODE>`
- WhatsApp: QR scan via `openclaw channels login`, then pairing
- Bump version to 0.17.16
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
All 7 other agents have a sh/docker/{agent}.Dockerfile; junie was added
in 2026-03 but its Dockerfile was never created, meaning no Docker image
exists for it. This adds the missing file following the codex pattern
(npm-based agent, Node.js 22 via n).
Note: .github/workflows/docker.yml also needs `junie` added to its
matrix.agent array — tracked in a separate GitHub issue.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- billing-guidance.test.ts: move stderrSpy.mockRestore() from each test
body to afterEach so restores run even when a test throws
- junie-agent.test.ts: add missing afterEach to restore stderrSpy that
was leaking across tests
- cloud-init.test.ts: consolidate repetitive needsNode/needsBun tests
into data-driven loops (8 individual its -> 2 parameterized loops)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: set telegram groupPolicy to open during channel setup
OpenClaw defaults groupPolicy to "allowlist" with an empty groupAllowFrom,
which silently drops all group messages. Set it to "open" after adding the
Telegram channel so group messages work out of the box.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use OpenClaw config file for Telegram setup instead of broken CLI commands
Telegram is a built-in channel in OpenClaw, not a plugin. The previous
approach used `openclaw plugins enable telegram` (caused OOM on 2GB) and
`openclaw channels add --channel telegram` (command doesn't exist).
Now writes Telegram config (botToken, enabled, groupPolicy) directly into
the atomic JSON config file during setup. Also sets groupPolicy to "open"
so group messages work out of the box instead of being silently dropped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use openclaw onboard for channel setup instead of manual config
OpenClaw has a built-in `openclaw onboard` command that interactively
guides users through Telegram/WhatsApp channel setup. Use that instead
of manually prompting for tokens and writing config ourselves.
- Remove custom Telegram token prompt from agent-setup.ts
- Remove broken `openclaw channels add` and `openclaw plugins enable`
- Run `openclaw onboard` after gateway starts for channel setup
- Base config (API key, gateway, model) still written atomically
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
openclaw-plugins OOMs on s-2vcpu-2gb (2GB) droplets during config
loading. Auto-upgrade to s-2vcpu-4gb when no custom size is set.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add missing `spawn feedback` command to commands table.
The command exists in packages/cli/src/commands/help.ts
getHelpUsageSection() but was absent from the README commands table.
Source-of-truth delta: help.ts line 42 adds 'spawn feedback "message"'
with description 'Send feedback to the Spawn team'.
-- qa/record-keeper
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Consolidate 5 tests in sprite-keep-alive.test.ts that had identical
boilerplate (capturing session script or command list) into 2 tests:
- 2 installSpriteKeepAlive tests merged into 1 (both captured capturedCmds
to check different assertions about the same function call)
- 4 interactiveSession tests merged into 1 (all captured capturedSessionScript
to check different properties of the generated session script)
1391 → 1387 tests, zero regressions.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Adds `spawn fix [spawn-id]` command that SSHes into an existing VM and
re-applies agent setup without destroying or re-provisioning the server:
- Re-injects OpenRouter credentials and env vars into ~/.spawnrc
- Re-runs the agent's install command to get the latest version
- Also accessible via `spawn list` → "Fix this server" menu option
- Accepts optional spawn name/ID as positional argument
- Falls back to interactive picker for multiple active servers
- Single active server is fixed directly without prompting
Uses dependency injection (FixScriptRunner) for testability, following
the same pattern as confirmAndDelete's deleteHandler parameter.
Fixes#2589
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: move Telegram/WhatsApp channel setup to after gateway starts
OpenClaw's `channels add` and `channels login` commands require a running
gateway. Previously, Telegram token configuration ran in setupOpenclawConfig
(pre-gateway) using `openclaw config set`, causing the gateway to hang on
startup when a token was present for a disabled-by-default plugin.
Now:
- Plugin enables stay in setupOpenclawConfig (pre-gateway)
- Channel config (token add, QR login) runs in orchestrate.ts step 11c
after the gateway is up, using `openclaw channels add/login`
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* security: use shellQuote instead of jsonEscape for Telegram token
jsonEscape uses JSON.stringify which produces double-quoted strings that
the shell interprets, creating a command injection vector. shellQuote
wraps in single quotes, preventing shell interpretation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: fix biome export ordering in interactive.ts and manifest.ts
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>
* feat: add --beta images for DO marketplace images
Gate pre-built DigitalOcean marketplace images behind --beta images.
When active, uses hardcoded marketplace slugs (e.g. openrouter-spawnclaude)
instead of fresh Ubuntu + cloud-init, skipping agent install entirely.
All 8 images verified working via e2e smoke test (2026-03-13).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: sort exports to satisfy biome organizeImports
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
GCP e2-micro VMs are slow and throttled. When the openclaw gateway is
killed during the resilience test, the lock file is held by the dead
process for ~5s. This causes the first systemd restart attempt to fail
with "lock timeout after 5000ms", requiring a second restart cycle.
Timeline on slow VMs: RestartSec(5) + lock-timeout(5) + RestartSec(5)
+ boot(5) ≈ 20s. The previous 30s window was too tight — the gateway
DID recover but just barely missed the polling window on throttled CPUs.
Increasing to 60s gives a comfortable 3x margin for all VM types.
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>
Added a note regarding the public anonymous survey and clarified that it is not a security vulnerability.
Signed-off-by: L <6723574+louisgv@users.noreply.github.com>
* feat: add `spawn feedback` subcommand
Sends anonymous feedback to the Spawn team via PostHog survey API.
Usage: spawn feedback "your message here"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update feedback survey ID and response key
Use the correct PostHog survey ID and $survey_response property.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use asyncTryCatch instead of try/catch in feedback command
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove 5 unused underscore-prefixed parameters that were accepted but
never read: extractFlagValue._flagLabel, performUpdate._remoteVersion,
reportDownloadFailure._primaryUrl/_fallbackUrl, buildRecordLabel._manifest,
and setupCodexConfig._apiKey. All callers updated accordingly.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
"should reject heredoc syntax in operator combinations" tested a single
case ("Input << EOF") that is fully covered by the broader "should reject
heredoc syntax" test (3 cases: << EOF, <<- HEREDOC, <<MARKER).
1 test removed, 0 expect() calls lost (the exact input pattern is covered
by the remaining test).
-- qa/dedup-scanner
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>
Add groups:history and groups:read OAuth scopes plus message.groups
event subscription so SPA can respond in private channels.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>