Install
openclaw skills install @psyb0t/servicepackBuild a Go service on psyb0t/servicepack — clone-and-own framework (not a go get library) providing a Service interface (Name/Run/Stop), a singleton ServiceManager that runs services concurrently with dependency-ordered topological start, automatic retry (Retryable), non-fatal failures (AllowedFailure), readiness gating (ReadyNotifier), per-service CLI subcommands (Commander), an App singleton with OnPreRun/OnPostStop lifecycle hooks, gofindimpl-based service auto-discovery codegen, ctxscope/slogging structured logging, and a graceful-shutdown Runner. Import path github.com/psyb0t/servicepack. Use when the user wants related Go services debugged together locally, then deployed as one binary or split into separate microservices, with retry/dependency/readiness semantics.
openclaw skills install @psyb0t/servicepackservicepack runs your Go services concurrently without you hand-rolling a supervisor loop. It is NOT a package you go get into an existing project — it's a template repo you clone and make your own, then you add services under internal/pkg/services/. This skill teaches you to build a service with it, not to run it as a standalone server (there's nothing to run until you write a service).
servicepack is source code you compile into your own binary — it has no runtime surface of its own, no network listener, no daemon to secure. Once cloned it's just Go files in your repo; whatever surface your SERVICE exposes (HTTP, gRPC, DB connections) is on you, same as any Go code you'd write by hand. The only things worth flagging:
make own MODNAME=... rewrites go.mod, nukes .git, and re-inits — irreversible on the clone, run it once at the start.internal/app/, internal/pkg/service-manager/, pkg/runner/, and cmd/main.go are framework-owned files that make servicepack-update overwrites — never hand-edit them (see "Framework boundaries" below).gonfiguration, not os.Getenv)../app <service> migrate) alongside the long-running ./app run.servicepack.version, Makefile.servicepack, or internal/pkg/service-manager/ present.main() with no concurrent workers — plain Go is less ceremony.aichteeteapee directly in a plain main.go; servicepack's value is the multi-service supervisor, not routing.cmd/, internal/app/, internal/pkg/services/. servicepack expects to own your project's top-level shape.git clone https://github.com/psyb0t/servicepack
cd servicepack
make own MODNAME=github.com/yourname/yourproject
make service NAME=my-worker
make service scaffolds internal/pkg/services/my-worker/my-worker.go:
package myworker
import (
"context"
"github.com/psyb0t/ctxerrors"
"github.com/psyb0t/ctxscope"
"github.com/psyb0t/gonfiguration"
)
const ServiceName = "my-worker"
type Config struct {
Value string `env:"MYWORKER_VALUE" default:"default-value"`
}
type MyWorker struct {
config Config
}
func New() (*MyWorker, error) {
cfg := Config{}
if err := gonfiguration.Parse(&cfg); err != nil {
return nil, ctxerrors.Wrap(err, "parse my-worker config")
}
return &MyWorker{config: cfg}, nil
}
func (s *MyWorker) Name() string {
return ServiceName
}
func (s *MyWorker) Run(ctx context.Context) error {
ctx = ctxscope.Set(ctx, ctxscope.Attr("service", ServiceName))
logger := ctxscope.GetLogger(ctx)
logger.Info("starting service")
<-ctx.Done()
logger.Info("service context cancelled")
return nil
}
func (s *MyWorker) Stop(ctx context.Context) error {
serviceCtx := ctxscope.Set(ctx, ctxscope.Attr("service", ServiceName))
ctxscope.GetLogger(serviceCtx).Info("stopping service")
return nil
}
Edit the Run() body, then make service-registration regenerates internal/pkg/services/services.gen.go (auto-discovers every Service implementation via gofindimpl). Build and run:
make build
./build/yourproject run
Every service implements:
type Service interface {
Name() string
Run(ctx context.Context) error
Stop(ctx context.Context) error
}
Run() listens for ctx.Done() and returns cleanly on cancellation; a non-nil return from Run() stops all services (unless the service is an AllowedFailure, see below). Stop() runs during shutdown for cleanup.
A service can implement any combination of these on top of Service:
// Retryable — service gets restarted on failure, up to MaxRetries times,
// waiting RetryDelay between attempts.
type Retryable interface {
MaxRetries() int
RetryDelay() time.Duration
}
// AllowedFailure — service can die (even after exhausting retries)
// without killing the rest of the process.
type AllowedFailure interface {
IsAllowedFailure() bool
}
// Dependent — service manager topologically sorts start order;
// services with no deps start first.
type Dependent interface {
Dependencies() []string // names of other services in this process
}
// ReadyNotifier — service manager waits for this channel to close
// before starting anything that depends on this service.
type ReadyNotifier interface {
Ready() <-chan struct{}
}
// Commander — exposes CLI subcommands under the service's own
// namespace: ./app <servicename> <subcommand>. Only that service
// gets instantiated when its command runs.
type Commander interface {
Commands() []*cobra.Command
}
Dependencies on services not present in the current process (e.g. another microservice) are skipped with a debug log, not an error — cyclic dependencies within the process ARE rejected at startup.
Dependent alone orders the LAUNCH, not the readiness. A service that does not implement ReadyNotifier is treated as ready the moment its goroutine is launched, so its dependents are started right after — possibly before its Run body has executed a single line. If a dependent genuinely must not start until the dependency is accepting work (a DB accepting connections, a listener bound), the dependency has to implement ReadyNotifier and close its channel when it is actually up. Combining Dependent with ReadyNotifier is what turns "started in the right order" into "started only once the dependency works".
cmd/init.go is yours; it's never overwritten by make servicepack-update. Register hooks on the App singleton:
// cmd/init.go
package main
import (
"context"
"github.com/yourname/yourproject/internal/app"
)
func init() {
app.GetInstance().OnPreRun(func(ctx context.Context) {
// runs before any service starts
})
app.GetInstance().OnPostStop(func(ctx context.Context) {
// runs after all services have stopped
})
}
Hooks run sequentially in registration order; multiple hooks are allowed.
cmd/commands.go is also yours — add standalone cobra commands separate from per-service Commander commands:
// cmd/commands.go
package main
import "github.com/spf13/cobra"
func commands() []*cobra.Command {
return []*cobra.Command{
{
Use: "seed",
Short: "Seed the database",
Run: func(_ *cobra.Command, _ []string) {
// your logic
},
},
}
}
ctxscope over log/slog, with github.com/psyb0t/slogging/slogconf wiring the default handler. Add extra slog.Handlers (Loki, Datadog, etc.) in cmd/init.go; set durable identity fields with ctxscope.Set(ctx, ...), then log through ctxscope.GetLogger(ctx).github.com/psyb0t/gonfiguration — struct tags (env:"MYWORKER_VALUE"), gonfiguration.Parse(&cfg), gonfiguration.SetDefaults(map[string]any{...}). Never os.Getenv directly.github.com/psyb0t/ctxerrors (ctxerrors.Wrap(err, "doing X")) for file/line/function context.internal/app/, internal/pkg/service-manager/, pkg/runner/, cmd/main.go, Makefile.servicepack, scripts/make/servicepack/, Dockerfile.servicepack*, servicepack.version are all overwritten by make servicepack-update. Customize behavior through the lifecycle hooks above, not by patching these files. Everything under internal/pkg/services/, docs/, and tests/, plus Makefile, Dockerfile, Dockerfile.dev, cmd/init.go, cmd/commands.go, is yours and never touched by updates.
export SERVICES_ENABLED="my-worker,another-service" # comma-separated; unset/empty = all
./build/yourproject run
references/setup.md has the install/module details, Docker/toolchain requirements, and a fuller worked example with Retryable + Dependent + ReadyNotifier combined.