Commit graph

2538 commits

Author SHA1 Message Date
A
cdf2bec2d1
fix(sprite): use new --flag forms required by sprite CLI v0.0.1-rc43+ (#3409)
Some checks failed
CLI Release / Build and release CLI (push) Has been cancelled
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
Lint / macOS Compatibility (push) Has been cancelled
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>
2026-05-13 05:58:27 -07:00
A
844808cd7d
feat(ssh): let user pick SSH key when handshake auth keeps failing (#3402)
Some checks failed
CLI Release / Build and release CLI (push) Has been cancelled
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
Lint / macOS Compatibility (push) Has been cancelled
* 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>
2026-05-11 22:16:33 -07:00
Ahmed Abushagur
c098cf1c04
fix(ssh): use spawn-owned key + IdentitiesOnly to fix MaxAuthTries flood (#3401)
Some checks failed
CLI Release / Build and release CLI (push) Has been cancelled
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
Lint / macOS Compatibility (push) Has been cancelled
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>
2026-05-10 11:40:48 -07:00
Ahmed Abushagur
af8ac24506
feat(cli): expand fast_provision experiment to images + docker + sandbox (#3398)
Some checks failed
CLI Release / Build and release CLI (push) Has been cancelled
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
Lint / macOS Compatibility (push) Has been cancelled
* 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>
2026-05-08 22:47:02 -07:00
Ahmed Abushagur
070be392f5
fix(ssh): auto-repair stale pub that does not pair with local priv (#3395)
Some checks failed
CLI Release / Build and release CLI (push) Has been cancelled
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
Lint / macOS Compatibility (push) Has been cancelled
* 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>
2026-05-06 17:01:11 -07:00
Ahmed Abushagur
3ae6554c21
ci(docker): build multi-arch images (amd64 + arm64) (#3392)
Some checks failed
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
Lint / macOS Compatibility (push) Has been cancelled
* 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>
2026-05-05 00:27:08 -07:00
A
53c7ead179
fix(growth): emphasise brevity in tweet prompt (no numbers, no limits) (#3390)
Some checks are pending
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
* 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>
2026-05-04 12:19:48 -07:00
Ahmed Abushagur
8a4334cbe7
fix(export): use '#' as sed delimiter so SECRET_REGEX '|' alternation parses (#3384)
Some checks failed
CLI Release / Build and release CLI (push) Has been cancelled
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
Lint / macOS Compatibility (push) Has been cancelled
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.
2026-05-02 00:42:15 -07:00
Ahmed Abushagur
3152a1911f
fix(export): redact secrets in-place instead of aborting (#3383)
* 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>
2026-05-02 00:30:28 -07:00
Ahmed Abushagur
46bbeea751
fix(export): support sprite spawns (drop sprite-console exclusion) (#3382)
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.
2026-05-02 00:13:47 -07:00
Ahmed Abushagur
3e99bdd604
feat(cli): spawn export — capture a claude session into a github repo (#3377)
* 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>
2026-05-01 23:45:50 -07:00
Ahmed Abushagur
83ecccf4ad
fix(docker): publish spawn-pi image so --beta sandbox works for pi (#3373)
Some checks failed
CLI Release / Build and release CLI (push) Has been cancelled
Build Docker Images / build (claude) (push) Has been cancelled
Build Docker Images / build (codex) (push) Has been cancelled
Build Docker Images / build (cursor) (push) Has been cancelled
Build Docker Images / build (hermes) (push) Has been cancelled
Build Docker Images / build (junie) (push) Has been cancelled
Build Docker Images / build (kilocode) (push) Has been cancelled
Build Docker Images / build (openclaw) (push) Has been cancelled
Build Docker Images / build (opencode) (push) Has been cancelled
Build Docker Images / build (pi) (push) Has been cancelled
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
Lint / macOS Compatibility (push) Has been cancelled
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>
2026-04-29 15:43:42 -07:00
Ahmed Abushagur
1ac0171daf
fix(local): install OrbStack from DMG when Homebrew is missing (#3372)
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.
2026-04-29 15:40:21 -07:00
Ahmed Abushagur
55990700b6
feat(cli): narrow fast_provision experiment to images-only (#3371)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
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.
2026-04-29 09:41:54 -07:00
Ahmed Abushagur
c907359fb1
feat(cli): --repo accepts any git URL, not just GitHub slugs (#3364)
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>
2026-04-29 09:28:37 -07:00
Ahmed Abushagur
762508e9eb
fix(tarball): rewrite absolute /root symlinks after non-root extract (#3368)
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>
2026-04-29 09:25:53 -07:00
Ahmed Abushagur
4e87523c4f
fix(packer): repair cursor tarball + hermes interactive install (#3367)
Some checks failed
CLI Release / Build and release CLI (push) Has been cancelled
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
Lint / macOS Compatibility (push) Has been cancelled
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>
2026-04-27 18:31:40 -07:00
Ahmed Abushagur
f7652de45b
feat(cli): posthog feature flags + fast_provision experiment (#3366)
* 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>
2026-04-27 17:50:56 -07:00
Ahmed Abushagur
3e6c8768d1
feat(cli): --repo flag clones a template repo and applies spawn.md (#3360)
Some checks failed
Lint / macOS Compatibility (push) Has been cancelled
CLI Release / Build and release CLI (push) Has been cancelled
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
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>
2026-04-24 23:42:23 -07:00
Ahmed Abushagur
f0e93a508d
ci(gate): stop auto-closing issues from non-collaborators (#3359)
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.
2026-04-24 23:26:47 -07:00
Ahmed Abushagur
b917e3f280
fix(security): add collaborator filter to all agent prompts (#3351)
Some checks are pending
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
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>
2026-04-23 23:46:13 -07:00
Ahmed Abushagur
71c61ed7e7
fix(telemetry): init telemetry in cloud bundle entry points (#3346)
Some checks failed
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
CLI Release / Build and release CLI (push) Has been cancelled
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>
2026-04-23 18:49:21 -07:00
Ahmed Abushagur
75a22f2d06
fix(update): auto-install minor bumps, version 1.0.20 for patch delivery (#3342)
Some checks failed
CLI Release / Build and release CLI (push) Has been cancelled
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
Lint / macOS Compatibility (push) Has been cancelled
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>
2026-04-22 14:07:41 -07:00
Ahmed Abushagur
3824f6d6c8
feat(oss): add collaborator gate to all agent team bots (#3333)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
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>
2026-04-22 00:32:07 -07:00
Ahmed Abushagur
cd3537c051
fix(telemetry): send events immediately — no more lost funnel data (#3339)
* 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>
2026-04-22 00:28:33 -07:00
A
c1cfd7ef2d
fix(growth): x engagement approve now actually posts the reply (#3340)
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>
2026-04-22 00:16:58 -07:00
A
37d144dfd6
feat(digitalocean): guided readiness before deploy (#3336)
* 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>
2026-04-21 21:55:01 -07:00
A
ede351e2b4
fix(ux): add 'spawn last' to reconnect hints in cloud modules (#3337)
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>
2026-04-21 21:18:38 -07:00
A
de2883ee2b
chore(x-engage): drop disclosure line from X replies (#3335)
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>
2026-04-21 21:15:34 -07:00
A
61551928dd
test(guidance-data): add unit tests for buildDashboardHint (#3330)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-20 23:22:38 -07:00
A
98599d77b2
fix(growth): simpler tweets, shorter chill replies, ban jargon (#3332)
Tweet prompt: target non-technical devs. Ban jargon (ps aux, OAuth,
SigV4, TLS, CORS, RBAC). Prefer feature commits over security/infra.
Skip cycle if change cannot be explained in plain English.

X engagement prompt: demand short chill replies (5-25 words, under
120 chars ideal). Add vibe examples. Kill corporate pitch style.

Reddit prompt: tighten to 1-3 sentences max, ban feature lists.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 17:43:25 -07:00
A
fe075190ea
fix(growth): migrate Phase 0b to OAuth 2.0, block em dashes, wire SPA tweet posting (#3331)
Some checks are pending
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
- 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>
2026-04-20 17:21:34 -07:00
A
2306fb1914
feat(growth): migrate X posting from OAuth 1.0a to OAuth 2.0 PKCE (#3329)
Some checks are pending
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
- Replace OAuth 1.0a signing with OAuth 2.0 Bearer token auth
- Add x-auth.ts: one-time PKCE authorization flow that saves tokens to state.db
- Add auto-refresh: tokens refresh transparently when expired (2hr TTL)
- Add x_tokens table to state.db schema (via helpers.ts openDb)
- Env vars simplified: X_CLIENT_ID + X_CLIENT_SECRET (no more 4 keys)
- x-post.ts rewritten to read tokens from DB, refresh if needed

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:35:04 -07:00
A
95da999efb
feat(growth): add X/Twitter auto-posting on tweet approval (#3328)
- 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>
2026-04-20 00:02:30 -07:00
A
165601bb46
fix(assets): replace t3code logo with official T3 Code icon (#3326)
Some checks failed
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
CLI Release / Build and release CLI (push) Has been cancelled
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>
2026-04-19 01:37:08 -07:00
A
8640cf78bc
test(skills): add unit tests for getAvailableSkills filtering (#3324)
* 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>
2026-04-19 01:35:11 -07:00
A
97c073247a
fix(ux): replace stale 'spawn connect' hints with 'spawn last' (#3312)
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>
2026-04-19 01:31:30 -07:00
A
cfd428d213
fix(ux): document --fast flag in help text (#3323)
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>
2026-04-19 01:29:48 -07:00
A
57174a0f15
feat(agent): add T3 Code agent (web GUI for Claude/Codex) (#3322)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
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.
2026-04-18 01:14:37 -07:00
Ahmed Abushagur
51e36d2154
feat(telemetry): install referrer attribution for growth channels (#3318)
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>
2026-04-18 00:59:22 -07:00
Ahmed Abushagur
dc4fb59f67
fix(openclaw): batch config set calls into single exec (#3319)
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.
2026-04-18 00:56:37 -07:00
Ahmed Abushagur
acd3e2339e
fix(agent-team): trim prompts 80% — shared rules + teammate micro-prompts (#3315)
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>
2026-04-18 00:52:05 -07:00
A
e0f37f0753
feat(growth): add Phase 0 — daily tweet draft + X mention engagement (#3316)
Some checks failed
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
Lint / macOS Compatibility (push) Has been cancelled
* 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>
2026-04-16 17:40:13 -07:00
A
21fd1949d5
fix(growth): increase hard timeout from 600s to 1800s (#3314)
Some checks are pending
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
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>
2026-04-16 12:28:45 -07:00
A
b290a3bb10
fix(type-safety): replace manual typeguards with valibot schemas in SPA and reddit-fetch (#3313)
Replace all `as Record<string, unknown>` casts and manual multi-level
typeguard chains with proper valibot schema validation in:

- main.ts: Reddit token response, error parsing, jQuery comment URL extraction
- reddit-fetch.ts: Reddit auth, listing extraction, user comment fetching

Adds RedditTokenSchema, RedditListingSchema, RedditChildDataSchema, and
RedditCommentDataSchema with v.safeParse() for all external API data.

Closes #3200

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:17:15 -07:00
A
513d3448d4
fix(ux): correct reconnect command suggestion from "spawn connect" to "spawn last" (#3311)
Some checks failed
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
CLI Release / Build and release CLI (push) Has been cancelled
"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>
2026-04-16 15:39:36 +07:00
Ahmed Abushagur
21eb1bf6e0
fix(agent-team): cut token spend — reduce cron frequency + downgrade team-lead to Sonnet (#3310)
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>
2026-04-16 00:06:56 -07:00
A
84331173fd
fix(link): add pi and cursor to agent auto-detection (#3309)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
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>
2026-04-16 07:44:20 +07:00
Ahmed Abushagur
a179fdbbab
fix(telemetry): opt-in default + picker funnel events (#3308)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
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).
2026-04-15 15:43:30 +07:00
A
d1d51fb06d
fix(security): guarantee temp file cleanup in performAutoUpdate (#3307)
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>
2026-04-15 12:48:12 +07:00