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>
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>
* 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>
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>
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>
* fix: correct CLI entry point path in E2E provision script
The path resolution went up 2 levels (../../) from sh/e2e/lib/ which
landed in sh/ instead of the repo root. After the monorepo restructure,
packages/cli/src/index.ts is at the repo root — need 3 levels (../../..).
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>
- Set SPAWN_NAME_KEBAB alongside SPAWN_NAME in runBash/runBashHeadless
so cloud scripts (DO, Fly, AWS, etc.) skip their redundant name prompts
- Call prepareStdinForHandoff() before spawnBash() to clean up stdin
state left by @clack/prompts, preventing child process hangs
- Race prompt() against stdin close event so it rejects with an error
instead of hanging forever when stdin dies unexpectedly
- Bump CLI version to 0.10.5
Fixes#1884
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
GCP instance creation was failing with 'Invalid value for field
resource.networkInterfaces[0].subnetwork' when the project VPC uses
custom subnet mode. Add --network and --subnet flags defaulting to
'default', with GCP_NETWORK and GCP_SUBNET env var overrides for
custom VPC setups.
Fixes#1882
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The SSH key multiselect (@clack/prompts) creates and destroys its own
readline interface on stdin. Due to Bun #1707, subsequent Node readline
interfaces silently receive an immediate "close" event — causing the
model selection prompt to exit the process without reading input.
Fix: replace the readline-based prompt() with p.text() from @clack/prompts,
so all stdin interactions use the same library and never trigger the bug.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace JSON.stringify double-quoting with single-quote escaping for the
cmd argument in interactiveSession(). Double-quoted strings in bash allow
$() and ${} expansion, making the previous pattern vulnerable to injection
if cmd ever contained shell metacharacters. Single-quoted strings prevent
ALL shell expansion, matching the defense-in-depth approach Fly already uses.
Fixes#1879
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: remove broken clone_cli() — go straight to pre-built binary
The clone_cli() function (added before the monorepo migration, PR #1853)
only fetches top-level .ts files via the GitHub Contents API. Since the
monorepo reorganised source code into subdirectories (aws/, fly/, hetzner/,
shared/, etc.), clone_cli() silently downloads an incomplete source tree.
bun run build then always fails because cross-directory imports cannot
resolve, and the installer falls through to the pre-built binary anyway.
Every install was burning ~12 unnecessary GitHub API requests (rate-limited
at 60/hr for unauthenticated clients) and several seconds of wasted bun
install + failed build time.
Fix: remove clone_cli() entirely, replace build_and_install() with a
direct binary download. Behaviour is identical for all users (binary path
was already the universal outcome); installs are now faster and cheaper on
the API rate limit.
Agent: code-health
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: update install.sh tests for simplified binary-only installer
Remove tests for clone_cli() and source builds which were removed in
the parent commit. Add tests verifying the direct binary download
approach and asserting that the old clone/build code is gone.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: fix biome format errors in commands.ts and duplicate-detection test
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: Claude Sonnet 4.6 <noreply@anthropic.com>
When `spawn <agent> <cloud> --name "foo"` is run and an active instance
named "foo" already exists for that agent + cloud, cmdRun now checks
getActiveServers() after resolving the spawn name. On a match it shows
a warning and presents the same action picker the user gets from
`spawn ls` (Enter agent / SSH / Spawn a new VM / Delete), instead of
silently provisioning a duplicate VM.
When "Spawn a new VM" is chosen from the picker, SPAWN_NAME is cleared
so the user is prompted for a fresh name, preventing an infinite
duplicate-detection loop.
Bumps CLI to 0.10.3.
Fixes#1864
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The project is ESM-only ("type": "module") and CLAUDE.md bans
require(). All 9 `const { homedir } = require("node:os")` calls
replaced with a single top-level `import { homedir } from "node:os"`.
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: clean up SPA code — extract helpers, use isString, DRY Slack calls
helpers.ts:
- Extract parseAssistantEvent/parseUserEvent/formatToolHint from parseStreamEvent
- Eliminate redundant second scans for toolName and isError (captured during loop)
- Use isString() from @openrouter/spawn-shared instead of typeof checks
- Split long node:fs import across multiple lines
main.ts:
- Extract postOrUpdate() to DRY the post-vs-update pattern (was 3 copy-paste blocks)
- Add SlackClient type alias (replaces 4x InstanceType<typeof App>["client"])
- Remove unused Mapping import
- Inline REQUIRED_VARS into for loop
- Rename currentMsgTs → msgTs
- Tighten formatting throughout
spa.test.ts:
- Remove unused beforeEach import
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add biome config to SPA, fix all lint/format, add PostToolUse hook
- Add biome.json extending lint/biome.json (2-space indent, 120 line width,
useBlockStatements, expand:always, etc.)
- Fix all 36 useBlockStatements violations (braceless if/continue/return)
- Fix all format issues (line width, expand, trailing commas)
- Add biome lint+format to PostToolUse hook — runs automatically on any
.ts file edit when a biome.json is found in the file's directory
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: enforce separated type imports via biome useImportType
Add `style: "separatedType"` to the base biome config's useImportType
rule. This enforces `import type { T }` on its own line instead of
mixing `type` into value imports (`import { type T, foo }`).
Auto-fixed 21 violations across CLI (18) and SPA (3).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When an agent process dies on a cloud VM (SIGTERM, OOM, crash), it now
automatically restarts after 5 seconds, up to 10 times. Clean exits
(code 0) break out immediately. Local execution is unaffected.
Fixes#1860
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add swap space before ZeroClaw install to prevent OOM on nano instances
ZeroClaw's Rust compilation gets OOM-killed on nano_3_0 (512 MB) — build
fails at a random dependency each run. Add ensureSwapSpace() that creates
a 1 GB swap file before running the installer:
- Idempotent: skips silently if swap already exists
- Non-fatal: logs a warning if sudo fails (larger instances won't need it)
- Timeout bumped from 5 min to 10 min (swap-backed builds are slower)
- Defense-in-depth: --prefer-prebuilt avoids compilation in the common
case, but fallback source builds still need memory
Fixes#1840
Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add input validation to ensureSwapSpace() to prevent command injection
Validate sizeMb is a positive integer before interpolating into shell
commands, as requested in security review.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* refactor: split SPA into helpers + main, add build script and tests
Split slack-bot.ts into helpers.ts (pure functions) and main.ts (entry
point) for testability. Add build.ts to bundle SPA into spa.js. Add
spa.test.ts with 19 tests covering stream parsing and text helpers.
Improved streaming: tool_use and tool_result events get their own Slack
messages instead of concatenating everything into one. Prompt is passed
via stdin to avoid CLI flag parsing issues with user content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: drop build.ts — run main.ts directly via bun
Bun runs TypeScript natively, no bundling step needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: move Result monad to shared, add Claude Code fixtures, use Result in SPA
- Move Result type/Ok/Err from packages/cli/src/shared/result.ts to
packages/shared/src/result.ts and re-export from @openrouter/spawn-shared
- Update CLI imports (ui.ts) to use the shared package
- Add fixtures/claude-code/ with realistic stream-json events covering
all event types (assistant text, tool_use, user tool_result, result)
- Refactor SPA helpers to return Result<T> instead of throwing/returning null:
loadState() → Result<State>, saveState() → Result<void>,
downloadSlackFile() → Result<string>, addMapping() → Result<void>
- Update main.ts call sites to handle Result returns
- Update SPA tests to import events from fixtures and test Result returns
- Bump CLI version 0.10.0 → 0.10.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: biome format issues in aws.test.ts, aws.ts, daytona.ts
Expand inline objects/arrays to multi-line format to satisfy biome
formatter rules. No logic changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restructure the repo as a Bun workspace monorepo:
- Move cli/ → packages/cli/
- Create packages/shared/ (@openrouter/spawn-shared) with type-guards and parse utilities
- Add root package.json with workspace configuration
- Update all CLI imports to use @openrouter/spawn-shared
- Deduplicate toRecord/toObjectArray helpers from 4 cloud modules
- Update SPA (slack-bot) to use shared package instead of local toObj()
- Update 48 agent shell scripts for new packages/cli/ path
- Update install.sh, install.ps1, e2e, and test scripts
- Update all GitHub workflows, .gitignore, pre-commit hooks
- Update CLAUDE.md, README.md, and skill prompt references
- Pin all dependency versions (no ^ ranges)
- Bump CLI version 0.9.1 → 0.10.0
All 1908 tests pass. Lint clean. All 8 cloud bundles build.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>