Commit graph

8 commits

Author SHA1 Message Date
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
37fa334d78
fix: navigate back to list after delete/remove errors (#2488)
* fix: navigate back to list after delete/remove errors instead of exiting

Previously, choosing "Delete this server" or "Remove from history" from
the action menu would always exit the picker — even if the operation
failed. Now handleRecordAction returns "back" for delete/remove actions,
and activeServerPicker refreshes the remaining list and loops back to
the picker. Cancel on the action menu also returns to the list.

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

* feat: add ValueOf<T> type helper and GritQL enum ban rule

- Add shared ValueOf<T> type that extracts value unions from const objects
  and readonly tuples
- Update RecordActionOutcome to use ValueOf<typeof RecordActionOutcome>
- Add lint/no-ts-enum.grit GritQL rule that bans TypeScript enum keyword
- Register new rule in biome.json plugins

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

* fix: sort type export before value exports in shared index

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

* feat: add biome config for shared package, fix export sort order

Add biome.json to packages/shared so lint + format + import organization
is enforced on the shared library. Fix ValueOf export position to match
biome's organizeImports sort order (type specifiers after value exports).

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

* fix: hoist type re-exports to top of shared index

Split inline `type Result` and `type ValueOf` out of mixed export
statements into separate `export type { ... }` re-exports, hoisted
to the top per biome's organizeImports group config.

biome's useExportType rule doesn't flag re-exports (only locally
defined types), so these must be manually separated.

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

* refactor: consolidate biome config to single root biome.json

Remove per-package biome.json files (packages/cli, packages/shared,
.claude/scripts, .claude/skills/setup-spa) and consolidate into a
single root config with includes glob covering packages/**/*.ts.

Update GritQL rule exclusions to also match shared/src/ paths now
that the shared package is covered by the root config. Fix build-clouds.ts
lint issues (node: protocol, block statements, import sort) that were
newly caught.

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

* refactor: replace grit filename exclusions with biome-ignore comments

Remove all $filename exclusion logic from GritQL rules and instead add
biome-ignore-all comments at the top of files that legitimately need
the banned patterns (result.ts, parse.ts, type-guards.ts).

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-03-11 00:04:51 -07:00
A
46b1e9d42c
refactor: add no-try-catch + no-try-finally grit rules, eliminate all violations (#2481)
Add two new GritQL biome plugins (matching ori repo patterns) that ban
all try/catch and try/finally in TypeScript code. Convert all remaining
blocks across production and test files to use tryCatch/asyncTryCatch
from @openrouter/spawn-shared.

no-try-catch.grit covers all 4 variants:
- try/catch with binding, try/catch without binding
- try/catch/finally with binding, try/catch/finally without binding

no-try-finally.grit covers bare try/finally.

Both exclude shared/result.ts and shared/parse.ts (the implementation layer).

Production files (18): aws, hetzner, digitalocean, gcp, sprite, index,
update-check, ui, ssh, agent-setup, picker, agent-tarball, shared,
run, connect, delete, list

Test files (12): cmdlast, cmd-interactive, cmdrun-happy-path,
commands-resolve-run, commands-swap-resolve, commands-error-paths,
download-and-failure, preload, ssh-keys, update-check, orchestrate,
fs-sandbox, prompt-file-security, security, script-failure-guidance

Bumps CLI version to 0.16.6

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-10 21:27:25 -07:00
L
65a81edc57
fix: add unique spawn IDs to prevent history record corruption (#2235)
* fix: add unique spawn IDs to prevent history record corruption

History records were matched by heuristic ("most recent record for this
cloud without a connection"), which caused saveVmConnection and
saveLaunchCmd to overwrite the wrong record during concurrent or failed
spawns.

Fix: every SpawnRecord now has a unique `id` (UUID). All history
operations (saveVmConnection, saveLaunchCmd, removeRecord,
markRecordDeleted, mergeLastConnection) match by id when available,
falling back to the old heuristic for pre-migration records.

The orchestrator (TS path) now creates the history record AFTER server
creation succeeds, not before — so failed provisions don't leave orphan
entries.

Also adds "Remove from history" option to the spawn ls action picker,
restoring the ability to soft-delete entries without destroying the VM.

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

* test: add 18 unit tests for spawn ID history behavior

Tests cover:
- generateSpawnId returns unique UUIDs
- saveSpawnRecord auto-generates id when not provided
- saveVmConnection matches by spawnId (not heuristic)
- saveVmConnection does not cross-contaminate concurrent spawns
- saveVmConnection falls back to heuristic without spawnId
- saveLaunchCmd matches by spawnId (not heuristic)
- saveLaunchCmd falls back without spawnId
- removeRecord matches by id, not by timestamp+agent+cloud
- removeRecord handles duplicate timestamps correctly
- removeRecord falls back for legacy records without id
- markRecordDeleted targets correct record by id
- mergeLastConnection uses spawn_id from last-connection.json
- mergeLastConnection falls back to heuristic without spawn_id

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

* style: enable biome import sorting with grouped imports

Adds organizeImports to biome assist config with groups:
1. Type imports
2. Node built-ins
3. Third-party packages
4. @openrouter/* packages
5. Aliases

Auto-fixed import order and lint issues across all TypeScript files,
including .claude/skills/ and packages/cli/src/.

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

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 21:57:50 -08:00
A
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
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