Commit graph

1598 commits

Author SHA1 Message Date
A
80c044dbb3
fix: correct CLI entry point path in E2E provision script (#1887)
* 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>
2026-02-24 15:16:07 -05:00
A
43627ca793
fix(digitalocean): fix silent exit after droplet name prompt (#1885)
- 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>
2026-02-24 14:27:50 -05:00
A
2a1ed4164e
fix(gcp): add network/subnet flags to fix custom VPC subnet mode (#1883)
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>
2026-02-24 14:27:46 -05:00
Ahmed Abushagur
5ffa0f663b
fix: use @clack/prompts for all text input to prevent stdin death after multiselect (#1881)
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>
2026-02-24 13:52:40 -05:00
A
0edc588629
fix(daytona): use single-quote escaping in interactiveSession to prevent shell injection (#1880)
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>
2026-02-24 12:28:26 -05:00
A
7419ff8290
fix: use npm install for OpenClaw to fix Node module resolution (#1878)
OpenClaw runs under Node, but bun's flat node_modules layout breaks
Node's require() resolver when dynamically loading channel plugins.
Switching to npm install ensures standard node_modules layout.

Fixes #1875

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 04:51:09 -05:00
A
52b59bab17
fix: export PATH after bun install to fix current-shell availability (#1877)
When bun is freshly installed, rc file patching only takes effect
in new shells. Strengthen the post-install PATH export to include
both hard-coded $HOME/.bun/bin and $HOME/.local/bin alongside
$BUN_INSTALL/bin, so bun and spawn are available in the current
execution context immediately after install.

Fixes #1874

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-24 04:51:00 -05:00
A
4e1467a521
fix: remove broken clone_cli() — go straight to pre-built binary (#1873)
* 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>
2026-02-24 01:19:30 -08:00
A
107b52401c
feat(spa): add duplicate issue check before creating new issues (#1876)
SPA now searches both open and closed issues for keyword matches before
filing. If a closed issue matches, it verifies the fix is still in the
codebase before deciding to skip, reopen, or create new.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 01:17:18 -08:00
A
038c94c72a
feat(cli): detect duplicate --name and route to existing-instance picker (#1866)
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>
2026-02-24 03:50:22 -05:00
A
58c91571f1
fix: make macOS compat linter blocking and add 6 missing rules (#1867)
The linter was running in CI with --warn-only, meaning it never
blocked anything — effectively vaporware. This removes --warn-only
to make it a real gate.

Also adds rules for bash 4.0+ features that were documented in
CLAUDE.md but not enforced:
- MC014: readarray/mapfile (bash 4.0+)
- MC015: coproc (bash 4.0+)
- MC016: &>> redirect (bash 4.0+)
- MC017: relative source paths (breaks curl|bash)
- MC018: wait -n (bash 4.3+)
- MC019: declare -g (bash 4.2+)

Excludes .claude/worktrees/ from scanning (temp copies, not
committed code).

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:50:12 -05:00
A
f3b5e141ca
fix: update stale paths in QA quality prompt (#1872)
- `shared/*.sh` → `sh/shared/*.sh` (scripts moved to sh/ directory)
- Remove hardcoded `test/record.sh`, `test/mock.sh`, `test/e2e.sh`,
  `test/run.sh` references — the old `test/` directory at repo root
  no longer exists. Replace with general guidance pointing to the
  current file structure (`sh/`, `packages/cli/src/`, etc.)

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 00:42:28 -08:00
A
7c53b4e4f6
fix: replace 9 CommonJS require() calls with ESM import in history test (#1871)
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>
2026-02-24 00:16:22 -08:00
A
01b23042b8
style: enforce separated type imports via biome (#1870)
* 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>
2026-02-24 00:16:03 -08:00
A
c701b745ab
fix: add process supervision restart loop for agents on cloud VMs (#1862)
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>
2026-02-24 02:51:31 -05:00
A
9058cf6897
feat: SPA Block Kit layout — text in section, tools in context footer (#1865)
Restructure SPA Slack output using Block Kit:
- Text (Claude's response) → section block, continuously updated
- Tool activity → compact context footer (e.g. "🔧 Bash · Read · Glob")
- :openrouter-loading: in footer while streaming, removed when done
- 👀 reaction instead of :robot_face:
- Remove 🧠 Thinking... initial message (no placeholder needed)
- Use typed KnownBlock/SectionBlock/ContextBlock from @slack/bolt
- Add toolName/isError fields to SlackSegment for structured tracking
- Tool errors marked with  in the footer

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 02:51:15 -05:00
A
dad130b6d3
fix: add SIGTERM trap and restart loop to DigitalOcean agent scripts (#1863)
Agent processes on DigitalOcean droplets were dying silently on SIGTERM
with no logging or recovery. This adds:

- SIGTERM trap handler that logs signal, timestamp, and dashboard URL
- SIGHUP trap handler for terminal disconnection
- Restart loop with exponential backoff (up to 3 attempts) on SIGTERM
- Child process forwarding so bun receives the signal cleanly

Replaces bare `exec bun run` with a managed foreground process that
the shell can monitor and restart.

Fixes #1859

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-24 02:51:14 -05:00
A
9f6bc423ac
fix: add swap space before ZeroClaw install to prevent OOM on nano instances (#1851)
* 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>
2026-02-23 23:12:13 -08:00
A
4664b1123f
fix: convert Markdown to Slack mrkdwn in SPA output (#1861)
* fix: convert Markdown to Slack mrkdwn in SPA assistant output

Claude Code outputs standard Markdown but Slack uses its own mrkdwn
format. Add markdownToSlack() converter that handles:
- **bold** → *bold* (Slack uses single asterisks)
- [text](url) → <url|text> (Slack link format)
- ![alt](url) → <url|alt> (image links)
- ## Headers → *Headers* (bold line)
- ~~strike~~ → ~strike~ (strikethrough)

Code blocks (```) and inline code (`) are preserved untouched.

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

* fix: use slackify-markdown library for Markdown→Slack conversion

Replace hand-rolled markdownToSlack() with the slackify-markdown
package (179K weekly downloads, actively maintained). Handles bold,
links, headers, strikethrough, code blocks, tables, blockquotes,
and more edge cases than a custom regex approach.

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>
2026-02-23 23:09:19 -08:00
A
16cad3ebc9
fix: add error handling to _ensure_bun() in all Hetzner scripts (#1855)
All 6 Hetzner agent scripts silently ignored bun installation failures.
Now matches the robust pattern used by every other cloud (aws, fly, gcp,
digitalocean, daytona, local, sprite): prints status, exits on curl failure,
and verifies bun is available after install.

Fixes #1854

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-24 01:51:44 -05:00
A
f5f020d250
feat: Result monad in shared + Claude Code fixtures + SPA Result adoption (#1858)
* 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>
2026-02-23 22:48:51 -08:00
A
65f6f1be32
feat: Bun workspace monorepo — packages/cli + packages/shared (#1853)
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>
2026-02-23 22:07:05 -08:00
A
8049b8b257
fix: prevent duplicate PRs in refactor issue-fixer workflow (#1856)
Adds an "Existing PR Check" guard to refactor-issue-prompt.md that
runs before the fix workflow. If a PR already exists for the issue:

- Open PR: review it (run tests, approve or comment) instead of
  creating a duplicate
- Merged PR with open issue: verify the fix shipped and close the
  issue, or note the prior attempt if a new fix is needed
- No PR: proceed normally

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 22:02:51 -08:00
A
ac7fa14c61
fix: cache AWS credentials to avoid re-prompting on retry (#1841) (#1852)
After a successful interactive credential entry, credentials are now
saved to ~/.config/spawn/aws.json (chmod 600). On the next run, cached
credentials are loaded and validated before prompting again.

Supports --reauth flag / SPAWN_REAUTH=1 to force fresh credential entry.

Fixes #1841

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-24 00:53:34 -05:00
A
a96a396e79
fix: add Lightsail activation prerequisite to docs and error messages (#1850)
- Adds a Prerequisites section to sh/aws/README.md (updated path after
  shell scripts reorganization in #1843) with the Lightsail activation
  URL as the first step
- Surfaces the Lightsail activation URL in createInstance error hints
  for both CLI and REST paths so users get actionable guidance on failure
- Bumps CLI to 0.9.1

Fixes #1838

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-24 00:53:28 -05:00
A
c487ea215f
refactor: move test fixtures to root /fixtures directory (#1849)
* refactor: move test fixtures to root /fixtures directory

Moves test/fixtures/ → fixtures/ at the repo root for easier
discoverability. Updates all references in CLAUDE.md and QA
prompt files.

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

* refactor: move install.ps1 to sh/cli/ and fixtures to root

- Moves cli/install.ps1 → sh/cli/install.ps1 (consistent with install.sh)
- Moves test/fixtures/ → fixtures/ at the repo root
- Updates all references in README, CLAUDE.md, QA prompts, and the ps1 itself

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

---------

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 21:38:18 -08:00
A
b84adfb74e
refactor: move all shell scripts to /sh directory (#1843)
Reorganizes the project so all shell scripts live under a dedicated
/sh directory, enabling the OpenRouter rewrite URL to point at /sh/
instead of the repository root.

Moves:
- cli/install.sh → sh/cli/install.sh
- shared/*.sh → sh/shared/*.sh
- {cloud}/{agent}.sh → sh/{cloud}/{agent}.sh (48 scripts)
- {cloud}/README.md → sh/{cloud}/README.md
- e2e/*.sh → sh/e2e/*.sh
- test/macos-compat.sh → sh/test/macos-compat.sh
- test/fixtures/**/*.sh → sh/test/fixtures/**/*.sh

Updates all references:
- RAW_BASE path construction in commands.ts, update-check.ts
- GitHub auth URL in agent-setup.ts
- Self-referencing URLs in install.sh, github-auth.sh
- CI workflow paths in lint.yml, cli-release.yml
- Test file paths in install-script-validation, manifest-integrity
- Documentation in README.md, cli/README.md, CLAUDE.md
- QA scripts in .claude/skills/

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 21:14:54 -08:00
A
8812f693c0
fix: switch ZeroClaw install to bootstrap.sh with --prefer-prebuilt (#1836)
The pinned scripts/install.sh is deprecated and does `git clone --depth 1`
of the latest ZeroClaw main branch, pulling in commit 63f485e which added
leak_detector.rs with Rust 2021 edition string literal errors.

Fix by switching to scripts/bootstrap.sh (the canonical installer) and
adding --prefer-prebuilt so ZeroClaw installs from a pre-built release
binary instead of compiling from source. The v0.1.6 release binary was
compiled before the problematic code was merged.

Fixes #1829

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-24 00:08:49 -05:00
A
31c6bec2f6
fix: route spawn <cloud> to cloud info in interactive TTY mode (#1834)
* fix: route `spawn <cloud>` to cloud info in interactive TTY mode

When a user runs `spawn digitalocean` in interactive mode, the CLI was
treating "digitalocean" as an agent name, producing "Unknown agent: digitalocean".
This broke the error message in credential checks which tells users to
"Run spawn <cloud> for setup instructions."

Fix: Before routing a single argument to `cmdAgentInteractive`, check if
it resolves to a cloud name and route to `cmdCloudInfo` instead. This
matches documented behavior (`spawn <cloud>` = show available agents).

Fixes #1830

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

* fix: expand stdout/stderr mock objects to pass Biome format check

The node:child_process mock had stdout and stderr property values
on single lines, which Biome's formatter requires to be expanded
to multi-line object style.

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-24 00:08:43 -05:00
A
8394386cd4
fix: install openclaw with npm instead of bun to fix gateway plugin loading (#1833)
* fix: install openclaw with npm instead of bun to fix gateway plugin loading

The OpenClaw gateway daemon runs on Node.js, but openclaw was being
installed via `bun install -g`. Bun and Node use incompatible module
resolution strategies, causing channel plugins (Telegram, Discord, etc.)
to silently fail to load at gateway startup.

Switch both install paths to `npm install -g openclaw` so the daemon's
Node runtime can resolve its dependencies correctly.

Fixes #1828

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

* fix: apply biome formatting to commands-update-download.test.ts

The file was added in #1831 without passing Biome's format check.
Auto-formatted with `bunx @biomejs/biome format --write`.

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

* fix: use bun install/runtime for openclaw instead of npm

npm install is broken on target VMs. Switch all openclaw install
commands back to bun and remove npm prefix from gateway PATH.

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

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
2026-02-24 00:08:40 -05:00
A
d3563a0596
fix: match actual Claude Code stream-json format (#1835)
Claude Code emits complete messages, not Anthropic streaming deltas:
- {"type":"assistant","message":{"content":[...]}} for text + tool_use
- {"type":"user","message":{"content":[{"type":"tool_result",...}]}}
- {"type":"result","session_id":"..."}

The old code looked for stream_event/content_block_delta which is the
raw Anthropic API format — Claude Code wraps these into full messages.

Also shows tool input hints (command, pattern, file_path) alongside
the tool name for better context in Slack.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 20:24:48 -08:00
A
693e18bfef
test: mock node:child_process to fix flaky cmdUpdate timeout (#1831)
The "should handle update failure gracefully" test triggered a real
execSync("curl -fsSL .../install.sh | bash") via performUpdate() when
the mocked remote version differed from the current version. In isolation
this completes in ~5s, but under full-suite concurrency (52 files, 1897
tests) network contention caused it to timeout at 58267ms — far exceeding
the 5000ms limit. This also violated CLAUDE.md: "no subprocess spawning".

Also mock spawn() used by spawnBash() for cmdRun tests, firing the "close"
event immediately (exit code 0) so Promise-based callers resolve without
hanging. Result: 1897 pass, 0 fail, full suite runs in 3.15s.

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-23 23:19:39 -05:00
A
afde749d68
feat: SPA improvements — streaming, downloads, cleanup, @mention-only (#1832)
- Stream all Claude Code events (tool calls, results, text) to Slack
- Add --verbose flag required by stream-json output format
- Use named import { App } from @slack/bolt (Bun compatibility)
- Only respond to @mentions, not every thread reply
- Download Slack files/images to /tmp/spa-downloads/{threadTs}/
- Hourly cleanup of stale downloads (30+ days) with persisted timestamp
- System prompt reads .github/ISSUE_TEMPLATE/ for issue title format

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 20:17:36 -08:00
A
6a5e0c5161
feat: SPA — Spawn's Personal Agent (#1825)
* feat: add Slack issue bot for #proj-spawn

Socket Mode bot that listens for @mentions in a configured Slack channel,
files GitHub issues via `gh` CLI, and syncs thread replies as issue comments.
State persisted to ~/.config/spawn/slack-issues.json.

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

* feat: rewrite Spawnis to pipe threads into Claude Code sessions

- @mention triggers Claude Code with full thread as prompt
- Subsequent thread replies in tracked threads auto-trigger new runs
- System prompt focuses on GitHub issue management via `gh` CLI
- Streams Claude Code responses back to Slack in real-time
- Bot resolves own user ID at startup to skip self-messages
- Adds slack-manifest.yml for one-click Slack app creation

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

* fix: lowercase display name to spawnis

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

* feat: rename to SPA — Spawn Processes Autonomously

Display name: spa

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

* feat: SPA — Spawn's Personal Agent

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

* fix: lowercase app name to Spa

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

* fix: add biome config and fix lint/format to match CLI rules

Adds local biome.json mirroring cli/biome.json rules (minus GritQL
plugins). Fixes all useBlockStatements errors and applies expand:always
formatting to match the project style.

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

* refactor: share biome config via root biome.json + extends

Move shared linter rules, formatter, and JS formatter settings to a
root-level biome.json. Both cli/ and .claude/skills/slack-bot/ extend
from it — CLI adds its GritQL plugins and test overrides, slack-bot
just overrides includes and disables VCS.

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

* refactor: move GritQL lint rules to repo root lint/

Move no-type-assertion.grit and no-typeof-string-number.grit from
cli/lint/ to lint/ at the repo root. Both cli/ and slack-bot share
the no-type-assertion rule; cli/ additionally uses no-typeof-string-number.

Plugin paths live in each child biome.json (not root) because biome
resolves plugin paths relative to the consumer config, not the extended
config.

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

* refactor: move shared biome.json into lint/

All shared lint config now lives under lint/:
  lint/biome.json
  lint/no-type-assertion.grit
  lint/no-typeof-string-number.grit

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

* feat: add no-banner-comments lint rule, fix slack-bot

GritQL can't match comments (they're trivia in biome's CST), so this
is a Bun script at lint/no-banner-comments.ts that catches decorative
// --------- separator blocks and suggests /** Section */ or #region.

Replace all 9 banner blocks in slack-bot.ts with /** */ headers.

Usage: bun run lint/no-banner-comments.ts [files...]

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

* refactor: use // #region instead of /** */ section headers

Switch slack-bot.ts to // #region / // #endregion for all section
markers (collapsible in most editors). Update no-banner-comments lint
script to recommend #region as the preferred style.

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

* refactor: replace lint script with PostToolUse hook for banner comments

Move banner comment detection into the existing PostToolUse hook on
Write|Edit in .claude/settings.json. Runs inline on every .ts file
edit — no separate bun script needed. Delete lint/no-banner-comments.ts.

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

* fix: simplify Slack manifest description

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

* refactor: rename skill from slack-bot to setup-spa

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

* fix: use named import for @slack/bolt App class

Bun resolves `import App from "@slack/bolt"` as the App constructor
directly, not a module with a `.default` property. Switch to named
import `{ App }` and remove all `.default` usage.

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

* fix: add --verbose flag required by stream-json output format

Claude Code requires --verbose when using --output-format=stream-json
with --print. Also fix systemd PATH to include ~/.local/bin for claude.

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

* feat: stream all Claude Code events to Slack (tools, results, text)

Replace text-only streaming with full event parsing:
- Tool use: shows 🛠️ *ToolName*
- Tool result: shows truncated output in code block
- Text delta: accumulates as before
- Errors: shows  prefix

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

* feat: enforce issue title templates in system prompt

Add mandatory bracket prefix format matching the repo's issue templates:
[Bug]:, [CLI]:, [Agent]:, [Cloud]:, [Team]:. Also instructs Claude to
apply matching labels (bug + pending-review, cli + enhancement, etc.).

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

* fix: reference issue templates at runtime instead of hardcoding

Tell Claude to read .github/ISSUE_TEMPLATE/ for the correct title
prefix, labels, and fields rather than hardcoding them in the prompt.

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>
2026-02-23 19:52:14 -08:00
A
34161f7175
fix: prevent sed delimiter injection via SLACK_WEBHOOK pipe chars (#1827)
Tighten SLACK_WEBHOOK validation regex to only allow alphanumeric chars,
slashes, hyphens, and underscores in the URL path. The previous regex
only checked the domain prefix, allowing pipe characters that could
inject arbitrary sed commands when used in pipe-delimited substitutions.

Fixes #1817

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 19:33:23 -08:00
A
18a7f7e96a
fix: improve AWS Lightsail error message for new accounts (#1826)
* fix: improve AWS Lightsail error message for new accounts

Fixes #1824

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

* style: fix biome formatting in AWS Lightsail error message

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 22:05:50 -05:00
A
0bca426980
security: validate RAW_BASE immediately before curl|bash invocation (#1822)
Fixes #1819

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-23 19:31:53 -05:00
A
fccb73d147
feat: add Fly.io E2E test suite and QA e2e mode (#1823)
- Add e2e/ directory with fly-e2e.sh orchestrator and lib/ helpers
  (provision, verify, teardown, cleanup) that provision real Fly.io VMs,
  verify agent installation, and tear everything down
- Fix openclaw E2E failure by setting MODEL_ID=openrouter/auto to bypass
  interactive model selection prompt in headless mode
- Add e2e mode to qa.sh (reason=e2e) that launches a Claude agent to run
  the E2E suite and investigate/fix any failures
- Update qa.yml with reason dropdown (e2e/schedule/fixtures), kept disabled

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 19:31:44 -05:00
A
a4e805d83b
fix: restore gateway port wait and batch openclaw setup into fewer SSH sessions (#1820)
The previous refactor (b43d3f1) deleted the gateway port wait entirely,
causing the TUI to launch before the gateway was listening on port 18789.

Changes:
- startGateway() now starts the daemon AND polls port 18789 in the same
  SSH session (up to 60s), using /dev/tcp with nc fallback.
- New setupOpenclawBatched() combines install verification + env var
  setup + openclaw config into a single SSH session (was 6 separate
  SSH calls, now 2 total for the whole openclaw flow).
- New optional `setup` hook on AgentConfig lets agents opt into the
  batched path; other agents are unaffected.

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:46:19 -05:00
A
f97079a35d
fix: use correct default server type in Hetzner createServer fallback (#1818)
The hardcoded fallback "cx23" is a stale Hetzner server type that no
longer exists in their API. Replace it with DEFAULT_SERVER_TYPE ("cx22")
which is consistent with the SERVER_TYPES array and promptServerType().

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-23 16:55:22 -05:00
A
de2ba517ed
security: add defensive guards to rm -rf cleanup paths (#1814)
Adds safe_rm_worktree() helper to all 4 agent team scripts that
validates the target path starts with /tmp/spawn-worktrees/ before
executing rm -rf. This prevents accidental deletion if WORKTREE_BASE
is empty or contains an unexpected path.

Affected files: discovery.sh, refactor.sh, security.sh, qa.sh

Fixes #1791

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 15:19:30 -05:00
A
aa88e70488
fix: add concurrency guard and workflow_dispatch to CLI release (#1812)
The race condition: two PRs merged 3 seconds apart both triggered the
CLI Release workflow. The second run (v0.7.12) finished last and
overwrote the release with a stale binary, even though the repo HEAD
was at v0.8.0.

- Add concurrency group so concurrent releases cancel the older one
- Add workflow_dispatch trigger for manual re-runs

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 13:52:17 -05:00
A
c76d930046
fix: prevent GCP delete from hanging on interactive project prompt (#1813)
When deleting a GCP instance, resolveProject() could trigger an
interactive prompt ("Use project?") that collides with the deletion
spinner, causing the command to hang indefinitely. This happened when
instance metadata was missing the project (pre-ee653ca instances) or
when GCP_PROJECT was set to an empty string.

Fix: run resolveProject() in non-interactive mode during deletion so it
auto-accepts the gcloud config default. Also fail fast instead of
showing an interactive picker when no project is available.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 13:52:16 -05:00
A
463c7a2efb
feat: add --custom flag for machine type/region selection (#1810)
* feat: add --custom flag for interactive machine type/region selection

By default, all clouds now skip size/region prompts and use sensible
defaults for faster provisioning. The --custom flag enables interactive
pickers on all clouds, unifying the previously inconsistent behavior
where some clouds always prompted and others never did.

- AWS: promptRegion/promptBundle gated on SPAWN_CUSTOM
- GCP: promptMachineType/promptZone gated on SPAWN_CUSTOM
- Fly: promptVmOptions gated on SPAWN_CUSTOM
- Hetzner: new promptServerType/promptLocation with type/location arrays
- DigitalOcean: new promptDropletSize/promptDoRegion with size/region arrays
- Daytona: new promptSandboxSize with cpu/memory/disk presets
- Sprite: no change (managed platform, no meaningful size options)
- --custom + --headless is an error (incompatible modes)
- Version bump to 0.8.0 (new feature)

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

* style: fix biome format violations in --custom flag code

Auto-format object literals in arrays (expand to multi-line), wrap
long console.error line, and expand inline array in test assertion.

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

---------

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 12:29:51 -05:00
A
b51b5aa11e
feat: configure zeroclaw for full autonomy in spawned VMs (#1811)
Adds ~/.zeroclaw/config.toml with autonomy settings (equivalent to
Claude Code's dangerouslySkipPermissions) so zeroclaw runs without
approval prompts inside sandbox VMs.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 12:29:48 -05:00
A
ee653ca4a6
fix: persist GCP zone/project in connection metadata for deletion (#1809)
GCP delete was re-prompting for project/zone because saveVmConnection
didn't save metadata. Now createInstance passes zone and project as
metadata, and mergeLastConnection reads it back into history.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 08:17:44 -08:00
A
430390bcf5
feat: enforce worktree-first workflow via PreToolUse hook (#1808)
Replace the "block on main" hook with a stricter worktree check:
edits are only allowed when the target file lives inside a git
worktree, not the main checkout. Also blocks edits on the main
branch even within a worktree. CLAUDE.md updated with the new
worktree-first workflow instructions.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 08:03:48 -08:00
A
d961df7f61
fix: wrap fly ssh console -C command in bash -c for session resume (#1806) (#1807)
When resuming a Fly.io session via 'spawn list' → Enter, cmdEnterAgent
passed the stored launch command directly to 'fly ssh console -C'. The
stored command uses shell builtins (source), operators (;), and export
statements that require a bash interpreter. Without a shell wrapper,
fly exec'd the command directly (no shell), causing failures.

The fix wraps the command in `bash -c '...'`, matching the pattern already
used by interactiveSession(), runServer(), runServerCapture(), and
uploadFile() — all of which consistently wrap fly -C arguments in bash.

Fixes #1806

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-23 10:22:37 -05:00
A
493ca7a1cd
fix: use SSH_INTERACTIVE_OPTS in interactiveSession() for hetzner/do/aws/gcp (#1805)
BatchMode=yes in SSH_BASE_OPTS actively blocks TTY prompts in interactive
sessions. Four cloud providers (Hetzner, DigitalOcean, AWS, GCP) were
using SSH_BASE_OPTS in their interactiveSession() functions despite
SSH_INTERACTIVE_OPTS being purpose-built for this (added in PR #1795).
This also adds Compression=yes, IPQoS=lowdelay, StrictHostKeyChecking=accept-new,
and the -t flag (already included), aligning with the commands.ts reconnect path.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 09:25:26 -05:00
A
9aa41bfa67
fix: reject non-ASCII filenames in install.sh download validation (#1802)
Fixes #1800 - explicit ASCII check blocks Unicode lookalike bypass

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-23 08:35:24 -05:00