Commit graph

1631 commits

Author SHA1 Message Date
A
f49cd97cdf
fix(ux): apply resolveListFilters to cmdDelete so bare positional args work (#2002)
spawn delete hetzner was silently returning "No active servers to delete"
even when the user had active Hetzner servers. The positional arg was
parsed as agentFilter, but no agent is named "hetzner", so the filter
matched nothing. cmdList already calls resolveListFilters() which
auto-promotes a bare arg to cloudFilter when no agent matches — cmdDelete
was missing this call entirely.

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-27 14:18:55 -05:00
A
ce1f13748f
fix(security): restrict file permissions on history and config directories (#2000)
* fix(security): restrict file permissions on history and config directories

History files (history.json, last-connection.json) were created with
default permissions (0644), making server IPs, usernames, and cloud
provider details readable by other local users on shared systems.
Config directories (~/.config/spawn/) were created via mkdir -p with
default umask (0755), making them world-listable.

- Add mode: 0o600 to all writeFileSync calls in history.ts
- Add mode: 0o700 to mkdirSync calls for ~/.spawn/ directory
- Replace Bun.spawn(["mkdir","-p",...]) with mkdirSync({mode:0o700})
  in hetzner, aws, digitalocean, and daytona modules

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

* style: fix Biome formatting for multiline object literals

Expand inline `{ mode: 0o600 }` and `{ recursive: true, mode: 0o700 }`
to multiline format to satisfy Biome's formatter.

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-02-27 13:18:16 -05:00
A
97c8ad0a78
refactor: extract shared TTY scaffolding in picker.ts (#1999)
* refactor: extract shared TTY scaffolding in picker.ts

pickToTTYWithActions (248 lines) and multiPickToTTY (204 lines) shared
~120 lines of identical TTY lifecycle code: open /dev/tty, save/restore
stty settings, raw mode, write helper, restore helper, key-read buffer,
and the read loop skeleton.

Extract withTTYKeyLoop<T>() that owns the entire TTY lifecycle and
delegates rendering and key handling via callbacks. Both picker functions
now focus solely on their mode-specific logic.

Net: 672 -> 561 lines (-111), with TTY management in a single place.

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

* style: apply biome formatting to picker.ts

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

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-27 13:17:01 -05:00
A
d46960969f
refactor: Remove stale comments referencing non-existent test files (#1997)
- cmdlist-integration.test.ts: removed references to list-display.test.ts,
  list-table-rendering.test.ts, list-empty-footer.test.ts,
  list-filter-suggestions.test.ts, and list-prompt-display.test.ts (none exist)
- history.test.ts: removed stale formatTimestamp reference; the function was
  removed from production code, leaving the describe block name misleading

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 12:17:55 -05:00
A
d92d0e6e21
fix(security): prevent flag injection via hyphen-leading remote paths (#1996)
* fix(security): prevent flag injection via hyphen-leading path segments in uploadFile

Reject remote paths where any segment starts with "-" (e.g., "-e", "/tmp/-evil")
across all 6 cloud providers. This prevents potential CLI flag injection in
commands like base64, printf, mv, and scp.

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

* style: fix biome format for path validation conditions

Break long if-conditions across multiple lines and add parentheses
around arrow function parameters to satisfy biome formatter.

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

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-27 11:17:13 -05:00
A
aa0948ab07
test: Remove duplicate listing command tests (#1991)
* test: remove duplicate listing command tests from commands-display.test.ts

The cmdMatrix, cmdAgents, cmdClouds, and related edge-case tests were
duplicated between commands-display.test.ts and cmd-listing-output.test.ts.
The listing-output file provides more thorough end-to-end coverage with
grid/compact view testing, type grouping, cross-command consistency checks,
and better edge case coverage. Remove the weaker duplicates (21 tests) from
commands-display.test.ts, keeping the unique cmdAgentInfo and cmdHelp tests.

-- qa/dedup-scanner

* style: fix trailing blank line in commands-display.test.ts

Remove extra blank line before closing brace that caused Biome format check failure.

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

---------

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 09:21:13 -05:00
A
d6e262e39c
fix: prevent security bot from closing fresh draft PRs (#1993)
The security bot was closing draft PRs that were only hours old,
claiming "no updates for 7+ days." Two root causes:

1. Step 1 said "skip draft PRs" but Step 3.5 referenced "the PR list
   from Step 1" to find drafts — contradictory instructions meant the
   bot either had no drafts to check or applied stale logic to
   non-drafts.

2. Step 3.5 had no explicit date arithmetic — just "updatedAt older
   than 7 days" which the bot interpreted loosely without verification.

Fix: Step 1 now saves the full list (drafts included) for Step 3.5.
Step 3.5 now requires mandatory date arithmetic (epoch comparison) and
draft/non-draft timeline checks before closing. Safety section also
strengthened with explicit draft age requirement.

Fixes the false positive that closed PR #1974 (2 hours old).

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 09:19:49 -05:00
A
5ed1d621ab
refactor: remove dead code (formatTimestamp, unused test helpers) (#1992)
- Remove `formatTimestamp` from commands.ts: exported but never imported
  or called anywhere in the codebase
- Remove `mockFetchWithStatus` from test-helpers.ts: exported but never
  imported by any test file
- Remove `createProcessExitMock` from test-helpers.ts: exported but
  never imported by any test file

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:30:15 -05:00
A
a15a9da32f
test: remove false-confidence do-oauth.test.ts (#1990)
* test: remove false-confidence do-oauth.test.ts

This file imported zero functions/constants from the DigitalOcean
source module. Every test re-created private constants (tokenRegex,
codeRegex, DO_SCOPES, SUCCESS_HTML, etc.) inline and tested them
against themselves. If source code changed, these tests still passed
silently — providing false confidence worse than no tests.

Per CLAUDE.md: "If a function is not exported, do NOT test it
(don't re-implement it inline)."

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

* chore: remove plan file from commit

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

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-27 07:21:57 -05:00
A
e13d809f37
fix(security): add path traversal defense-in-depth to uploadFile (#1988)
Add `|| remotePath.includes("..")` check to hetzner, digitalocean,
and aws uploadFile functions. The regex `/^[a-zA-Z0-9/_.~-]+$/`
allows `.` characters, so paths like `../../etc/passwd` pass the
regex but are path traversal attempts. gcp, daytona, and sprite
already include this explicit check — this makes all providers
consistent.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-27 03:20:32 -08:00
A
2eb623e386
fix(security): prevent command injection in branch deletion loops (#1989)
Add is_safe_branch_name() validation to all four cycle scripts
(discovery.sh, refactor.sh, qa.sh, security.sh) to reject branch
names containing shell metacharacters before passing them to git
push --delete or git branch -D. Also adds -- end-of-options
separator to all git branch commands to prevent flag injection.

Fixes #1960

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-27 11:18:08 +00:00
A
d02e298488
test: Remove duplicate and theatrical tests (#1986)
* test: Remove duplicate and theatrical tests

- cmd-listing-output: Fix always-pass guard (if (localLine) → expect defined then check)
- with-retry-result: Replace conditional if (!r.ok) expects with toMatchObject
- run-path-credential-display: Remove 96 lines of duplicate tests
  - parseAuthEnvVars for credential status (duplicate of commands-exported-utils.test.ts)
  - credential function edge cases with weak OR assertions (duplicate of credential-hints.test.ts)
  - Migrated 2 unique edge cases (whitespace trimming, empty separator) to commands-exported-utils.test.ts

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

* fix: apply biome format to test files in qa/dedup-scanner

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

---------

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-02-27 05:18:03 -05:00
A
edcdf78ba8
fix(e2e): correct env var names passed to AWS CLI in provision.sh (#1985)
The provision.sh was setting wrong env var names that the TypeScript CLI
does not read:
- AWS_LIGHTSAIL_INSTANCE_NAME → LIGHTSAIL_SERVER_NAME (read by aws.ts:getServerName)
- AWS_REGION → AWS_DEFAULT_REGION (read by aws.ts:authenticate/promptRegion)
- AWS_BUNDLE → LIGHTSAIL_BUNDLE (read by aws.ts:promptBundle)

Without the correct names, each provisioning run created an instance with a
random generated name instead of app_name, causing the post-provision
existence check to fail every time.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 01:47:41 -08:00
A
fe7cf1ba4b
refactor: Remove dead code and stale references (#1987)
Remove stale Fly.io references from shared shell scripts. Fly.io was
removed as a cloud provider (#1979) but comments referencing its
specific token format ("FlyV1 <macaroon>") and container behavior
remained in key-request.sh and github-auth.sh.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 04:19:53 -05:00
A
d8131c3df6
fix(hetzner): update deprecated server types to cx23/cpx22 gen (#1983)
* fix(hetzner): update deprecated cx22/cpx21 server types to cx23/cpx22

Hetzner deprecated the entire cx*2 and cpx*1 server lines on Jan 1, 2026.
New orders fail with "server type is deprecated". Updates to the current
gen3 CX and gen2 CPX lines (cx23, cx33, cx43, cx53, cpx22, cpx32).

Also shows the server type picker by default instead of requiring --custom,
so users can choose their instance size on every deploy.

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

* fix(zeroclaw): append autonomy config instead of overwriting onboard output

zeroclaw onboard generates a complete config with required fields like
default_temperature. Our setup was overwriting that with a partial config
missing required fields, causing a crash loop on startup. Now appends
the security/shell settings instead so onboard's fields are preserved.

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

* style: fix biome formatting in agent-setup.ts

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

---------

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-02-27 00:20:31 -08:00
A
3e0b35e23d
fix(security): document DigitalOcean OAuth public client pattern (#1980) (#1984)
DigitalOcean's token exchange endpoint requires client_secret and does
not support PKCE-only public client flows. The embedded secret follows
the same pattern used by gh CLI, doctl, gcloud, and az CLI. Expanded
the comment to explain:
- Why client_secret is required (no PKCE support)
- Why embedding it is acceptable (public client, RFC 6749 §2.1)
- What security mechanisms are actually relied upon
- When the secret should be removed (if DO adds PKCE)

Fixes #1980

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 03:17:55 -05:00
A
4436a01372
fix(aws): increase OpenClaw gateway timeout and default to medium bundle (#1982)
* fix(aws): increase OpenClaw gateway timeout to 120s and default to medium bundle

OpenClaw gateway consistently times out on AWS Lightsail because the 60s
timeout is too short for cold starts (npm install of 713 packages + gateway
init). Doubles the timeout to 120s and sets the default bundle for OpenClaw
to medium_3_0 (4 GB RAM) since it's too heavy for nano (512 MB).

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

* fix: resolve openclaw binary path for setsid and add npm-global to Sprite PATH

setsid replaces the process image and doesn't inherit the parent shell's
exported PATH, causing "No such file or directory" on Sprite (and potentially
other clouds). Fix by resolving the full binary path with `command -v` before
passing it to setsid. Also adds ~/.npm-global/bin to Sprite's persisted shell
PATH config so openclaw is discoverable in all session types.

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

* fix(codex): update wire_api from "chat" to "responses"

Codex CLI dropped support for wire_api = "chat" — it now requires
"responses". This was never updated since the original codex integration,
causing an immediate crash loop on launch.

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

* fix: enable GitHub CLI auth for all agents, not just Claude Code

Only Claude Code had preProvision: promptGithubAuth — all other agents
(codex, openclaw, opencode, kilocode, zeroclaw) skipped GitHub auth
entirely. These are all coding agents that need gh access for PRs,
cloning, etc.

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

* fix: add missing spawn import that crashes headless mode (#1981)

runBashHeadless calls spawn() from node:child_process at line 1112,
but only spawnSync was imported. This causes a ReferenceError crash
whenever --headless mode is used.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-02-27 01:58:17 -05:00
A
9bc8e5a0d0
fix: add missing spawn import that crashes headless mode (#1981)
runBashHeadless calls spawn() from node:child_process at line 1112,
but only spawnSync was imported. This causes a ReferenceError crash
whenever --headless mode is used.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 01:50:37 -05:00
A
7d83486077
refactor: Remove dead code and fix stale references in QA sweep (#1978)
- Replace local rec() helper in hetzner.ts with shared toRecord() from
  @openrouter/spawn-shared, eliminating a duplicate implementation that
  already existed in the shared package with equivalent behavior
- Fix stale comment in key-request.sh referencing non-existent qa.sh

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-02-27 00:10:16 -05:00
A
d04096a15b
feat!: remove Fly.io cloud provider support (#1979)
* feat!: remove Fly.io cloud provider support

Drop Fly.io as a supported cloud provider. Sprite (which uses Fly.io
infrastructure internally) is retained.

- Delete packages/cli/src/fly/ module, sh/fly/ scripts, fixtures/fly/
- Remove fly cloud entry and 6 fly matrix entries from manifest.json
- Remove fly imports, destroy cases, and connection handlers from commands.ts
- Remove fly-ssh sentinel from security.ts
- Port E2E test suite from Fly.io to AWS Lightsail (fly-e2e.sh → aws-e2e.sh)
- Update README (7 clouds, 42 combinations), CLAUDE.md, and skill prompts
- Clean up fly references in build config, gitignore, icon sources
- Bump CLI version to 0.11.0

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

* chore: restore Docker image build under sh/docker/

Move openclaw Dockerfile from sh/fly/docker/ to sh/docker/ and rename
workflow from fly-docker.yml to docker.yml with updated paths.

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

* style: fix extra blank lines in commands.ts

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

---------

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-02-27 00:06:32 -05:00
A
b9b0b40856
test: remove duplicate isValidManifest block from commands-swap-resolve (#1977)
The manifest validation (isValidManifest) describe block in
commands-swap-resolve.test.ts used an always-pass anti-pattern:
try { await loadManifest() } catch {} followed by console.error.some()
assertions. This pattern silently passes even when the expected rejection
path is not triggered.

The same coverage (missing agents/clouds/matrix fields, null data, HTTP
errors, valid manifests) is already provided by manifest-cache-lifecycle.test.ts
with proper expect().rejects.toThrow() assertions.

Remove the duplicate 145-line block. No regression: pass/fail counts unchanged.

-- qa/dedup-scanner

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 00:04:41 -05:00
A
f1c2b4d4e4
refactor: Remove dead code and stale references (#1976)
* refactor: remove dead offerGithubAuth exports from cloud agents.ts files

The per-cloud offerGithubAuth re-exports in each cloud's agents.ts were
never called from outside their own module. The actual GitHub auth
orchestration is handled by shared/orchestrate.ts which calls
offerGithubAuth from shared/agent-setup.ts directly.

Also update stale comment in sh/test/fixtures/_shared_agent_assertions.sh
that referenced mock.sh, a test harness file that no longer exists in
the repository.

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

* style: collapse multi-line imports to single-line per biome format

After removing offerGithubAuth exports, the remaining 2-import blocks
should be single-line. Also collapse fly/agents.ts 4-import block and
remove trailing blank line.

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

---------

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-02-26 22:04:33 -05:00
A
2e8f30c863
test: Remove duplicate and theatrical tests (#1975)
* test: Remove duplicate and theatrical tests

- Remove aws/agents describe block from aws.test.ts — it duplicated
  the identical resolveAgent, agent configs, and generateEnvConfig
  tests already present in fly.test.ts; both test the same shared
  createAgents/resolveAgent logic from shared/agent-setup.ts
- Remove duplicate dotenv + interactive_prompts checks from
  manifest-type-contracts.test.ts "Agent optional field types" section
  — these are fully covered by the dedicated "Dotenv configuration"
  and "Interactive prompts structure" sections below
- Fix always-skip test in history.test.ts: guard was silently skipping
  when running as root (CI environment); replaced with explicit early
  return inside block statement
- Fix conditional expects in commands-display.test.ts: the
  if (line.includes("cloud")) / if (line.includes("agent")) guards
  were unnecessary since every agent/cloud line always contains the
  count string; rewrote to unconditional output assertions
- Fix redundant if (resolved) guard in run-path-credential-display.test.ts
  after expect(resolved).toBe("claude") already guarantees non-null

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

* style: fix biome format issues in test files

Remove trailing blank line in aws.test.ts and expand single-line
if-block to multi-line in history.test.ts per biome format rules.

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

---------

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-02-26 22:03:35 -05:00
A
b7b8576511
fix(ux): prevent &; syntax error when pre_launch ends with & (#1973)
Fixes #1971

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 18:41:21 -08:00
A
cb4e6839b7
fix: add ~/.npm-global/bin to OpenClaw PATH for gateway, launch, and reconnect (#1972)
* fix: add ~/.npm-global/bin to OpenClaw PATH for gateway, launch, and reconnect

OpenClaw installs to ~/.npm-global/bin/ via npm, but startGateway() and
launchCmd() only included ~/.bun/bin and ~/.local/bin in PATH — so the
`openclaw` binary was never found on non-Fly clouds (DigitalOcean, Hetzner,
AWS, GCP). Fly was unaffected because it uses setupOpenclawBatched() which
correctly includes the npm-global path.

Three fixes:
1. startGateway(): add $HOME/.npm-global/bin to PATH
2. launchCmd(): add $HOME/.npm-global/bin to PATH
3. install(): persist PATH to ~/.bashrc and ~/.zshrc (matching codex/kilocode
   pattern) so reconnects via `spawn openclaw <cloud> --name ...` also work

Closes #1965

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

* fix: correct command chaining and idempotency in npm-global PATH setup

- Use curly braces to group grep||echo so PATH append only runs after
  successful npm install (fixes operator precedence bug)
- Skip ~/.zshrc modification when file doesn't exist (avoids creating
  it on non-zsh systems)
- Use grep -qF for literal string matching (no regex interpretation)
- Apply fix to all three affected agents: openclaw, codex, kilocode

Co-Authored-By: Claude Sonnet 4.6 <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: B <6723574+louisgv@users.noreply.github.com>
2026-02-26 20:45:36 -05:00
A
e2f40b3d8b
docs(digitalocean): document DO_REGION, DO_DROPLET_SIZE, and --custom flag (#1970)
Add Environment Variables section to sh/digitalocean/README.md with:
- DO_REGION: list of all 10 available regions with default (nyc3)
- DO_DROPLET_SIZE: list of all 6 available sizes with default (s-2vcpu-4gb)
- --custom flag: interactive region + size picker

Fixes #1968

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 16:35:47 -08:00
A
0260b605ce
test: fix always-pass and err:any anti-patterns in manifest tests (#1967)
Two tests used try/catch with assertions in both branches, meaning they
passed whether loadManifest succeeded or threw. The comment claimed local
manifest fallback could be used, but tryLoadLocalManifest() returns null
in test environments (NODE_ENV=test), so the function always throws here.

Replace with expect().rejects.toThrow() which fails if no error is thrown
and eliminates the banned `err: any` type assertion.

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 16:31:32 -08:00
A
15d8828638
fix: add npm-global/bin to PATH for openclaw gateway on non-Fly clouds (#1969)
* fix: add npm-global/bin to PATH for openclaw startGateway and launchCmd

Fixes crash where openclaw gateway fails to start on non-Fly clouds
(DigitalOcean, Hetzner, AWS, GCP) because ~/.npm-global/bin was absent
from PATH in startGateway() and launchCmd(). Fly was unaffected because
setupOpenclawBatched() already included the correct PATH.

Fixes #1965

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

* style: fix Biome format error on launchCmd line

Agent: pr-maintainer

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

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 18:46:55 -05:00
A
0f968eb142
fix(ux): replace non-existent --non-interactive flag in list message (#1966)
The "no active servers" message suggested `spawn list --non-interactive`,
but --non-interactive is not a recognized CLI flag. Running it would
trigger an "Unknown flag" error since checkUnknownFlags() rejects it
before any subcommand dispatch.

Replace with `spawn list | cat`, which correctly forces non-interactive
output by making process.stdout.isTTY falsy.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 18:45:19 -05:00
A
a64b432d1d
fix(security): prevent command injection in fly.ts uploadFile (#1964)
The uploadFile function interpolated base64-encoded file content
directly into a shell command string via ${b64}, allowing potential
shell metacharacter injection and RCE on the Fly.io machine.

Fix: pipe base64 data through stdin instead of embedding it in
the command string, and add base64 character validation as
defense-in-depth (matching the pattern in daytona.ts).

Fixes #1961

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 17:13:23 -05:00
A
892830689c
refactor: Remove dead code and consolidate duplicate parseJson helpers (#1963)
- Remove CACHE_DIR dead export from manifest.ts (was defined but never imported anywhere)
- Add parseJsonObj() to @openrouter/spawn-shared for parsing JSON objects
- Remove 4x duplicate local parseJson/LooseObject definitions from hetzner, digitalocean, daytona, fly cloud modules
- Remove now-unused `import * as v from "valibot"` from all 4 cloud modules
- Bump CLI to 0.10.24

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-02-26 16:17:47 -05:00
A
6ee0bbed77
test: fix always-pass and try/catch anti-patterns in test suite (#1962)
- manifest-helpers.test.ts: 7 tests used try/catch where the catch block
  held all assertions. Since loadManifest() loads the local manifest.json
  when NODE_ENV is not "test", these tests passed silently with 0 assertions.
  Fix: set NODE_ENV=test + call _resetCacheForTesting() in beforeEach, and
  replace try/catch with expect(...).rejects.toThrow(). Also remove `any`
  type annotations on agentKeys/cloudKeys helper manifests.

- security-edge-cases.test.ts: "should use custom field name in error messages"
  used a manual guard (throw new Error in try) instead of expect().toThrow().
  Replace with 2 clean expect(() => ...).toThrow() calls.

- prompt-file-security.test.ts + security.test.ts: tests that checked multiple
  error message properties used try/catch with `catch (e: any)`. Replace with
  proper instanceof narrowing so the caught value is typed without `any`.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 16:16:04 -05:00
A
4b45d7295a
test: Remove duplicate and theatrical tests (#1956)
- Delete commands.test.ts (only contained a weak cmdHelp test already
  covered more thoroughly in commands-display.test.ts)
- Delete integration.test.ts (all 6 tests duplicate coverage from
  manifest.test.ts for agentKeys, cloudKeys, matrixStatus,
  countImplemented, and loadManifest)
- Remove duplicate cmdUpdate section from commands-display.test.ts
  (3 weak tests already covered by 7 tests in commands-update-download.test.ts)
- Remove dead variable callCount = 0 in commands-update-download.test.ts

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:09:10 -08:00
A
266552b99f
test: add coverage for cloud-init tier selection functions (#1958)
* test: add coverage for cloud-init tier selection functions

getPackagesForTier, needsNode, and needsBun had zero test coverage
despite non-trivial branching logic (4-way tier switch). Any change
to package lists or tier membership would be silently undetected.

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

* fix: format cloud-init.test.ts to pass biome format check

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

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 13:21:18 -05:00
A
987a577fce
fix(ux): show accurate message when history exists but no active servers (#1959)
spawn list in interactive mode showed "No spawns recorded yet" even when
spawn history existed but no active servers were reachable (e.g. after a
failed spawn or deleted server). Now shows the correct count and hints.

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 12:30:04 -05:00
A
40c1622e5a
docs: Fix stale references in CLAUDE.md (#1957)
- Remove OVH cloud from the curated clouds list (never implemented, not
  in manifest.json) and update count from 9 to 8
- Replace NanoClaw with ZeroClaw in the agents example list (NanoClaw
  does not exist; ZeroClaw is an actual agent in the manifest)
- Remove src/version.ts from the file structure diagram (file does not
  exist in the codebase)
- Fix duplicate "### 4." section heading — "Extend tests" is now "### 5."

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 12:28:38 -05:00
A
7a2031678c
fix(spa): remove --no-session-persistence to allow thread session resume (#1955)
The --no-session-persistence flag prevented Claude Code sessions from
being saved to disk, but the bot was still capturing and storing session
IDs in state.mappings and attempting to --resume them on subsequent
messages in the same thread. Since the session was never persisted,
--resume would fail and Claude Code would exit with a non-zero code,
causing the error block to be posted to Slack instead of a real reply.

Removing --no-session-persistence lets sessions persist normally so that
thread continuity via --resume works as intended.

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 11:24:18 -05:00
A
623b4aca64
fix: add npm-global/bin to PATH for codex and kilocode installs (#1953)
Fixes #1947

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 06:37:51 -08:00
A
0b7f035e4a
fix: update opencode GitHub org from anomalyco to sst/opencode (#1951)
The opencode project migrated from github.com/anomalyco/opencode to
github.com/sst/opencode. The old org's releases may no longer be
updated, causing opencode provisioning to fail.

Updates:
- Release download URL in agent-setup.ts
- url, creator, and repo fields in manifest.json
- Agent table link in README.md

Fixes #1948

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 06:34:26 -08:00
A
a1ab6093f8
test: Remove duplicate and theatrical tests (#1949)
* test: Remove duplicate and theatrical tests

- Remove duplicate getScriptFailureGuidance describe block from
  download-and-failure.test.ts (already covered by script-failure-guidance.test.ts)
- Remove duplicate getStatusDescription and getErrorMessage describe blocks
  from download-and-failure.test.ts (covered by commands-exported-utils.test.ts)
- Remove duplicate buildRetryCommand, isRetryableExitCode, getScriptFailureGuidance,
  and getErrorMessage describe blocks from run-path-credential-display.test.ts
  (all covered by dedicated test files)
- Remove duplicate hasCloudCredentials and credentialHints describe blocks
  from run-path-credential-display.test.ts (covered by cloud-credentials.test.ts
  and credential-hints.test.ts respectively)
- Fix always-pass conditional patterns in manifest-type-contracts.test.ts:
  remove tautological "at least one agent uses X" tests that only registered
  when the condition was already true, making them guaranteed-pass noise

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-- qa/dedup-scanner

* fix: Apply biome format to fix trailing blank lines in test files

Remove trailing blank lines in download-and-failure.test.ts and
run-path-credential-display.test.ts to satisfy biome format check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-- qa/team-lead

---------

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 06:14:39 -08:00
A
95d4ca93bb
refactor: Remove dead code and stale references (#1950)
Remove the `runWithRetry` function exported from 4 cloud modules (aws, hetzner, gcp, digitalocean)
that were defined but never called anywhere in the codebase. Only `fly.ts` uses its own
`runWithRetry` internally, so that definition is preserved.

Also bump CLI version to 0.10.22 per version policy.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 08:36:08 -05:00
A
0a8d9b7440
fix: add missing timeout to Daytona runServer/runServerCapture (#1945)
Daytona was the only cloud provider without process timeouts in
runServer() and runServerCapture(). All other providers (AWS, Fly,
Hetzner, DigitalOcean, GCP) implement setTimeout + killWithTimeout
to prevent the CLI from hanging forever on stalled remote commands.

This adds the same timeout pattern: default 300s, configurable via
the timeoutSecs parameter that the CloudRunner interface already
declares but Daytona was silently ignoring.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 05:51:41 -05:00
A
9f57e2b506
fix: replace non-null assertions with proper null guards in fly.ts and oauth.ts (#1946)
Replace 6 non-null assertion operators (!) with safe alternatives:
- fly.ts: 4x getCmd()! -> null guard with clear error message
- fly.ts: 1x .pop()! -> fallback with || ""
- oauth.ts: 1x .get("code")! -> hoist value from outer if-check

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 05:49:48 -05:00
A
433708709c
test: Remove 5 duplicate and theatrical test files (#1943)
* test: remove 5 duplicate and theatrical test files

Remove test files that are fully duplicated by more comprehensive
counterparts, plus one theatrical test that only grep-checks shell
script text without testing behavior.

Duplicates removed:
- manifest-validation.test.ts (subset of manifest-cache-lifecycle.test.ts)
- matrix-compact-footer.test.ts (subset of commands-exported-utils.test.ts)
- commands-output.test.ts (subset of commands-display.test.ts)
- cloud-info.test.ts (subset of commands-cloud-info.test.ts)

Theatrical test removed:
- install-script-validation.test.ts (reads install.sh as string, checks
  substring presence -- tests that functions "exist" not that they work)

All 1657 remaining tests pass. Zero regressions.

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

* test: Fix always-pass pattern and stale comments

- integration.test.ts: remove conditional `if (cacheExists)` block that
  silently skipped the cache-file assertion when the file wasn't written;
  the second loadManifest() call already exercises in-memory caching
  without needing the conditional; remove now-unused readFileSync/existsSync
  imports
- commands.test.ts: remove stale references to cloud-info.test.ts and
  commands-output.test.ts (deleted in prior commit) from inline comment;
  remove unused createMockManifest import

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-02-26 00:54:15 -08:00
A
fe6fd20143
refactor: remove duplicate sleep() definitions in fly and daytona modules (#1944)
Both fly.ts and daytona.ts defined a local `sleep` helper identical to the
one already exported from shared/ssh.ts. Remove the local copies and import
the shared function instead, consistent with all other cloud modules.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 03:51:03 -05:00
A
27996f20d4
fix: harden sed substitution pattern in orchestration scripts (#1914) (#1940)
* fix(security): harden sed substitution in orchestration scripts

Replace raw `sed -i "s|...|${VAR}|g"` calls with a `safe_substitute`
helper that escapes backslashes, ampersands, and pipe delimiters in
the replacement value before passing to sed. This prevents silent
missubstitution or sed errors when variables contain sed metacharacters
(most likely with SLACK_WEBHOOK URLs containing `/`).

Applied to all four orchestration scripts: qa.sh, refactor.sh,
discovery.sh, and security.sh.

Fixes #1914

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

* fix: use sed -i.bak for macOS BSD sed compatibility

BSD sed on macOS requires a backup extension with -i flag. Changed
safe_substitute in discovery.sh, refactor.sh, and security.sh to use
sed -i.bak followed by rm -f of the backup file, matching the existing
working pattern in qa.sh.

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

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 00:30:54 -08:00
Ahmed Abushagur
bdcde7bfc4
fix: use spawnSync for script execution to eliminate fd 0 competition (#1942)
The cmdRun path (the main user flow) was still using async
child_process.spawn for script execution. This left Bun's event loop
running while SSH (a grandchild process inside the bash script)
competed for fd 0 input bytes — causing intermittent keystroke loss.

Switch spawnBash to use spawnSync, which blocks the event loop entirely
and gives the child process exclusive terminal access. This matches
what we already did for runInteractiveCommand in #1939.

Also removes dead spawnCalls tracking code from cmdrun-happy-path test.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 02:50:59 -05:00
A
954f3b4893
fix: use user-local npm prefix for openclaw install (#1941)
npm install -g openclaw fails with EACCES on non-root users (e.g.,
ubuntu on AWS Lightsail) because /usr/local/lib/node_modules isn't
writable. Use the same ~/.npm-global prefix pattern already used by
codex and kilocode agents.

Fixes both the standard installAgent path and the batched
setupOpenclawBatched path (used by Fly).

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 00:57:09 -05:00
Ahmed Abushagur
357132b506
fix: eliminate keystroke loss during interactive agent sessions (#1939)
* fix: eliminate keystroke loss during interactive agent sessions

Three root causes were identified and fixed:

1. **Event loop fd competition**: Bun.spawn with stdio:"inherit" shares
   fd 0 between the parent event loop and the child SSH process. The
   kernel arbitrarily splits input bytes between them, causing random
   keystroke drops. Introduced spawnInteractive() using Node's
   child_process.spawnSync to block the event loop entirely.

2. **Unnecessary shell layers**: AWS and GCP wrapped the SSH command in
   an extra `bash -c '...'` layer, creating 3 shell processes before the
   agent. Aligned to match Hetzner/DO which pass directly.

3. **stty sane side effects**: prepareStdinForHandoff() ran `stty sane`
   which enables ixon (XON/XOFF flow control), causing periodic input
   freezes. Removed — setRawMode(false) is sufficient. Also removed
   process.stdin.destroy() which could corrupt fd 0.

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

* fix: biome format + remove stdin unref that broke async spawn

- Fix biome formatting in ssh.ts and commands.ts
- Remove process.stdin.unref() from prepareStdinForHandoff — it
  allowed the event loop to exit before async child_process.spawn
  finished, causing test failures and potential production issues
  with the spawnBash (legacy script execution) path

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 00:08:38 -05:00
Ahmed Abushagur
193cae0b29
fix: pause stdin before interactive handoff to stop keystroke loss (#1938)
The parent process called process.stdin.resume() which put stdin into
flowing mode, making it actively read from fd 0 and discard bytes
(no listeners). This caused the parent to race with the child SSH
process for keystrokes — the kernel gave each byte to whichever
process called read() first, resulting in random keystroke drops.

Switching to pause() makes the parent stop reading from fd 0, so
Bun.spawn(stdio: "inherit") gives the child exclusive access to
the terminal input via dup2().

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 20:38:57 -05:00