Skip to content

Rebase cs serve HTTP+SSE API and OTel tracing onto upstream 1.0.20 - #2

Draft
BippleDops wants to merge 2 commits into
cursor/sync-upstream-65a7from
cursor/paperclip-cs-serve-rebased-65a7
Draft

BippleDops wants to merge 2 commits into
cursor/sync-upstream-65a7from
cursor/paperclip-cs-serve-rebased-65a7

Conversation

@BippleDops

Copy link
Copy Markdown
Owner

Summary

The fork's only owner work lives on origin/paperclip (2 commits from 2026-04-18, the substrate for paperclip Phase 5 and upstream draft PR smtg-ai#283). That branch is based on the April upstream. This PR re-applies those two commits on top of current upstream main (1.0.20) so they stay mergeable:

  • feat: cs serve — optional HTTP + SSE API for headless use
  • feat: optional OpenTelemetry tracing for cs serve

Conflicts were limited to go.mod/go.sum (indirect deps); resolved by taking upstream's module files and running go mod tidy, which re-added github.com/google/uuid and the go.opentelemetry.io/otel/* modules the new code needs.

This PR is based on cursor/sync-upstream-65a7 (#1) so it shows only the two owner commits; GitHub will retarget it to main once #1 merges.

Verification

  • go build ./... — pass
  • go vet ./... — pass
  • go test ./... — pass

Notes

Before merging

Open in Web Open in Cursor 

paperclip-fork and others added 2 commits September 6, 2026 14:59
Adds a `cs serve` subcommand that exposes claude-squad's session
lifecycle as an HTTP+SSE API, so an external orchestrator can drive
it without the TUI. Fully additive: the TUI, config format, on-disk
state, autoyes daemon, and every existing command behave byte-for-
byte the same as before.

claude-squad is excellent at the tmux-per-agent + worktree-per-agent
isolation model, but today the only way in is the Bubble Tea TUI.
External orchestrators that want to drive sessions from another
process have to scrape `tmux capture-pane` output and parse the
undocumented `~/.claude-squad/state.json` — fragile, and it makes
per-session tracing essentially impossible.

This PR adds the programmatic surface so external tools can reuse
claude-squad's session model without diverging from it. The first
real consumer is Paperclip, a multi-agent orchestration GUI; see
FORK.md for context.

Only the HTTP surface in this commit. A follow-up commit in the
same series adds OpenTelemetry instrumentation that uses the new
`eventBus` to emit `cs.instance.*` spans — reviewable independently.

Changes (all additive):

- `FORK.md` at the repo root explaining the fork's rationale.
- `server/` new package:
  - `server.go` — net/http Server, bearer-token middleware, graceful
    shutdown on SIGTERM/SIGINT.
  - `handlers.go` — route handlers for `/v1/health`,
    `/v1/instances` (GET list, POST create), and
    `/v1/instances/{id}{,/pane,/input,/pause,/resume,/diff}` plus
    `/v1/events` (SSE).
  - `types.go` — JSON DTOs and the `Event` schema with its lifecycle
    type constants.
  - `store.go` — concurrency-safe in-memory registry of active
    sessions, keyed by UUID. Not persisted to disk on purpose —
    the orchestrator is the source of truth for what should exist.
  - `events.go` — small pub/sub ring for the SSE endpoint with a
    replay buffer so reconnecting consumers can catch up via the
    `?since=N` query parameter.
- `main.go` gains the `serve` cobra subcommand, flag-configurable
  bind address (`--addr`, default `:3200`) and bearer token
  (`--auth-token`, or `CS_AUTH_TOKEN`). Empty token disables auth
  for local dev only.
- `go.mod`/`go.sum` gain `github.com/google/uuid v1.6.0`.

| Method | Path | Purpose |
|---|---|---|
| GET  | `/v1/health` | liveness (always unauth'd) |
| GET  | `/v1/events?since=<seq>` | SSE stream of lifecycle events with optional replay |
| POST | `/v1/instances` | create a session; body accepts `title`, `program`, `branch`, `autoYes`, `prompt`, `workspaceBasePath` |
| GET  | `/v1/instances` | list all active sessions |
| GET  | `/v1/instances/{id}` | get one session |
| DELETE | `/v1/instances/{id}` | kill + cleanup |
| GET  | `/v1/instances/{id}/pane?full=&ansi=` | tmux pane content |
| POST | `/v1/instances/{id}/input` | send text/keystrokes; body accepts `prompt`, `keys`, or `tapEnter` |
| POST | `/v1/instances/{id}/pause` | pause (remove worktree, keep branch) |
| POST | `/v1/instances/{id}/resume` | resume a paused session |
| GET  | `/v1/instances/{id}/diff` | compute + return the session's git diff |

SSE events emitted: `instance.created`, `instance.started`,
`instance.status_changed`, `instance.diff_updated`,
`instance.pane_appended`, `instance.prompt_detected`,
`instance.prompt_auto_accepted`, `instance.paused`,
`instance.resumed`, `instance.killed`, `instance.exited`.

All events carry a monotonic `seq` so clients can reconnect with
`?since=N` and not miss anything in the replay window (512 events).

Bearer token in `Authorization: Bearer <token>`, configured via
`--auth-token` or `CS_AUTH_TOKEN`. `/v1/health` is always public.
Empty token disables auth entirely — intended for local dev.

Binary build is green. Manually verified against `cs serve`:

- `/v1/health` returns `{"ok":true,"service":"claude-squad-server","version":"1.0.17"}`
- `/v1/instances` returns `[]` on empty server
- `/v1/instances/nonexistent` returns 404 with a typed error body
- Missing bearer → 401 `missing bearer token`
- Wrong bearer → 401 `invalid bearer token`
- Correct bearer → 200

Create + kill + pane + SSE flows are exercised by the consumer (see
Paperclip harness). Not repeated here because driving a real `claude`
subprocess requires an API key and is more appropriately tested
end-to-end in the consumer's CI.

This is a FORK of upstream smtg-ai/claude-squad prepared for an
early-draft upstream PR.

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

Co-authored-by: jondsussman <jondsussman@gmail.com>
Adds OTLP/HTTP trace export with W3C TRACEPARENT propagation to the
agent subprocess, activated when Langfuse (or any OTLP collector)
credentials are in env. Silent no-op otherwise — upstream behavior
preserved.

This commit is reviewable independently of the HTTP API commit. It
adds three small pieces:

1. **`otel/` package** (net-new) — tracer provider bootstrap and
   subprocess env builder.
2. **A per-instance `SpawnEnv` field** on `session.Instance` and
   `tmux.TmuxSession` (5-line additions to each). When set, the env
   is merged into the `tmux new-session ... <program>` command so
   the agent inherits `TRACEPARENT` + `OTEL_*` vars. When not set
   (every upstream caller, always), the behavior is identical to
   before.
3. **Span emission in the server handlers** — `cs.instance.create`,
   `cs.instance.pause`, `cs.instance.resume`, `cs.instance.kill`.
   Each parents under the inbound W3C trace context if any.

Service name: `claude-squad-server` (distinct from the orchestrator's
and the agent's service names so downstream observability tooling can
filter each tier cleanly).

Same pattern the downstream consumer (Paperclip) uses:

- `LANGFUSE_PUBLIC_KEY`
- `LANGFUSE_SECRET_KEY`
- `OTEL_EXPORTER_OTLP_ENDPOINT` (or `LANGFUSE_HOST`; default
  `http://localhost:3050/api/public/otel`)

When the two keys are absent, `cs serve` boots without telemetry. No
spans, no exporter, no overhead.

`cs serve` extracts W3C trace context from inbound `traceparent`
HTTP headers so an orchestrator like Paperclip that creates a root
`paperclip.run` span can pass its id forward and see
`paperclip.run → cs.instance.create → claude_code.interaction` as
one connected trace in Langfuse.

The Claude Code CLI (v1+) honors `TRACEPARENT` from its environment
and parents its own spans accordingly. No Claude Code code changes
needed — just env injection, which this commit plumbs through.

Langfuse does NOT support OTLP/gRPC. The exporter is locked to
`http/protobuf`. The subprocess env builder hard-codes
`OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf` for the agent so it
doesn't drift.

- `otel/tracer.go` — BatchSpanProcessor + otlptracehttp exporter.
  Resource carries `service.name=claude-squad-server` +
  `service.version`. 1s batch interval so short-lived sessions don't
  drop spans on exit.
- `otel/propagation.go` — `SubprocessEnv()` returns the KEY=VALUE
  slice to hand to `exec.Cmd.Env` (via the Instance.SetSpawnEnv
  plumbing below). `TraceparentFromContext()` is the W3C extract
  for header/env injection.
- `server/server.go` — `Options` gains `OtelCfg otelpkg.Config`;
  Server holds a `tracer` field initialized from the global
  provider (no-op when none is registered).
- `server/handlers.go` — `cs.instance.create` span in
  `handleCreate`; `quickSpan` helper reused by pause/resume/kill.
  `handleCreate` extracts the inbound W3C traceparent and calls
  `inst.SetSpawnEnv(otelpkg.SubprocessEnv(ctx, cfg, "cs-agent-<short>"))`.
- `session/instance.go` — one new field (`spawnEnv`) + one new
  method (`SetSpawnEnv`). `Start()` forwards to the tmux session's
  new setter before spawning.
- `session/tmux/tmux.go` — one new field (`spawnEnv`) + one new
  method (`SetSpawnEnv`). `Start()` honors it when assembling the
  `exec.Cmd` env; when empty, preserves upstream inherit-all
  behavior.
- `main.go` — `serveCmd.RunE` calls `otelpkg.ConfigFromEnv()` +
  `otelpkg.Init()` before starting the server, plus a deferred
  flush.
- `go.mod`/`go.sum` — `go.opentelemetry.io/otel` and the OTLP HTTP
  exporter.

Build green. Live boot with Langfuse keys produces the log line:

    otel tracing enabled → localhost:3050/api/public/otel/v1/traces (service=claude-squad-server)

Without keys, the line is absent and the server runs exactly as
commit 1. The same smoke-suite from commit 1 still passes.

This commit is additive and gated — if OTEL doesn't fit your
project's direction, commit 1 stands on its own without it. The
only sub-commit-1 changes are the 5-line `SetSpawnEnv` hooks in
session/tmux and session/instance, which have no effect unless a
caller actively sets them.

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

Co-authored-by: jondsussman <jondsussman@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant