The sprite CLI rejects the legacy single-dash long-flag forms we still
emit, so `spawn run <agent>` fails on sprite at the very first remote
call (`sprite create -skip-console NAME`) before any agent install runs.
Updates the calls + tests to the supported forms:
- sprite create -skip-console → --skip-console
- sprite exec -tty → --tty
- sprite exec -file SRC:DST → --file SRC:DST
- sprite version (subcommand gone) → sprite --version
- sprite url (deprecated) → sprite info (URL still extracted
via the existing https://… regex)
Bumps @openrouter/spawn to 1.0.44 so users on sprite pick up the fix on
their next run.
Reported by Andrew via Slack; tracking issue #3408.
Co-authored-by: Claude <claude@anthropic.com>
* feat(ssh): let user pick SSH key when handshake auth keeps failing
When `waitForSsh` sees consecutive "Permission denied (publickey)"
responses from the remote, the auto-discovered set of `~/.ssh/*` keys
clearly isn't the right one. Today the user just watches the same
error scroll past until the retry budget runs out.
This adds an interactive picker after 2 consecutive auth failures (and
only in interactive mode). The picker lists every discovered key (with
type + an "already tried" hint), lets the user paste a custom path, or
keep retrying with the current set. The chosen path replaces the `-i`
identity flags on the next handshake attempt.
- New `promptForSshKey()` in `shared/ssh-keys.ts` — clack-driven picker
with custom-path support and `~` expansion. Returns `null` in
non-interactive mode so unattended runs are unaffected.
- `waitForSsh` tracks consecutive publickey rejections, ignores
unrelated transient failures (timeouts, connection refused), and
offers the picker once per call when the threshold is hit.
- Adds dedicated unit tests covering non-interactive bypass, the skip
path, key selection, custom-path entry, `~/` expansion, and the
"already tried" hint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): resolve rebase conflict — use discoverLegacyKeys in promptForSshKey
After #3401 refactored ssh-keys.ts (renamed discoverSshKeys → discoverLegacyKeys
+ getSpawnKey), the key-picker PR's promptForSshKey still called the old
discoverSshKeys name. Update to use discoverLegacyKeys() directly for picker
options (spawn key is excluded since it's always the default, not a choice).
Also remove now-unused readdirSync import.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ssh): final-fallback picker after exhausted retries + remember choice
Builds on the mid-loop picker: now if every auto-discovered key still
gets rejected after the retry budget runs out, the user gets one more
chance to point spawn at the right key — and the choice is persisted
to ~/.config/spawn/preferences.json (sshKeyPath) so subsequent spawn
runs use it directly with no further prompts.
- getPreferredSshKeyPath() reads the saved path and gracefully ignores
stale entries (file deleted, malformed JSON, missing field).
- setPreferredSshKeyPath() merges the value into preferences.json
while preserving every other field (models, starPromptShownAt).
- clearPreferredSshKeyPath() is added for completeness / recovery.
- ensureSshKeys() honors the saved preference — if set, only that key
is offered to SSH (rather than diluting the explicit choice with
the spawn-managed key + legacy fallbacks).
- waitForSsh now offers a final fallback picker after the main loop
exhausts (only if every failure was publickey-auth and the user is
interactive). One extra handshake attempt with the picked key. On
success — mid-loop swap or final fallback — the chosen key is
persisted via setPreferredSshKeyPath.
- 10 new unit tests cover the preference round-trip, malformed-JSON
/ stale-path resilience, field preservation, directory creation,
clearPreferredSshKeyPath, and ensureSshKeys honoring/falling back
as expected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
When a user has multiple SSH keys loaded in ssh-agent, OpenSSH offers them
all before the -i keys we pass. With sshd's default MaxAuthTries=6, the
auth flood causes "Permission denied (publickey)" before our key is even
tried — observed by Chris on hermes/digitalocean.
Two changes:
1. Add IdentitiesOnly=yes to SSH_BASE_OPTS and SSH_INTERACTIVE_OPTS so ssh
ignores agent-loaded identities and only tries the explicit -i keys.
2. Refactor ssh-keys.ts to a spawn-owned key (~/.ssh/spawn_ed25519):
- getSpawnKey() ensures it exists, generated on first use
- new VMs are provisioned with ONLY the spawn key (DO/Hetzner/AWS/GCP)
- the user's personal keys never get registered with cloud providers
(a privacy win — fixes Alex's dad scenario where personal keys were
uploaded to a fresh DO account on first run)
- ensureSshKeys() returns [spawnKey, ...legacy] capped at 3 — pre-existing
id_ed25519/id_rsa/id_ecdsa stay as -i fallbacks so droplets provisioned
by older Spawn versions remain SSH-reachable
Reconnect hints across all clouds now print `ssh -i ~/.ssh/spawn_ed25519`
since the custom filename isn't auto-tried by bare `ssh root@<ip>`.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): expand fast_provision experiment to images + docker + sandbox
Builds on the existing PostHog `fast_provision` flag (already wired via
shared/feature-flags.ts). The `test` variant now bundles the full
provisioning-speed stack rather than images alone:
- images: pre-built DO marketplace images (cloud-side faster boot)
- docker: Docker CE host image on Hetzner/GCP (cloud-side faster boot)
- sandbox: local agents run in a Docker container (local-side faster boot)
Users who explicitly pass --beta or --fast still take precedence and skip
the experiment bucket. Exposure events are still captured for both
variants so PostHog can compute conversion across the broader bundle.
Bumps CLI to 1.0.39.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(cli): extract fast_provision bundle helper, align --fast
Addresses review feedback on the fast_provision experiment expansion:
- Align `--fast` with the experiment `test` variant. Previously --fast
pushed [tarball, images, parallel, docker] (no sandbox) while the
silent A/B pushed [images, docker, sandbox]. Add `sandbox` to --fast
so the explicit user opt-in exercises the same surface as the silent
experiment, plus the speed-ups outside the experiment scope.
- Extract the experiment bundle into a pure `expandFastProvisionVariant`
helper in shared/feature-flags.ts so the bundle composition is
testable in isolation from main() arg parsing. Drop-in replacement
in index.ts; behavior unchanged for the test variant.
- Add unit tests pinning the bundle: test variant -> [images, docker,
sandbox], control -> [], unknown variants -> [] (fail-closed). These
guard against silent drift when someone tweaks the list later.
- Bump CLI 1.0.39 -> 1.0.40 per the version-on-every-cli-change rule.
Verified: bunx biome check src/ clean, bun test 2188/2188 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* revert(cli): leave --fast untouched, experiment owns its own bundle
My previous commit (8f87330) added `sandbox` to --fast to align it with
the experiment test variant. That was wrong — --fast is unrelated to
the fast_provision experiment, has its own meaning, and shouldn't
inherit changes from the experiment bundle.
- Restore --fast to [tarball, images, parallel, docker] (its prior set).
- Drop the cross-reference comments tying --fast to the experiment.
- Keep expandFastProvisionVariant() and its unit tests intact — the
experiment bundle still lives in feature-flags.ts as a testable
pure helper, just no longer claims alignment with --fast.
- Bump CLI 1.0.40 -> 1.0.41.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
* fix(ssh): verify pub/priv keypair before registering with cloud providers
When a local SSH .pub file doesn't actually pair with the corresponding
.priv (e.g. .pub copied from another machine, regenerated mid-flow, or
edited by hand), spawn would still register the .pub with the cloud
provider's key store. The registration check passes by fingerprint, the
droplet boots with that key in authorized_keys, and SSH then fails with
"Permission denied (publickey)" because the local .priv can't prove
ownership of the registered .pub. This produced the silent failure mode
where users saw "SSH key 'id_ed25519' already registered with
DigitalOcean" immediately followed by 33 "Permission denied" retries.
Adds verifyKeyPair() which derives the public key from the private key
via `ssh-keygen -y -P "" -f priv` and compares it (key type + base64,
ignoring the comment field) to the .pub file. discoverSshKeys() now
filters out mismatched pairs with a clear warning naming the offending
file, and silently skips passphrase-protected or otherwise
unverifiable keys (BatchMode SSH can't use them anyway).
Bumps CLI to 1.0.37.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): auto-repair stale .pub instead of skipping mismatched pair
When the local .pub doesn't derive from the matching .priv (stale copy
from another machine, etc.), the priv is still authoritative — any .pub
that doesn't derive from it is wrong by definition. Previously spawn
printed a warning and skipped the pair; now it backs up the stale .pub
as .pub.spawn-backup-<timestamp> and rewrites the .pub from the derived
key. The next launch uses the correct pub end-to-end, so the droplet
boots with a public key that actually pairs with the local priv and SSH
handshake succeeds instead of failing 33 times with "Permission denied
(publickey)".
Passphrase-protected keys (ssh-keygen -y cannot derive without the
passphrase) are still skipped silently — nothing to repair with.
Bumps CLI to 1.0.38.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: SPA <spa@openrouter.ai>
* ci(docker): build multi-arch images (amd64 + arm64)
The agent images at ghcr.io/openrouterteam/spawn-<agent>:latest were built
linux/amd64 only. On Apple Silicon (arm64) hosts running --beta sandbox,
OrbStack pulls the amd64 image and runs it under Rosetta. Inside the
container, github-auth.sh's `apt-get update` to install gh hangs in
emulation — confirmed by `[rosetta] /usr/lib/apt/methods/http` processes
sleeping forever. A native arm64 ubuntu:24.04 finishes the same apt-get
update + curl install in ~12s; the amd64-emulated run was still stuck
after 5+ minutes.
Adds docker/setup-qemu-action + docker/setup-buildx-action and sets
platforms: linux/amd64,linux/arm64 on the build-push step. Builds will
take longer (arm64 layer compiles under QEMU on the amd64 runner), but
the resulting multi-arch manifest gives Apple Silicon users native arm64
binaries and unblocks the sandbox flow.
All current Dockerfiles (claude, codex, cursor, hermes, junie, kilocode,
openclaw, opencode, pi) install via npm or arch-aware curl scripts, so
they're already arch-portable.
* ci(docker): verify the pushed manifest contains both architectures
Post-build step runs `docker buildx imagetools inspect` and greps the
manifest for both linux/amd64 and linux/arm64. Catches regressions
where setup-qemu/buildx gets dropped or the `platforms:` flag gets
lost in a future refactor — silent single-arch publishes would be
invisible until an Apple Silicon user hit the Rosetta hang again.
One post-build step per matrix entry keeps the check local to the
agent that was just pushed.
---------
Co-authored-by: Claude <claude@anthropic.com>
* fix(growth): enforce brevity in tweet prompt (target ≤140, hard cap 180)
Recent posted tweets were running 170-200 chars and reading as long-form
marketing copy. Tighten the tweet drafting prompt:
- Add a dedicated "BREVITY IS THE #1 RULE" section at the top
- Target 80-140 chars, hard cap 180 chars (down from the 280 platform limit)
- Good/bad length examples drawn from actual prior drafts
- Require a trim pass before output
- Update output format and rules to reflect the new caps
* fix(growth): drop numeric char targets, lean on taste
Numeric targets (target 140, cap 180) just become the new goal. The model
will pad to hit them instead of writing a punchy 40-char banger. Remove
all char-count anchors from the brevity guidance and let the good/bad
examples carry the signal.
- Rename section from 'BREVITY IS THE #1 RULE' to 'Keep it short'
- Drop 80-140 target, 180 cap, char-count example annotations
- Drop Chars line from output format and charCount from JSON
- Keep only the qualitative rules: don't pad, don't explain twice,
prefer one sentence, cut what isn't pulling weight
- Platform 280 cap mentioned once as a ceiling, not a goal
* fix(growth): strip all length references from tweet prompt
Even mentioning '280 char limit' or 'platform limit' anchors the model
to the wrong end of the range. Remove both remaining length references
so the prompt is purely about taste: say less, don't pad, cut what
isn't pulling weight. The platform enforces its own limit.
- Drop the 'platform 280 char limit is a ceiling' paragraph
- Drop 'Must fit in a tweet (under the platform limit)' from rules
---------
Co-authored-by: Claude <claude@anthropic.com>
Reproduces with: any staged file matching the secret regex (e.g. a test
fixture containing an OpenRouter / Anthropic / OpenAI key shape).
The redact step ran:
sed -i -E "s|${SECRET_REGEX}|${REDACT_PLACEHOLDER}|g" "$f"
SECRET_REGEX is itself a `|`-separated alternation of provider patterns
(`(sk-or-v1-...)|(sk-ant-api...)|(...)`). After bash expansion, sed sees
multiple fields where it expected three, and bails with:
sed: -e expression #1, char 67: unknown option to `s'
The export then fails on the VM ("sprite exec failed (exit 1)") even
though the user already approved the redaction in the host prompt.
Fix: switch the sed delimiter to '#'. None of the regex tokens
(provider prefixes, char classes, quantifiers, PEM marker) contain '#',
and neither does the placeholder, so the substitution is unambiguous.
'|' inside the pattern is now correctly interpreted by sed -E as
alternation, which is what we wanted all along.
Adds a regression test asserting the script uses 's#...#...#g' and
not 's|...|...|g'.
Bumps CLI 1.0.35 -> 1.0.36.
* fix(export): redact secrets in-place instead of aborting
Before: any staged file matching the secret regex caused the export
to fail with `{"ok":false,"error":"Possible secrets detected..."}`,
forcing the user to SSH in and clean things up by hand.
After: matched strings are replaced with `***REDACTED-BY-SPAWN-EXPORT***`
via sed -i -E, the file is re-staged, and the export proceeds. The list
of redacted files is included in the success result and surfaced as a
warning on the host CLI:
✓ Exported to https://github.com/alice/my-vm
⚠ Redacted potential secrets in 1 file:
- project/test/brain-sync.test.ts
The regex is unchanged. The redact placeholder is intentionally loud so
a casual reader of the published repo can tell that something was
scrubbed and isn't just blank.
Bumps CLI 1.0.33 -> 1.0.34.
* fix(export): gate redaction behind a host-side confirmation prompt
Previously the VM would silently redact any staged files matching the
secret regex and push the repo — meaning a regex miss (#3381 tracks
broadening) would publish a real secret without the user ever seeing
the file list. That's a fail-open posture on a tool that can push to
public GitHub.
New flow:
- buildExportScript takes allowRedact: boolean.
- First pass (allowRedact=false): VM stages, runs the secret scan,
and on hits writes a needs_confirmation result (hits=[...]) and
exits 0 before any commit or push. No hits → commit + push as
before.
- Host reads the result. If needs_confirmation: print the file list,
explain that the regex has known gaps, and ask "Redact these N files
and continue pushing?" (initialValue false). Decline → exit 0, no
push. Approve → re-run the script with allowRedact=true, which now
actually does the sed + re-stage + commit + push.
Other changes:
- ResultSchema gains the needs_confirmation variant.
- cmdExport factors the runServer + downloadFile + parse cycle into
runPassAndParseResult so the two-pass orchestration is readable.
- Tests: 4 new cases cover the gate scripting (ALLOW_REDACT=0 writes
needs_confirmation and exits 0, ALLOW_REDACT=1 redacts) and the
end-to-end host flow (approve → two passes with ALLOW_REDACT 0→1;
decline → one pass, exit 0; no-secrets happy path → one pass, no
confirm). 38/38 export tests, 2176/0 fail overall.
- CLI 1.0.34 → 1.0.35.
---------
Co-authored-by: Claude <claude@anthropic.com>
The export filter was excluding records with ip === "sprite-console" on
the assumption they weren't reachable. Sprite has its own exec channel
(sprite exec / sprite cp), so they ARE reachable — we just weren't
wiring it up.
- exportableClaudeRecords no longer drops sprite-console records.
- New buildRunnerForRecord() branches on record.cloud:
sprite -> sprite.runSprite / uploadFileSprite / downloadFileSprite
else -> makeSshRunner with the connection's ip/user
- sprite/sprite.ts gains setSpriteName(name) so the export flow can
point the sprite module at an existing sprite by name (the runner
functions key off the module's _state.name, which previously was
only set during the createSprite() flow).
Bumps CLI 1.0.32 -> 1.0.33.
* feat(cli): spawn export — capture a claude session into a github repo
Adds `spawn export [name|id]` as a top-level subcommand. Captures a
running claude spawn into a redistributable github repo whose README
contains the canonical re-spawn command — the symmetric inverse of
`--repo`.
What gets exported:
- `~/project/` working tree (with aggressive .gitignore)
- `~/.claude/` sanitized agent system dir: skills, commands,
hooks, CLAUDE.md, AGENTS.md, settings.json
(with token-shaped fields stripped)
- `spawn.md` generated re-spawn metadata
- `README.md` generated; renders a re-auth checklist on github
The export runs over the existing SshRunner. v1 is claude-only; non-claude
agents return a clear "not yet supported" error. Bumps CLI 1.0.27 -> 1.1.0
because this is a real new surface, not a fix.
Followups (not in v1):
- claude introspects its own session (MCP servers, OAuth providers) and
writes them into spawn.md's setup steps
- local cloud target uses a direct branch (currently routed through SSH)
- in-session `:export` slash command
* fix(cli): use patch bump (1.0.28) to match team versioning cadence
The update-check tests hardcode 1.0.x patch-bump scenarios as test fixtures
(e.g. mocking "latest" as 1.0.99 to verify patch auto-install policy). A
1.1.0 bump made those mocked versions look like downgrades and failed CI.
Realign with the team's recent patch-bump cadence — every recent feature
PR has shipped under 1.0.x.
* feat(export): list picker, claude picks the slug, pre-commit secrets scan
Three changes per review feedback:
1. **Picker.** When the user has multiple exportable claude spawns and
no positional target arg, show a clack `select` listing them. Auto-pick
on a single match. Filter out non-claude / no-connection / deleted /
sprite-console records up front.
2. **Claude decides the repo name.** No more slug prompt. The on-VM script
runs `claude -p` with a name-suggestion prompt asking for a kebab-case
project name (max 40 chars, [a-z0-9-]). Falls back to basename(~/project)
then a timestamp slug if claude is unavailable or returns garbage.
`gh api user --jq .login` provides the username; missing gh auth aborts
with a structured JSON failure the CLI surfaces verbatim.
3. **Pre-commit secrets scan.** After `git add`, scan all staged files for
known API-key shapes — Anthropic (sk-ant-api...), OpenRouter (sk-or-v1-),
OpenAI (sk-proj-), GitHub PAT/OAuth/server (gh[ops]_), AWS (AKIA...),
Hetzner (hcloud_), DigitalOcean (dop_v1_), and PEM private keys. Any
match aborts the export with `{"ok":false,"error":"..."}` to the result
file. The settings.json scrubber now recurses; previously it only
stripped top-level + env keys.
Also expands the .gitignore deny-list with .spawnrc, .bash_history,
.aws/, .config/spawn/, .config/gcloud/, .gnupg/, *.token, *.credentials.
Bumps CLI 1.0.28 -> 1.0.29.
* feat(export): default to public visibility
Exports are share-friendly artifacts — public by default makes the
'spawn link' (the printed re-spawn command) usable by anyone the user
hands it to. Override path stays via options.visibility for callers
that need private.
Bumps CLI 1.0.29 -> 1.0.30.
* feat(export): bake --steps into the spawn link for zero-prompt respawn
The printed spawn link is now:
spawn claude <cloud> --repo <slug> --steps <list>
Source of the steps list:
1. Parse `--steps <value>` (or `--steps=<value>`) out of the original
record.connection.launch_cmd.
2. Fall back to 'github,auto-update,security-scan' when the launch_cmd
doesn't carry it (older spawns, or interactive launches that didn't
pass the flag).
The respawn consumer reads SPAWN_ENABLED_STEPS from --steps and skips
the interactive setup picker entirely, so handing someone the spawn
link is a true zero-choice replay.
Adds parseStepsFromLaunchCmd + resolveSteps helpers, both exported
from the barrel for testability. README template grows a __STEPS__
placeholder; bash sed adds it to the substitution pass.
Bumps CLI 1.0.30 -> 1.0.31.
* fix(export): safe defaults and tighter flag parsing
Review follow-ups to #3377:
- Visibility: default private; interactive "make public?" confirm when
the caller doesn't force one. Prior default-public + one-shot `gh repo
create --push` was a public-leak footgun when the secret regex missed.
- `parseStepsFromLaunchCmd`: anchor both regexes to start-or-whitespace
so `--no-steps=foo` no longer over-matches and returns `foo`.
- `--exclude=.git` on the claude/{skills,commands,hooks} rsync so a
nested git checkout inside a skill doesn't leak its history.
- Replace `record!` / `conn!` non-null assertions with explicit
narrowing — matches the project's type-safety rule (no `as`, no `!`).
- Tests: lock in private default, the .git exclude, and the negative
`--no-steps=` regex case (14 new expects, 32/32 pass).
- Bump CLI to 1.0.32.
---------
Co-authored-by: Claude <claude@anthropic.com>
The sandbox flow on local pulls ghcr.io/openrouterteam/spawn-<agent>:latest,
but the Docker build matrix in .github/workflows/docker.yml didn't include
`pi` and there was no sh/docker/pi.Dockerfile, so the image was never
published. `spawn pi local --beta sandbox` failed with "denied" pulling
spawn-pi:latest.
Adds the Dockerfile (mirrors kilocode/codex: Ubuntu 24.04 + Node 22 +
`npm install -g @mariozechner/pi-coding-agent`) and registers `pi` in
the workflow matrix. The next scheduled or manual run will publish
ghcr.io/openrouterteam/spawn-pi:latest.
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
ensureDocker() on macOS unconditionally shelled out to `brew install
orbstack`, then on failure printed "install OrbStack manually: brew
install orbstack" — circular dead-end for Macs without Homebrew.
Now:
- Probe `which brew`. If present, keep using brew (existing happy path).
- If brew is missing, download the official OrbStack DMG over HTTPS
from orbstack.dev (arch-aware: arm64 vs amd64), mount it via
`hdiutil`, copy OrbStack.app into /Applications, and clear the
quarantine xattr so it launches cleanly.
- If both paths fail, the new error message points users at the
OrbStack download page (not back at brew).
DMG handling uses tryCatch + sequential cleanup (no try/finally, per
the `lint/plugin` rule in this repo). Adds a test for the brew-missing
fallback and updates the existing brew-present test to account for the
new `which brew` probe.
Bumps version to 1.0.24.
Drop `tarball` from the experiment's `test` variant so the A/B
hypothesis isolates the marketplace-image speedup. Tarball install
remains available via `--beta tarball` and `--fast` for power users —
just no longer auto-applied to bucketed users.
Bumps version to 1.0.24.
Adds normalizeRepoUrl() to accept:
- GitHub shorthand (user/repo) — still expanded to https://github.com/...
- HTTP(S) URLs (any host)
- ssh:// and git:// URLs
- SCP-style git@host:path
The clone URL is shellQuote'd before interpolation; values containing
shell metacharacters, whitespace, NUL bytes, or a leading `-` (which
git would parse as a flag) are rejected.
Bumps CLI to 1.0.23.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tarballs are built as root with absolute symlinks (e.g.
~/.local/bin/claude -> /root/.claude/local/claude). When extracted on
a non-root user (Sprite, scp-style installs without sudo), tar's
--transform rewrites file PATHS but not symlink TARGETS, so the
symlinks land in $HOME pointing at /root/... — which doesn't exist.
The agent binary appears installed but is a dangling link, and PATH
lookup fails with "command not found".
After the transform extract succeeds, walk $HOME for symlinks whose
target starts with /root/ and rewrite them to point under $HOME.
Idempotent (ln -snf), no-ops on healthy installs, cheap (one find on
a fresh VM's $HOME).
Bumps CLI to 1.0.24.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
agent-tarballs.yml has been failing nightly since 2026-03-27 and
packer-snapshots.yml since 2026-04-25. Two distinct breakages.
cursor:
capture-agent.sh's allowlist was missing cursor, so the install
step succeeded but the capture step rejected the agent name.
Adds cursor to the allowlist plus its capture paths
(~/.local/bin/ for the `agent` symlink, ~/.local/share/cursor-agent/
for the extracted package, matching what verify.sh and cursor-proxy
already expect).
hermes:
The upstream installer launches an interactive setup wizard after
install, which fails in CI with `/dev/tty: No such device or
address`. Production code already passes `--skip-setup` (see
packages/cli/src/shared/agent-setup.ts:1336); packer/agents.json
was the lone exception. Adds the same flag.
Both pipelines read from packer/agents.json, so this single edit
unblocks both the daily tarball build and the DO marketplace image
build for hermes.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): posthog feature flags + fast_provision experiment
Wires PostHog `/decide` into the CLI so we can A/B-test provisioning
behaviors. First experiment: `fast_provision` — for users who didn't
pass --beta or --fast manually, the `test` variant turns on
`tarball + images` by default. Hypothesis: faster provisioning →
fewer drop-offs in the "VM ready → install completed" leg of the
funnel.
What's added:
- `shared/install-id.ts` — stable per-machine UUID, persisted at
~/.config/spawn/.telemetry-id. Reuses telemetry's existing path
so existing users keep their PostHog identity. Falls back to an
ephemeral UUID on disk-write failure.
- `shared/feature-flags.ts` — hand-rolled POST to PostHog /decide
(no SDK dep). 1.5s timeout, fail-open. On-disk cache at
$SPAWN_HOME/feature-flags-cache.json with 1h TTL so cold starts
don't pay the network cost. SPAWN_FEATURE_FLAGS_DISABLED=1 kill
switch. Captures `$feature_flag_called` exposure events for both
arms so PostHog can compute conversion.
- `shared/telemetry.ts` — moves user-id loading into install-id.ts
so flags and events share the same `distinct_id`.
- `index.ts` — `await initFeatureFlags()` at the top of `main()`,
then applies `fast_provision`'s `test` variant by appending
`tarball,images` to SPAWN_BETA — but only if the user didn't
pass --beta or --fast (those always win, so opt-out is free).
Why tarball+images and not all four (`+parallel,docker`):
clean attribution. The hypothesis is about tarball/image; if we
ship the full --fast bundle we can't tell which feature moved the
metric. Keep --fast as the user-facing power-user knob.
Tests: 14 new (install-id roundtrip + format guard, feature-flags
fetch/timeout/HTTP500/malformed/disabled/idempotent/stale-cache,
exposure-event behavior). Full suite: 2183 pass, same 4 pre-existing
failures as upstream/main.
Bumps CLI to 1.0.23.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): skip feature-flag fetch in pick/feedback fast path; implement real SWR
Two review-fix commits from PR feedback squashed into one:
1. Move `await initFeatureFlags()` below the `spawn pick` and
`spawn feedback` bypass clauses in `main()`. Both commands are called
from bash scripts and must stay fast; neither gates on a flag, so
there's no reason to pay up to 1.5s of network latency on cold cache.
2. Implement real stale-while-revalidate in `shared/feature-flags.ts`.
The prior implementation did a synchronous fetch on stale cache,
which contradicted the docstring and PR description. Now:
- fresh cache (<TTL) → use cache, no network
- stale cache (>=TTL) → use cache immediately, refresh in background
- no cache → await sync fetch (first run only)
Adds `_awaitBackgroundRefreshForTest()` so tests can deterministically
wait for the background refresh before asserting. Updated the existing
"stale cache" test to verify SWR semantics (stale served first, fresh
lands next invocation) and added a "fresh cache does not fetch" test.
All 2127 tests pass; biome clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
spawn <agent> <cloud> --repo user/template
Clones https://github.com/user/template.git to ~/project on the VM,
parses spawn.md (YAML frontmatter), and applies its custom-setup
contract:
- `setup`: oauth (open URL + wait for Enter), cli_auth (run on VM),
api_key (no-echo prompt → /etc/spawn/secrets, sourced from .bashrc),
command (run on VM)
- `mcp_servers`: env values stay as ${NAME} placeholders so secrets
never end up in the template repo. Replay routes through the
existing skills.ts helpers (Claude settings.json, Cursor mcp.json,
Codex config.toml) — no `node -e` injection.
- `setup_commands`: run inside ~/project
When the clone succeeds, the agent launches with `cd ~/project && ...`
so the user lands in their template's working directory. Reconnect via
`spawn last` replays the same launchCmd.
Built-in steps (github auth, auto-update, etc.) stay in the CLI
--steps flag — spawn.md only handles custom setup that Spawn doesn't
know about natively.
Bumps CLI to 1.0.22.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the `issues: opened` trigger and the issue-closing branch from
the gate workflow. PRs from non-collaborators are still auto-closed
(scripted contributions are higher-risk than feedback). Issues stay
open — agents already gate replies on collaborator status, so external
issues simply sit untouched instead of being auto-closed with a stock
message.
Raw `gh issue list` / `gh pr list` in agent prompts bypassed the
bash collaborator gate, letting Claude read non-collaborator issues
(potential prompt injection vector). All prompts now pipe through
a jq filter using the cached collaborator list.
- Added collaborator gate section to _shared-rules.md
- Patched 10 prompt files with inline jq collaborator filter
- High-risk: community-coordinator, security-issue-checker,
qa-record-keeper, security-scanner (read issue bodies)
- Lower-risk: PR list commands in refactor/security prompts
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cloud bundles (hetzner.js, digitalocean.js, etc.) never called
initTelemetry(), so _enabled was false and every captureEvent/trackFunnel
call in orchestrate.ts was a silent no-op. All orchestration funnel
events (funnel_cloud_authed through funnel_handoff) were lost.
Adds initTelemetry(pkg.version) to all 7 cloud entry points so
funnel events actually reach PostHog.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The 1.0.x → 1.1.0 minor bump blocked auto-update for all users since
only patch bumps were auto-installed. Users without SPAWN_AUTO_UPDATE=1
were stuck on 1.0.x and never received the telemetry fix.
Version set to 1.0.20 so existing 1.0.x users see it as a patch bump
and auto-install it. The new update logic then allows future minor bumps
(same major) to auto-install too. Only major bumps (2.0.0+) require
SPAWN_AUTO_UPDATE=1.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the repo goes public, anyone can open issues/PRs. The agent team
must only engage with collaborators — external submissions are invisible.
Shell scripts (refactor, security, qa): source collaborator-gate.sh and
exit 0 if SPAWN_ISSUE author is not a collaborator. The bots never see
the issue — no comment, no triage, no response.
Prompts (discovery issue-responder, refactor community-coordinator,
security issue-checker): check gh api collaborators endpoint before
engaging with any issue.
Collaborator list is cached for 10 minutes to avoid API rate limits.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
* fix(telemetry): send events immediately, persistent user ID, session continuity
Root cause: events were batched (threshold: 10) but orchestration only fires
~8 funnel events. process.exit() kills the process before beforeExit flushes.
Zero real funnel events ever reached PostHog.
Fixes:
- Send each event immediately via fetch (no batching, no lost events)
- Persistent user ID in ~/.config/spawn/.telemetry-id (same across all runs)
- Session ID inherited via SPAWN_TELEMETRY_SESSION env var (parent → child)
- source: "cli" on every event (filter from website data in PostHog)
Removed: _events array, _flushScheduled, flush(), flushSync(), batch logic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(telemetry): remove process.exit(0) so telemetry fetches complete
process.exit(0) was called immediately after main() resolved, aborting
any in-flight fire-and-forget telemetry fetches. This silently dropped
spawn_deleted, funnel, and lifecycle events. Now the process exits
naturally when the event loop drains, giving pending requests time to
complete.
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: A <258483684+la14-1@users.noreply.github.com>
The xeng_approve and xeng_edit_submit handlers marked the reply as
approved in state.db but never called postToX(). Replies were silently
stuck in "ready to post on X" limbo forever.
Both handlers now call postToX(replyText, sourceTweetId) so the reply
goes out as an actual threaded reply on X, and the Slack card shows
the live tweet URL. Mirrors the tweet_approve flow.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
* feat(digitalocean): guided readiness checklist before deploy
Runs evaluateDigitalOceanReadiness after cloud auth and before region/size
selection so users fix billing/SSH/OpenRouter blockers early, with a
checklist UI that rechecks after each fix. Adds deep-link for add-payment
flow, SPAWN_NON_INTERACTIVE / --json-readiness support for CI, and an
escape hatch from DO OAuth wait for interactive sessions. Other clouds
unchanged.
Ported from digitalocean/spawn#2 (Scott Miller @scott). Bumps CLI to 1.1.0.
Refactors the new preflight TTY-gating test to drive process.std*.isTTY
directly with descriptor save/restore and clears stale
~/.config/spawn/digitalocean.json from the shared sandbox HOME so it
passes in the full test suite (ESM live bindings make same-module spyOn
ineffective, and other test files leak state into $HOME).
Co-Authored-By: Scott Miller <scottmiller@digitalocean.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): update-check mock versions for 1.1.0 version bump
Mock "newer" versions (1.0.99) were no longer newer than the current
1.1.0 version, causing all update-check tests to fail. Bumped mock
versions to 99.0.0 for general tests, 1.1.99 for patch, 1.2.0 for
minor, keeping 2.0.0 for major.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(readiness): expand coverage + remove aspirational coverage threshold
- Add evaluateDigitalOceanReadiness tests: auth failure, all-pass,
email/payment/droplet/ssh/openrouter blockers, multi-blocker ordering,
saved key fallback, edge cases (limit=0, count API failure)
- Expand checklistLineStatus tests: all 6 blocker codes, pending-when-
do_auth-blocked, all-blockers-active scenario
- Add READINESS_CHECKLIST_ROWS validation tests
- Expand sortBlockers tests: empty input, dedup, canonical order, single
- Remove coverageThreshold from bunfig.toml — main was already at 82.99%
functions vs 90% threshold (never enforced on push, only on PRs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Scott Miller <scottmiller@digitalocean.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
The reconnect hints shown after provisioning in all 5 cloud providers
(Hetzner, AWS, DigitalOcean, GCP, Sprite) only showed raw SSH/CLI
commands. Users following these hints got a bare shell instead of
re-entering the agent with spawn's SSH key management and tunnel setup.
Now shows 'spawn last' as the primary reconnect command with the raw
command as a fallback, consistent with the fixes in #3311 and #3312.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Per product decision, X/Twitter replies should not include the
'(disclosure: i help build this)' attribution. Reddit disclosures
in growth-prompt.md are unchanged.
Co-authored-by: Claude <claude@anthropic.com>
- growth.sh: guard Phase 0b on X_CLIENT_ID (was checking stale X_API_KEY)
- x-fetch.ts: rewrite to use OAuth 2.0 Bearer tokens from state.db w/ auto-refresh
- Strip em/en dashes from all generated JSON output (tweet, engagement, reddit)
- Tighten prompt language against em dashes in all 3 growth prompts
- SPA system prompt: tell Claude how to post tweets via x-post.ts and query
tweets/candidates tables from state.db for context-aware Twitter conversations
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add x-post.ts script for posting tweets via X API v2 (OAuth 1.0a)
- Wire postToX() into SPA's tweet_approve and tweet_edit_submit handlers
- Approved tweets now post directly to X instead of just marking "ready"
- Slack card updates with link to live tweet on success, error msg on failure
- Add X_API_KEY/SECRET/ACCESS_TOKEN/SECRET env vars to SPA environment
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes#3325
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
* test(skills): add unit tests for getAvailableSkills filtering
getAvailableSkills() had zero test coverage despite being the entry
point for --beta skills flag filtering. Covers: empty manifest, agent
mismatch, correct filtering, isDefault flag, envVars collection.
Agent: test-engineer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test(skills): add coverage for promptSkillSelection, collectSkillEnvVars, installSkills
The Mock Tests CI check was failing because importing skills.ts in
tests caused bun to instrument it for coverage, but only getAvailableSkills
was tested (12.5% function coverage). Added tests for the remaining
exported functions to bring coverage above the 50% threshold.
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>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Two user-facing reconnect hints missed by #3311 still showed
'spawn connect <name>', which is not a registered command. Users
following the hint get 'Unknown agent or cloud: connect'.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
The --fast flag enables all speed optimizations (images, tarballs,
parallel, docker) but was completely invisible in help output. Users
had to read source or manually stack 4 --beta flags.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
All CI green. Rebased from #3321, added Daytona support, resolved conflicts. Security reviewed: no injection vectors — all env var values come from hardcoded config, shell scripts follow existing patterns.
Tracks whether installs came from Reddit, X, or organic by baking a
ref tag into the install command.
Growth bot shares:
curl -fsSL ... | SPAWN_REF=reddit bash
curl -fsSL ... | SPAWN_REF=x bash
install.sh: if SPAWN_REF is set, sanitizes it (alphanumeric + hyphens,
max 32 chars) and writes to ~/.config/spawn/.ref. Only written once —
never overwritten on updates.
index.ts: on startup, reads .ref and sets it as telemetry context via
setTelemetryContext("ref", ref). Every PostHog event (funnel, lifecycle,
errors) now carries ref=reddit or ref=x for attributed installs, or no
ref for organic.
PostHog query: filter any event by ref=reddit to see "how many Reddit-
sourced users made it through the funnel" vs organic.
Bumps 1.0.15 -> 1.0.16.
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
Merges 4 separate runner.runServer() calls (model, sandbox, browser,
channel stubs) into one exec with commands chained by `;`. On Sprite
(container-exec, not persistent SSH), many sequential execs exhaust the
connection and cause "connection closed" / "context deadline exceeded"
on later steps like gateway startup.
Before: 4 execs → 14 "Config overwrite" log lines → flaky connection
After: 1 exec → same config result → stable connection for gateway
Individual commands use `;` not `&&` so a failure in one (e.g. browser
path not found) doesn't skip the rest — these are all non-fatal prefs.
Bumps 1.0.15 -> 1.0.16.
Phase 2+3 of the token-savings plan (follows #3310 which reduced cron
frequency and downgraded team leads to Sonnet).
Extracts duplicated rules into _shared-rules.md (72 lines) and moves
teammate-specific protocols into individual micro-prompts that team
leads read on-demand via Read tool instead of carrying in every turn.
New: _shared-rules.md + teammates/ directory (16 files, 246 lines)
Rewritten: 4 team prompts from 1,199 total lines to 243 (80% reduction)
refactor-team-prompt.md 319 -> 67 (79%)
security-review-all-prompt.md 245 -> 64 (74%)
qa-quality-prompt.md 302 -> 43 (86%)
discovery-team-prompt.md 333 -> 69 (79%)
Also merges shell-scanner + code-scanner into one scanner teammate
for security reviews (4 -> 3 teammates per cycle).
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
* feat(growth): add Phase 0 — daily tweet draft + X mention engagement
Adds a new Phase 0 to the growth agent cycle that runs before Reddit
scanning:
Phase 0a — Tweet Draft (always runs):
- Gathers last 7 days of git commits
- Claude drafts a single ≤280 char tweet about features, fixes, or best
practices
- Posts Block Kit card to #C0ARSCAP4MN with Approve/Edit/Skip buttons
Phase 0b — X Mention Search (runs only if X_API_KEY is set):
- x-fetch.ts searches X API v2 for Spawn/OpenRouter mentions
- Claude scores mentions and drafts engagement replies
- Posts engagement card to #C0ARSCAP4MN with approval buttons
- Gracefully skips when no X credentials are configured
All cards require human approval — nothing is ever auto-posted.
New files:
- tweet-prompt.md: Claude prompt for tweet generation
- x-engage-prompt.md: Claude prompt for X engagement scoring
- x-fetch.ts: X API v2 search client with OAuth 1.0a
Modified files:
- growth.sh: Phase 0a + 0b insertion, cleanup trap updates
- helpers.ts: tweets table schema, TweetRow CRUD, logTweetDecision()
- main.ts: TweetPayloadSchema, XEngagePayloadSchema, postTweetCard(),
postXEngageCard(), 8 new Slack action handlers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update URL format in tweet prompt guidelines
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
* Update URL for Spawn reference in engagement prompt
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
---------
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Claude scoring phase has been timing out at the 600s mark when
processing 500+ Reddit posts. Bump to 1800s (30 min) to give
enough headroom for large post sets.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
"spawn connect" is not a valid top-level CLI command — users following
this guidance after SSH reconnect failure would see "Unknown agent or
cloud: connect". Replace with "spawn last" which correctly reconnects
to the most recent spawn.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Two high-impact, zero-risk changes to get daily agent team spend under $50:
1. Reduce cron frequency:
- Security: */30 → every 4 hours (48→6 cycles/day, 87% reduction)
- Refactor: */15 → every 2 hours (96→12 cycles/day, 87% reduction)
Most cycles find nothing to do (no new PRs/issues). Issue-triggered runs
(on labeled issues) still fire instantly via the `issues` event type,
so response time to real work is unchanged. The trigger-server already
returns 409 when a cycle is in-progress, so high cron frequency was just
idle-polling cost.
2. Downgrade team-lead model from Opus to Sonnet:
- Security: --model sonnet for review_all and scan modes (triage was
already using gemini-3-flash-preview)
- Refactor: --model sonnet
The team lead's job is coordination — spawn teammates, monitor them,
shut down. This is routing, not reasoning. Sonnet handles it fine and
its output tokens are ~5x cheaper than Opus. Teammates (spawned by the
lead) use their own model flags and are unaffected.
Combined effect: ~90% fewer cycles × ~80% cheaper per cycle on the team
lead = estimated 95%+ cost reduction on team-lead tokens alone.
Follow-up PR will trim prompt sizes (Phase 2) and consolidate security
teammates (Phase 3) per the plan, but this Phase 1 closes most of the gap.
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
KNOWN_AGENTS was missing pi and cursor, so `spawn link` could not
auto-detect these agents on remote servers. Also adds a binary-name
mapping for cursor (whose CLI binary is `agent`).
Bump CLI to 1.0.14.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Two bugs from the #3305 rollout:
1. Test pollution: orchestrate.test.ts imports runOrchestration directly
and never calls initTelemetry, but _enabled defaulted to true in the
module so captureEvent happily fired real events at PostHog tagged
agent=testagent. The onboarding funnel filled up with CI fixture data.
2. Funnel started too late: funnel_* events fired inside runOrchestration,
which is only called AFTER the interactive picker completes. Users who
bail at the agent/cloud/setup-options/name prompts were invisible —
yet that's exactly where real drop-off happens.
Fix 1 — telemetry.ts:
- Default _enabled = false. Nothing fires until initTelemetry is
explicitly called. Production (index.ts) calls it; tests that need
telemetry (telemetry.test.ts) call it with BUN_ENV/NODE_ENV cleared.
- Belt-and-suspenders: initTelemetry now short-circuits when
BUN_ENV === "test" || NODE_ENV === "test", so even if future code
calls it from a test context, events stay local.
Fix 2 — picker instrumentation:
New events fired before runOrchestration in every entry path:
spawn_launched { mode: interactive | agent_interactive | direct | headless }
menu_shown / menu_selected / menu_cancelled (only when user has prior spawns)
agent_picker_shown
agent_selected { agent } — also sets telemetry context
cloud_picker_shown
cloud_selected { cloud } — also sets telemetry context
preflight_passed
setup_options_shown
setup_options_selected { step_count }
name_prompt_shown
name_entered
picker_completed
Wired into:
commands/interactive.ts cmdInteractive + cmdAgentInteractive
commands/run.ts cmdRun (direct `spawn <agent> <cloud>`)
cmdRunHeadless (only spawn_launched)
runOrchestration's existing funnel_* events continue to fire unchanged.
The final funnel in PostHog:
spawn_launched → agent_selected → cloud_selected → preflight_passed
→ setup_options_selected → name_entered → picker_completed
→ funnel_started → funnel_cloud_authed → funnel_credentials_ready
→ funnel_vm_ready → funnel_install_completed → funnel_configure_completed
→ funnel_prelaunch_completed → funnel_handoff
Tests:
- telemetry.test.ts: 2 new env-guard tests (BUN_ENV, NODE_ENV), plus
updated beforeEach to clear both env vars so existing tests still
exercise initTelemetry.
- Full suite: 2131/2131 pass, biome 0 errors.
Bumps 1.0.12 -> 1.0.13 (patch — auto-propagates under #3296 policy).
Restructure temp file write-execute-cleanup in performAutoUpdate so
cleanup is unconditionally reached after tryCatch captures any exec
error. Previously, the Windows and Unix paths each had separate
tryCatch+cleanup+rethrow sequences that could diverge under future
edits. Now a single tryCatch wraps the platform-branching exec, with
cleanup always running before any error is re-thrown.
Fixes#3306
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>