Commit graph

2340 commits

Author SHA1 Message Date
A
2dd87c986d
feat(cli): add star-the-repo nudge after successful spawns (#3025)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
Shows a non-intrusive " Enjoying Spawn? Star us on GitHub!" message
to returning users (2+ successful spawns) after a successful spawn
session completes. Shown at most once per 30 days.

- New `maybeShowStarPrompt()` in `shared/star-prompt.ts`
- Tracks `starPromptShownAt` in `~/.config/spawn/preferences.json`
- Called after `execScript()` returns success in cmdRun, cmdInteractive,
  and cmdAgentInteractive (skipped in headless mode)
- The `execScript()` return type changed from `void` to `boolean`
  to indicate whether the script ran successfully
- Added 7 unit tests covering all gate conditions

Fixes #3020

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-27 03:15:12 +07:00
A
255ffbf8b7
fix(security): use grep -F for literal string matching in PATH checks (#3021)
Fixes #3019

Replace `grep -qx` with `grep -qxF` in the `ensure_in_path` function
to prevent regex pattern injection. Without -F, attacker-controlled
SPAWN_INSTALL_DIR or BUN_INSTALL env vars containing regex metacharacters
(e.g. `/.*`) could cause false positive/negative PATH matches, potentially
bypassing the symlink creation logic.

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-27 02:56:07 +07:00
A
f2044f8d62
fix: add --yes/-y to KNOWN_FLAGS so spawn delete --name <name> --yes works (#3024)
PR #3015 added --yes and -y flags to the delete command but didn't add
them to KNOWN_FLAGS in flags.ts. This caused `spawn delete --name foo --yes`
to fail with "Unknown flag: --yes" because checkUnknownFlags runs before
dispatchDeleteCommand strips these flags.

Also adds delete-specific flags to --help documentation.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 02:54:33 +07:00
Ahmed Abushagur
0f48e4dae5
feat: headless delete via spawn delete --name <name> --yes (#3015)
Agents running on spawned VMs couldn't delete child spawns because
`spawn delete` requires an interactive terminal for the picker UI.

Added --name and --yes flags: when both are provided in non-interactive
mode, the server matching the name is deleted without prompts. This
enables agents to manage their own child VMs programmatically.

Updated all skill files to teach agents the headless delete syntax.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
2026-03-26 12:30:15 -07:00
Ahmed Abushagur
73bb52e2f5
fix: use sprite exec -tty instead of sprite console for entering agents (#3014)
sprite console does not accept arguments — it's a pure interactive shell.
When entering an agent on Sprite, use `sprite exec -s NAME -tty` which
supports passing commands via `-- bash -lc CMD`.

Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-27 01:30:54 +07:00
A
defca448b0
fix(e2e): load GCP_ZONE from ~/.config/spawn/gcp.json in E2E driver (#3017)
The GCP E2E cloud driver defaulted to us-central1-a when GCP_ZONE was
not set in the environment. The QA VM stores zone config in
~/.config/spawn/gcp.json (alongside GCP_PROJECT) but _gcp_validate_env
only read GCP_PROJECT from the environment — it never loaded GCP_ZONE.

This caused E2E failures when us-central1-a had insufficient resources:
3 agents (openclaw, opencode, kilocode) failed with "SSH port never
opened" because GCP couldn't provision instances in that zone.

Fix: load both GCP_PROJECT and GCP_ZONE from the config file in
_gcp_validate_env when they are not already set in the environment,
matching how key-request.sh loads GCP_PROJECT for provisioning.

Verified: all 3 previously failing agents now pass on europe-west1-b.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:27:46 +07:00
A
6d46a52f6f
test: remove duplicate tests from cmd-link-cov (#3013)
remove 3 tests that duplicate scenarios already covered in
cmd-link.test.ts:
- "saves record" (same as "saves a spawn record when agent/cloud given")
- "exits with error for invalid IP" (same as in cmd-link)
- "generates default name" (same as "generates a default name")

remaining 7 tests cover unique paths (IMDS detection, which-binary
fallback, spinner behavior, short flags) not in cmd-link.test.ts.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 00:31:20 +07:00
A
988f5bb7a9
fix(security): validate bun path before symlinking in install.sh (fixes #3009) (#3011)
Add allowlist validation for the bun binary path resolved via `command -v bun`
before using it in symlink operations that may run with sudo privileges. If bun
is found at an unexpected location, skip the symlink and warn the user. This
prevents a privilege escalation attack where a malicious binary on PATH could be
symlinked to /usr/local/bin/bun with elevated privileges.

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 05:37:45 -07:00
A
405dbc6ba6
refactor: use getSpawnCloudConfigPath(), remove dead _cloudName param (#3010) (#3012)
Replace hand-constructed openrouter.json path with getSpawnCloudConfigPath("openrouter")
for single-source-of-truth path resolution. Remove unused _cloudName parameter since
the function delegates ALL cloud credentials unconditionally.

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 19:26:09 +07:00
A
fd36ff0e3d
fix(security): add base64 validation guards in orchestrate.ts (fixes #3006) (#3007)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
Add /^[A-Za-z0-9+/=]+$/ validation after each .toString("base64") call
in delegateCloudCredentials() and injectEnvVars(), consistent with the
pattern established in agent-setup.ts by #2988.

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:25:40 +07:00
A
463b8398f2
fix: add ai-review.sh to bash -n syntax check list in e2e-lib.sh (#3005)
ai-review.sh is sourced by e2e.sh but was missing from the bash -n
syntax check loop in sh/test/e2e-lib.sh. This means syntax errors in
ai-review.sh would not be caught by the test harness.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-26 03:12:07 -07:00
A
45a1266abe
test: remove duplicate validator tests from ui-cov.test.ts (#3004)
the `validators` describe block in ui-cov.test.ts duplicated 6 tests
that already exist with full edge-case coverage in ui-utils.test.ts:
- validateServerName (2 tests) → duplicated by 5 tests in ui-utils.test.ts
- validateRegionName (2 tests) → duplicated by 4 tests in ui-utils.test.ts
- validateModelId (2 tests) → duplicated by 6 tests in ui-utils.test.ts

removed tests only checked one accept+one reject per validator, providing
no signal beyond what ui-utils.test.ts already covers exhaustively. also
removed the now-unused imports from the import statement.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 16:27:58 +07:00
A
52d06c4cb5
fix: resolve ANSI spinner corruption and garbled output (#3001) (#3003)
* fix(ux): replace download spinner with stderr logging, reset terminal before SSH handoff

Fixes two UX issues from live E2E session (#3001):

1. Download spinner (p.spinner from @clack/prompts) wrote ANSI escape codes
   to stdout. When stdout is captured (E2E harness, piped output), these
   sequences appeared as raw text rather than rendered colors. Replace
   p.spinner() in downloadScriptWithFallback and downloadBundle with
   logStep/logInfo/logError from shared/ui.ts, which write to stderr and
   correctly check isTTY before emitting ANSI codes.

2. Garbled output at start of interactive session (overlapping status lines
   from the remote agent's TUI) may be caused by residual ANSI state from
   @clack/prompts (hidden cursor, active color attributes). Emit
   ESC[?25h ESC[0m to stderr before prepareStdinForHandoff() to explicitly
   restore cursor visibility and reset all attributes before the SSH session
   takes over.

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

* fix: resolve ANSI spinner corruption and garbled output in interactive mode (#3001)

Three root causes fixed:

1. Spinner wrote to stdout while all other CLI status output goes to stderr,
   causing ANSI escape sequence interleaving and corruption when both streams
   are merged on a terminal. Redirected all p.spinner() calls to process.stderr.

2. unicode-detect.ts (which sets TERM=linux for SSH sessions to force ASCII
   fallback) was only imported in commands/shared.ts but not in shared/ui.ts.
   Cloud module entry points (hetzner/main.ts, etc.) that import shared/ui.ts
   loaded @clack/prompts without the TERM override, causing Unicode spinner
   frames in environments that can't render them.

3. After an interactive SSH session ends, the remote agent's TUI (e.g. Claude
   Code) may leave the terminal in raw mode with altered attributes. Added
   terminal reset (ANSI attribute reset + stty sane) after spawnInteractive()
   returns to prevent garbled post-session output.

Agent: ux-engineer
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.6 <noreply@anthropic.com>
2026-03-26 15:28:32 +07:00
A
af37ad2db5
style: remove unnecessary as never casts from oauth-cov.test.ts (#3002)
`spyOn(Bun, "serve")` works without the `as never` type assertion.
These casts violated the documented no-type-assertion rule
(`.claude/rules/type-safety.md`). Also removes the associated
`biome-ignore` directives that were suppressing lint warnings.

Agent: style-reviewer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-26 14:33:58 +07:00
A
7378cab0b2
fix(security): add defensive validation to tmpdir cleanup in install.sh (#3000)
Adds a non-empty check after mktemp and guards the EXIT trap so rm -rf
only fires when tmpdir is non-empty and still a directory. This is a
defense-in-depth hardening — the current code is safe due to set -e,
but explicit validation is best practice for rm -rf operations.

Fixes #2998

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 11:26:56 +07:00
A
88980c15a1
docs: Sync README with source of truth (#2995)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
* docs: sync README commands table with help.ts source of truth

remove 5 command rows from the README commands table that are not present
in packages/cli/src/commands/help.ts getHelpUsageSection():
- spawn list --flat
- spawn list --json
- spawn tree
- spawn tree --json
- spawn history export

these commands exist in code (index.ts, list.ts) but are not listed in the
canonical help section, which is the Gate 2 source of truth per qa/record-keeper
protocol.

* fix: restore documentation for working commands (spawn tree, list --flat, --json, history export)

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

---------

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 10:12:45 +07:00
A
ff7315202e
fix: add missing --beta parallel and --beta recursive to --help text (#2990)
The CLI help output only listed 3 of 5 beta features (tarball, images,
docker). The error output on invalid beta flags and the README both
correctly listed all 5. This adds the missing parallel and recursive
entries to --help for consistency.

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-26 10:10:52 +07:00
Ahmed Abushagur
7fe36b8aa0
fix: delegate ALL cloud credentials, not just the current cloud (#2994)
delegateCloudCredentials only copied the current cloud's config file
(e.g. sprite.json when spawning on Sprite). Child VMs couldn't spawn
on other clouds because their tokens weren't forwarded.

Now iterates all known clouds and copies every credential file that
exists locally, so the agent can spawn children on any cloud.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 19:02:42 -07:00
A
665780b6b2
test: remove duplicate checkForUpdates tests from update-check-cov.test.ts (#2997)
Two tests in update-check-cov.test.ts were exact duplicates of tests in
update-check.test.ts:
- "skips when recently checked successfully" duplicated "should skip fetch
  when last successful check was recent"
- "does not skip when checked timestamp is old (>1h)" duplicated "should
  fetch when last successful check is older than 1 hour"

Also removed the now-unused writeUpdateChecked helper function.

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-25 18:57:18 -07:00
A
a7f3e9da82
refactor: remove dead code and stale references (#2996)
- Remove `export` from `getTerminalWidth` in commands/info.ts — only
  used internally, not exported from commands/index.ts barrel
- Remove `export` from `makeDockerExec` in shared/orchestrate.ts — only
  used internally by `makeDockerRunner`, no external callers
- Bump CLI version to 0.26.6

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-26 08:40:42 +07:00
Ahmed Abushagur
90dde882d0
fix: installSpawnCli fails on Sprite — bun shim doesn't work (#2993)
Sprite has a bun shim at /.sprite/bin/bun that delegates to
$HOME/.bun/bin/bun, but that binary doesn't exist on fresh VMs.
`command -v bun` returns true (finds the shim) so the install script
skips bun installation, then bun fails when actually invoked.

Fixed in two places:
- installSpawnCli: source shell profiles, test `bun --version` (not
  just existence), and install bun fresh if it doesn't work
- install.sh: replace `command -v bun` with `bun --version` to detect
  broken shims

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:36:12 +07:00
Ahmed Abushagur
b47d6bbe1d
fix: embed skill content instead of reading from disk (#2992)
* fix: spawn step skipped when no explicit --steps passed

The spawn skill injection condition used `enabledSteps?.has("spawn")`
which is falsy when enabledSteps is undefined (no --steps flag). Now
checks the recursive beta flag directly and falls through when no
explicit steps are selected, matching how auto-update works.

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

* fix: embed skill content in spawn-skill.ts instead of reading from disk

The skills/ directory exists in the repo but isn't bundled when the CLI
is installed via npm. readSkillContent() couldn't find the files at
runtime, causing "No spawn skill file for agent" on every deploy.

Fixed by embedding all skill content directly as string constants in the
module. Removed fs-based getSkillsDir/readSkillContent/getSpawnSkillSourceFile
in favor of a single AGENT_SKILLS config map with inline content.

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-26 06:16:52 +07:00
Ahmed Abushagur
17817533a4
feat: skill injection — teach agents how to use spawn on recursive VMs (#2989)
When `--beta recursive` is active, a new "Spawn CLI" setup step injects
agent-native instruction files teaching each agent how to use the `spawn`
CLI to create child VMs. Skill files live in `skills/` at the repo root
and use each agent's native format (YAML frontmatter for Claude/Codex/
OpenClaw, plain markdown for others, append mode for Hermes).

- Add `skills/` directory with 8 agent-specific skill files
- Add `spawn-skill.ts` module with path mapping, file reading, and injection
- Register "spawn" as a conditional setup step gated by `--beta recursive`
- Wire `injectSpawnSkill()` into orchestrate.ts postInstall flow
- Add 52 tests covering path mapping, append mode, file existence, injection
- Bump CLI version to 0.26.0 (minor: new feature)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:32:20 -07:00
A
7194058c64
fix(security): add input validation to makeDockerExec (#2987)
Adds non-empty guard to makeDockerExec to make the security boundary
explicit and prevent silent misuse with empty commands.

Fixes #2985

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 15:17:53 -07:00
A
82ab6d35dc
fix(security): add base64 validation for all shell-interpolated values (#2988)
Previously only `settingsB64` had a validation check. Added the same
`/^[A-Za-z0-9+/=]+$/` guard for wrapperB64, unitB64, and timerB64
before they are interpolated into shell commands, closing the consistency gap.

Fixes #2986

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 05:16:02 +07:00
A
3b68a77526
fix(test): fix flaky delegateCloudCredentials test due to cross-file sandbox pollution (#2984)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
The `skips when no credential files exist` test in recursive-spawn.test.ts
was failing in the full suite (1911 pass, 1 fail) because other test files
(oauth-cov.test.ts, cmd-uninstall-cov.test.ts) write openrouter.json and
hetzner.json to $HOME/.config/spawn/ without cleanup, contaminating the
shared sandbox HOME used by bun's test runner. The test passed in isolation
but failed 100% of the time in the full suite.

Fix: add a beforeEach inside the delegateCloudCredentials describe block
that removes $HOME/.config/spawn/ before each test, making the test
self-contained and immune to cross-file pollution.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 02:22:36 +07:00
Ahmed Abushagur
b0674550c6
feat: recursive spawn (--beta recursive) (#2978)
* feat: add recursive spawn (--beta recursive)

Enables VMs to spawn child VMs. When --beta recursive is active:
- Injects SPAWN_PARENT_ID, SPAWN_DEPTH, SPAWN_BETA=recursive into .spawnrc
- Installs spawn CLI on the VM via install.sh
- Delegates cloud + OpenRouter credentials to the VM
- Tracks parent_id and depth on SpawnRecord for tree relationships
- Adds `spawn tree` command for full recursive tree view
- Adds `spawn history export` for pulling child history via SSH
- Adds `spawn list --json` and `spawn list --flat` flags
- Adds tree rendering in `spawn list` when parent-child relationships exist
- Adds cascade delete support in delete.ts
- Adds mergeChildHistory() for backward-pass history sync

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

* docs: add recursive spawn to README

Add --beta recursive to beta features table, new commands
(spawn tree, spawn history export, spawn list --flat/--json)
to commands table, and a dedicated Recursive Spawn section
with usage examples for tree view and cascade delete.

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

* test: add cmdTree coverage tests to fix mock test CI

The CI coverage threshold (90% functions, 80% lines) was failing
because tree.ts had 0% coverage. Added tests that exercise cmdTree
with empty history, tree rendering, JSON output, flat records,
and deleted/depth labels. tree.ts now has 100% coverage.

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

* fix(security): validate cloudName and use valibot in pullChildHistory

- Add cloudName validation against ^[a-z0-9-]+$ to prevent
  command injection in delegateCloudCredentials
- Export SpawnRecordSchema from history.ts and replace loose
  type guard with valibot schema validation in pullChildHistory
- Resolve merge conflicts with main (include both docker and
  recursive beta features)

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

* test: add installSpawnCli and delegateCloudCredentials coverage

Export and test installSpawnCli (success + timeout failure paths)
and delegateCloudCredentials (no creds, with creds, write failure,
mkdir failure paths) to improve orchestrate.ts function coverage.

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

* fix: gritQL rule false positives and delete.ts coverage

- use TsAsExpression() AST node instead of backtick pattern to avoid
  matching import aliases as type assertions
- export and test findDescendants() and pullChildHistory() to bring
  delete.ts line coverage above the 35% threshold
- add 8 new tests for descendant finding and history pull edge cases

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: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
2026-03-25 10:42:09 -07:00
A
76bdaf2042
fix: pin GitHub Actions to commit SHAs, version-lock CI tools (#2983)
* fix: pin all GitHub Actions to commit SHAs and version-lock tools

Addresses supply chain hardening findings from issue #2982:

- Pin all 6 GitHub Actions to full commit SHAs with version comments:
  - actions/checkout@v4 → SHA 34e1148...
  - oven-sh/setup-bun@v2 → SHA 0c5077e...
  - actions/github-script@v7 → SHA f28e40c...
  - docker/login-action@v3 → SHA c94ce9f...
  - docker/build-push-action@v6 → SHA 10e90e3...
  - hashicorp/setup-packer@main → SHA c3d53c5... (v3.2.0)
- Pin Packer version: latest → 1.15.0 (in packer-snapshots.yml)
- Pin bun version: latest → 1.3.11 (in agent-tarballs.yml)
- Pin shellcheck: replace apt-get (no version) with pinned download
  of v0.10.0 from GitHub releases with SHA256 integrity check

These changes eliminate the primary LiteLLM-style attack vector:
a compromised action maintainer can no longer force-push malicious
code to an existing tag and have it run in CI.

Fixes #2982

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

* fix: exclude import aliases from no-type-assertion lint rule

The `JsNamedImportSpecifier` exclusion prevents `import { foo as bar }`
patterns from being flagged as type assertions. Previously, any `as`
keyword in import/export statements triggered the ban because the GritQL
pattern `$value as $type` matched import specifiers as well as actual
TypeScript type assertions.

This also removes the `as _foo` import aliases in the script-failure-guidance
test file (replaced with direct imports + distinctly-named wrapper functions)
which were the original manifestation of this bug.

All 1944 tests pass. Biome check clean across 169 files.

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-03-26 00:27:58 +07:00
A
d161458c13
test: remove duplicate and theatrical tests (#2981)
Remove weaker duplicates found during QA quality sweep:

orchestrate-cov.test.ts: remove "orchestrate restart loop" describe block
(2 tests) — duplicates tests already in orchestrate.test.ts with fewer
assertions (missing "my-agent --run" and "Restarting in 5s" checks).

cmd-delete-cov.test.ts: remove theatrical "intercepts stderr writes to
update spinner" test — handler was a no-op mock, only asserted return
value, never verified actual stderr interception. Duplicate of
"calls custom deleteHandler and reports success" in the same file.
Real stderr/spinner behavior is covered by delete-spinner.test.ts.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 20:37:47 +07:00
A
d57d82d04f
fix: resolve UX issues in spawn claude hetzner (#2977) (#2980)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
- Suppress remote command output in Hetzner runServer() by piping
  stdout/stderr instead of inheriting. This prevents raw ANSI escape
  sequences from remote install commands (spinners, progress bars)
  from leaking into the local terminal as garbled characters, and
  eliminates duplicate status messages that were repeated 15+ times.
  Captured stderr is logged via logDebug on failure for debugging.

- Add LC_ALL=C.UTF-8 to both the interactive SSH session and the
  .spawnrc env config to ensure consistent UTF-8 locale across all
  locale categories, preventing garbled Unicode rendering in Claude
  Code's TUI welcome interface.

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-25 15:50:51 +07:00
Ahmed Abushagur
53189b80a2
fix: remove docker from --fast and fix docker cp into container (#2976)
* fix: remove docker from --fast and fix docker cp into container

Two fixes for --beta docker:

1. Remove "docker" from --fast beta features — --fast was auto-enabling
   --beta docker, pulling ghcr images that hang the session.
   Users must now opt in explicitly with --beta docker.

2. Fix uploadFile in docker mode — .spawnrc was uploaded to the host
   but never copied into the container. Add docker cp after SCP upload
   so env vars and configs reach the agent inside the container.

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

* fix: keep docker in --fast beta features

The docker cp fix resolves the hang — no need to remove docker from
--fast. The issue was missing file copy into the container, not the
docker mode itself.

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

* fix: extract makeDockerRunner helper, fix uploadFile into container

Add makeDockerRunner() that wraps a CloudRunner so all commands and
file uploads target the Docker container. Replaces inline lambdas in
hetzner/main.ts and gcp/main.ts with a clean one-liner.

The key fix: uploadFile now docker cp's files into the container after
SCP — previously .spawnrc (API keys, env vars) only landed on the host,
so the agent inside the container had no config and hung.

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

* fix(security): shellQuote remotePath in docker cp command

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-25 14:52:05 +07:00
A
9e48136e8a
test: remove duplicate saveLaunchCmd fallback test from history-spawn-id (#2975)
The "falls back to most recent record with connection when no spawnId"
test in history-spawn-id.test.ts duplicates the same-named test in
history-cov.test.ts. The history-cov version is more thorough: it uses
two records where the first lacks a connection, exercising the
"skip records without connection" logic. The history-spawn-id version
only had one record, providing no additional signal.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 12:11:00 +07:00
Ahmed Abushagur
a551cb2401
fix: remove local tarball download path (#2970)
* fix: remove local tarball download, use remote-only tarball install

The local-download-then-SCP-upload path was unnecessary complexity —
downloading a tarball to the user's machine just to re-upload it to the
VM is wasteful. The VM downloads directly from GitHub instead.

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

* fix: force zeroclaw native runtime to prevent Docker container hang

ZeroClaw auto-detects Docker and launches in a container (pulling
ghcr.io/openrouterteam/spawn-zeroclaw), which hangs the interactive
session. Force native mode via ZEROCLAW_RUNTIME=native env var and
adapter = "native" in config.toml.

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

* fix: disable openclaw Docker sandbox to prevent container hang

Same issue as zeroclaw — openclaw auto-detects Docker and runs agents
in containers, hanging the interactive session. Disable via
agents.defaults.sandbox.mode = off in config and fallback JSON.

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

* fix: disable codex Docker sandbox to prevent container hang

Codex CLI also auto-detects Docker for sandboxing. Set
sandbox_mode = "danger-full-access" in config.toml — the VM itself
provides isolation, Docker sandboxing just causes hangs.

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-24 21:42:31 -07:00
Ahmed Abushagur
934dfd309f
test: add unit tests for E2E bash test infrastructure (#2968)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
136 tests covering common.sh, verify.sh, provision.sh, and e2e.sh:
- format_duration, make_app_name, track_app/untrack_app
- get_provision_timeout/get_agent_timeout with env overrides
- Numeric validation (injection resistance for timeout vars)
- OpenRouter API key fallback logic
- _validate_timeout and _validate_base64 security checks
- run_input_test dispatch (unknown agent, TUI skips, SKIP_INPUT_TEST)
- provision_agent app_name validation (injection resistance)
- e2e.sh argument parsing (--help, missing args, invalid clouds/agents)
- ALL_AGENTS completeness (verify_* and input_test_* for every agent)
- Cloud driver interface compliance (all 5 drivers implement required fns)
- bash -n syntax check on all E2E scripts
- macOS compat linter on core E2E libraries

Also documents a known limitation: _validate_base64 uses per-line grep
matching, so multiline strings pass if each line is valid (low risk since
base64 encoding always strips newlines).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
2026-03-24 18:42:48 -07:00
A
f85e573cee
test: remove duplicate junie envVars test from agent-setup-cov (#2969)
The 'junie agent envVars include JUNIE_OPENROUTER_API_KEY' test in
agent-setup-cov.test.ts was a weaker duplicate of the more precise
coverage in junie-agent.test.ts, which verifies the exact env var value.

1890 → 1889 tests (1 duplicate removed, 0 regressions).

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 08:40:33 +07:00
A
650708e30d
refactor: remove dead code and stale references (#2966)
Extract duplicate dockerExec helper from gcp/main.ts and hetzner/main.ts
into shared makeDockerExec() in orchestrate.ts. Both local functions were
identical — wrapping commands with docker exec using DOCKER_CONTAINER_NAME
and shellQuote.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 14:24:33 -07:00
A
824bef6045
test: remove duplicate and theatrical tests (#2967)
Remove 5 duplicate test cases from orchestrate-cov.test.ts that were
already covered by orchestrate.test.ts with stronger assertions:
- orchestrate checkAccountReady throws (duplicate, weaker version)
- orchestrate preProvision throws (duplicate, weaker version)
- tarball falls back to install when tarball returns false (exact duplicate)
- tarball skips for local cloud (exact duplicate)
- skipTarball agent flag (exact duplicate)

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 03:49:00 +07:00
A
d3889519bc
fix(e2e): fix --fast mode for native binary agents on Sprite (#2965)
Add 180s timeout to uploadFileSprite to prevent indefinite hangs during
tarball uploads. Without a timeout, large tarballs or stalled Sprite
connections block the entire provisioning pipeline past the 720s E2E
provision timeout, causing agent binary not-found failures for openclaw,
zeroclaw, and codex.

Also skip the redundant remote tarball download fallback when a local
tarball was already downloaded but its upload/extract failed -- the
remote download would face the same extraction issues. This saves ~150s
in the fallback chain, leaving enough time for the live install to
complete within the provision timeout.

Fixes #2960

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 02:59:11 +07:00
A
4ee4bd71e6
fix: rewrite git+ssh to HTTPS for hermes pip install on cloud VMs (#2963)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
The hermes install script's mini-swe-agent pip dependency uses
git+ssh:// URLs that timeout on fresh cloud VMs (hetzner/gcp/digitalocean)
where outbound SSH to GitHub is blocked or slow.

Add `git config --global url.https://github.com/.insteadOf` rules
before the hermes install and update commands to force git to use
HTTPS instead of SSH for all GitHub URLs. This eliminates the SSH
connection timeout that was causing install failures.

Fixes #2955

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 12:10:21 -07:00
A
e045cf6f78
fix(security): prevent sed delimiter injection and harden SPAWN_ISSUE validation (#2964)
safe_substitute: Switch sed delimiter from | to \x01 (SOH control char) across
qa.sh, refactor.sh, security.sh, and discovery.sh. This eliminates delimiter
injection regardless of value content, since \x01 cannot appear in normal input.
Values containing \x01 are explicitly rejected as defense-in-depth.

SPAWN_ISSUE: Fix qa.sh validation from ^[0-9]+$ to ^[1-9][0-9]*$ to reject
leading zeros and zero itself. Add 32-bit signed integer range check
(max 2147483647) to all three scripts (qa.sh, refactor.sh, security.sh)
to prevent integer overflow in downstream consumers.

Fixes #2961
Fixes #2962

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 01:51:41 +07:00
A
ad00f93cf7
test: merge duplicate createCloudAgents describe blocks and use beforeEach (#2959)
Merged "createCloudAgents" and "createCloudAgents detailed" into a single
describe block. Both blocks tested the same function with no structural
distinction, causing duplicate organization without value.

Eliminated 26 repetitive inline runner object constructions by moving
runner and result setup into beforeEach. This removes ~115 lines of
boilerplate while keeping all 21 tests and their assertions intact.

1895 tests still pass.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 23:53:35 +07:00
A
a6940fdaad
fix(e2e): improve interactive harness failure logging (#2951)
On interactive provision failure, save the harness log to a persistent
path (/tmp/spawn-interactive-harness-last.log) for post-mortem inspection,
and filter output to only show [harness] prefixed lines (30 lines) instead
of dumping 50 raw lines of mixed output.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
2026-03-24 08:45:19 -07:00
A
65320abf05
refactor(test): extract shouldSkipCloudInit helper and add unit tests (#2958)
Extracts the inline docker-mode condition from hetzner/main.ts and
gcp/main.ts into a testable exported function in shared/cloud-init.ts,
then adds real unit tests that import from the source. Fixes #2952.

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 22:32:53 +07:00
A
6c742bdd11
fix(e2e): increase hermes install timeout to fix failures on Hetzner/DO/GCP (#2956)
Hermes installs a Python virtualenv which takes 20+ min on fresh VMs.
The previous 300s install timeout caused the CLI to give up before
writing .spawnrc, leading to 30-min E2E timeouts on Hetzner, DigitalOcean,
and GCP (but not Sprite, which has a manual .spawnrc fallback).

Changes:
- agent-setup.ts: hermes installAgent timeout 300s → 600s
- common.sh: add hermes per-agent overrides (_PROVISION_TIMEOUT_hermes=720,
  _AGENT_TIMEOUT_hermes=3600) to give the install enough headroom
- package.json: bump CLI version 0.25.26 → 0.25.27

-- qa/e2e-tester

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-24 21:34:41 +07:00
A
b2606084e6
test: remove theatrical source-grep test for docker-mode waitForReady (#2953)
docker-cloudinit-skip.test.ts was reading source file contents with readFileSync
and checking for the presence of specific string literals — a source-grep
anti-pattern that tests the text exists, not that the behavior works.

The waitForReady() closure in hetzner/main.ts and gcp/main.ts cannot be directly
unit tested without refactoring (tracked in #2952). The source-grep tests are
removed to avoid false confidence.

Filed https://github.com/OpenRouterTeam/spawn/issues/2952 to track proper
behavioral testing via extracting the skip-cloud-init condition into a testable
exported helper.

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-24 20:40:03 +07:00
A
77dbeb95ae
fix(fix): add missing LANG export to buildFixScript (#2954)
`buildFixScript()` was missing `export LANG='C.UTF-8'` that was added to
the canonical `generateEnvConfig()` in commit f93c799d. Users running
`spawn fix` would get a `.spawnrc` without the UTF-8 locale export,
causing garbled Unicode in agent TUIs — the same regression that f93c799d
fixed for fresh provisioning.

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-24 20:38:05 +07:00
A
c3cdd7ec8d
test: remove theatrical source-grep tests, replace with real unit tests (#2948)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
do-min-size.test.ts was reading source file contents with readFileSync
and checking for the presence of specific strings (bash-grep anti-pattern).
Fixes:
- Export slugRamGb and AGENT_MIN_SIZE from digitalocean.ts
- Import them in main.ts instead of re-defining
- Rewrite do-min-size tests to call functions with inputs and assert outputs
  (3 source-grep tests → 6 behavior tests)

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 02:08:45 -07:00
A
f93c799db8
fix(ux): suppress duplicate install message and set UTF-8 locale (#2950)
1. Suppress Claude Code curl installer stdout — the remote installer
   prints its own "Installation complete!" which duplicated the local
   "Claude Code agent installed successfully" message.

2. Export LANG=C.UTF-8 in both the interactive SSH session command and
   the .spawnrc env config. Fresh cloud VMs often default to the C
   locale which cannot render Unicode properly, causing garbled ANSI
   output in agent TUIs (e.g. "⏵⏵bypasspermissionson" instead of
   properly spaced text).

Fixes #2946

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 01:59:11 -07:00
A
0f3cb8b2eb
docs(tests): add missing test entries to __tests__/README (#2949)
Two test files (do-min-size.test.ts, docker-cloudinit-skip.test.ts) existed
on disk but were not documented in the README. Add entries for both.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 15:51:43 +07:00
A
056ce252c7
fix(e2e): suppress matrix email on targeted re-runs via SPAWN_E2E_SKIP_EMAIL (#2944)
When the quality cycle e2e-tester re-runs only failed agents
(e.g. `e2e.sh --cloud hetzner zeroclaw codex`), e2e.sh was firing
a matrix email showing only those 2 agents — both PASS if the retry
succeeded. This looked like "2 tests ran, all passed" when in reality
32 tests ran with 2 failures.

- Add SPAWN_E2E_SKIP_EMAIL=1 env var check at the top of send_matrix_email
- Update qa-quality-prompt.md to set SPAWN_E2E_SKIP_EMAIL=1 on re-runs

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 00:17:10 -07:00