Replace shell string interpolation with array-based exec arguments in
uploadFileSprite. Previously, remotePath and tempRemote were interpolated
into a bash -c string (`mkdir -p $(dirname '${normalizedRemote}') && mv
'${tempRemote}' '${normalizedRemote}'`), which is inherently unsafe
even with regex validation.
Now uses two separate sprite exec calls with paths passed as discrete
array arguments after `--`, and computes dirname in TypeScript using
node:path/posix instead of shell command substitution. Also fixes the
mockBunSpawn test helper to return fresh ReadableStream instances per
call, preventing "ReadableStream already used" errors.
Fixes#2880
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
checkAccountStatus() now queries the account's droplet_limit and
current droplet count. When at capacity it warns interactively and
throws immediately in headless/E2E mode with a clear message instead
of attempting creation and getting a cryptic 422.
Also adds specific detection of droplet limit 422 errors in
createServer() with actionable guidance (limit increase URL).
Bump CLI to 0.25.14.
Fixes#2865
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
These two describe blocks in oauth-cov.test.ts were redundant subsets of the more
comprehensive coverage already in oauth-pkce.test.ts (which includes RFC 7636 test
vectors, uniqueness checks, padding validation, and base64url character checks).
Duplicates found: 1 function pair (generateCodeVerifier + generateCodeChallenge)
Tests removed: 2
Tests rewritten: 0
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The reference to "Hetzner Packer" was removed in #2869.
Updated the comment to accurately describe the snapshot naming convention.
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
19 tests across 7 files were calling functions with no expect() calls —
they verified "does not throw" implicitly but provided zero signal on
side effects or return values.
Added assertions to each:
- agent-setup-cov: expect runServer called after graceful failure
- auto-update: expect runServer called on non-fatal SSH error
- aws-cov: assert state.awsRegion set by promptRegion env var paths,
spawnSync call counts for ensureAwsCli, fetch called for destroyServer
- do-cov: assert SPAWN_NAME_KEBAB preserved on early return,
fetch NOT called when no token in checkAccountStatus
- gcp-cov: assert spy call counts for authenticate, destroyInstance,
ensureGcloudCli; spawnSync NOT called when GCP_PROJECT env set;
fetch NOT called when no project in checkBillingEnabled
- hetzner-cov: assert fetch called for ensureHcloudToken validation
and for destroyServer REST calls
- ssh-cov: assert connectSpy and bunSpawnSpy called in waitForSsh
All 1925 tests pass. expect() calls increased from 4555 to 4575.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The sprite was going idle and shutting down during long npm install
operations because the remote keep-alive script wasn't installed yet
and sprite exec alone doesn't count as activity.
- Add local keep-alive that pings the sprite's public URL every 30s
from the client machine during provisioning and agent install
- Stop it when the interactive session starts (remote script takes over)
- Add i/o timeout to spriteRetry's transient error regex so connection
timeouts are retried instead of failing immediately
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove 7 redundant tests that test the same code paths as existing tests:
- history.test.ts: consolidate 4 separate "unrecognized JSON value" tests
(non-array object, JSON string, null, number) into one data-driven test.
All 4 hit the identical parseHistoryData "Unrecognized format" branch.
- cmd-link-cov.test.ts: remove "exits with error when no IP provided" —
duplicate of the same test in cmd-link.test.ts with identical behavior.
- update-check-cov.test.ts: remove "skips in test environment" and "skips
when SPAWN_NO_UPDATE_CHECK=1" — both already covered in update-check.test.ts.
- orchestrate-cov.test.ts: remove "calls preLaunch when defined" — identical
to the same test in orchestrate.test.ts (same mock setup, same assertion).
All 1866 remaining tests pass. Lint clean.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- remove stale reference to `commands-update-download.test.ts` (renamed to `cmd-update-cov.test.ts`)
- remove stale reference to `picker.test.ts` (renamed to `picker-cov.test.ts`)
- add 25 missing `-cov.test.ts` files that exist on disk but were undocumented
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
GCP's default 10 GB boot disk is insufficient for coding agents — node_modules,
apt packages, and build caches easily exceed it. Default to 40 GB and allow
override via GCP_DISK_SIZE env var.
Closes#2866
Co-authored-by: Claude <claude@anthropic.com>
preflight-credentials.test.ts: all 7 tests had zero expect() calls with
comments like "// No crash = pass". Rewrote to capture logWarn mock calls
from mockClackPrompts() and assert on warning presence and credential names.
sprite-cov.test.ts: 13 out of 23 tests had no expect/rejects calls (just
called functions and discarded results). Added assertions on Bun.spawn call
counts to verify: authenticated paths skip login, unauthenticated paths
trigger login, createSprite reuses vs creates based on list output,
verifySpriteConnectivity calls sprite twice, setupShellEnvironment runs
multiple exec commands.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Defense-in-depth: explicitly shellQuote(cmd) inside runServer() so the
cmd parameter is always protected by single-quote escaping, regardless
of how the surrounding command string is constructed.
Previously, cmd was interpolated raw into fullCmd before the outer
shellQuote() wrapper. While the outer wrapper did protect it, this
made the safety non-obvious and fragile against future refactors.
The new pattern matches interactiveSession() where cmd gets its own
shellQuote() call.
Fixes#2859
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Consolidate DOCKER_CONTAINER_NAME and DOCKER_REGISTRY constants from
gcp/main.ts and hetzner/main.ts into shared/orchestrate.ts. Both files
defined identical values ("spawn-agent" and "ghcr.io/openrouterteam"); they
now import the shared exports instead.
Bumps CLI patch version to 0.25.11.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
- manifest.test.ts: remove 4 duplicate loadManifest error/fallback tests
(HTTP 500 stale-cache, no-cache-HTTP500-throws, invalid-manifest-throws,
network-error-throws) — all covered more thoroughly by
manifest-cache-lifecycle.test.ts
- ssh-keys.test.ts: remove 2-key sorting test superseded by ssh-keys-cov.test.ts
which validates the full 3-way sort order (ED25519 > RSA > ECDSA)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The --beta docker feature (PR #2854) was missing from `spawn help`
output, and its error description said "Hetzner" only but it also
works on GCP.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: add --beta docker for Hetzner Docker CE app image
Uses Hetzner's pre-built docker-ce app image when --beta docker
(or --fast) is active, giving faster boot times similar to DO
marketplace images. Snapshots still take priority when available.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: pull and run pre-built agent Docker images on Hetzner
When --beta docker (or --fast) is active, boots Hetzner with docker-ce
app image, then pulls ghcr.io/openrouterteam/spawn-{agent}:latest and
runs it. All runServer commands are routed through docker exec into
the container, and the interactive session uses docker exec -it.
Skips agent install since the agent is pre-baked in the image.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add --beta docker support for GCP with Container-Optimized OS
When --beta docker (or --fast) is active on GCP, uses cos-stable
from cos-cloud (Docker pre-installed, read-only OS). Skips cloud-init
startup script (incompatible with COS), pulls the pre-built agent
image from ghcr.io, and routes all commands through docker exec.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: correct import path for logInfo/logStep (shared/log.js -> shared/ui.js)
The log.js module does not exist; these functions are exported from ui.ts.
Also merge duplicate ui.js imports per biome organizeImports.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
- Replace 10x `expect(true).toBe(true)` in update-check-cov.test.ts with
meaningful assertions: skip-condition tests now verify fetch was NOT called,
fetch-failure tests use `resolves.toBeUndefined()`, backoff edge-case tests
verify fetch WAS called (proving the skip was bypassed)
- Remove theatrical executor existence check (`typeof executor.execFileSync === "function"`)
that proved nothing about behavior
- Replace structural `typeof agent.install/envVars/launchCmd === "function"` checks in
agent-setup-cov.test.ts with assertion that agent names are non-empty strings;
the downstream tests already prove the functions work by calling them
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Sprite CLI exits with code 1 on "connection closed" (not 255 like SSH).
The reconnect loop now treats exit code 1 on Sprite as a connection
drop, retrying up to 5 times with a 3s delay between attempts.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In fast mode, Promise.allSettled runs server boot, OAuth, and tarball
download concurrently. When all operations complete — especially after
Bun.serve.stop(true) in the OAuth flow removes its event loop handle —
the event loop can appear empty before the await continuation starts
new I/O operations. This causes Bun to exit silently with code 0,
dropping the user back to their shell after "Successfully obtained
OpenRouter API key via OAuth!" with no error.
Fix: keep a dummy setInterval handle alive during the fast-mode
concurrent section so the event loop never drains prematurely.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes#2847
Removes 273 lines of false-confidence tests that copy-paste
shouldForceAscii() logic inline 9x with zero imports from
unicode-detect.ts. Every test passed even if the real source
was deleted — a theatrical test is worse than no test.
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The test assumed _state.project would be empty, but module-level state
persists across tests due to import caching. Prior resolveProject tests
set _state.project, so checkBillingEnabled would attempt a real
gcloudSync call and time out at 5s. Mock spawnSync to handle both cases.
Agent: pr-maintainer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- history-cov.test.ts: remove duplicate filterHistory ordering test and
no-cap saveSpawnRecord test — both are already covered more thoroughly
in history-trimming.test.ts
- unicode-cov.test.ts: remove theatrical pattern where each test
re-implemented shouldForceAscii as an inline lambda (testing an inline
copy instead of the real function). consolidate into a single shared
helper that mirrors the actual module logic, tested once per scenario.
-- qa/dedup-scanner
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
DigitalOcean and Hetzner runServer() passed the command string directly
to SSH without shell-quoting, allowing metacharacters (;, |, $(), etc.)
to be interpreted by the remote shell. AWS and GCP already used
`bash -c ${shellQuote(fullCmd)}` — this applies the same pattern to the
two affected modules.
Fixes#2836
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Check for ".." path traversal in the raw input BEFORE normalize() strips
it, fixing CWE-22 where crafted paths like "/tmp/../../etc/passwd"
normalized to "/etc/passwd" and bypassed the post-normalize ".." check.
Extracts a shared validateRemotePath() into shared/ssh.ts and replaces
the duplicated inline validation in all 5 providers (DigitalOcean,
Hetzner, GCP, AWS, Sprite) plus agent-setup.ts.
Fixes#2835
Agent: complexity-hunter
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: use base64 encoding for GITHUB_TOKEN to prevent injection
Aligns GITHUB_TOKEN handling with the existing base64 pattern used for
OPENROUTER_API_KEY in orchestrate.ts, eliminating the single-quote
escaping vulnerability.
Fixes#2834
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: apply shellQuote to base64-encoded GITHUB_TOKEN
Address security review feedback: wrap the base64-encoded token in
shellQuote() for defense-in-depth, preventing any theoretical shell
metacharacter escape from the interpolated value.
Co-Authored-By: Claude Opus 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>
Deduplicate identical mockBunSpawn helper that was copy-pasted across
five test files (aws-cov, gcp-cov, do-cov, hetzner-cov, sprite-cov).
Centralise it in test-helpers.ts and import from there instead.
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove 10 duplicate test cases from cmd-list-cov.test.ts and
cmd-run-cov.test.ts that were already covered by dedicated test files:
- buildRecordLabel (3 tests) — duplicated from cmdlast.test.ts
- buildRecordSubtitle (3 tests) — duplicated from cmdlast.test.ts
- cmdListClear (2 tests) — weaker duplicates of clear-history.test.ts
- cmdLast (1 test) — duplicated from cmdlast.test.ts
- cmdRun detectAndFixSwappedArgs (1 test) — duplicated from
commands-swap-resolve.test.ts which has 10 thorough swap tests
-- qa/dedup-scanner
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
- delete manifest-cov.test.ts: it duplicated stripDangerousKeys,
agentKeys/cloudKeys/matrixStatus/countImplemented from manifest.test.ts;
unique tests (isStaleCache, getCacheAge, richer loadManifest edge cases)
consolidated into manifest.test.ts
- remove sprite/interactiveSession from sprite-cov.test.ts: superseded by
sprite-keep-alive.test.ts which tests actual script content
- remove sprite/installSpriteKeepAlive from sprite-cov.test.ts: superseded
by sprite-keep-alive.test.ts
- remove startGateway from agent-setup-cov.test.ts: superseded by
gateway-resilience.test.ts which checks systemd config, cron, and port-wait
all 2050 tests pass
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Add .js extensions to 124 relative imports that were missing them.
The codebase is "type": "module" (ESM) and the dominant pattern already
used .js extensions, but 35 files had a mix of extensionless and .js
imports — sometimes within the same file. Standardize to .js everywhere.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
spawn link is a fully implemented command (440 lines) that was
completely missing from `spawn help`. Users had no way to discover
it through the CLI's self-documentation.
Also adds --fast to the KNOWN_FLAGS set for consistency — it was
accepted by the CLI but not registered in the flag validation set.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The MAX_HISTORY_ENTRIES=100 cap silently archived records when you
spawned more than 100 times, making older active servers vanish from
`spawn list`. The cap was solving a non-problem — 1000 records is ~500KB.
Removed:
- MAX_HISTORY_ENTRIES constant and trimming logic
- archiveRecords() and readExistingArchive() (no longer needed)
- Smart trim tests (history-trimming.test.ts rewritten to test ordering only)
Existing archive files (~/.spawn/history-YYYY-MM-DD.json) are still
readable by recoverFromArchives() for corruption recovery.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- delete commands-update-download.test.ts (7 tests): superseded by
cmd-update-cov.test.ts which has 13 tests with better fallback URL
coverage and uses clack mocks properly
- remove saveSpawnRecord id generation describe from history-cov.test.ts
(1 test): superseded by history-spawn-id.test.ts which has 3 more
thorough tests covering the same scenario
- remove 4 describe blocks from cmd-run-cov.test.ts (18 tests):
getSignalGuidance, getScriptFailureGuidance, getScriptFailureGuidance
additional, and getSignalGuidance additional are all covered more
thoroughly by the dedicated script-failure-guidance.test.ts; the
"additional" blocks were theatrical (only checked joined.length > 0)
- delete picker.test.ts and merge its 8 parsePickerInput tests into
picker-cov.test.ts to eliminate duplicate describe name collision
2063 -> 2036 tests (-27), 0 failures
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Remove thin duplicate test blocks that were redundant with more comprehensive
coverage elsewhere:
- ui-cov.test.ts: drop shellQuote (4 tests → gcp-shellquote.test.ts has 11),
jsonEscape (1 test → ui-utils.test.ts has 4), toKebabCase (2 tests →
ui-utils.test.ts has 5), sanitizeTermValue (2 tests → ui-utils.test.ts has
6), withRetry (3 tests → with-retry-result.test.ts has 8)
- agent-setup-cov.test.ts: drop wrapSshCall (5 tests → with-retry-result.test.ts
has 7 plus integration tests)
- run-path-credential-display.test.ts: drop isRetryableExitCode (2 tests →
cmd-run-cov.test.ts has 5)
- history-cov.test.ts: drop generateSpawnId (2 tests → history-spawn-id.test.ts
has 2 with UUID format check) and clearHistory (2 tests →
clear-history.test.ts has extensive coverage)
- cmd-list-cov.test.ts: drop formatRelativeTime (9 tests →
commands-exported-utils.test.ts has 10 with an extra boundary case)
All 2063 tests pass, biome lint clean.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
The top-level arg parser in index.ts:820 claims -n for --dry-run before
any subcommand sees it. Running `spawn link 1.2.3.4 -n my-server` silently
drops the intended name value — the user gets no error, the spawn is
registered without the name they specified.
Removing -n from link's --name extractFlag call eliminates the conflict.
The --name long form is unaffected and documented in the usage string.
Also updates cmd-link-cov.test.ts to use --name in the short-flags test.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 24 TypeScript strict mode errors across 7 production files:
- interactive.ts: guard against undefined `val` in validate callback
- list.ts: use already-narrowed `conn` variable instead of `selected.connection`
- run.ts: widen `buildCloudLines` defaults param to `Record<string, unknown>`
- digitalocean.ts: use `toRecord()` to safely drill into nested API responses;
capture narrowed `oauthCode` in const for async closure
- history.ts: backfill missing record IDs via `backfillRecordIds()` helper;
use `v.safeParse` output directly to get properly typed records
- index.ts: use `Manifest` type for `showUnknownCommandError` parameter
- orchestrate.ts: capture narrowed `tunnel` and `getConnectionInfo` in const
variables before async closures
Fixes#2821
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
History records were being silently lost when concurrent spawn processes
did load→modify→save simultaneously (last writer wins, first record
vanishes). This explains records disappearing from `spawn list`.
Changes:
- Add mkdir-based advisory file locking (withHistoryLock) around all
write operations: saveSpawnRecord, saveLaunchCmd, saveMetadata,
markRecordDeleted, removeRecord, updateRecordIp, updateRecordConnection
- Stale lock detection (>30s) prevents deadlocks from crashed processes
- Backfill IDs on legacy records without them during loadHistory()
- Validate archive records during merge (readExistingArchive)
- Limit archive recovery scan to 30 most recent files
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both functions were added in recent commits but had zero test coverage:
- retryOrQuit (ed127cf): non-interactive mode now verified to throw
- skipCloudInit (2280550): 4 cases verify correct tier/cloud/mode conditions
1468 tests pass, 0 failures.
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- GCP coverage tests (6 failures): getServerIp, listServers, and
authenticate tests did not mock the `which gcloud` spawnSync call
inside requireGcloudCmd(), causing "gcloud CLI not found" errors.
Add mockSpawnSyncWithGcloud/mockWhichGcloud helpers that satisfy
the gcloud discovery call before the test-specific mock.
- Sandbox guardrail test (1 failure): cmd-uninstall-cov deletes
~/.spawn and other sandbox directories but never re-creates them.
Since Bun runs test files in the same process, the fs-sandbox
test then fails. Add afterEach restoration of sandbox dirs.
- Add coverageThreshold to bunfig.toml with correct syntax
(coverageThreshold under [test], not [test.coverage])
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The pre-merge hook and `cd packages/cli && bun test` need a local
bunfig.toml so the preload path resolves correctly for the sandbox.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move bunfig.toml to repo root with valid coverageThreshold syntax
(line=80%, function=0 to avoid per-file false positives)
- Add --coverage flag to CI test step
- Delete packages/cli/bunfig.toml (superseded by root config)
- Add tests for packages/shared (type-guards, parse, result)
- Colocate billing config into each cloud directory (aws/billing.ts,
gcp/billing.ts, hetzner/billing.ts, digitalocean/billing.ts)
- Refactor billing-guidance.ts: BillingConfig interface replaces
cloud-string-keyed Record maps
- Bump CLI version to 0.25.1
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 SSH exits with code 255 (connection dropped/timed out), retry up
to 5 times with 3s delay between attempts. Clean exits (0), Ctrl+C
(130), and agent crashes exit immediately without retrying.
Only applies to remote clouds — local sessions skip reconnect logic.
Signed-off-by: L <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Consolidated two overlapping describe blocks that both iterated over the
same config_files data:
- 'Agent optional field types' had a test checking config_files keys were
strings with length > 0
- 'Config files structure' had a separate describe checking the same keys
match a path regex and values are non-null objects
Merged into a single test within 'Agent optional field types' that checks
all constraints: key is string, key is non-empty, key matches path regex
(/[/~./]), and value is a non-null object. Removed the now-redundant
'Config files structure' describe block.
-- qa/dedup-scanner
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
* feat: never-give-up resilience layer — retry every failure instead of exiting
Add retryOrQuit() helper to shared/ui.ts that prompts "Try again? (Y/n)"
after any recoverable failure. Wrap all fatal exit points with retry loops:
- Cloud auth (Hetzner, DigitalOcean, AWS, GCP): retry after 3 failed tokens
- API key acquisition: retry after 3 failed OAuth+manual attempts
- Server creation: retry on any createServer failure (both fast & sequential)
- SSH readiness: retry on waitForReady timeout
- Agent install: retry on install failure
- Pre-launch hooks: retry on preLaunch failure
Non-interactive mode (SPAWN_NON_INTERACTIVE=1) still throws immediately.
Ctrl+C at any retry prompt exits cleanly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(e2e): add AI-driven interactive test harness
Add --interactive mode to the E2E test framework. Instead of running spawn
in headless mode (SPAWN_NON_INTERACTIVE=1), this spawns the CLI in a real
PTY and uses Claude Haiku to respond to prompts like a human user would.
New files:
- sh/e2e/interactive-harness.ts — Bun script that drives the PTY + AI loop
- sh/e2e/lib/interactive.sh — Bash integration with the E2E framework
Usage:
e2e.sh --cloud hetzner claude --interactive
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(qa): wire interactive E2E into scheduled QA pipeline
- Add `e2e-interactive` option to workflow_dispatch in qa.yml
- Add `e2e-interactive` run mode to qa.sh (loads cloud creds + ANTHROPIC_API_KEY)
- Runs `e2e.sh --cloud hetzner claude --interactive` directly (no Claude Code needed)
- Defaults to hetzner (cheapest), overridable via E2E_INTERACTIVE_CLOUD/AGENT env vars
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(qa): schedule interactive E2E daily at 6am UTC
Runs one agent (claude) on one cloud (hetzner) with AI-driven prompts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(qa): offset soak cron to avoid GitHub Actions schedule dedup
GitHub Actions deduplicates overlapping cron schedules into one run,
making `github.event.schedule` unpredictable. The soak test at `0 3 * * 1`
was getting absorbed by the `0 */4 * * *` quality sweep and never firing
as reason=soak.
Move soak to `30 1 * * 1` (Monday 1:30am UTC) — safely between the
0am and 4am quality sweep slots. Interactive E2E at `0 6 * * *` is
already safe (between the 4am and 8am slots).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(qa): add e2e-interactive to trigger server valid reasons
The trigger server validates reason query params against an allowlist.
Without this, the `e2e-interactive` dispatch returns 400.
Also note: `soak` is already in VALID_REASONS in the repo but the running
service on the QA VM is stale — needs a restart to pick up both soak and
e2e-interactive reasons.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* perf: skip cloud-init for minimal-tier agents with tarballs/snapshots
Ubuntu 24.04 base images already have curl + git, so minimal-tier
agents (claude, opencode, zeroclaw, hermes) don't need the cloud-init
package install step when using tarballs or snapshots.
Adds skipCloudInit flag to CloudOrchestrator — set automatically when
(tarball || snapshot) && tier === "minimal". Each cloud's waitForReady
checks this flag and calls waitForSshOnly instead of waitForCloudInit.
Saves ~30-60s on minimal-tier agent deploys with --fast or --beta tarball.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add --fast mode and updated beta features to README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: remove timing table from README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Add 6 test cases verifying the Promise.allSettled parallel orchestration
path introduced in #2796. Tests cover: happy path, server boot failure
propagation, API key failure propagation, tarball fallback to
agent.install, local cloud exclusion from fast mode, and non-fatal
preProvision/checkAccountReady failures.
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add --fast flag for parallel server boot + setup
Adds `--fast` flag that runs server creation concurrently with API key
prompt, account check, pre-provision hooks, tarball download, and env
config generation. Once SSH is up, uploads tarball and applies config.
--fast implies --beta tarball and --beta images, enabling snapshots
and pre-built tarballs automatically.
Flow without --fast (sequential):
auth → API key → preProvision → size → create → boot → install → configure
Flow with --fast (parallel):
auth → size → [create+boot | API key | preProvision | tarball download | accountCheck]
→ upload tarball → inject env → configure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add --beta parallel as standalone opt-in for parallel setup
--beta parallel enables the parallel orchestration without implying
tarball/images. --fast still implies all three (tarball + images +
parallel).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>