simpleswe is a Kubernetes-native supervisor for software-engineering tasks. Its CLI and k9s-style terminal UI create, observe, and control tasks that run as immutable Kubernetes Jobs. OpenCode edits, commits, pushes, and creates or updates a Bitbucket or GitHub pull request; the worker validates the published result and the controller verifies it against provider truth.
The CLI is also the automation boundary for external agents. Any agent that can run commands can create tasks and observe their results; agent choice, chat, conversation, and orchestration stay outside simpleswe.
Human operator / external agent
-> simpleswe CLI or TUI
-> simpleswe controller
-> Kubernetes Job
-> OpenCode edits, commits, pushes, and creates or updates the PR
-> OpenCode reports the PR number
-> worker validation and controller provider verification
A Vaxis terminal UI provides a k9s-style operational view of tasks, attempts, Kubernetes resources, logs, validation results, and pull requests.
Upgrade note: in-flight attempts from versions that emitted branch_pushed must be retried after upgrade. Existing verified/open pull-request lifecycle data remains readable.
The initial vertical slice supports:
- SQLite task state in WAL mode;
- one Kubernetes Job and task Secret per immutable attempt;
- controller restart reconciliation;
- live and persisted Pod logs;
- bounded OpenCode validation/fix loops;
- deterministic OpenCode-owned task branches, commits, pushes, and pull requests;
- Bitbucket Cloud and GitHub provider verification;
- cancellation and retry without rewriting attempt history;
- task creation and operations through the internal HTTP API, CLI, and Vaxis TUI;
- machine-safe task creation and observation for external agents;
- namespace-scoped Helm installation without public ingress.
simpleswe is intentionally not a generic workflow engine, CI system, Kubernetes operator, browser dashboard, or autonomous merge service.
- Kubernetes 1.29 or newer;
- Helm 3;
- a default or configured
ReadWriteOnceStorageClass suitable for SQLite WAL locking; - Bitbucket or GitHub credentials for each configured repository;
- a webhook signing secret for each configured forge provider;
- a repository-specific worker image containing
simpleswe, OpenCode, Git, forge tooling and credentials, SSH, language runtimes, and validation tools; - Go 1.26.5 when building locally;
kubectllocally for automatic port-forwarding and TUI shell access.
Build the binary:
go build -o simpleswe ./cmd/simplesweBuild the controller image:
docker build --target controller -t ghcr.io/example/simpleswe:0.1.0 .The worker target extends the pinned official OpenCode image:
docker build \
--target worker \
-t ghcr.io/example/widget-worker:0.1.0 .Repository-specific worker images should extend this target and add only the runtimes and tools required by that repository. They must also provide the forge access OpenCode uses to create or update the PR: install gh and expose GH_TOKEN for GitHub, or install and configure the Bitbucket MCP with suitable credentials. OpenCode reports the finished PR number with simpleswe worker report --pull-request NUMBER.
With Docker, kind, kubectl, and Helm 3 installed, start or refresh the local controller:
make localThis creates a pinned Kubernetes 1.36 cluster, builds and loads the controller image, installs the Helm chart, and verifies the API through a port-forward. Optional agent sidecars are disabled; examples/values-kind.yaml registers the committed ptqa/simpleswe GitHub repository. make local creates non-overwriting placeholder Secrets so the controller can start and observe.
Inspect the controller:
./simpleswe task list --context kind-simpleswe --namespace simpleswe
./simpleswe tui --context kind-simpleswe --namespace simplesweThe placeholders are only for startup and observation. Before executing tasks, replace or provide real GitHub, webhook, and OpenAI credentials, then build and load a worker image:
docker build --target worker -t simpleswe-worker:kind .
kind load docker-image --name simpleswe simpleswe-worker:kindDelete the local cluster when finished:
make local-downCreate separate controller credentials for each Bitbucket repository. The Secret must contain username and app-password:
kubectl -n simpleswe create secret generic bitbucket-widget \
--from-literal=username='automation@example.com' \
--from-literal=app-password='...'For GitHub, create a repository-scoped fine-grained token. github.credentials_secret_name identifies the controller Secret used to inspect reported pull requests and process webhooks. When no separate worker Secret is configured, HTTPS workers also mount this Secret for Git access:
kubectl -n simpleswe create secret generic github-widget \
--from-literal=token='github_pat_...'GitHub PR creation and review follow-ups use gh. Expose a worker token with PR write access as GH_TOKEN, as shown in examples/values-kind.yaml; the local example reuses the repository-scoped github-simpleswe Secret rather than duplicating the PAT. The controller never creates or edits a fallback PR.
For SSH Git access, create a worker Secret whose private key is named ssh-privatekey:
kubectl -n simpleswe create secret generic widget-git-ssh \
--from-file=ssh-privatekey="$HOME/.ssh/widget_deploy_key"With GitHub SSH clone URLs, git.ssh_secret supplies worker clone/push credentials while github.credentials_secret_name supplies the controller inspection token. To separate GitHub HTTPS privileges, set credentials.secret_name to a worker-only Secret containing a token key. Only that Secret is mounted in the worker at /run/secrets/repository; the controller inspection Secret does not enter the Job. Git scopes the token helper to the configured clone URL and will not return it for another host or repository.
Use repository-scoped credentials with only the permissions needed to clone, push task branches, and read or create/update pull requests. Bitbucket workers must expose these capabilities through their configured MCP. Secret values never belong in Helm values or controller configuration.
Repositories are registered by name. A minimal configuration is:
controller:
listen_address: ":8080"
webhook_listen_address: ":8081"
namespace: simpleswe
deadline: 30m
review_debounce: 30m
max_fix_attempts: 3
worker:
image: ghcr.io/example/default-worker:0.1.0
command: opencode
branch_prefix: simpleswe/
deadline: 30m
bitbucket:
base_url: https://api.bitbucket.org
webhook_secret:
env: BITBUCKET_WEBHOOK_SECRET
github:
base_url: https://api.github.com
webhook_secret:
env: GITHUB_WEBHOOK_SECRET
repositories:
widget:
clone_url: git@bitbucket.org:acme/widget.git
default_branch: main
worker:
image: ghcr.io/example/widget-worker:0.1.0
resources:
requests:
cpu: "2"
memory: 4Gi
limits:
cpu: "8"
memory: 16Gi
node_selector:
workload: agents
git:
branch_prefix: simpleswe/
ssh_secret: widget-git-ssh
opencode:
command: [opencode, run]
config_secret: widget-opencode
validation:
max_fix_attempts: 2
commands:
- [go, test, ./...]
- [go, vet, ./...]
bitbucket:
workspace: acme
repository: widget
credentials_secret_name: bitbucket-widgetA public GitHub repository can omit clone_url and default_branch; they default to its public HTTPS URL and main:
repositories:
public-widget:
worker:
image: ghcr.io/example/widget-worker:0.1.0
github:
owner: octo-org
repository: widget
credentials_secret_name: github-widgetConfiguration is strict: unknown fields, inline credentials, invalid Secret names, and invalid resource settings fail controller startup. Agent integrations are configured separately from the controller; see examples/config.yaml and examples/values-eks.yaml for complete examples.
For a standalone controller, set BITBUCKET_WEBHOOK_SECRET and GITHUB_WEBHOOK_SECRET to the exact secrets for configured providers, then run ./simpleswe controller --config config.yaml --database tasks.db. Configure Bitbucket to POST to /v1/webhooks/bitbucket; GitHub uses /v1/webhooks/github.
Validation commands are argv arrays and are executed directly without shell interpolation.
Create the namespace and provider signing keys first (omit an unconfigured provider):
kubectl create namespace simpleswe
kubectl -n simpleswe create secret generic simpleswe-webhooks \
--from-literal=github='github-webhook-secret' \
--from-literal=bitbucket='bitbucket-webhook-secret'Then install a values file containing image references and repository configuration:
helm upgrade --install simpleswe ./deploy/helm/simpleswe \
--namespace simpleswe \
--values path/to/values.yamlFor EKS, the example also enables the optional Hermes agent sidecar:
helm upgrade --install simpleswe ./deploy/helm/simpleswe \
--namespace simpleswe \
--create-namespace \
--values examples/values-eks.yamlThe chart creates one controller Deployment, private API Service, signed simpleswe-webhooks Service, ServiceAccount, namespace Role and RoleBinding, controller PVC, ConfigMap, and default-deny NetworkPolicies. It can also deploy the packaged Hermes sidecar and its PVC. The chart references pre-existing Secrets and never creates credential values. Deliberately expose only the signed webhook listener and configure networkPolicy.webhookIngress; keep the unauthenticated API private.
The API is unauthenticated and is intended for kubectl port-forward access only. Keep the default NetworkPolicy enabled unless another access boundary is in place.
The local commands automatically run kubectl port-forward to the simpleswe Service using the selected kube context and namespace:
simpleswe tui --context production --namespace simpleswe
simpleswe task create --context production --namespace simpleswe widget "Fix the failing ClaimService tests"
simpleswe task create --context production --namespace simpleswe --idempotency-key request-123 widget "Fix the failing ClaimService tests"
simpleswe task list --context production --namespace simpleswe
simpleswe task show --context production --namespace simpleswe swe-...
simpleswe task wait --context production --namespace simpleswe swe-...
simpleswe task logs --context production --namespace simpleswe swe-...
simpleswe task cancel --context production --namespace simpleswe swe-...
simpleswe task retry --context production --namespace simpleswe swe-...The create command accepts a configured repository name followed by the task prompt. Quote prompts that contain spaces. --idempotency-key is optional and lets machine callers safely retry task creation. task wait polls until a pull-request URL appears or the task reaches failed, cancelled, or ready, then writes the final JSON.
Use --address http://127.0.0.1:8080 to connect to an existing port-forward instead.
TUI keys:
| Key | Action |
|---|---|
j / k, ↑ / ↓ |
Move selection |
g / G |
Jump to first or last task |
n |
Create task |
enter |
Task and attempt details |
l |
Live logs |
e |
Event history |
d |
Kubernetes Job details |
p |
Kubernetes Pod details |
s |
Shell into the running worker with kubectl exec |
r |
Retry task |
ctrl-d |
Cancel task |
R |
Refresh |
t |
Choose color theme |
? |
Help |
h, q, esc |
Back or quit |
Any command-capable agent can use the same CLI as a human operator. An integration only needs to preserve the user's request, select a configured repository, and create a stable idempotency key for retries:
simpleswe task create \
--address http://127.0.0.1:8080 \
--idempotency-key REQUEST_KEY \
REPOSITORY "ENGINEERING REQUEST"The agent can report the accepted task ID immediately and observe completion in a background process:
simpleswe task wait --address http://127.0.0.1:8080 TASK_IDtask wait outputs the final task JSON when a pull-request URL appears or the task reaches failed, cancelled, or ready. Retrying task create with the same idempotency key returns the existing task instead of creating a duplicate. Agents can also use task list, show, logs, cancel, and retry; confirmation and presentation are the integrating agent's responsibility.
Use --address when the agent already has a network path to the private API. Otherwise, the CLI's --context and --namespace flags manage a local kubectl port-forward.
Hermes is the chart's default bundled agent example, and examples/values-eks.yaml enables it. Base chart values leave it disabled so a controller-only install does not require Slack or model-provider credentials. Hermes is not a SimpleSWE runtime dependency; OpenClaw or another agent can use the same CLI contract without the sidecar.
Build the pinned Hermes image containing the simpleswe CLI:
docker build --target hermes \
-t ghcr.io/simpleswe/simpleswe-hermes:v0.20.0-simpleswe.1 .Publish that image with an immutable tag or digest. Configure hermes values and pre-existing Slack/model-provider Secrets as shown in examples/values-eks.yaml. Generate and apply the Slack app manifest from the matching Hermes release:
hermes slack manifest --agent-view --writeThe bundled skill limits Hermes to the seven simpleswe task commands above, points it at http://127.0.0.1:8080, and keeps repository access, Kubernetes execution, and pull-request ownership in SimpleSWE.
The controller is the only component that owns task intent and logical state. Kubernetes remains the execution scheduler and source of truth for Job and Pod lifecycle.
- The CLI and TUI are the supported user interfaces; the CLI is also the automation contract for external agents.
- The controller has no chat or agent runtime. SQLite stores task intent, attempts, events, validation results, Git results, pull requests, log checkpoints, and idempotency records.
- One immutable attempt maps to one deterministic Kubernetes Job.
- The task prompt is delivered in a task-specific read-only Secret.
- External agents own conversation, authorization, and result presentation. They invoke task commands but do not own task state or Kubernetes resources.
- The optional Hermes sidecar follows this boundary and has no controller database, forge, webhook, or repository mounts.
- Workers have no Kubernetes API token and cannot create tasks or retries.
- Only
@@simpleswe:JSON log lines are interpreted as lifecycle events; all other output remains raw logs. - Retrying creates a new attempt, Job, task branch, and history entry.
- The controller reconciles persisted intent and labelled Kubernetes resources after restart.
The OpenAPI contract is available at api/openapi.yaml.
The current schema does not migrate databases created by releases with the former built-in Slack integration. Before upgrading from one of those releases, stop the old Deployment and delete the existing controller PVC/database:
kubectl -n simpleswe scale deployment/simpleswe --replicas=0
kubectl -n simpleswe delete pvc simpleswe-dataDeploy the new controller with the CLI/TUI only or with the agent integration of your choice. If using the packaged Hermes sidecar, update the Slack app and Secrets for Hermes before enabling it. Rollback requires reinstalling the old application version and creating a new empty database compatible with that version. Task history is not migrated.
Run the complete local checks:
pre-commit run --all-filesOr run the primary commands directly:
go test -race -cover ./...
go vet ./...
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 run ./...
go run github.com/fe3dback/go-arch-lint@v1.15.0 check --project-path .
go run github.com/daveshanley/vacuum@v0.29.2 lint \
--no-banner --no-style --no-update-check \
--ruleset vacuum.ruleset.yaml \
--fail-severity warn api/openapi.yamlNormal tests use fake Kubernetes clients, fake executables, and local HTTP servers; they do not require real agent, OpenCode, or forge credentials.
- Run exactly one controller replica. SQLite and process-local task serialization do not support active-active controllers.
- Use a block-backed PVC with reliable POSIX file locking. The EKS example uses EBS
gp3. - The API has no application authentication. Cluster networking is the security boundary.
- Keep the namespace dedicated to SimpleSWE: every Pod container, including optional agent sidecars, inherits the Pod ServiceAccount token. RBAC remains namespace-scoped; no cluster-scoped permissions are required.
- Pull requests are never merged automatically.
Apache License 2.0. See LICENSE.