Documentation
¶
Overview ¶
Package cldpd is an async pod lifecycle library for Claude Code agent teams.
Each zoobzio repository carries its own agent workflows, standing orders, and skills. cldpd spawns pods — Docker containers running Claude Code — pointed at GitHub issues, and returns a Session handle for non-blocking lifecycle management.
Basic usage ¶
d := cldpd.NewDispatcher(podsDir, &cldpd.DockerRunner{})
session, err := d.Start(ctx, "myrepo", "https://github.com/org/repo/issues/42")
if err != nil {
log.Fatal(err)
}
for event := range session.Events() {
if event.Type == cldpd.EventOutput {
fmt.Println(event.Data)
}
}
code, err := session.Wait()
Session lifecycle ¶
Dispatcher.Start builds the pod's Docker image synchronously, then returns a *Session immediately. The container runs in the background. The Session emits typed events on its Events() channel:
BuildStarted → BuildComplete → ContainerStarted → Output* → ContainerExited
Call session.Stop(ctx) for graceful shutdown (SIGTERM with timeout, then SIGKILL). Call session.Wait() to block until the container exits.
Pod definitions ¶
Pods are directories under ~/.cldpd/pods/<name>/ containing:
- Dockerfile (required) — the container image definition
- pod.json (optional) — configuration: env, mounts, image tag, etc.
- template.md (optional) — standing orders prepended to the prompt on Start
See PodConfig for available configuration fields including credential passthrough (InheritEnv, Mounts). Mount source paths beginning with ~/ are expanded to the user's home directory.
Constraints ¶
cldpd uses the Docker CLI via os/exec and has no external dependencies beyond the Go standard library.
Index ¶
- Variables
- func DefaultPodsDir() (string, error)
- type Dispatcher
- type DockerRunner
- func (d *DockerRunner) Build(ctx context.Context, tag string, dir string, buildArgs map[string]string) error
- func (d *DockerRunner) Exec(ctx context.Context, container string, cmd []string, stdout io.Writer) (int, error)
- func (d *DockerRunner) Preflight(ctx context.Context) error
- func (d *DockerRunner) Run(ctx context.Context, opts RunOptions, stdout io.Writer) (int, error)
- func (d *DockerRunner) Stop(ctx context.Context, container string, timeout time.Duration) error
- type Event
- type EventType
- type Mount
- type Pod
- type PodConfig
- type RunOptions
- type Runner
- type Session
Constants ¶
This section is empty.
Variables ¶
var ErrBuildFailed = errors.New("image build failed")
ErrBuildFailed is returned when the Docker image build exits with a non-zero status.
var ErrContainerFailed = errors.New("container exited with error")
ErrContainerFailed is returned when a container exits with a non-zero status.
ErrDockerUnavailable is returned when the Docker daemon cannot be reached.
var ErrInvalidPod = errors.New("invalid pod: Dockerfile not found")
ErrInvalidPod is returned when a pod directory exists but contains no Dockerfile.
var ErrPodNotFound = errors.New("pod not found")
ErrPodNotFound is returned when a pod directory does not exist.
var ErrSessionNotFound = errors.New("no running session for pod")
ErrSessionNotFound is returned when no running session exists for the given pod name.
var ErrStopFailed = errors.New("container stop failed")
ErrStopFailed is returned when docker stop exits with a non-zero status.
Functions ¶
func DefaultPodsDir ¶
DefaultPodsDir returns the conventional pods directory: ~/.cldpd/pods/.
Types ¶
type Dispatcher ¶
type Dispatcher struct {
// contains filtered or unexported fields
}
Dispatcher coordinates pod discovery, image building, and container lifecycle. Use NewDispatcher to create one.
Dispatcher is stateless — it does not track running sessions. Each returned *Session is self-contained. The caller is responsible for calling Stop or Wait.
func NewDispatcher ¶
func NewDispatcher(podsDir string, runner Runner) *Dispatcher
NewDispatcher returns a Dispatcher that discovers pods from podsDir and executes Docker operations via runner.
func (*Dispatcher) Resume ¶
Resume returns a *Session wrapping a follow-up exec into an already-running container for the named pod. Resume does not build an image.
The Session emits events in the following order:
ContainerStarted → Output* → ContainerExited
Returns ErrSessionNotFound if no container named cldpd-<podName> is running. The caller is responsible for calling session.Stop or session.Wait.
func (*Dispatcher) Start ¶
Start builds the pod's Docker image synchronously, then returns a *Session representing the running container. The image build completes before Start returns — if the build fails, Start returns an error and no Session is created.
If the pod's template.md is non-empty, its contents are prepended to the prompt passed to Claude Code: template + "\n\n" + "Work on this GitHub issue: " + issueURL. When template.md is absent, the prompt is the issue URL directive alone.
The Session emits events in the following order:
BuildStarted → BuildComplete → ContainerStarted → Output* → ContainerExited
On build failure: BuildStarted → Error (no Session returned). On runtime failure: events up to ContainerStarted, then Output*, then Error.
The caller is responsible for calling session.Stop or session.Wait.
type DockerRunner ¶
type DockerRunner struct{}
DockerRunner implements Runner using the Docker CLI via os/exec.
func (*DockerRunner) Build ¶
func (d *DockerRunner) Build(ctx context.Context, tag string, dir string, buildArgs map[string]string) error
Build builds a Docker image tagged with tag from the Dockerfile in dir.
func (*DockerRunner) Exec ¶
func (d *DockerRunner) Exec(ctx context.Context, container string, cmd []string, stdout io.Writer) (int, error)
Exec runs a command in an already-running container and streams its stdout. Returns ErrSessionNotFound if the container does not exist or is not running. For all other non-zero exits the exit code is returned with a nil error.
func (*DockerRunner) Preflight ¶
func (d *DockerRunner) Preflight(ctx context.Context) error
Preflight checks that the Docker daemon is reachable by running docker info. Returns ErrDockerUnavailable if the daemon cannot be contacted.
func (*DockerRunner) Run ¶
func (d *DockerRunner) Run(ctx context.Context, opts RunOptions, stdout io.Writer) (int, error)
Run starts a container with the given options, streams stdout, and blocks until the container exits. Returns the container's exit code.
type Event ¶
Event is a lifecycle or output event emitted by a Session.
Temporal ordering guarantees:
- Successful start: BuildStarted → BuildComplete → ContainerStarted → Output* → ContainerExited
- Build failure: BuildStarted → Error
- Runtime failure: BuildStarted → BuildComplete → ContainerStarted → Output* → Error
After the terminal event (ContainerExited or Error), the channel is closed.
type EventType ¶
type EventType int
EventType identifies the kind of event emitted by a Session.
const ( // EventBuildStarted is emitted when the Docker image build begins. // Data contains the image tag. EventBuildStarted EventType = iota // EventBuildComplete is emitted when the Docker image build succeeds. // Data contains the image tag. EventBuildComplete // EventContainerStarted is emitted when the container begins running. // Data contains the container name. EventContainerStarted // EventOutput is emitted for each line of container stdout. // Data contains the line content. EventOutput // EventContainerExited is emitted when the container exits normally. // Code contains the container's exit code. EventContainerExited // EventError is emitted when a fatal error terminates the session. // Data contains the error message. EventError )
type Pod ¶
type Pod struct {
Name string // directory name, used as the pod identifier
Dir string // absolute path to the pod directory
Dockerfile string // absolute path to the Dockerfile within Dir
Template string // contents of template.md; empty string if absent
Config PodConfig // parsed from pod.json; zero-value if pod.json is absent
}
Pod is a discovered pod definition. It holds the pod name, the absolute path to its directory, the parsed configuration, the absolute path to its Dockerfile, and the optional template contents loaded from template.md.
func DiscoverAll ¶
DiscoverAll loads all valid pods from the given pods directory. Entries that are not directories, or directories without a Dockerfile, are skipped. The returned slice is sorted by pod name.
func DiscoverPod ¶
DiscoverPod loads a single pod by name from the given pods directory. It returns ErrPodNotFound if the pod directory does not exist, and ErrInvalidPod if the directory exists but contains no Dockerfile. If pod.json is absent the pod is returned with a zero-value PodConfig. If pod.json is present but malformed, an error is returned. Mount source paths beginning with ~ or ~/ are expanded to the user's home directory. ~user expansion is not supported. If template.md is absent, Pod.Template is an empty string. If template.md is present but cannot be read, an error is returned.
type PodConfig ¶
type PodConfig struct {
Env map[string]string `json:"env"` // environment variables passed to the container
BuildArgs map[string]string `json:"buildArgs"` // --build-arg values passed to docker build
Image string `json:"image"` // Docker image tag; defaults to cldpd-<name> if empty
Workdir string `json:"workdir"` // working directory inside the container
InheritEnv []string `json:"inheritEnv"` // host env var names to forward to the container
Mounts []Mount `json:"mounts"` // bind mounts to pass to the container
}
PodConfig holds the optional configuration parsed from a pod's pod.json file. All fields are optional; absent values use zero values (empty string, nil map, nil slice).
type RunOptions ¶
type RunOptions struct {
Env map[string]string // environment variables (-e K=V)
Image string // Docker image to run
Name string // container name (--name); used for deterministic resume
Workdir string // working directory inside the container (-w)
Cmd []string // command and arguments to run inside the container
InheritEnv []string // host env var names to forward as -e NAME=VALUE
Mounts []Mount // bind mounts (-v source:target[:ro])
Remove bool // remove the container after it exits (--rm)
}
RunOptions configures a docker run invocation.
type Runner ¶
type Runner interface {
// Preflight checks that the Docker daemon is reachable.
// Returns ErrDockerUnavailable if the daemon cannot be contacted.
Preflight(ctx context.Context) error
// Build builds a Docker image tagged with tag from the Dockerfile in dir.
// buildArgs are passed as --build-arg K=V flags.
// Returns ErrBuildFailed if the build exits with a non-zero status.
Build(ctx context.Context, tag string, dir string, buildArgs map[string]string) error
// Run starts a container with the given options, streams its stdout to the
// provided writer, blocks until the container exits, and returns the exit code.
// A non-zero exit code is not itself an error — the caller interprets it.
Run(ctx context.Context, opts RunOptions, stdout io.Writer) (int, error)
// Exec runs a command in an already-running container, streams its stdout
// to the provided writer, blocks until the command exits, and returns the exit code.
// Returns ErrSessionNotFound if the container is not running.
Exec(ctx context.Context, container string, cmd []string, stdout io.Writer) (int, error)
// Stop sends SIGTERM to the named container via docker stop, waits up to timeout,
// then SIGKILL if needed. Returns ErrStopFailed on non-zero exit from docker stop.
// If the container is not found (already removed), Stop returns nil.
Stop(ctx context.Context, container string, timeout time.Duration) error
}
Runner is the interface over Docker CLI operations. All methods block until the operation completes and stream output to the provided io.Writer where applicable.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session represents an active pod lifecycle. It is returned by Dispatcher.Start and Dispatcher.Resume. The caller owns the Session and is responsible for calling Stop or Wait.
Events and Wait are independent consumption paths — neither requires the other. Stop is idempotent.
func (*Session) Events ¶
Events returns a receive-only channel of typed events. The channel is closed after the terminal event (ContainerExited or Error). Callers may range over this channel to consume the full event stream.
Consuming Events() is optional. Wait() returns as soon as the container exits, independent of whether Events() is consumed. Under high output volume, output events may be dropped if the buffer fills; the terminal event may also be dropped if the buffer is full when the container exits, but the channel is always closed as the definitive terminal signal.
func (*Session) Stop ¶
Stop initiates graceful shutdown of the container. It calls runner.Stop with a 10-second SIGTERM timeout, then blocks until the container goroutine exits or ctx expires.
Stop is idempotent: calling it on an already-stopped session returns nil immediately.