Go CLI tool for orchestrating Claude Code loops on Sprites.
Wisp automates RFC implementation using autonomous Claude Code loops in isolated Sprites. Developer provides an RFC, wisp generates tasks, runs Claude until completion or blockage, produces a PR.
cmd/wisp/ # main entry point
internal/
cli/ # command implementations
config/ # configuration loading
session/ # session management
sprite/ # Sprite client wrapper
state/ # state file handling
tui/ # terminal UI
pkg/ # public API (if any)
- Go 1.21+ with modules
gofmtandgo vetmust pass- Follow Effective Go
- Errors are values — handle explicitly, no panics for recoverable errors
- Accept interfaces, return structs
- Keep packages focused and minimal
Use cobra for command structure:
var startCmd = &cobra.Command{
Use: "start",
Short: "Start a new session",
RunE: runStart,
}- Commands in
internal/cli/ - One file per command
RunEoverRun(return errors, don't os.Exit)- Flags defined in
init(), validated inRunE
// wrap errors with context
return fmt.Errorf("failed to create sprite: %w", err)
// check specific errors
if errors.Is(err, os.ErrNotExist) { ... }
// custom error types for domain errors
type SessionNotFoundError struct { Branch string }
func (e SessionNotFoundError) Error() string { ... }- Use
viperfor config file loading - Environment variables override file values
- Validate early, fail fast
type Config struct {
Limits LimitsConfig `yaml:"limits"`
}
func Load(path string) (*Config, error) { ... }
func (c *Config) Validate() error { ... }When you build:
- always use
go install ./cmd/wisp - never use
go build -o wisp ./cmd/wisp
- Table-driven tests
testify/assertfor assertionst.Parallel()where safe- Test files alongside source:
foo.go,foo_test.go
func TestParseState(t *testing.T) {
tests := []struct {
name string
input []byte
want *State
wantErr bool
}{
{"valid", []byte(`{"status":"CONTINUE"}`), &State{Status: "CONTINUE"}, false},
{"invalid json", []byte(`{`), nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseState(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}Integration tests in integration_test.go with build tag:
//go:build integration
func TestFullWorkflow(t *testing.T) { ... }Run with: go test -tags=integration ./...
IMPORTANT: When you run integration tests that could hang, make sure you use tight timeouts.
github.com/spf13/cobra # CLI framework
github.com/spf13/viper # configuration
github.com/superfly/sprites-go # Sprites SDK
golang.org/x/term # terminal raw mode
github.com/stretchr/testify # test assertions
For TUI, use raw mode via x/term:
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil { return err }
defer term.Restore(int(os.Stdin.Fd()), oldState)ANSI escapes for display:
\033[2J— clear screen\033[H— cursor home\033[K— clear line\a— bell
// state files
type State struct {
Status string `json:"status"`
Summary string `json:"summary"`
Question string `json:"question,omitempty"`
Error string `json:"error,omitempty"`
}
// marshal with indent for human-readable files
json.MarshalIndent(state, "", " ")Pass context.Context through call chain:
func (s *Session) Run(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := s.runIteration(ctx); err != nil {
return err
}
}
}
}client := sprites.New(token)
sprite, err := client.CreateSprite(ctx, name, nil)
// execute command
cmd := sprite.Command("claude", "-p", prompt)
cmd.Dir = workDir
cmd.Env = env
output, err := cmd.CombinedOutput()
// file transfer via pipes
cmd := sprite.Command("bash", "-c", "cat > "+path)
stdin, _ := cmd.StdinPipe()
cmd.Start()
stdin.Write(content)
stdin.Close()
cmd.Wait()go build -o wisp ./cmd/wisp
go test ./...
go test -tags=integration ./...- Claude Code CLI
refs/claude-code-cli.md - Sprites Go client
refs/sprites-do.md(also ../../sandbox/sprites-go)