Commit graph

1598 commits

Author SHA1 Message Date
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
A
b579555494
fix: run n install as root in cloud-init so npm is on PATH (#1932)
On AWS and GCP, cloud-init ran `n install 22` via `su - ubuntu` (non-root).
The n version manager needs write access to /usr/local/bin/ which the
non-root user may not have reliably in non-interactive cloud-init context.
This caused npm to not be installed/on PATH, breaking `npm install -g
openclaw` with "npm: command not found".

Fix: run n install as root (cloud-init already runs as root) so node/npm
install directly to /usr/local/bin/ which is always on PATH. This matches
what Hetzner and DigitalOcean already do. Also removes the now-unnecessary
npm global prefix configuration since /usr/local is the default.

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>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
2026-02-25 15:50:56 -08:00
A
2321352a6b
fix: don't mark server deleted when cloud API delete fails (#1935)
When spawn delete encounters a cloud API error (network timeout, 500,
auth failure), the server is still running. Marking the record as
deleted in this case hides it from spawn delete/spawn list, preventing
retry and causing untracked billing.

Only mark as deleted on: (1) successful deletion, (2) server already
gone/404. Error paths keep the record active for retry.

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-25 18:46:34 -05:00
A
556f32ecfc
fix: reset terminal state before interactive session handoff (#1934)
* fix: reset terminal state before interactive session handoff

The stdin handoff from TS orchestration to the interactive SSH session
was leaving the terminal in a dirty state, causing users to need 2+
Enter presses or random keystrokes before input worked.

Three fixes:
1. Unconditionally call setRawMode(false) instead of checking isRaw
   first — @clack/core's close() already resets the flag but the
   terminal can still be dirty after multiple readline instances
2. Run `stty sane` to fully reset the terminal line discipline,
   undoing any damage from readline's emitKeypressEvents
3. Resume stdin instead of pausing it — Bun.spawn with stdio:"inherit"
   needs an active stream, a paused stdin causes the child to see
   blocked input

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

* style: fix Biome formatting for Bun.spawnSync call

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: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-02-25 18:45:27 -05:00
A
b6f021ecf2
fix(security): clarify base64+single-quote pattern in fly_ssh (#1937)
Fixes #1933. The comments incorrectly implied base64 encoding alone
prevents injection. Safety relies on the combination of base64 output
(no single quotes in alphabet) + single-quote wrapping. Made this
explicit in all 7 affected comments.

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-25 18:44:51 -05:00
A
98d5a6612b
fix: add SIGKILL fallback to process timeout kills (#1931)
* fix: add SIGKILL fallback to process timeout kills

proc.kill() only sends SIGTERM; SSH processes stuck in network I/O can
ignore SIGTERM and cause the CLI to hang forever waiting on proc.exited.

Add killWithTimeout() to shared/ssh.ts that sends SIGTERM then SIGKILL
after a 5s grace period. Replace all 10 proc.kill() timeout sites across
Fly, AWS, DigitalOcean, GCP and Hetzner providers.

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

* chore: format files with biome

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-25 16:50:18 -05:00
A
a0c2ce0dcf
fix(ux): use space instead of slash in connection failure hint (#1930)
The error messages in handleRecordAction() recommended
`spawn agent/cloud` (slash notation), but the CLI itself shows
"Tip: use a space instead of slash" when users follow that advice.
Changed to `spawn agent cloud` to match canonical syntax.

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-25 16:47:09 -05:00
A
fbb48514e9
fix: drain piped stdout/stderr in aws waitForCloudInit and sprite destroyServer (#1929)
Same pipe-buffer deadlock pattern fixed by PRs #1903, #1915, #1920, #1922.
Two instances were missed in those passes.

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-25 15:48:43 -05:00
A
b09295131a
fix(security): replace eval with pipe-to-sh in fly_ssh functions (#1928)
Eliminates nested quote eval pattern in favor of direct pipe to sh,
removing potential injection surface in fly_ssh and fly_ssh_long.
Fixes #1927

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-25 14:50:24 -05:00
A
7a5e7580bd
fix(security): validate server_id in cmdConnect and cmdEnterAgent (#1925)
All other connection fields (ip, user, server_name) are validated
against injection before being passed to shell commands, but server_id
was skipped in both cmdConnect and cmdEnterAgent despite being used as
a daytona ssh argument (line 2922). This inconsistency existed while
execDeleteServer, mergeLastConnection, and the headless code path all
correctly validated server_id.

Adds the missing `if (connection.server_id) { validateServerIdentifier(...) }`
guard in both functions, matching the existing server_name pattern.

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-25 12:56:57 -05:00
A
f9c1568f9c
fix(security): use explicit exports in provision.sh subshell (#1926)
Replace inline env-var prefix pattern (VAR=value command) with explicit
export statements inside the subshell. While the inline prefix is
POSIX-compliant and not a real injection vector, explicit exports are
clearer about intent, eliminate the fragile backslash-continuation chain,
and prevent future copy-paste of the pattern into unsafe contexts.

Fixes #1924

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-25 12:54:58 -05:00
A
cac06b2706
fix(security): base64-encode INPUT_TEST_PROMPT in E2E verify.sh to prevent injection (#1923)
Fixes #1921

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-25 10:26:18 -05:00
A
2907ff6068
fix: drain piped stderr in CLI installers and uploadFile to prevent deadlock (#1922)
PR #1920 fixed pipe buffer deadlock in runServerCapture and
waitForCloudInit but missed 6 other locations where Bun.spawn uses
"pipe" for stderr without draining it before await proc.exited.

When a child process writes >64KB to a piped stderr, the OS pipe
buffer fills, the child blocks on write(), and the parent blocks on
exited — classic deadlock.

Fix: change stderr from "pipe" to "inherit" in all 6 locations since
the stderr output is never read programmatically. This also lets
users see installation errors and SCP errors in real time.

Affected functions:
- fly.ts ensureFlyCli()
- sprite.ts ensureSpriteCli()
- gcp.ts ensureGcloudCli()
- hetzner.ts uploadFile()
- digitalocean.ts uploadFile()
- aws.ts uploadFile()

-- refactor/code-health

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-25 09:26:27 -05:00
A
b279cc7c8b
fix: drain piped stderr in Fly/Daytona runServerCapture to prevent deadlock (#1920)
The runServerCapture functions in fly.ts and daytona.ts spawn processes
with stdio: ["pipe", "pipe", "pipe"] but only drain stdout. If stderr
output exceeds the 64KB pipe buffer, the child process blocks on write
and deadlocks. This was already fixed in Hetzner, DigitalOcean, AWS,
GCP, and shared/ssh.ts (commit 2e79d71b) but Fly and Daytona were
missed.

Apply the same Promise.all pattern to drain both pipes concurrently.

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-25 07:19:15 -05:00
A
7e263e706b
fix: remove fake tests that copy-paste source functions instead of importing (#1919)
Delete resolve-prompt.test.ts entirely - it defined replicas of
extractFlagValue, resolvePrompt, and handleDefaultCommand from index.ts
rather than importing them. The replicas had already diverged from the
real code (different parameters, missing flag aliases).

Remove replica functions (renderCompactList, renderMatrixFooter) and
their tests from matrix-compact-footer.test.ts while keeping the valid
tests for exported functions (getImplementedClouds, getMissingClouds,
calculateColumnWidth, getTerminalWidth).

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 03:26:58 -08:00
A
638250e744
fix(security): use base64 encoding in fly_ssh to prevent command injection (#1918)
Replace single-quote escaping (which only handled ' but not other shell
metacharacters like $(), backticks, ;, ||, &&, |) with base64 encoding.
Base64 output contains only [A-Za-z0-9+/=] characters, completely
eliminating shell metacharacter injection risks regardless of command
content. Compatible with both GNU coreutils (Linux) and BSD (macOS).

Fixes #1912

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-25 03:25:31 -08:00
A
4994c28594
fix(security): harden shell scripts - fix sed portability, curl HTTPS enforcement, token expiry (#1917)
- MEDIUM: Validate flyctl auth status before empty FLY_API_TOKEN fallback
  in provision.sh (fail fast instead of silent failure)
- LOW: Fix sed -i portability in qa.sh (use sed -i.bak for macOS compat)
- LOW: Increase FLY_API_TOKEN expiry from 2h to 8h in common.sh
- LOW: Add --proto '=https' to all curl -L calls in digitalocean scripts
  (6 files) to prevent HTTP downgrade on redirects

Fixes #1913

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-25 03:23:32 -08:00
A
9d7175bc1b
fix(ux): correct broken setup guide URL in cloud info page (#1916)
The "Full setup guide" link shown by `spawn <cloud>` pointed to
`/tree/main/{cloud}` which is a 404. The actual READMEs live under
`sh/{cloud}/`, so the URL should be `/tree/main/sh/{cloud}`.

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 02:35:30 -08:00
A
2e79d71bd6
fix: drain piped stderr in runServerCapture/waitForCloudInit to prevent deadlock (#1915)
PR #1903 fixed a pipe buffer deadlock in awsCli() by draining both
stdout and stderr before awaiting proc.exited. The same pattern existed
in runServerCapture() across 4 cloud providers and waitForCloudInit()
across 3 providers. If SSH produces >64KB of stderr, the child blocks
writing to the full pipe while the parent blocks waiting for exit.

Fixes: hetzner, aws, digitalocean, gcp — 7 locations total.

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-02-25 05:24:02 -05:00
A
5458cef9f1
fix: AWS Claude Code install failures on nano instances (#1911)
- Always show instance size picker (remove SPAWN_CUSTOM gate) so users
  can choose bigger instances instead of silently defaulting to nano
- Add 1GB swap in cloud-init so curl installer doesn't get OOM-killed
  on 512MB nano instances
- Set N_PREFIX=$HOME/.n in installClaudeCode so the Node.js fallback
  via `n` works as non-root (ubuntu user can't write to /usr/local/n)
- Add $HOME/.n/bin to Claude Code PATH so node is found after fallback

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
2026-02-25 03:50:45 -05:00
A
32fd908101
fix: reduce SSH keystroke latency with EscapeChar=none (#1910)
SSH scans every byte for ~ escape sequences by default, adding
per-keystroke overhead. Disable this for interactive agent sessions
where escape sequences aren't needed. Also add AddressFamily=inet
to skip IPv6 resolution stalls.

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>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
2026-02-25 00:16:47 -08:00
Ahmed Abushagur
338ae57f71
fix: replace @clack/prompts multiselect with /dev/tty picker for SSH keys (#1907)
* fix: run bun in foreground in DigitalOcean scripts to unfreeze interactive prompts

The _run_with_restart function backgrounded bun with `& + wait` so a SIGTERM
trap could forward the signal. But backgrounding removes bun from the terminal's
foreground process group, which prevents @clack/prompts multiselect from entering
raw mode — arrow keys print as raw escape sequences (^[[A^[[B) and the SSH key
selection prompt freezes.

Fix: run bun in the foreground and detect SIGTERM from exit code 143 (128+15)
instead of using a trap flag + PID tracking. This preserves the restart-on-signal
behavior while giving bun full terminal access for interactive prompts.

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

* fix: replace @clack/prompts multiselect with /dev/tty picker for SSH keys

When the CLI (parent bun) spawns bash → child bun for cloud scripts,
the parent's event loop keeps fd 0 registered and races with the child's
@clack/prompts for terminal input. This causes the SSH key multiselect
to render but freeze — arrow keys print as raw escape sequences.

Fix: add multiPickToTTY() in picker.ts that opens /dev/tty directly,
bypassing process.stdin entirely. Replace the @clack/prompts multiselect
in ssh-keys.ts with this new function. Also add process.stdin.unref()
to prepareStdinForHandoff() so the parent stops polling fd 0.

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

* perf: disable SSH compression for interactive sessions

Compression=yes adds per-keystroke CPU overhead that causes
noticeable input lag on normal connections. Only beneficial
on slow/high-latency links.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:54:54 -08:00
A
d8e03c7a04
fix: drain piped stderr before awaiting process exit to prevent deadlock (#1903)
* fix: drain piped stderr before awaiting process exit to prevent deadlock

Awaiting `proc.exited` before reading piped stderr causes a deadlock when
the child process writes enough stderr output to fill the OS pipe buffer
(~64KB). The process blocks waiting for the buffer to drain, but we never
drain it because we're waiting for the process to exit first.

Fix sprite/sprite.ts (createSprite, uploadFileSprite) and aws/aws.ts
(awsCli) to start draining stderr before awaiting exit, matching the
established pattern in gcp/gcp.ts and shared/ssh.ts.

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

* fix: apply biome format fixes to pass CI

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

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 01:54:56 -05:00
A
9e309f1ff5
fix(security): escape single quotes in standard SSH enter-agent path (#1902)
The standard SSH path in cmdEnterAgent() interpolated remoteCmd into a
single-quoted bash -lc wrapper without escaping embedded single quotes.
If launch_cmd (from history.json) or the manifest's launch/pre_launch
fields contained a single quote, the shell quoting would break, allowing
unintended command execution on the remote server.

The Fly.io path already had this escaping (PR #1880, #1893) but the
generic SSH fallback did not. This adds the same replace(/'/g, "'\\''")
pattern used everywhere else in the codebase.

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-24 22:06:13 -08:00
A
c55b78235c
test: fix inline source copies — import real isValidManifest instead of stale replica (#1900)
The isValidManifest copy in manifest-cache-lifecycle.test.ts was already
out of sync with the real implementation (missing typeof check,
!Array.isArray guard, and "in" operator checks added since the copy was
made). Export the real function and import it so tests break when the
source changes.

Also remove the CSRF state generation describe block from do-oauth.test.ts —
it tested an inline copy of a private function, not the real source.

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-24 22:04:03 -08:00
A
588cecc435
fix: resolve biome nested root configuration conflict (#1896)
Move shared biome.json from lint/ to repo root so it's the single root
config. Nested configs (packages/cli, .claude/skills/setup-spa) get
`"root": false` via `biome migrate`. This fixes lint failing when run
from the repo root.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 21:57:50 -08:00
A
9e54f0cf57
ci: add Mock Tests job to satisfy required status check (#1904)
* ci: add Mock Tests job to satisfy required status check

Split the unit-tests job into mock-tests (runs bun test) and unit-tests
(verifies cloud bundles build). The repo ruleset requires "Mock Tests",
"Unit Tests", and "Biome Lint" checks — the missing "Mock Tests" job was
blocking all PR merges.

Fixes #1901

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

* style: fix pre-existing Biome format issues in 9 files

Auto-applied Biome formatter to src/ to resolve failing "Biome Lint"
required status check. No logic changes — formatting only.

Agent: issue-fixer
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-25 00:54:33 -05:00
A
b2bddc4ba5
ci: bump QA cron from daily to every 4 hours (#1895)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 16:46:55 -08:00
A
98a0d0f68f
feat(qa): add e2e-tester as subagent in scheduled quality sweep (#1894)
E2E tests now run as a 4th teammate alongside test-runner,
dedup-scanner, and code-quality-reviewer during schedule-triggered
QA cycles. The standalone e2e mode is preserved for on-demand use.

- Add e2e-tester teammate to qa-quality-prompt.md
- Increase quality mode timeout from 35 to 40 min
- Add "e2e" to trigger-server valid reasons
- Re-enable daily schedule in qa.yml, default to "schedule"

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 19:34:35 -05:00
A
40417d845d
fix: use single-quote escaping for restart loop in interactiveSession (#1893)
JSON.stringify double-quoting caused two bugs in the restart wrapper:
1. Literal \n instead of newlines (bash doesn't interpret \n in "...")
2. Shell variables ($vars) expanded to empty strings before script ran

Affected clouds: fly, gcp, hetzner, digitalocean, aws.
Daytona already had the correct single-quote escaping.

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 18:47:23 -05:00
A
d423ea57d5
refactor: deduplicate pickToTTY as wrapper around pickToTTYWithActions (#1891)
Fixes #1890

Agent: complexity-hunter

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 17:48:57 -05:00
A
fda44b10f7
fix: remove unused biome-ignore suppression comments in shared type-guards (#1892)
The `packages/shared/src/type-guards.ts` file had 4 `biome-ignore lint/plugin:`
comments that were ineffective because the shared package does not configure the
GritQL plugin in its biome config. Biome reported these as `suppressions/unused`
warnings. Removing them brings the shared package to zero lint warnings.

Fixes #1889

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-24 17:48:10 -05:00
A
5b9ceef060
perf: replace child_process.spawn with Bun.spawn for interactive sessions (#1888)
Using Node's child_process.spawn() to launch interactive SSH/shell sessions
from inside a Bun process adds unnecessary overhead: an extra process fork,
PTY negotiation indirection, and a forced Bun→Node stdio context switch.

Switch all interactiveSession() functions to Bun.spawn() with
stdio: ["inherit","inherit","inherit"], which hands off file descriptors
directly without forking a Node wrapper process.

Also removes the 500ms hardcoded sleep in orchestrate.ts that was a
band-aid for the old child_process handoff latency. The synchronous
prepareStdinForHandoff() is sufficient on its own.

Affected clouds: hetzner, aws, gcp, digitalocean, fly, daytona, sprite, local
Also fixes runInteractiveCommand() in commands.ts (spawn connect).

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 15:17:53 -05:00
A
154112fb41
feat: add live input/output E2E verification for agents (#1886)
* feat: add live input/output E2E verification for agents

The E2E suite previously only verified static artifacts (binaries, config
files, env vars). An agent with a broken API key or crash-on-launch bug
would pass all checks. This adds an input test phase that sends a real
prompt to each agent and verifies the response contains a marker string.

- Add fly_ssh_long() with configurable timeout for long-running commands
- Add per-agent input test functions (claude -p, codex -q, openclaw -p,
  zeroclaw agent -p; opencode/kilocode skip as TUI-only)
- Add run_input_test() dispatcher with SKIP_INPUT_TEST env var support
- Add --skip-input-test CLI flag to fly-e2e.sh
- Chain input test after verify in run_single_agent() pipeline
- Add INPUT_TEST_TIMEOUT constant (default 120s, env-overridable)

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

* fix: format p.text({ message }) to multi-line for biome

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>
2026-02-24 15:16:30 -05:00