Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9b9ed61
fix(ci): remove stale paths from biome check step in lint.yml
Mar 29, 2026
610c75f
feat: add OpenRouter proxy for Cursor CLI agent (#3100)
la14-1 Mar 30, 2026
b56f3e0
fix(digitalocean): use canonical DIGITALOCEAN_ACCESS_TOKEN env var (#…
la14-1 Mar 30, 2026
29a96c2
fix: remove --trust flag from Cursor CLI launch command (#3101)
la14-1 Mar 30, 2026
4726bfa
fix(cursor): set CURSOR_API_KEY to skip browser login (#3104)
la14-1 Mar 30, 2026
81e53f2
docs: sync README with source of truth (#3097)
la14-1 Mar 30, 2026
07b86fa
fix(cursor): update proxy model list to current models (#3105)
la14-1 Mar 30, 2026
4872069
feat(status): add agent alive probe via SSH (#3109)
la14-1 Mar 30, 2026
d56f554
fix: add cursor to agent lists in spawn skill files (#3108)
la14-1 Mar 30, 2026
f36b757
fix(security): expand $HOME before path validation in downloadFile (#…
la14-1 Mar 30, 2026
7771f8c
fix(manifest): correct cursor repo to cursor/cursor and update star c…
la14-1 Mar 30, 2026
518ee5e
fix(spawn-fix): load API keys via config file, not just process.env (…
la14-1 Mar 30, 2026
55e0323
docs: sync README commands table with help.ts (--prompt, --prompt-fil…
la14-1 Mar 30, 2026
d98f67e
fix(e2e): reduce Hetzner batch parallelism from 3 to 2 (#3112)
la14-1 Mar 30, 2026
64c2483
refactor(e2e): normalize unused-arg comments in headless_env function…
la14-1 Mar 30, 2026
001df29
test: Remove duplicate and theatrical tests (#3089)
la14-1 Mar 30, 2026
53d0d1c
fix(config): extend biome.json includes to cover .claude/**/*.ts
louisgv Mar 30, 2026
13d68b5
fix(prompts): stop infinite shutdown loop after TeamDelete in non-int…
la14-1 Mar 30, 2026
e10b642
fix(zeroclaw): remove broken zeroclaw agent (repo 404) (#3107)
la14-1 Mar 30, 2026
776ff62
fix(cli): allow --headless and --dry-run to be used together (#3117)
la14-1 Mar 30, 2026
93ab505
fix(cli): allow --headless and --dry-run to be used together (#3118)
la14-1 Mar 31, 2026
8b47d23
test: remove redundant theatrical assertions (#3120)
la14-1 Mar 31, 2026
8d89377
docs: sync README tagline with manifest (9 agents/54 → 8 agents/48 co…
la14-1 Mar 31, 2026
b1b212a
docs: remove stale ZeroClaw references after agent removal (#3122)
la14-1 Mar 31, 2026
53b366e
fix(e2e): redirect DO max_parallel log_warn to stderr (#3110)
la14-1 Mar 31, 2026
493bf38
refactor: remove stale ZeroClaw references from docs and code comments
Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .claude/rules/agent-default-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ Last verified: 2026-03-13
| Claude Code | _(routed by Anthropic)_ | `ANTHROPIC_BASE_URL=https://openrouter.ai/api` — model selection handled by Claude's own routing |
| Codex CLI | `openai/gpt-5.3-codex` | Hardcoded in `setupCodexConfig()` → `~/.codex/config.toml` |
| OpenClaw | `openrouter/auto` | `modelDefault` field in agent config; written to OpenClaw config via `setupOpenclawConfig()` |
| ZeroClaw | _(provider default)_ | `ZEROCLAW_PROVIDER=openrouter` — model selection handled by ZeroClaw's OpenRouter integration |
| OpenCode | _(provider default)_ | `OPENROUTER_API_KEY` env var — model selection handled by OpenCode natively |
| Kilo Code | _(provider default)_ | `KILO_PROVIDER_TYPE=openrouter` — model selection handled by Kilo Code natively |
| Hermes | _(provider default)_ | `OPENAI_BASE_URL=https://openrouter.ai/api/v1` + `OPENAI_API_KEY` — model selection handled by Hermes |
Expand Down
2 changes: 1 addition & 1 deletion .claude/rules/discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Do NOT add agents speculatively. Only add one if there's **real community buzz**
Agents that ship compiled binaries (Rust, Go, etc.) need separate ARM (aarch64) tarball builds. npm-based agents are arch-independent and only need x86_64 builds. When adding a new agent:
- If it installs via `npm install -g` → x86_64 tarball only (Node handles arch)
- If it installs a pre-compiled binary (curl download, cargo install, go install) → add an ARM entry in `.github/workflows/agent-tarballs.yml` matrix `include` section
- Current native binary agents needing ARM: zeroclaw (Rust), opencode (Go), hermes, claude
- Current native binary agents needing ARM: opencode (Go), hermes, claude

To add: same steps as before (manifest.json entry, matrix entries, implement on 1+ cloud, README).

Expand Down
10 changes: 5 additions & 5 deletions .claude/skills/setup-agent-team/qa-fixtures-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Cloud credentials are stored in `~/.config/spawn/{cloud}.json` (loaded by `sh/sh

For each cloud with a fixture directory, check if its required env vars are set:
- **hetzner**: `HCLOUD_TOKEN`
- **digitalocean**: `DO_API_TOKEN`
- **digitalocean**: `DIGITALOCEAN_ACCESS_TOKEN`
- **aws**: `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`

Skip clouds where credentials are missing (log which ones).
Expand All @@ -53,11 +53,11 @@ curl -s -H "Authorization: Bearer ${HCLOUD_TOKEN}" "https://api.hetzner.cloud/v1
curl -s -H "Authorization: Bearer ${HCLOUD_TOKEN}" "https://api.hetzner.cloud/v1/locations"
```

### DigitalOcean (needs DO_API_TOKEN)
### DigitalOcean (needs DIGITALOCEAN_ACCESS_TOKEN)
```bash
curl -s -H "Authorization: Bearer ${DO_API_TOKEN}" "https://api.digitalocean.com/v2/account/keys"
curl -s -H "Authorization: Bearer ${DO_API_TOKEN}" "https://api.digitalocean.com/v2/sizes"
curl -s -H "Authorization: Bearer ${DO_API_TOKEN}" "https://api.digitalocean.com/v2/regions"
curl -s -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" "https://api.digitalocean.com/v2/account/keys"
curl -s -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" "https://api.digitalocean.com/v2/sizes"
curl -s -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" "https://api.digitalocean.com/v2/regions"
```

For any other cloud directories found, read their TypeScript module in `packages/cli/src/{cloud}/` to discover the API base URL and auth pattern, then call equivalent GET-only endpoints.
Expand Down
3 changes: 2 additions & 1 deletion .claude/skills/setup-agent-team/refactor-issue-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Track lifecycle: "pending-review" → "under-review" → "in-progress". Check la
7. Keep pushing commits to the same branch as work progresses
8. When fix is complete and tests pass: `gh pr ready NUMBER`, post update comment linking PR
9. Do NOT close the issue — `Fixes #SPAWN_ISSUE_PLACEHOLDER` auto-closes on merge
10. Clean up: `git worktree remove WORKTREE_BASE_PLACEHOLDER`, shutdown teammates
10. Clean up: run `git worktree remove WORKTREE_BASE_PLACEHOLDER` and call `TeamDelete` in ONE turn, then output a plain-text summary with **NO further tool calls**. A text-only response ends the non-interactive session immediately.

## Commit Markers

Expand All @@ -84,5 +84,6 @@ Every commit: `Agent: issue-fixer` + `Co-Authored-By: Claude Sonnet 4.5 <noreply

- Run tests after every change
- If fix is not straightforward (>10 min), comment on issue explaining complexity and exit
- **NO TOOLS AFTER TeamDelete.** After calling `TeamDelete`, do NOT call any other tool. Output plain text only to end the session. Any tool call after `TeamDelete` causes an infinite shutdown prompt loop in non-interactive (-p) mode. See issue #3103.

Begin now. Fix issue #SPAWN_ISSUE_PLACEHOLDER.
8 changes: 6 additions & 2 deletions .claude/skills/setup-agent-team/refactor-team-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ Setup: `mkdir -p WORKTREE_BASE_PLACEHOLDER`. Cleanup: `git worktree prune` at cy

**The session ENDS when you produce a response with NO tool calls.** EVERY iteration MUST include at minimum: `TaskList` + `Bash("sleep 15")`.

**EXCEPTION — After TeamDelete:** Once `TeamDelete` has been called and completed (step 4 of the shutdown sequence), your VERY NEXT response MUST be plain text only with **NO tool calls**. Do NOT call `TaskList`, `Bash`, or any other tool after `TeamDelete`. A text-only response is the termination signal for the non-interactive harness. Any tool call after `TeamDelete` causes an infinite loop of shutdown prompt injections.

Keep looping until:
- All tasks are completed OR
- Time budget is reached (10 min warn, 12 min shutdown, 15 min force)
Expand All @@ -289,11 +291,13 @@ Follow this exact shutdown sequence:
1. At 10 min: broadcast "wrap up" to all teammates
2. At 12 min: send `shutdown_request` to EACH teammate by name
3. Wait for ALL shutdown confirmations — keep calling `TaskList` while waiting
4. After all confirmations: `git worktree prune && rm -rf WORKTREE_BASE_PLACEHOLDER`
5. Print summary and exit
4. In ONE turn: call `TeamDelete`, then run `git worktree prune && rm -rf WORKTREE_BASE_PLACEHOLDER` — do everything in this single turn
5. **Output a plain-text summary and STOP** — do NOT call any tool after `TeamDelete`. This text-only response ends the session.

**NEVER exit without shutting down all teammates first.** If a teammate doesn't respond to shutdown_request within 2 minutes, send it again.

**CRITICAL — NO TOOLS AFTER TeamDelete.** After `TeamDelete` returns (whether success or "No team name found"), you MUST NOT make any further tool calls. Output your final summary as plain text and stop. Any tool call after `TeamDelete` triggers an infinite shutdown prompt loop in non-interactive (-p) mode. See issue #3103.

## Safety

- **NEVER close a PR.** No teammate, including team-lead and pr-maintainer, may close any PR — not even PRs created by refactor teammates. Closing PRs is the **security team's responsibility exclusively**. The only exception is if you are immediately opening a superseding PR (state the replacement PR number in the close comment). If a PR is stale, broken, or should not be merged, **leave it open** and comment explaining the issue — the security team will close it during review.
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/agent-tarballs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ jobs:
# Native-binary agents need ARM builds too.
# npm-based agents (codex, openclaw, kilocode) are arch-independent — x86_64 only.
include:
- agent: zeroclaw
arch: arm64
- agent: opencode
arch: arm64
- agent: hermes
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
strategy:
fail-fast: false
matrix:
agent: [claude, codex, cursor, openclaw, opencode, kilocode, zeroclaw, hermes, junie]
agent: [claude, codex, cursor, openclaw, opencode, kilocode, hermes, junie]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
run: bun install

- name: Run Biome check (all packages)
run: bunx @biomejs/biome check packages/cli/src/ packages/shared/src/ .claude/scripts/ .claude/skills/setup-spa/
run: bunx @biomejs/biome check packages/cli/src/ packages/shared/src/

macos-compat:
name: macOS Compatibility
Expand Down
22 changes: 11 additions & 11 deletions .github/workflows/packer-snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,18 @@ jobs:
- name: Generate variables file
run: |
jq -n \
--arg token "$DO_API_TOKEN" \
--arg token "$DIGITALOCEAN_ACCESS_TOKEN" \
--arg agent "$AGENT_NAME" \
--arg tier "$TIER" \
--argjson install "$INSTALL_COMMANDS" \
'{
do_api_token: $token,
digitalocean_access_token: $token,
agent_name: $agent,
cloud_init_tier: $tier,
install_commands: $install
}' > packer/auto.pkrvars.json
env:
DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }}
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DO_API_TOKEN }}
AGENT_NAME: ${{ matrix.agent }}
TIER: ${{ steps.config.outputs.tier }}
INSTALL_COMMANDS: ${{ steps.config.outputs.install }}
Expand All @@ -96,7 +96,7 @@ jobs:
if: cancelled()
run: |
# Filter by spawn-packer tag to avoid destroying builder droplets from other workflows
DROPLET_IDS=$(curl -s -H "Authorization: Bearer ${DO_API_TOKEN}" \
DROPLET_IDS=$(curl -s -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" \
"https://api.digitalocean.com/v2/droplets?per_page=200&tag_name=spawn-packer" \
| jq -r '.droplets[].id')

Expand All @@ -107,28 +107,28 @@ jobs:

for ID in $DROPLET_IDS; do
echo "Destroying orphaned builder droplet: ${ID}"
curl -s -X DELETE -H "Authorization: Bearer ${DO_API_TOKEN}" \
curl -s -X DELETE -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" \
"https://api.digitalocean.com/v2/droplets/${ID}" || true
done
env:
DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }}
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DO_API_TOKEN }}

- name: Cleanup old snapshots
if: success()
run: |
PREFIX="spawn-${AGENT_NAME}-"
SNAPSHOTS=$(curl -s -H "Authorization: Bearer ${DO_API_TOKEN}" \
SNAPSHOTS=$(curl -s -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" \
"https://api.digitalocean.com/v2/images?private=true&per_page=100" \
| jq -r --arg prefix "$PREFIX" \
'[.images[] | select(.name | startswith($prefix))] | sort_by(.created_at) | reverse | .[1:] | .[].id')

for ID in $SNAPSHOTS; do
echo "Deleting old snapshot: ${ID}"
curl -s -X DELETE -H "Authorization: Bearer ${DO_API_TOKEN}" \
curl -s -X DELETE -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" \
"https://api.digitalocean.com/v2/images/${ID}" || true
done
env:
DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }}
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DO_API_TOKEN }}
AGENT_NAME: ${{ matrix.agent }}

- name: Submit to DO Marketplace
Expand Down Expand Up @@ -162,7 +162,7 @@ jobs:
HTTP_CODE=$(curl -s -o /tmp/mp-response.json -w "%{http_code}" \
-X PATCH \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${DO_API_TOKEN}" \
-H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" \
-d "$(jq -n \
--arg reason "Nightly rebuild — $(date -u '+%Y-%m-%d')" \
--argjson imageId "$IMG_ID" \
Expand All @@ -177,6 +177,6 @@ jobs:
exit 1 ;;
esac
env:
DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }}
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DO_API_TOKEN }}
AGENT_NAME: ${{ matrix.agent }}
MARKETPLACE_APP_IDS: ${{ secrets.MARKETPLACE_APP_IDS }}
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Spawn is a matrix of **agents x clouds**. Every script provisions a cloud server
## The Matrix

`manifest.json` is the source of truth. It tracks:
- **agents** — AI agents and self-hosted AI tools (Claude Code, OpenClaw, ZeroClaw, ...)
- **agents** — AI agents and self-hosted AI tools (Claude Code, OpenClaw, Codex CLI, ...)
- **clouds** — cloud providers to run them on (Sprite, Hetzner, ...)
- **matrix** — which `cloud/agent` combinations are `"implemented"` vs `"missing"`

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ spawn delete -c hetzner # Delete a server on Hetzner
| `spawn <agent> <cloud> --dry-run` | Preview without provisioning |
| `spawn <agent> <cloud> --zone <zone>` | Set zone/region for the cloud |
| `spawn <agent> <cloud> --size <type>` | Set instance size/type for the cloud |
| `spawn <agent> <cloud> -p "text"` | Non-interactive with prompt |
| `spawn <agent> <cloud> --prompt-file f.txt` | Prompt from file |
| `spawn <agent> <cloud> --prompt "text"` | Non-interactive with prompt (or `-p`) |
| `spawn <agent> <cloud> --prompt-file <file>` | Prompt from file (or `-f`) |
| `spawn <agent> <cloud> --headless` | Provision and exit (no interactive session) |
| `spawn <agent> <cloud> --output json` | Headless mode with structured JSON on stdout |
| `spawn <agent> <cloud> --model <id>` | Set the model ID (overrides agent default) |
Expand Down Expand Up @@ -139,7 +139,7 @@ spawn claude hetzner --fast
What `--fast` does:
- **Parallel boot**: server creation runs concurrently with API key prompt and account checks
- **Tarballs**: installs agents from pre-built tarballs instead of live install
- **Skip cloud-init**: for lightweight agents (Claude, OpenCode, ZeroClaw, Hermes), skips the package install wait since the base OS already has what's needed
- **Skip cloud-init**: for lightweight agents (Claude, OpenCode, Hermes), skips the package install wait since the base OS already has what's needed
- **Snapshots**: uses pre-built cloud images when available (Hetzner, DigitalOcean)

#### Beta Features
Expand Down Expand Up @@ -206,7 +206,7 @@ export OPENROUTER_API_KEY=sk-or-v1-xxxxx
# Cloud-specific credentials (varies by provider)
# Note: Sprite uses `sprite login` for authentication
export HCLOUD_TOKEN=... # For Hetzner
export DO_API_TOKEN=... # For DigitalOcean
export DIGITALOCEAN_ACCESS_TOKEN=... # For DigitalOcean

# Run non-interactively
spawn claude hetzner
Expand Down Expand Up @@ -258,7 +258,7 @@ If spawn fails to install, try these steps:
2. **Set credentials via environment variables** before launching:
```powershell
$env:OPENROUTER_API_KEY = "sk-or-v1-xxxxx"
$env:DO_API_TOKEN = "dop_v1_xxxxx" # For DigitalOcean
$env:DIGITALOCEAN_ACCESS_TOKEN = "dop_v1_xxxxx" # For DigitalOcean
$env:HCLOUD_TOKEN = "xxxxx" # For Hetzner
spawn openclaw digitalocean
```
Expand Down Expand Up @@ -324,12 +324,12 @@ If an agent fails to install or launch on a cloud:
|---|---|---|---|---|---|---|
| [**Claude Code**](https://claude.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [**OpenClaw**](https://github.com/openclaw/openclaw) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [**ZeroClaw**](https://github.com/zeroclaw-labs/zeroclaw) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [**Codex CLI**](https://github.com/openai/codex) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [**OpenCode**](https://github.com/sst/opencode) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [**Kilo Code**](https://github.com/Kilo-Org/kilocode) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [**Hermes Agent**](https://github.com/NousResearch/hermes-agent) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [**Junie**](https://www.jetbrains.com/junie/) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [**Cursor CLI**](https://cursor.com/cli) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |

### How it works

Expand Down
4 changes: 0 additions & 4 deletions assets/agents/.sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
"url": "https://openclaw.ai/apple-touch-icon.png",
"ext": "png"
},
"zeroclaw": {
"url": "https://avatars.githubusercontent.com/u/261820148?s=200&v=4",
"ext": "png"
},
"codex": {
"url": "https://avatars.githubusercontent.com/u/14957082?s=200&v=4",
"ext": "png"
Expand Down
8 changes: 7 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"files": {
"ignoreUnknown": false,
"includes": ["packages/**/*.ts"]
"includes": ["packages/**/*.ts", ".claude/**/*.ts"]
},
"formatter": {
"enabled": true,
Expand Down Expand Up @@ -100,6 +100,12 @@
}
}
}
},
{
"includes": [".claude/**"],
"linter": {
"enabled": false
}
}
],
"plugins": [
Expand Down
Loading
Loading