Commit graph

1826 commits

Author SHA1 Message Date
A
9e26d74ddb
fix: add --prune and --json to KNOWN_FLAGS for spawn status (#2263)
The status command (PR #2254) added --prune and --json flags but did not
register them in KNOWN_FLAGS. This caused the CLI to reject them with
"Unknown flag" errors before the command could even dispatch.

Bump CLI version 0.15.4 -> 0.15.5.

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-06 19:31:07 -05:00
A
035e4bf830
Remove Daytona cloud provider from codebase (#2261)
Simplify the cloud matrix by removing Daytona. All Daytona-specific code,
scripts, tests, and configuration have been removed. Daytona has been moved
to "Previously Considered" in the Cloud Provider Wishlist (#1183) and can
be revived on community demand.

Closes #2260

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-06 18:53:08 -05:00
A
50397f19a3
fix: narrow validatePrompt patterns to prevent false positives on developer phrases (#2259)
Fixes #2249

The overly broad `>>? word` pattern and generic doubled-operator check
were blocking legitimate natural-language developer prompts like:
- "Fix the merge conflict >> registration flow"
- "Run tests && deploy if they pass"

Root cause: `validatePrompt` is called before the prompt is set as the
`SPAWN_PROMPT` env var. Inside double-quoted shell arguments, `>>` and
`&&` are not interpreted as shell operators, so blocking them provided
no real security benefit while creating confusing UX rejections.

Changes:
- Remove `/>>?\s*[a-zA-Z_]\w{2,}/` pattern (false-positive on >> in English)
- Remove generic `hasDoubledOperators` check (false-positive on && in English)
- Keep all targeted patterns: $(cmd), backticks, ${var}, | bash/sh,
  ; rm -rf, fd redirections, heredoc, process substitution, path redirects
- Update tests: split broad && / || tests into "commands" vs "natural language"
- Add tests asserting all issue #2249 example prompts are now accepted

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-06 15:20:39 -08:00
A
2fd3175103
fix: add schema versioning to history.json (v0 bare array → v1 wrapped) (#2256)
Fixes #2252

history.json now uses a versioned envelope:
  { "version": 1, "records": [...] }

This creates a migration escape hatch for future SpawnRecord shape changes.
loadHistory() transparently reads both v0 (bare array) and v1 formats,
automatically migrating v0 files on next write. All write operations now
use writeHistory() to stamp the current schema version consistently.

Validation uses valibot schemas (VMConnectionSchema, SpawnRecordSchema,
HistoryFileV1Schema) so the structure is verified and typed without `as`
casts. Updated all affected tests to check data.records instead of data.

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 15:17:47 -08:00
A
abc15107eb
feat: add spawn status command to show live server state (#2254)
Implements the `spawn status` command requested in #2253. The command:
- Reads active (non-deleted) cloud servers from history
- Queries Hetzner and DigitalOcean REST APIs in parallel using saved tokens
- Shows a live-state table: ID, Agent, Cloud, IP, State, Since
- States: running (green), stopped (yellow), gone (dim), unknown (dim)
- --prune flag marks gone servers as deleted in history
- --json flag outputs machine-readable JSON for scripting
- `spawn ps` is an alias for `spawn status`

Other clouds (AWS, GCP, Sprite, Daytona) require CLI auth flows that cannot
run non-interactively; they report "unknown" with a helpful hint.

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-06 15:13:24 -08:00
A
f862ee563e
refactor: replace module-level mutable globals with typed state objects in cloud providers (#2255)
Each cloud module (aws, daytona, digitalocean, gcp, hetzner, sprite) previously
stored per-operation state in bare module-level `let` variables, making them
process-global singletons. This is safe for single-cloud CLI invocations today
but creates latent bugs for multi-cloud orchestration and test isolation.

Replace scattered `let` globals with a single typed `_state` object per module:
- `AwsState` / `resetAwsState()` — 8 fields including `selectedBundle`
- `DaytonaState` / `resetDaytonaState()` — 5 fields
- `DigitalOceanState` / `resetDigitalOceanState()` — 3 fields
- `GcpState` / `resetGcpState()` — 5 fields
- `HetznerState` / `resetHetznerState()` — 3 fields
- `SpriteState` / `resetSpriteState()` — 2 fields

Each module exports a `resetXxxState()` function for test isolation. No function
signatures or existing exports were changed.

Fixes #2251

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 18:11:46 -05:00
Ahmed Abushagur
141254c4e1
feat: ARM tarball builds + arch-aware download (#2248)
* feat: ARM tarball builds + arch-aware download

- Add ARM64 matrix entries for native binary agents (zeroclaw, opencode,
  hermes, claude) in agent-tarballs.yml workflow
- Update agent-tarball.ts to detect remote VM arch via uname -m and
  download the correct tarball (x86_64 or arm64)
- Change release strategy to support multiple arch assets per tag
- Document ARM build requirements in discovery.md for future agents
- Bump CLI version to 0.15.2

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

* fix: use sudo for tarball extraction on non-root SSH clouds

On AWS Lightsail, SSH connects as 'ubuntu' (not root), but tarballs
extract to /root/. Without sudo, tar fails with "Permission denied".
Conditionally use sudo when not running as root (id -u != 0).

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:10:33 -05:00
A
5541295012
refactor: fix stale parseJsonRaw references in docs (#2247)
parseJsonRaw was removed in 8b99fe0a but CLAUDE.md and
.claude/rules/type-safety.md still referenced it. Updated
to parseJsonObj which is the current function name.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-06 15:46:55 -05:00
Ahmed Abushagur
849e980bf3
refactor: remove Docker install wrapper, tarballs replace it (#2244)
Docker delivery is superseded by the tarball approach (#2232) which is
faster (curl|tar ~5-15s vs Docker install ~30s + pull ~60s) and works
on every cloud without Docker as a dependency.

- Remove tryInstallFromDocker, withDockerInstall, DOCKER_IMAGE_PREFIX
- Remove dockerImage and slowInstall from AgentConfig
- Remove Docker cloud-init from DigitalOcean
- Unwrap openclaw and zeroclaw to direct install (tarball is tried
  first in orchestrate.ts, these are the fallback)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:19:22 -05:00
A
8b99fe0a37
refactor: remove dead code (parseJsonRaw, stale re-exports) (#2246)
- Remove parseJsonRaw from packages/shared — exported but never imported
- Remove dead re-exports from agent-setup.ts (AgentConfig type, generateEnvConfig)
  that no consumer imports (all callers use the original modules directly)

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-06 11:49:26 -05:00
Ahmed Abushagur
4ac19a375a
fix: capture claude symlink target + verify PATH (#2245)
* fix: tarball workflow failures (root ownership, swapfile, hermes TTY)

- Use sudo mv + chown for tarball in release step (root-owned from capture)
- Skip swapfile creation if /swapfile already exists (GitHub Actions runners)
- Tolerate hermes setup wizard failure when /dev/tty unavailable in CI

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

* fix: capture claude symlink target in tarball + fix verify PATH

The claude installer creates a symlink at ~/.local/bin/claude pointing
to ~/.local/share/claude/versions/X.Y.Z. The capture script was missing
~/.local/share/claude/, causing a broken symlink in the tarball.

Also add ~/.npm-global/bin to the verify PATH check for claude (npm
fallback install path).

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-06 10:55:09 -05:00
A
c6b09531c2
refactor: fix require() usage and stale test README references (#2243)
- Replace require() calls with ESM import in history-spawn-id.test.ts
  (require() violates ESM-only rule per shell-scripts.md)
- Fix stale parseJsonRaw reference in test README (cli parse.ts does
  not export parseJsonRaw; only packages/shared does)
- Add 5 missing test file entries to test README

-- qa/code-quality

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-06 04:53:19 -08:00
A
435bc9c43e
test: remove duplicate boundary tests in history-trimming (#2242)
Remove two tests from "sequential saves at the boundary" that were
exact duplicates of tests in the "MAX_HISTORY_ENTRIES trimming" section:
- "99 to 100 entries" duplicated "should keep all entries when at exactly 100"
- "100 to 101 entries" duplicated "should trim to 100 when adding entry that exceeds the limit"

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 07:50:11 -05:00
A
ee9ae46221
fix: reject control characters in GITHUB_TOKEN validation (#2241)
GITHUB_TOKEN containing newlines, tabs, or carriage returns could
corrupt ~/.config/gh/hosts.yml before permissions are set (line 314)
and bypass validation in downstream consumers. Defense-in-depth fix
following the pattern established in sh/shared/key-request.sh:78.

Fixes #2239

Agent: team-lead

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-06 03:11:23 -08:00
Ahmed Abushagur
ba9690ea23
fix: tarball workflow failures (root ownership, swapfile, hermes TTY) (#2240)
- Use sudo mv + chown for tarball in release step (root-owned from capture)
- Skip swapfile creation if /swapfile already exists (GitHub Actions runners)
- Tolerate hermes setup wizard failure when /dev/tty unavailable in CI

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 05:48:46 -05:00
Ahmed Abushagur
8072c084c2
feat: pre-built agent tarballs for fast install (#2232)
* feat: pre-built agent tarballs on GitHub Releases for fast install

Adds a nightly GitHub Actions workflow that builds and uploads agent
tarballs to rolling GitHub Releases. During provisioning, the CLI now
attempts to download and extract a tarball before falling back to live
install. Priority chain: snapshot > tarball > live install.

- New workflow: .github/workflows/agent-tarballs.yml
- New capture script: packer/scripts/capture-agent.sh
- New module: packages/cli/src/shared/agent-tarball.ts
- Orchestrate tries tarball first on non-local clouds
- Skip tarball when using DO snapshot (skipTarball flag)
- Tests for tarball install + orchestration integration

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

* fix: use global.fetch mock pattern and address security review

- Use `global.fetch = mock(...)` instead of `spyOn(globalThis, "fetch")`
  to match codebase convention and fix CI mock interception
- Add URL validation regex to reject shell metacharacters (CRITICAL)
- Add agent name validation in workflow input (MEDIUM)
- Add `jq has()` check before executing install commands (CRITICAL)
- Use `tar -T` instead of unquoted word-splitting in capture-agent.sh (MEDIUM)
- Resolve merge conflicts with upstream/main (keep Docker fields, adapt
  to simplified DO flow, bump version to 0.15.0)

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

* fix: use globalThis.fetch for testability in CI

Bun's native fetch binding doesn't go through global.fetch property
lookup, so global.fetch = mock(...) doesn't intercept it. Using
globalThis.fetch explicitly ensures the mock interception works.

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

* fix: add missing packer dependencies and harden install command safety

- Add packer/agents.json (agent tier + install command definitions)
- Add packer/scripts/tier-{minimal,node,bun,full}.sh (dependency scripts)
- Add basic command safety check rejecting suspicious patterns
- Document packer/agents.json as a trust boundary requiring PR review

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

* fix(tarballs): fix npm prefix mismatch, add apt-get update, cleanup

- Add apt-get update -y before apt-get install in all tier scripts
- Add --prefix ~/.npm-global to npm install commands in agents.json
  so installed packages land where capture-agent.sh expects them
- Rename misleading MARKER_DIR → MARKER_FILE in capture-agent.sh
- Remove stale comment referencing packer snapshots in workflow

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

* fix(tarballs): detect empty agent installs in capture script

The "no files found" check was dead code — the marker file is always
created before filtering, so FILTERED_FILE always had at least one
entry. Now we count non-marker entries to catch cases where the agent
install silently fails and no actual files are on disk.

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

* fix(tarballs): use bare fetch() for Bun mock compatibility in CI

In Bun, global.fetch = mock(...) overrides bare fetch() calls but NOT
globalThis.fetch() calls. Every other source file in the codebase uses
bare fetch() and their mocks work fine in CI. Switch to match.

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

* fix(tarballs): use dependency injection for fetch in tests

Bun's global.fetch mock doesn't reliably intercept bare fetch() calls
across all Bun versions in CI. Instead of fighting the runtime, accept
an optional fetchFn parameter (defaults to fetch) and pass mock fetch
directly in tests.

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

* fix(tarballs): bypass mock.module bleed in agent-tarball tests

orchestrate.test.ts uses mock.module("../shared/agent-tarball", ...)
which is process-global in Bun and bleeds into agent-tarball.test.ts.
Import via URL (import.meta.url resolution) to bypass the specifier-
based mock matching and get the real module.

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

* fix(tarballs): eliminate mock.module bleed between test files

Bun's mock.module is process-global — orchestrate.test.ts mocking
agent-tarball poisoned agent-tarball.test.ts (the mock function
ignored the fetchFn parameter and always returned false).

Fix: make tryTarballInstall injectable via OrchestrationOptions.
orchestrate.test.ts passes the mock directly via options instead
of using mock.module. agent-tarball.test.ts imports the real module.

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

* fix(tests): mock Bun.which in credential priority tests

Tests assumed no cloud CLIs were installed, but machines with hcloud/
doctl would get "CLI installed" hint overrides, failing the assertion.
Spy on Bun.which to return null so tests are environment-independent.

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

* chore: fix import ordering after rebase

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

* security: add curl domain allowlist and expand command blocklist

Addresses security review findings:
- Add domain allowlist for curl/wget targets (claude.ai, opencode.ai,
  raw.githubusercontent.com, registry.npmjs.org, crates.io, github.com)
- Expand suspicious command blocklist (python -c, perl -e, ruby -e, dd, /dev/)
- Document 4-layer security model in workflow comments

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

* security: add rm -rf to command blocklist

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

---------

Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 04:49:39 -05:00
A
8bc45b4283
refactor: Remove dead code and stale references (#2238)
- Remove sh/e2e/aws-e2e.sh: dead backwards-compat wrapper with no
  references (superseded by unified e2e.sh --cloud aws)
- Remove getStatusDescription from commands/shared.ts: defined and
  tested but never called in production code
- Remove parseJsonRaw from packages/cli/src/shared/parse.ts: zero
  production usages (still available in packages/shared if needed)
- Update corresponding test files to remove dead code tests
- Bump CLI version to 0.14.4

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 03:49:47 -05:00
A
df0593fb21
feat(e2e): Run E2E tests on all configured clouds, not just AWS (#2236)
- manifest.json: change aws auth to AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY
  so the key-request system includes AWS in its missing-key emails
- sh/e2e/e2e.sh: clouds missing credentials now SKIP (not FAIL), so
  running --cloud all is safe and only tests what's configured
- qa.sh: include e2e mode in cloud credential loading (was fixtures+quality only)
- qa-quality-prompt.md: e2e-tester now runs e2e.sh --cloud all --parallel 6 --skip-input-test
- qa-e2e-prompt.md: standalone e2e bot now runs e2e.sh --cloud all --parallel 6

Also wires KEY_SERVER_URL + KEY_SERVER_SECRET into /etc/spawn-qa-auth.env
(system change, not in this commit) so missing-key emails are actually sent.

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>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
2026-03-06 00:06:36 -08:00
A
9291dd9c76
fix: Exclude Daytona from key-request emails (#2237)
Add key_request: false to Daytona in manifest.json and update
_parse_cloud_auths() to skip clouds with that flag set.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 00:04:48 -08:00
L
65a81edc57
fix: add unique spawn IDs to prevent history record corruption (#2235)
* fix: add unique spawn IDs to prevent history record corruption

History records were matched by heuristic ("most recent record for this
cloud without a connection"), which caused saveVmConnection and
saveLaunchCmd to overwrite the wrong record during concurrent or failed
spawns.

Fix: every SpawnRecord now has a unique `id` (UUID). All history
operations (saveVmConnection, saveLaunchCmd, removeRecord,
markRecordDeleted, mergeLastConnection) match by id when available,
falling back to the old heuristic for pre-migration records.

The orchestrator (TS path) now creates the history record AFTER server
creation succeeds, not before — so failed provisions don't leave orphan
entries.

Also adds "Remove from history" option to the spawn ls action picker,
restoring the ability to soft-delete entries without destroying the VM.

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

* test: add 18 unit tests for spawn ID history behavior

Tests cover:
- generateSpawnId returns unique UUIDs
- saveSpawnRecord auto-generates id when not provided
- saveVmConnection matches by spawnId (not heuristic)
- saveVmConnection does not cross-contaminate concurrent spawns
- saveVmConnection falls back to heuristic without spawnId
- saveLaunchCmd matches by spawnId (not heuristic)
- saveLaunchCmd falls back without spawnId
- removeRecord matches by id, not by timestamp+agent+cloud
- removeRecord handles duplicate timestamps correctly
- removeRecord falls back for legacy records without id
- markRecordDeleted targets correct record by id
- mergeLastConnection uses spawn_id from last-connection.json
- mergeLastConnection falls back to heuristic without spawn_id

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

* style: enable biome import sorting with grouped imports

Adds organizeImports to biome assist config with groups:
1. Type imports
2. Node built-ins
3. Third-party packages
4. @openrouter/* packages
5. Aliases

Auto-fixed import order and lint issues across all TypeScript files,
including .claude/skills/ and packages/cli/src/.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-05 23:27:03 -08:00
A
699df354a9
test: Remove duplicate and theatrical tests (#2233)
* test: Remove duplicate and theatrical tests

- Remove duplicate countImplemented empty-matrix test from
  manifest-cache-lifecycle.test.ts (already covered in manifest.test.ts)
- Remove duplicate agentKeys/cloudKeys empty-manifest test from
  manifest-cache-lifecycle.test.ts (already covered in manifest.test.ts)
- Consolidate gateway-resilience.test.ts from 9 identical startGateway()
  invocations into 3 grouped tests, reducing redundant async setup overhead
  while keeping the same assertion coverage (18 expects)
- Move stderrSpy.mockRestore() from each it() into afterEach() in
  gateway-resilience.test.ts

-- qa/dedup-scanner

* test: Remove dead guards after expect(parsed.success).toBe(true) in icon-integrity

Replace v.safeParse + success-check + dead-return guard pattern with v.parse,
which throws on invalid input and removes 20 redundant expect() calls and 5
unreachable return statements across agent and cloud icon tests.

-- qa/dedup-scanner

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>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-06 00:03:07 -05:00
A
7f4b64ce1b
fix: Add SSH key identity opts to reconnect path (#2231)
The reconnect path in connect.ts (cmdConnect and cmdEnterAgent) was
missing SSH key identity file opts (-i flags). Every cloud provider's
interactiveSession includes getSshKeyOpts(await ensureSshKeys()) but
the reconnect path omitted them, causing "Permission denied" failures
for users with non-default SSH key paths.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-06 00:01:45 -05:00
A
b6a0a8d28b
refactor: Remove dead isOAuthConfigured() stub from DigitalOcean module (#2229)
The isOAuthConfigured() function always returned true unconditionally,
making the two !isOAuthConfigured() guards in tryRefreshDoToken() and
tryDoOAuth() unreachable dead code. Remove the function and inline the
always-true behavior by dropping the dead branches entirely.

Bump CLI patch version to 0.14.1.

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-05 17:40:34 -08:00
A
989e9bee12
test: Remove duplicate and theatrical tests (#2230)
- Remove duplicate countImplemented empty-matrix test from
  manifest-cache-lifecycle.test.ts (already covered in manifest.test.ts)
- Remove duplicate agentKeys/cloudKeys empty-manifest test from
  manifest-cache-lifecycle.test.ts (already covered in manifest.test.ts)
- Consolidate gateway-resilience.test.ts from 9 identical startGateway()
  invocations into 3 grouped tests, reducing redundant async setup overhead
  while keeping the same assertion coverage (18 expects)
- Move stderrSpy.mockRestore() from each it() into afterEach() in
  gateway-resilience.test.ts

-- qa/dedup-scanner

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-05 20:39:33 -05:00
A
73a770236a
docs: Sync README with source of truth (#2228)
Add missing --zone and --size flag entries to the commands table.
Both flags exist in packages/cli/src/commands/help.ts getHelpUsageSection()
but were absent from the README commands table.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 20:38:42 -05:00
Ahmed Abushagur
c71f01725b
test: unit tests for openclaw gateway resilience config (#2224)
* test(e2e): add openclaw gateway kill/restart resilience test

Verifies that the openclaw gateway auto-restarts after being killed
with SIGKILL, validating the systemd Restart=always supervision.

The test runs as part of verify_openclaw:
1. Confirms gateway is listening on :18789
2. Kills it with SIGKILL (simulates a hard crash)
3. Waits up to 30s for systemd to auto-restart it
4. Verifies port 18789 comes back online

If the gateway isn't running (e.g. non-systemd env), the test is
skipped gracefully. On failure, dumps systemd status and gateway
logs for diagnostics.

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

* Revert "test(e2e): add openclaw gateway kill/restart resilience test"

This reverts commit 39b79d5c12.

* test: add unit tests for openclaw gateway resilience config

Verifies that startGateway() produces correct systemd and cron
configuration for auto-restart after a gateway crash:

- Restart=always and RestartSec=5 in the systemd unit
- Cron heartbeat checks port 18789 and restarts if dead
- Wrapper script sources .spawnrc and execs openclaw gateway
- Multiple port-check fallbacks (ss, /dev/tcp, nc)
- Non-systemd fallback to setsid/nohup
- 300s startup timeout

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

* test(e2e): add openclaw gateway kill/restart resilience test

Kills the gateway with SIGKILL during verify_openclaw and verifies
systemd Restart=always brings it back within 30s. Skips gracefully
on non-systemd environments.

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-05 19:38:10 -05:00
Ahmed Abushagur
4cfdb0ad9b
feat: Docker-based agent delivery with optimized provisioning (#2225)
* feat(digitalocean): use Docker marketplace image for agent deployments

Use DigitalOcean's Docker marketplace image (docker-20-04) instead of
plain Ubuntu + installing Docker via cloud-init. Docker is pre-installed
so cloud-init only needs to `docker pull` the agent image.

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

* fix: use docker-22-04 marketplace image (Ubuntu 22.04)

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

* revert: back to docker-20-04 marketplace image

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

* feat(digitalocean): use Docker marketplace image with SSH/UFW setup

The docker-20-04 marketplace image has Docker pre-installed but our
user_data replaces its default first-boot script. Add UFW allow for
SSH + sshd restart at the top of cloud-init to restore SSH access.

Skip Docker installation when using the marketplace image since it's
already available.

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

* fix: remove SSH ForceCommand block from marketplace image

DO marketplace images ship with an SSH ForceCommand that blocks login
with "Please wait..." until the image's first-boot script removes it.
Since our user_data replaces that first-boot script, we must strip the
ForceCommand ourselves before sshd restarts.

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

* fix(digitalocean): don't provide user_data to Docker marketplace image

The Docker marketplace image (docker-20-04) has its own first-boot
process that removes the SSH ForceCommand and configures UFW. Providing
user_data conflicts with this and prevents SSH from ever becoming
accessible.

Instead, boot without user_data and run all setup (package install,
Node/bun, docker pull) via SSH after the marketplace image completes
its own initialization.

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

* fix(digitalocean): use docker-22-04 marketplace image slug

The Docker marketplace image is Ubuntu 22.04 based, not 20.04.
docker-20-04 was causing SSH timeouts due to deprecated first-boot process.

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

* fix(digitalocean): revert to docker-20-04 slug (is actually Ubuntu 22.04)

DO API confirms docker-20-04 is the correct slug — it maps to
"Docker on Ubuntu 22.04". docker-22-04 is not a valid slug.

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

* fix(digitalocean): use ubuntu + cloud-init Docker install instead of marketplace image

The Docker marketplace image (docker-20-04) has a slow first-boot
process (~90-180s before SSH opens). Using ubuntu-24-04-x64 with
Docker installed via cloud-init (get.docker.com) is faster end-to-end
because SSH opens in ~30-60s and Docker installs in parallel.

Cloud-init now installs Docker and starts docker pull in background
when an agentName is provided. tryInstallFromDocker() checks if the
image is ready at install time.

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

* fix: wait for in-progress docker pull before extraction

The docker pull started during cloud-init runs in background (&).
If tryInstallFromDocker() runs before the pull completes, it falls
back to normal install unnecessarily. Now waits for any in-progress
docker pull process to finish before checking image availability.

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

* fix: use nohup for background docker pull in cloud-init

The docker pull was backgrounded with bare & in the cloud-init script.
When the script exits after touching .cloud-init-complete, the
background process receives SIGHUP and gets killed. Using nohup
prevents this so the pull survives the script exit.

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

* debug: add diagnostic output to tryInstallFromDocker

Temporary debug logging to diagnose why docker pull isn't available.
Also increased timeout from 60s to 120s.

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

* perf: optimize provisioning — Docker only for slow agents, reorder cloud-init

- Only ZeroClaw (slow Rust build) gets Docker image extraction via
  withDockerInstall + slowInstall flag
- Fast agents (claude, codex, openclaw, opencode, kilocode, hermes)
  skip Docker entirely — their native install is faster than Docker overhead
- Reorder cloud-init: Docker install first, pull in background, then
  apt-get/node/bun run in parallel with the pull
- Remove debug output from tryInstallFromDocker()
- Version bump to 0.14.0

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

* fix: poll for Docker image availability instead of relying on pgrep

The docker CLI process exits while dockerd continues pulling layers
internally. pgrep-based wait exited early, then the image check failed.

Now polls `docker images -q` every 5s for up to 5min until the image
actually appears. Also increases SSH timeout to 600s to match.

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

* fix: clear pre-existing zeroclaw config before onboard

Docker image extraction copies ~/.zeroclaw/config.toml from the image,
which already contains [security]. Then setupZeroclawConfig appends
another [security] section → TOML duplicate key error.

Fix: rm the old config before zeroclaw onboard generates a fresh one.

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

* feat: re-add Docker image extraction for OpenClaw

OpenClaw benefits from Docker pre-pull since npm install is slower
than docker cp extraction. Add slowInstall + withDockerInstall back.

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

* fix: sed zeroclaw config in-place instead of appending duplicate sections

zeroclaw onboard already generates [security] and [shell] sections.
Appending duplicate sections causes TOML parse errors. Now uses sed
to modify existing values in-place, with fallback to append if the
sections don't exist.

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

---------

Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 18:50:59 -05:00
L
9dff1296f0
feat: add --zone/--region and --size/--machine-type CLI flags (#2223)
Adds cross-cloud flags for specifying zone/region and instance size
directly from the command line instead of env vars:

  spawn claude gcp --zone us-east1-b --size e2-standard-4
  spawn claude digitalocean --region lon1 --size s-4vcpu-8gb
  spawn claude hetzner --zone ash --size cx32

Each flag maps to the appropriate cloud-specific env var:
  --zone/--region  → GCP_ZONE, DO_REGION, HETZNER_LOCATION, AWS_DEFAULT_REGION
  --size/--machine-type → GCP_MACHINE_TYPE, DO_DROPLET_SIZE, HETZNER_SERVER_TYPE, LIGHTSAIL_BUNDLE

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-05 14:06:06 -08:00
A
ae584632f1
test: remove duplicate download pipeline tests (#2222)
Remove 17 duplicate test cases from commands-update-download.test.ts and
download-and-failure.test.ts that were already covered by other test files:

- commands-update-download.test.ts: Removed entire "Script download and
  execution" describe block (8 tests) duplicated by download-and-failure.test.ts
  and cmdrun-happy-path.test.ts. File now only tests cmdUpdate.

- download-and-failure.test.ts: Removed "download - primary URL succeeds" (2 tests),
  "download - primary fails, fallback succeeds" (2 tests), and
  "execScript - script content validation" (2 tests) already covered by
  cmdrun-happy-path.test.ts with more thorough assertions.

Authoritative test homes:
- Success paths (primary/fallback download, script validation, env vars, history):
  cmdrun-happy-path.test.ts
- Failure paths (both-404, both-500, mixed errors, network errors):
  download-and-failure.test.ts
- Update command: commands-update-download.test.ts

398 lines removed, 0 test regressions. All 1395 tests pass.

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-05 16:15:46 -05:00
A
84ec491da0
refactor: remove dead mockFailedFetch test helper (#2221)
The mockFailedFetch function in test-helpers.ts was never imported or
used by any test file. Removed to reduce dead code.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-05 16:14:35 -05:00
L
9f00c26ef7
fix: nest workspace trust entry under "projects" key in .claude.json (#2220)
The hasTrustDialogAccepted entry was at the top level of .claude.json
but Claude Code expects it nested under "projects": { "/root": { ... } }.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-05 11:04:22 -08:00
Ahmed Abushagur
08cf5e6d8a
fix(e2e): DigitalOcean name mismatch and bash 3.2 compat (#2218)
1. promptSpawnName() now checks DO_DROPLET_NAME before generating a
   random name, matching getServerName() behavior. This fixes the e2e
   harness creating droplets as spawn-XXXX when it expects
   e2e-digitalocean-AGENT-TIMESTAMP.

2. Replace BASH_REMATCH with sed-based parsing in provision.sh for
   macOS bash 3.2 compatibility. BASH_REMATCH was returning empty
   values, causing `export: '=': not a valid identifier`.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-05 13:44:32 -05:00
Ahmed Abushagur
89e5e980c0
fix(docker): add xz-utils to Hermes Dockerfile for Node.js extraction (#2217)
The Hermes installer downloads Node.js as a .tar.xz archive. Without
xz-utils, tar cannot decompress it (exit code 2).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:41:53 -05:00
Ahmed Abushagur
77c3e34803
feat(docker): replace Packer snapshots with Docker-based agent delivery (#2206)
* feat(docker): replace Packer snapshots with Docker-based agent delivery

Docker images on GHCR are public and cross-account, unlike DO snapshots
which are private/account-scoped. Cloud-init installs Docker + pulls the
agent image during boot. The install step extracts pre-built binaries via
`docker cp` and falls back to normal install if unavailable.

- Add Dockerfiles for all 7 agents (claude, codex, openclaw, opencode,
  kilocode, zeroclaw, hermes)
- Convert docker.yml to matrix build for all agents
- Add tryInstallFromDocker() shared helper with Docker-first install
- Add Docker pull to DigitalOcean cloud-init userdata
- Remove Packer snapshot pipeline, lookup, and SSH-only wait
- Remove packer/ directory (HCL templates, tier scripts, agents.json)

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

* security: address review findings in docker agent delivery

- Add agentName validation regex (/^[a-z0-9-]+$/) in digitalocean.ts
  before interpolation into cloud-init script
- Quote dockerImage variable in all docker command strings in
  agent-setup.ts to prevent command injection
- Restrict docker cp to specific known directories (.claude, .bun,
  .local, .npm, .cargo, .opencode) instead of blanket /root/.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-03-05 11:23:56 -05:00
A
0098e60688
security: add path traversal guard to opencode tar extraction (CWE-22) (#2216)
PR #2213 fixed the identical vulnerability in github-auth.sh but missed
openCodeInstallCmd() in agent-setup.ts. A compromised sst/opencode
tarball could write to arbitrary paths on the remote VM (runs as root).

Add the same tar -tzf | grep -qE '(^/|\.\.)' check before extraction,
matching the established pattern from github-auth.sh.

Agent: team-lead

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-05 10:51:32 -05:00
A
0e4f41fa15
fix: openclaw and zeroclaw reconnect broken by launch command validation (#2215)
The launchCmd() for openclaw contained inline shell logic (if/while/$())
that fails validateLaunchCmd() allowlist on reconnect. The zeroclaw
launchCmd() used quoted export PATH="..." which also fails validation.

Users running `spawn enter` for these agents got a hard exit with
"corrupted history" error.

Fix: simplify openclaw launchCmd to remove redundant gateway startup
logic (already handled by systemd supervision), and remove quotes from
zeroclaw export PATH value.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-05 10:23:06 -05:00
A
5dfb91b747
security: fix checksum grep anchoring and tar path traversal in github-auth.sh (#2213)
* security: fix checksum grep anchoring and tar path traversal in github-auth.sh

- Anchor grep with -F " ${tarball}" to prevent partial filename matches
  in checksum validation (e.g. foo.tar.gz matching foo.tar.gz.sig)
- Add pre-extraction validation rejecting tarballs with absolute paths
  or ../ traversal components (CWE-22), cross-platform (GNU + BSD tar)

Fixes #2211
Fixes #2212

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

* fix: anchor checksum grep with two-space prefix and EOL to prevent partial match

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-03-05 05:41:48 -08:00
A
475a1772a7
test: Remove always-pass conditional guards in icon-integrity tests (#2214)
The `if (parsed.success)` and `if (id in SOURCES)` guards inside test
bodies were redundant — an `expect(...).toBe(true)` assertion always
precedes them, so the inner expects would only be skipped if the test
was already failing. Replace with early-return guards that make the
control flow explicit and remove the nested indirection.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 07:51:52 -05:00
A
02931cfa32
security: verify gh binary checksum and safe JSON parsing in github-auth.sh (#2210)
Fixes #2209

- Replace sed-based JSON parsing with jq/bun-eval for safe tag_name extraction
- Add SHA256 checksum verification before extracting gh binary tarball
- Add semver format validation for parsed version strings

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-05 05:49:23 -05:00
A
5cc75eb558
docs: Sync README with source of truth (#2207)
Remove --debug row from commands table: it exists in the codebase
(index.ts) but is not listed in getHelpUsageSection() in help.ts,
which is the source of truth for the README commands table.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-05 01:17:56 -08:00
A
a3ddb40f72
test: Remove remaining always-pass patterns in icon-integrity tests (#2208)
Two "is actual PNG data" tests (agent and cloud) silently passed without
asserting anything when the PNG file was missing. The `if (!existsSync)
{ return; }` guard let the test return early with no expectations, so a
missing file would register as a green test instead of a failure.

Fix: replace the early-return guard with an unconditional
`expect(existsSync(pngPath)).toBe(true)` so missing files fail the test
immediately. The "is actual PNG data" test is now self-contained and
does not rely on its sibling "exists" test having already failed.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 03:50:02 -05:00
Ahmed Abushagur
07c2c08e3a
revert: remove Packer snapshot pipeline (#2205)
DO snapshots are private and account-scoped — users on different
accounts cannot see snapshots built by the CI token. Docker images
are the better approach for cross-account pre-built agents.

Removes: packer/, packer-snapshots workflow, snapshot lookup code,
and snapshot test. Reverts DO CLI to plain cloud-init flow.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 02:48:52 -05:00
Ahmed Abushagur
96ffb3e201
fix(packer): pass var file explicitly to packer build (#2203)
Packer wasn't auto-loading build.auto.pkrvars.json, causing
"Unset variable" errors. Pass it explicitly with -var-file.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 01:16:41 -05:00
A
aa84bcf94c
test: Remove always-pass patterns in icon-integrity tests (#2202)
Three groups of tests in icon-integrity.test.ts silently passed without
asserting anything when their conditional guard was false:

- Agent manifest icon URL test: `if (parsed.success)` wrapped the only
  expect, so a missing `icon` field on any agent would silently pass
- Agent .sources.json ext test: double-conditional (`id in AGENT_SOURCES`
  then `if (parsed.success)`) hid both the membership check and parse
  result, providing zero signal when either condition failed
- Cloud .sources.json ext test: same double-conditional pattern

Fix: add unconditional `expect(...).toBe(true)` assertions before each
guard so failures surface as actual test failures rather than silently
passing. The TypeScript narrowing guards remain for type safety.

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-04 21:19:35 -08:00
Ahmed Abushagur
ed98a59318
feat(digitalocean): Packer nightly snapshot pipeline for fast boot (#2198)
* feat(digitalocean): Packer nightly snapshot pipeline for fast boot

Add pre-built Packer snapshots for DigitalOcean droplets. Instead of
10-20 min cloud-init + agent install on every boot, snapshot-based
droplets boot in ~2-3 min (SSH only, agent pre-installed).

- Packer HCL2 template with parametrized agent/tier builds
- Agent build matrix (packer/agents.json) for all 7 agents
- Tier scripts mirroring cloud-init.ts package tiers
- Nightly GitHub Actions workflow (4 AM UTC, max-parallel: 3)
- Automatic cleanup: keeps only latest snapshot per agent
- CLI: findSpawnSnapshot() looks up pre-built images via DO API
- CLI: waitForSshOnly() skips cloud-init when using snapshots
- CLI: createServer() accepts optional snapshotId, skips user_data
- CLI: main.ts routes to fast path when snapshot detected
- Tests for findSpawnSnapshot() (5 cases, all passing)

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

* fix(packer): use var-file for install_commands to avoid shell quoting issues

The previous approach passed install_commands as `-var` inline, but
GitHub Actions expands `${{ }}` before shell evaluation — JSON arrays
with `|`, `&&`, and `"` characters break shell quoting.

Fix: generate a `.auto.pkrvars.json` file (auto-loaded by Packer)
using jq with --argjson for safe JSON handling. Also route all
`${{ inputs }}` and `${{ matrix }}` values through env vars to
prevent script injection.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:47:46 -08:00
A
3242fa78f1
fix(oauth): URL-encode callback_url query parameter (#2201)
The OAuth callback URL (http://localhost:PORT/callback) was interpolated
directly into the auth URL query string without encoding. The colons and
slashes could cause ambiguous parsing on strict URL parsers or proxies,
potentially breaking the OAuth flow. Other parameters in the same URL
(spawn_agent, spawn_cloud) were already correctly encoded.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-04 23:19:15 -05:00
A
701e3af56e
fix: prevent timer leaks and event-loop stalls in SSH timeout handling (#2200)
- Unref the SIGKILL timer in killWithTimeout() so it doesn't keep the
  event loop alive for 5 extra seconds after a timed-out process exits
- Wrap all setTimeout/clearTimeout pairs in try/finally across 6 cloud
  providers (12 call sites) to guarantee cleanup on exceptions
- Add missing 60s timeout guard to runSpriteSilent() which could hang
  indefinitely on unresponsive sprite processes

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-04 19:04:47 -08:00
A
2fea3de685
refactor: Remove dead exports from delete.ts helper functions (#2199)
ensureDeleteCredentials() and execDeleteServer() were exported but never
imported outside of delete.ts itself. Remove the export keywords to match
their actual internal-only usage. No behavior change.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 22:03:39 -05:00
A
62a904b535
test: Remove duplicate and theatrical tests (#2197)
* test: Consolidate redundant per-property tests in script-failure-guidance

Each describe block for an exit code (127, 126, 1, default, null, 130,
137, 255, 2) and signal (SIGKILL, SIGTERM, SIGINT, SIGHUP) had multiple
separate it() tests all calling the same pure function with the same
arguments — one assertion per test. Since the function is pure and
deterministic, these redundant calls add overhead without adding signal.

Merge per-argument test groups into single tests that check all
properties at once. All 3240 expect() calls are preserved; 38 redundant
test wrappers are removed (1395 → 1357 tests).

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

* test: Remove duplicate and theatrical tests

Remove two redundant structural tests from getScriptFailureGuidance:
- "should always return an array of strings" — proven by every
  content-checking test above it (they all call the function and
  assert on its elements)
- "should never return an empty array" — same: every toContain/
  toHaveLength assertion already implies a non-empty result

Keeps the useful "different output per exit code" uniqueness test.

Test count: 1411 → 1409 (2 removed, 0 failures).

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

* test: Remove duplicate and theatrical tests

- Remove theatrical "should always return string arrays" test from
  getSignalGuidance: TypeScript already guarantees string[] return type;
  testing it at runtime with Array.isArray/typeof adds zero signal
- Replace 149 (c: any[]) parameter annotations with (c: unknown[])
  across 13 test files to comply with the no-as/no-any policy
- Fix mockSuccessfulFetch(data: any) → (data: unknown) in test-helpers.ts

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>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-04 18:41:03 -08:00
L
4a3a6ed27f
fix: suppress Claude Code workspace trust prompt on provisioned VMs (#2192)
The "Quick safety check: Is this a project you created or one you trust?"
prompt fires per-workspace and is not suppressed by hasCompletedOnboarding
or --dangerously-skip-permissions (anthropics/claude-code#28506).

Fix: inject a workspace trust entry keyed by $HOME into ~/.claude.json
with hasTrustDialogAccepted: true. The JSON is now constructed on the
remote side so $HOME resolves to the actual path (/root, /home/user, etc).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-04 17:11:42 -08:00