Previously, `spawn claude sprite --help` would warn about extra args
and proceed to provision a server. Now trailing help/version flags are
detected and handled correctly in both the default command path and
verb alias path (e.g., `spawn run claude sprite --help`).
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The regex `configPath.replace(/\/[^/]+$/, "")` only matches forward
slashes, so on Windows (which uses backslashes) it returns the full
path unchanged. `mkdirSync` then creates `digitalocean.json` as a
directory, causing EISDIR on the next write.
Replace with `dirname()` from `node:path` which handles both separators.
Affects digitalocean.ts, hetzner.ts, and aws.ts (oauth.ts already used
dirname correctly).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: PR Reviewer <pr-reviewer@spawn>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
validatePromptFilePath used path.resolve() which only normalizes the
string but doesn't follow symlinks. An attacker could create a symlink
(e.g., innocent.txt -> ~/.ssh/id_rsa) to bypass sensitive path checks
and exfiltrate credentials. Now uses realpathSync() to canonicalize
the path before pattern matching.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lightsail can report state=running before assigning a public IP. Continue
polling until both state is running and IP is non-empty, preventing SSH
connection failures from an empty IP address.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hetzner API defaults to 25 items per page. Users with >25 SSH keys would
hit SSH lockout on server creation because the newly registered key landed
on page 2+ and was omitted from the ssh_keys payload.
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>
Bun.write does not support the `mode` option, so credential config files
(Hetzner, DigitalOcean, AWS, OpenRouter) were created with 0644 permissions
instead of the intended 0600, exposing API tokens to other local users.
Switch to node:fs writeFileSync which correctly applies file permissions.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
On GCP VMs (running as root), npm installs openclaw to /usr/local/bin
instead of ~/.npm-global/bin because the system npm prefix is writable
and already in PATH. The E2E verify_openclaw() and related gateway
helper functions only explicitly listed ~/.npm-global/bin, ~/.bun/bin,
and ~/.local/bin — missing /usr/local/bin when .spawnrc sourcing
silently fails in the piped-bash SSH exec context.
Add /usr/local/bin explicitly to all openclaw-related PATH exports in
verify.sh so the binary check succeeds regardless of .spawnrc state.
Fixes#2732
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
Add missing 'spawn uninstall' command to the Commands table. The command
exists in packages/cli/src/commands/help.ts (getHelpUsageSection) but was
absent from the README commands table.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
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>
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>
* 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>
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>
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>
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>
The E2E framework's run_single_agent function had no overall timeout.
When provision/verify/input_test steps hung (e.g. cloud_exec blocking
on sprite-zeroclaw or digitalocean-opencode), the process would stall
indefinitely without writing a .result file, causing silent test failures.
Add a per-agent wall-clock timeout (default 1800s, 2400s for junie) that
wraps the core provision/verify/input_test logic in a killable subshell.
If the timeout expires, the subshell is killed and a "fail" result is
written, ensuring E2E batches always complete.
Fixes#2714
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Use ${CLAUDE_MODEL_FLAG:+"${CLAUDE_MODEL_FLAG}"} to prevent word-splitting
and glob expansion on values containing spaces or special characters.
When the variable is empty/unset, this expands to nothing (no empty arg).
Note: qa.sh does not use CLAUDE_MODEL_FLAG so no change needed there.
Fixes#2698
Agent: style-reviewer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
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>
The safe_substitute() function in discovery.sh, qa.sh, refactor.sh, and
security.sh escaped \, &, and | but not newlines. A newline in the
replacement value would break the sed s command, causing failure or
unexpected behavior. Add newline escaping (backslash + literal newline)
after the existing metacharacter escaping.
Fixes#2702
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* 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>
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>
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>
@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>
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>
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>
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>
- 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>
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>
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>
* 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>
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>
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>
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>
- 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>
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>
- 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#2686Fixes#2687
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(e2e): increase provision timeout for junie on hetzner
junie's install takes >720s on Hetzner, exceeding the default
PROVISION_TIMEOUT and causing 100% E2E failure for hetzner-junie.
Add a per-agent provision timeout mechanism in common.sh via
get_provision_timeout(). This checks (in order):
1. PROVISION_TIMEOUT_<agent> env var override
2. Built-in per-agent default (_PROVISION_TIMEOUT_junie=1200)
3. Global PROVISION_TIMEOUT (720s)
provision.sh now calls get_provision_timeout() to resolve the
effective timeout per agent instead of using the flat global.
Fixes#2680
Agent: code-health
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): whitelist-sanitize agent name before eval in get_provision_timeout
tr '-' '_' only replaced hyphens, allowing metacharacters like $, backticks,
and ; to pass through into eval, enabling shell injection via a crafted agent
name. Replace with sed whitelist [A-Za-z0-9_] to strip all unsafe chars.
Agent: team-lead
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.6 <noreply@anthropic.com>
Add a new style-reviewer agent to the refactor team that enforces project
rules from CLAUDE.md and .claude/rules/ (biome lint, shell script compat,
type safety, test conventions). Runs proactively during refactor cycles.
Also add `claude update --yes` to all 4 launcher scripts (refactor.sh,
discovery.sh, security.sh, qa.sh) so agents always run on the latest
Claude Code version before each cycle.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update the pr-reviewer protocol to use the GitHub Pull Request Review API
(POST /repos/.../pulls/NUMBER/reviews) with an inline comments array,
pinning each security finding to the exact file:line in the PR diff.
The summary body is preserved for overview, while each finding also
appears as an inline comment on the specific code location.
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>
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>
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>
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>
* 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>
* 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>
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>
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>
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>
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>