Commit graph

447 commits

Author SHA1 Message Date
A
7fe1bdf6b3
fix(junie): remove JUNIE_MODEL env var to fix 'Unknown model: openrouter/auto' crash (#2735)
Junie only accepts its own shorthand model names (gpt, opus, sonnet, etc.)
and not OpenRouter model IDs. Removing modelEnvVar lets junie handle its
own model routing via the OpenRouter API key instead.

Fixes #2734

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:22:32 -07:00
Ahmed Abushagur
c11879d547
fix(windows): download JS bundle instead of bash wrapper on Windows (#2730)
The bash wrapper scripts (.sh) contain bash syntax that PowerShell
cannot parse. On Windows, download the pre-built JS bundle from
GitHub releases and run it directly via `bun run {cloud}.js {agent}`,
which is exactly what the bash wrapper ultimately does.

Affects both interactive (execScript) and headless (cmdRunHeadless)
code paths. macOS/Linux behavior unchanged.

Closes #2726

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-17 19:09:44 -07:00
A
b1de116690
refactor: replace manual multi-level type guards with toRecord/isString in index.ts (#2731)
Two instances of the pattern `err && typeof err === "object" && "code" in err`
violated the type-safety rule requiring valibot or shared type-guard utilities
instead of manual multi-level type checks. Replaced with `toRecord(err)` and
`isString()` from @openrouter/spawn-shared for consistent, rule-compliant error
code extraction. Also bumps CLI patch version per cli-version.md.

-- qa/code-quality

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-17 18:40:16 -07:00
Ahmed Abushagur
6e92cc832b
feat: add systemd auto-update service for agents on cloud VMs (#2728)
Installs a systemd timer + oneshot service that updates the agent binary
and system packages every 6 hours without disrupting running instances.

Agent update safety:
- Binary agents (Go, Rust): Linux keeps old inode in memory; safe to replace
- npm agents: Node.js caches modules at startup; running processes unaffected
- New version takes effect on next restart via the existing restart loop

System update safety:
- Disables Ubuntu's unattended-upgrades to prevent dpkg lock contention
- Uses flock -w 300 on /var/lib/dpkg/lock-frontend before apt operations
- DEBIAN_FRONTEND=noninteractive with --force-confdef/--force-confold

User-facing:
- "Auto-update" option in setup multiselect (default on, user can uncheck)
- Skipped for local cloud and non-systemd systems
- Non-fatal: setup failure doesn't block agent launch
- Logs to /var/log/spawn-auto-update.log

Timer: 15min after boot, then every 6h with 30min random jitter.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 17:34:12 -07:00
Ahmed Abushagur
66b16d8651
feat: add Windows PowerShell support — remove bash dependency for local execution (#2727)
Replace hardcoded "bash" shell references with platform-aware utilities so
spawn works natively from PowerShell on Windows without WSL or Git Bash.

- New shared/shell.ts: isWindows(), getLocalShell(), getInstallScriptUrl(),
  getInstallCmd(), getWhichCommand() with platform override for testability
- local/local.ts: use getLocalShell() for runLocal() and interactiveSession()
- commands/run.ts: spawnScript/runScriptHeadless use getLocalShell()
- commands/update.ts: Windows downloads install.ps1, runs via PowerShell
- update-check.ts: Windows auto-update uses install.ps1; "where" replaces "which"
- shared/orchestrate.ts: PowerShell-compatible .spawnrc setup for local Windows
- Remote SSH commands unchanged — remote servers are always Linux

Closes #2726

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-17 16:35:23 -07:00
A
ba94f681b3
feat(cli): add spawn uninstall command (#2724)
* feat(cli): add `spawn uninstall` command

Adds a new `uninstall` subcommand that cleanly reverses the install:
- Removes ~/.local/bin/spawn binary and /usr/local/bin/spawn symlink
- Cleans spawn PATH entries from shell RC files (.bashrc, .zshrc, etc.)
- Removes ~/.cache/spawn/ cache directory
- Optionally removes ~/.spawn/ (history) and ~/.config/spawn/ (keys/config)
- Shows confirmation prompt before any destructive action

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: use start/end markers for shell RC blocks

- Add shared RC_MARKER_START/RC_MARKER_END constants in paths.ts
- Update install.sh to write `# >>> spawn >>>` / `# <<< spawn <<<` block markers
- Update uninstall.ts to remove content between markers (with legacy fallback)
- Addresses review feedback: shared markers make RC entries easier to audit/remove

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: share legacy RC marker from paths.ts

Move the legacy "# Added by spawn installer" string to RC_MARKER_LEGACY
in shared/paths.ts so both install.sh and uninstall.ts reference the
same source of truth for all marker strings.

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>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-17 16:33:09 -07:00
A
1733903a1f
fix(digitalocean): add OAuth recovery in doApi for mid-session 401 errors (#2723)
When a DigitalOcean token expires mid-session (after ensureDoToken succeeds),
API calls like ensureSshKey, createServer, listServers, destroyServer would
crash with "Fatal: DigitalOcean API error 401" because doApi had no recovery
path for 401 responses.

Now doApi detects 401, attempts OAuth browser flow recovery via tryDoOAuth(),
and retries the request with the new token. A re-entrancy guard prevents
infinite loops (doApi → tryDoOAuth → doApi → ...). If OAuth recovery fails,
the original 401 error is thrown as before.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:13:42 -07:00
A
00863b0172
fix(digitalocean): handle 401 gracefully in testDoToken instead of crashing (#2722)
testDoToken() used asyncTryCatchIf(isNetworkError, ...) which only caught
network errors. A 401 HTTP response threw a regular Error that escaped the
guard, propagating to main().catch() and printing "Fatal: DigitalOcean API
error 401...". Changed to asyncTryCatch() to catch all errors, returning
false for invalid tokens so ensureDoToken() naturally falls through to
OAuth recovery.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:14:30 -07:00
A
6509973154
test: remove duplicate terminal-width boilerplate in cmd-listing-output tests (#2721)
Consolidate 10 single-assertion cmdMatrix tests (5 wide-terminal + 5
narrow-terminal) into 2 comprehensive tests using beforeEach/afterEach for
terminal-width setup. Also fix a pre-existing environment-dependent failure
where HCLOUD_TOKEN being set on the host caused the auth-hint test to see
"ready" instead of "needs".

Changes:
- "grid view (wide terminal)": 5 tests → 1 test (8 fewer cmdMatrix() calls)
- "compact view (narrow terminal)": 5 tests → 1 test (same)
- Fix "should display auth hints" to clear host env vars before asserting

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 14:22:05 -07:00
A
c6087534aa
fix: populate connection fields in --headless --output json result (#2716)
After runBashHeadless() succeeds, read the spawn record saved during
orchestration and populate ip_address, ssh_user, server_id, and
server_name in the SpawnResult output.

Closes #2715

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 12:29:44 -07:00
A
0e5bfd830b
fix(e2e): double GCP cloud-init wait timeout to 10 minutes for Node install (#2713)
* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* fix(gcp): double cloud-init wait timeout to 120 attempts (10 min)

GCP startup scripts installing Node.js 22 via `n` from curl take longer
than 5 min on cold starts. The previous 60-attempt (5 min) poll timed
out with "Startup script may not have completed, continuing..." and
proceeded to run `npm install -g @kilocode/cli` before npm was available,
causing `npm: command not found` errors.

Increase `maxAttempts` from 60 to 120 (10 min) in `waitForCloudInit` to
give the Node install enough time to complete on GCP cold starts.

Confirmed by E2E run: GCP kilocode failed with npm not found after all 60
poll attempts exhausted; all other GCP agents passed (they don't need Node).

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>
2026-03-17 11:51:41 -07:00
Ahmed Abushagur
34785a9a63
feat(hermes): add YOLO mode toggle to setup menu (#2711)
Add HERMES_YOLO_MODE as a setup option for Hermes Agent, enabled by
default. This disables Hermes's security approval prompts so it can
self-install skill dependencies (e.g. himalaya for email) at runtime
on dedicated cloud VMs.

Users can uncheck it in the setup multiselect if they prefer Hermes
to prompt before installing tools.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 10:09:41 -07:00
A
5004a4db52
test: replace loose cloud-type count assertion with enumerated known-set check (#2709)
The "should have a reasonable number of distinct cloud types" test used
toBeGreaterThanOrEqual(2) and toBeLessThanOrEqual(10) — bounds so wide
they would never catch a real type-naming mistake. Replace it with an
explicit allowlist check so adding an unknown type fails immediately.

Current valid types (api, cli, local) are all in the set; vm, container,
sandbox, and cloud are pre-approved to avoid blocking planned additions.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:55:15 -07:00
A
eec83898e4
fix(kilocode): add binary verification after npm install to recover from silent postinstall failures (#2707)
@kilocode/cli v7+ uses a native binary postinstall that downloads a
platform-specific binary. On some clouds (notably GCP with cloudInitTier
"node"), this postinstall can fail silently, leaving the npm bin symlink
pointing to a JS wrapper with no actual native binary to exec.

The fix adds a KILOCODE_BINARY_VERIFY shell snippet that runs after npm
install and:
1. Checks if kilocode is already working (fast path)
2. If not, finds the npm package dir and re-runs the postinstall
3. If still not found, searches for the native binary in the package dir
   and symlinks it into a PATH-accessible location

Fixes #2706

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 08:30:00 -07:00
A
33014e3eed
docs: add cmd-link.test.ts to test README index (#2705)
cmd-link.test.ts was added but omitted from the test file index in README.md.
This keeps the index accurate as a reference for all 68 test files.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 05:51:51 -07:00
A
e8471b6136
test: remove duplicate Result constructors describe block (#2704)
The "Result constructors" describe block in with-retry-result.test.ts
(testing Ok/Err from shared/ui.js) was a duplicate of coverage already
provided by result-helpers.test.ts, which tests the same Ok/Err exports
from shared/result.ts (ui.ts re-exports them). The 3 trivial constructor
tests add no signal beyond what the withRetry and wrapSshCall tests
already exercise implicitly.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 02:23:24 -07:00
A
7f43e6bb9b
test: fix theatrical promptBundle test with real assertion (#2703)
promptBundle sets _state.selectedBundle via env var but the test was
calling promptBundle() without asserting anything about the result.
Added selectedBundle to getState() return value so tests can verify
the env var path is actually exercised.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 22:08:10 -07:00
A
8dd1db7fbb
test: remove duplicate and theatrical tests (#2700)
- Remove 2-test "flag registration" block from custom-flag.test.ts — both
  assertions (KNOWN_FLAGS.has("--custom") and findUnknownFlag returning null)
  were already covered by the KNOWN_FLAGS completeness test in unknown-flags.test.ts.

- Fix stale KNOWN_FLAGS completeness test: it was testing only 18 of 26 known
  flags, making it always-pass when new flags are added to flags.ts without
  updating the test. Now the test is bidirectionally exhaustive — every flag in
  the expected list must be in KNOWN_FLAGS, and every flag in KNOWN_FLAGS must
  be in the expected list. This absorbs the --steps/--config coverage.

- Remove findUnknownFlag(["--steps"]) / findUnknownFlag(["--config"]) test from
  steps-flag.test.ts — now redundant since the exhaustive completeness test
  already exercises those flags.

Net: -3 tests removed, +18 expect() calls added (exhaustive bidirectional check).

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-16 20:12:47 -07:00
A
5b2eddb763
fix(sprite): replace personal VM URL with official CDN for keep-alive script (#2701)
The sprite-keep-running.sh script was downloaded from a hardcoded personal
VM URL (kurt-claw-f.sprites.app) which would break all Sprite deployments
if that VM goes offline. Use the official CDN proxy at openrouter.ai/labs/spawn/.

Fixes #2699

-- refactor/code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 20:04:49 -07:00
A
b854917186
fix(security): validate tunnel URL and port from history before openBrowser() (#2697)
Add validateTunnelUrl() and validateTunnelPort() in security.ts to prevent
phishing attacks via tampered ~/.spawn/history.json. Apply both validations
in cmdEnterAgent() and cmdOpenDashboard() in connect.ts before any tunnel
data is used.

- validateTunnelUrl: enforce URL starts with http://localhost: or
  http://127.0.0.1: only (blocks external/phishing URLs)
- validateTunnelPort: enforce numeric value in range 1-65535
- Add comprehensive test cases for both validators

Fixes #2696

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 15:22:29 -07:00
A
644593eaea
fix(security): propagate path normalization to all cloud modules (#2693)
* fix(security): propagate path normalization to all cloud upload/download functions

PR #2690 added normalize() before path traversal checks in AWS but not
the other clouds. Apply the same defense-in-depth to GCP, DigitalOcean,
Hetzner, Sprite, and shared validateRemotePath.

Agent: code-health

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix(security): use normalized path in all file transfer operations

Addresses code review: replace original remotePath with normalizedRemote
in scp commands and bash operations to prevent validation bypass.

- digitalocean: use normalizedRemote in uploadFile scp and derive
  expandedPath from normalizedRemote in downloadFile
- hetzner: same pattern for uploadFile/downloadFile
- gcp: derive expandedPath from normalizedRemote.replace(...) in both
  uploadFile and downloadFile
- sprite: use normalizedRemote in bash mkdir/mv command and derive
  expandedPath from normalizedRemote in downloadFile

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(security): close validation bypass in agent-setup and AWS file ops

validateRemotePath() validated the normalized path but returned void,
so the caller still used the original unsanitized remotePath in shell
commands — bypassing the normalization check entirely.

Fix: return the normalized path and use it in all file operations.

Also fix AWS uploadFile/downloadFile which validated normalizedRemote
but used the original remotePath in scp commands.

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-16 14:48:59 -07:00
A
bae921a295
fix(digitalocean): retry on 404 in waitForDropletActive (#2695)
DigitalOcean sometimes returns 404 immediately after droplet creation
before the resource propagates across their API. Previously this caused
an immediate fatal error, failing all DO agent provisions.

Now 404 responses are treated as transient and retried with the same
5s polling interval, consistent with how non-active statuses are handled.

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>
2026-03-16 14:19:02 -07:00
A
0b346dffcc
test: remove duplicate and theatrical tests (#2694)
Consolidated 3 separate per-exit-code dashboard URL tests (130, 137, 42)
into a single data-driven loop. Merged 2 per-signal tests (SIGTERM, SIGINT)
into one. Removed a weak always-true test ("always return a non-empty array")
that was already implied by the adjacent test above it. Net: 4 fewer tests,
no coverage loss.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 14:17:27 -07:00
A
9e627dff29
refactor: remove dead code and stale references (#2691)
Remove stale '// --- Swap Space Setup' section header from agent-setup.ts
that had no associated code. Swap space setup was moved to cloud init
userdata scripts (aws.ts, hetzner.ts etc.) but the empty section header
was left behind.

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>
2026-03-16 05:53:01 -07:00
A
64a181c8ea
test: remove theatrical NODE_INSTALL_CMD test and fix banned homedir import (#2692)
- cloud-init.test.ts: remove the NODE_INSTALL_CMD describe block that just
  checked if a string constant contains "curl" and "22". This is a snapshot
  test of a string literal with no behavioral signal.

- paths.test.ts: remove the banned `import { homedir } from "node:os"`.
  Per testing rules, named imports of homedir() bypass the preload sandbox
  mock (os.homedir default-export patch) and return the real home directory,
  making tests non-isolated. Replace the "falls back to os.homedir()" test
  with a behavioral assertion (result is a non-empty string) instead of
  comparing against the banned homedir() call.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-16 05:51:32 -07:00
A
1696ecdaa9
fix(security): add defense-in-depth username validation in GCP startup script (#2689)
Add explicit username format validation (`/^[a-zA-Z0-9_-]+$/`) as
defense-in-depth in `getStartupScript()` and `createInstance()`. While
`resolveUsername()` currently returns a constant, this belt-and-suspenders
check prevents shell injection if the function is ever changed to accept
dynamic input.

Fixes #2688

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-16 01:38:21 -07:00
A
085759aeaf
fix(security): add AWS secret key validation and harden path traversal (#2690)
- Add validateAwsSecretKey() function checking 40-char format
- Validate secret key in loadCredsFromConfig() and lightsailRest()
- Add normalize() to canonicalize paths before traversal check
- Harden both uploadFile() and downloadFile() path validation
- Update test fixtures with properly-formatted mock secret keys
- Add test for invalid secret key format rejection

Fixes #2686
Fixes #2687

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-16 01:29:01 -07:00
A
e5725b9a66
fix(gcp): add /usr/local/bin to .spawnrc PATH for npm-global agents (#2681)
GCP VMs install kilocode (and other npm-global agents) to /usr/local/bin
via `npm install -g`. The .spawnrc PATH export relied on $PATH inheriting
/usr/local/bin from the SSH/login shell chain, but on GCP VMs the PATH
can be minimal depending on how the session is initiated (login shell
sourcing order, /etc/profile.d availability). Explicitly include
/usr/local/bin to ensure npm globally-installed binaries are always
findable regardless of base PATH.

Also updates fix.ts to keep its PATH in sync with generateEnvConfig().

Fixes #2679

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 00:00:09 -07:00
A
09576f16ef
fix(ui): remove confusing "None" checkbox from setup options (#2682)
The "None" sentinel option stayed checked alongside real selections,
which was confusing. Remove it — the multiselect already supports
submitting with nothing selected via `required: false`.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 23:43:01 -07:00
A
5cc9930769
feat(cli): add spawn link command to reconnect existing deployments (#2675)
Adds `spawn link <ip>` command that re-registers an existing cloud VM
in spawn's local state, so commands like `spawn list`, `spawn delete`,
and `spawn fix` work on it without reprovisioning.

Features:
- Auto-detects running agent via SSH (ps aux + which checks)
- Auto-detects cloud provider via IMDS metadata endpoints (Hetzner,
  AWS, DigitalOcean, GCP)
- Accepts --agent, --cloud, --user, --name flags to skip auto-detection
- TCP connectivity pre-check before SSH attempts
- Creates a SpawnRecord in history with full connection info
- Offers to connect immediately after linking
- Interactive picker fallback when auto-detection fails
- Non-interactive mode support (exits with clear error if detection
  fails without --agent/--cloud flags)

Also adds --user / -u to KNOWN_FLAGS for the unknown-flag checker.

Fixes #2673

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-15 23:11:13 -07:00
Ahmed Abushagur
6ef20ed437
fix(aws): auto-select server size by agent (#2676)
* fix(aws): auto-select server size instead of prompting

OpenClaw gets 4GB (medium_3_0), all other agents get 2GB (small_3_0).
Users can still override with SPAWN_CUSTOM=1 or LIGHTSAIL_BUNDLE env var.
Matches the auto-select behavior already used by DO and Hetzner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: guide Windows users to WSL at startup

Detects win32 platform and prints step-by-step WSL setup instructions
instead of failing with a confusing error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Revert "feat: guide Windows users to WSL at startup"

This reverts commit 8db72880ae.

* test: update DEFAULT_BUNDLE assertion to small_3_0

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>
2026-03-15 23:08:41 -07:00
A
c4961eb5cd
fix(e2e): prevent concurrent history write race and fix GCP HOME env (#2678)
* fix(history): use process-unique tmp file to prevent concurrent write race

Multiple spawn processes running in parallel (e.g. during E2E tests with
--parallel 6) all write to the same history.json.tmp path, causing ENOENT
when one process renames the file before another can. Use a pid+timestamp
suffix so each process writes to its own unique tmp file.

Fixes provision crashes seen in hetzner-junie E2E runs where the fatal
"rename history.json.tmp -> history.json" error aborted the session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(gcp): export HOME=/root in startup script to match cloud-init behavior

DigitalOcean and Hetzner cloud-init scripts both set `export HOME=/root`
before running Node installation. GCP's startup script did not, which
could cause `n` (the Node.js version manager) to install Node to an
unexpected location when HOME is unset or points elsewhere.

Without a consistent HOME, `npm prefix -g` may return a path that doesn't
match what the subsequent `npm install -g @kilocode/cli` expects, causing
the install to fail silently and leaving the kilocode binary absent.

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>
2026-03-15 23:06:23 -07:00
A
0f0bdf229f
test: remove duplicate and theatrical tests (#2677)
Consolidated redundant test setups in agent-tarball and cmdrun-happy-path
test suites:

- agent-tarball.test.ts: merged 4 mirror-cmd tests (all invoking the same
  tryTarballInstall call and inspecting the same mirrorCmd string) into a
  single test with shared beforeEach setup. Retained the non-fatal failure
  test separately since it has a different mock setup.

- cmdrun-happy-path.test.ts: collapsed 3 identical-setup dry-run tests into
  one consolidated test, and merged the two same-invocation launch-message
  tests into one. Each removed test was a pure duplicate of setup + assertion
  that could be expressed as additional expects in the same test.

Net: 1417 → 1411 tests (-6), 0 regressions.

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>
2026-03-15 22:20:53 -07:00
A
0ea2692e1e
fix(github-auth): always run gh setup when user explicitly opts in (#2674)
When the user selects the GitHub CLI step in setup options (interactive
prompt or --steps github), offerGithubAuth() was silently returning early
if no local gh token was found by detectGithubAuth(). This made the step
unreachable for users without gh installed locally — exactly the ones who
need remote setup most.

Fix: accept an `explicitlyRequested` parameter in offerGithubAuth(). When
true, skip the githubAuthRequested guard and always run the remote install.
The orchestrator passes enabledSteps?.has("github") as this flag.

detectGithubAuth() still auto-enables the step when a local token exists
(convenience forwarding), but can no longer block a user-explicit request.

Fixes #2672

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-15 22:19:38 -07:00
A
00df240f49
feat(openclaw): add channel selection to setup options (#2671)
Add BlueBubbles, Discord, Slack, Signal, and Google Chat to the
multi-select setup options for OpenClaw. Selected channels get
`enabled: true` stubs written via `openclaw config set`, so the
dashboard renders channel cards properly instead of showing
"Unsupported type: . Use Raw mode."

Channels are gated by enabledSteps — only user-selected channels
get stubbed. WhatsApp and Telegram remain in the list as before.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:21:42 -07:00
A
4f4b535c8d
fix(security): validate remotePath and harden base64 interpolation in uploadConfigFile (#2669)
Add strict character validation for remotePath to prevent command injection
via crafted paths. Use shellQuote for tempRemote in the shell command. Add
a base64 output assertion to document and enforce the safety of single-quoted
interpolation for settingsB64.

Fixes #2668

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-15 19:04:19 -07:00
A
6b2001def4
docs(tests): document 8 undocumented test files in __tests__/README.md (#2670)
The test README was missing entries for 8 test files that were added
after the initial documentation was written:
- cmd-feedback.test.ts
- cmd-fix.test.ts
- config-priority.test.ts
- delete-spinner.test.ts
- gcp-shellquote.test.ts
- oauth-pkce.test.ts
- result-helpers.test.ts
- steps-flag.test.ts
- spawn-config.test.ts

Added descriptions under the appropriate section headers so the README
accurately reflects all test coverage.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-15 18:39:57 -07:00
A
9ad89a414a
fix(cli): replace "spawn update" launch hint with "spawn feedback" (#2665)
Replace startup banner message from "Run spawn update to check for
updates." to "Run spawn feedback to tell us what to improve."

Bumps CLI patch version to 0.19.1.

Fixes #2664

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-15 14:11:46 -07:00
A
4bff229238
refactor: remove dead deepMerge export from parse.ts (#2663)
deepMerge was exported from shared/parse.ts but never imported or called
from any other module. Biome confirms it as an unused variable. Removing
it eliminates dead code and the now-unused isPlainObject import.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 13:57:47 -07:00
A
52eaa19466
fix: allow empty string values for CLI flags like --steps "" (#2662)
extractFlagValue() used `!args[idx + 1]` to detect a missing value,
which treated empty strings as missing. Change to `=== undefined` so
that `--steps ""` passes through correctly as documented.

Fixes #2661

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-15 13:45:39 -07:00
Ahmed Abushagur
0a7a95ec3c
feat: add custom model selection to all agents (#2659)
Move "Custom model" from OpenClaw-specific to common setup steps so
every agent shows it in the setup menu. Add modelEnvVar to agents that
support model override via environment variable:

- Kilo Code: KILOCODE_MODEL
- ZeroClaw: ZEROCLAW_MODEL
- Hermes: LLM_MODEL
- Junie: JUNIE_MODEL

When a custom model is selected, the env var is injected into .spawnrc
alongside the other agent env vars. OpenClaw continues to use its
existing configure() path. Claude and Codex don't have modelEnvVar
since they handle model routing differently.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 12:44:48 -07:00
Ahmed Abushagur
bc2aa89002
fix: enable channel stubs so openclaw extensions load their schemas (#2658)
Channel extensions only register their UI schemas when enabled. With
enabled=false the dashboard still shows "Unsupported type: . Use Raw
mode." Setting enabled=true lets the extensions load so users can
configure channels from the dashboard.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:48:40 -04:00
Ahmed Abushagur
9ca71f2da7
fix: write channel stubs in openclaw config for dashboard rendering (#2657)
Write disabled telegram and whatsapp channel entries during setup so
the OpenClaw dashboard renders proper channel cards instead of showing
"Unsupported type: . Use Raw mode." Users can then configure channels
from the dashboard UI.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 10:56:42 -07:00
A
6cf748e1b5
feat(openclaw): use openclaw onboard --non-interactive instead of manual config JSON (#2655)
Replace the manual config JSON construction + download-merge-upload flow
with `openclaw onboard --non-interactive`, which creates a properly
structured config with auth profiles, provider setup, gateway config,
and workspace. Follow up with `openclaw config set` for browser and
Telegram settings.

This fixes the broken dashboard channel setup caused by bypassing
OpenClaw's credential/auth profile system. Removes the gateway auth
re-assertion hack that was needed due to field-dropping during
config set cycles on manually-written JSON.

Includes a fallback path that writes minimal JSON if onboard fails.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:44:37 -04:00
A
8d3d7e4619
feat(oauth): add PKCE S256 code challenge to OpenRouter OAuth flow (#2654)
Implements RFC 7636 PKCE with S256 code challenge method for the
OpenRouter OAuth authorization flow. This prevents authorization code
interception attacks by binding the code to a cryptographic verifier.

Changes:
- Generate code_verifier (32 random bytes, base64url-encoded)
- Derive code_challenge via SHA-256 + base64url
- Send code_challenge + code_challenge_method=S256 in auth URL
- Send code_verifier + code_challenge_method in token exchange POST
- Add test suite with RFC 7636 Appendix B test vector validation

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>
2026-03-15 10:14:48 -07:00
A
87391b2a4a
test: remove duplicate and theatrical tests (#2652)
- security.test.ts: remove "comprehensively detect all command injection
  patterns from issue #1400" test (14 lines). All 6 attack vectors
  (&&, ||, >, <, &, ${}) are already tested individually in dedicated
  tests above it, making this aggregate loop purely redundant.

- gcp-shellquote.test.ts: remove 2 redundant startsWith/endsWith
  assertions from "should produce output that is safe for bash -c".
  The toBe("'$(rm -rf /)'") assertion already proves the single-quote
  wrapping; the follow-up checks add no signal.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 13:11:58 -04:00
A
d6e2eb3aad
refactor: add JSDoc to aws.getState() clarifying test-only usage (#2651)
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>
2026-03-15 05:51:24 -07:00
A
6a439021e5
test: remove duplicate and theatrical tests (#2650)
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>
2026-03-15 08:49:51 -04:00
Ahmed Abushagur
05c7070396
fix: re-upload openclaw config after config set calls to preserve channels (#2649)
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>
2026-03-15 06:46:52 -04:00
A
65b29e3757
test: remove duplicate and theatrical tests (#2646)
- 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>
2026-03-15 02:15:10 -07:00