Metaprompt

A Kubernetes control plane for swappable CLI coding harnesses. The control surface is MCP. A parent is whoever called run.create (or session.create). Every child is a Kubernetes Job — never an in-container Task / Agent / subagent.

The shape of a run

Metaprompt does not run agents inside the plane process. The plane is an MCP server in the cluster (deploy/metaprompt-mcp). It authenticates callers, enforces ACL, writes catalog and party state, and creates Kubernetes objects. The coding work happens in harness pods.

  1. A human or parent harness calls the plane: session.create (interactive root) or run.create (one-shot Job).
  2. The plane opens a party (or joins one), resolves repo / model / skills / MCPs, and mints a run token.
  3. It applies a HarnessJob CR (metaprompt.io/v1). Mode session → long-running Pod. Mode jobbatch/v1 Job.
  4. The pod starts the runner. The runner starts a loopback child MCP, materializes prefix files (system text, skills, MCP config), then execs the vendor CLI (OpenCode, Claude Code, Codex, Cursor) or the stub.
  5. If that harness needs more workers, it calls job.spawn. Each spawn is another Job, nested in the same party, with a parent pointer and a depth increment. Summaries roll up the tree. The parent tails descendants with run.logs / job.progress.

Independent work: one job.spawn with several agents (parallel Jobs). Dependent work: after: [id] or job.wait. Never vendor Task tools.

Control surface (MCP)

Callers talk to http://<plane>:3333/mcp with JSON-RPC tools/call and Authorization: Bearer …. In-cluster Jobs use Service http://metaprompt-mcp:3333. On a laptop you port-forward that Service.

curl -sS http://127.0.0.1:3333/mcp \
  -H 'Authorization: Bearer alice-token' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"harness.list","arguments":{}}}'

Ready check: GET /healthz{"ok":true}.

Tools are fixed names. Do not invent verbs. Groups:

GroupTools
Discoverharness.list harness.get model.list skill.list mcp.list repo.list asset.list
Party / coordparty.create party.get party.list party.add party.close coord.post coord.inbox coord.wait coord.signal coord.barrier coord.handoff coord.members coord.artifact.put coord.artifact.get
Runsrun.create run.instruct run.wait run.kill run.cancel run.logs run.result run.list run.get run.share run.unshare
Jobsjob.run job.spawn job.wait job.kill job.logs job.progress
Self (from a Job)self.suspend self.exit
Sessionssession.create session.list session.get session.attach session.delete
Croncron.create cron.list cron.get cron.delete cron.enable
jj / VCSjj.status jj.diff jj.log jj.new jj.describe jj.squash jj.rebase jj.bookmark jj.git.fetch jj.git.push vcs.cred.mint
Memorymemory.put memory.search memory.get memory.delete

Always-on MCP names written into every Job prefix: plane, party, jj. clanker is parent-only (PARENT_ONLY_MCPS). run.create / job.spawn reject it even if an operator adds it to their catalog. Never put Clanker on a Job.

Sessions vs Jobs

SessionJob
Createsession.createrun.create or job.spawn
KubernetesLong-running Pod, mode: sessionbatch/v1 Job, mode: job
Default harnesssession (fat image: OpenCode, Claude, Codex, Cursor). Stub is rejected.Catalog harness you pass (opencode, stub, …)
How you attachkubectl exec/workspace/.mp/attachLogs via run.logs; process exits and POSTs complete
ChildrenStill Jobs via job.spawnStill Jobs via job.spawn

Interactive root:

kubectl -n metaprompt port-forward svc/metaprompt-mcp 3333:3333
bash scripts/session-up.sh          # or: session-up.sh opencode|cursor|claude-code|codex
# kubectl --context metaprompt -n metaprompt exec -it mp-run-… -c harness -- /workspace/.mp/attach

Inside the pod: /workspace/.mp/attach, or opencode / cursor / claude / codex / shell.

Storage on a Job is tmpfs (emptyDir Memory) or pvc. Sessions use the same workspace volume rules. Local smoke prefers tmpfs.

HarnessJob and the run token

The CR is the launch record. It carries run id, owner, harness, image, repo, sha, model, party, parent/root, depth, skills, MCP list, assets, storage, and plane URL. It does not carry the run token, DATABASE_URL, or VCS PATs.

The run token lives in Secret mp-token-<run>, mounted into the pod as METAPROMPT_RUN_TOKEN. The harness uses that bearer against the plane (and the in-pod child MCP proxies with it). Jobs never get the memory database URL; they call memory.* on the plane.

Helm release, namespace, and labels stay metaprompt. Do not put repos in Helm config without currentSha — live run.create 409s.

What the runner does in the pod

Image CMD is bun /app/packages/runner/src/main.ts (Jobs) or session.ts (sessions). Order:

  1. Child MCP on 127.0.0.1:3334. Prefix: party/mcp, jj/jj. Proxies party / coord / job / self / jj.git.fetch / jj.git.push / vcs.cred.mint to the plane with the run token. Local jj.* runs in the pod (or stubs if jj is missing). Memory stays on the plane.
  2. Workspace pin. Writes .metaprompt-pin with lowerdir. When git-sync is on, Jobs mount host /var/lib/metaprompt/repos/<name>/current at /repos and set METAPROMPT_LOWERDIR=/repos/<repo>/current. Local k3s leaves git-sync off so smoke does not need the public repo clone.
  3. jj colocate if the binary exists.
  4. Prefix files — system text, always-on MCP schemas, skill names, MCP names — hashed as prefixHash so identical stubs can share cache.
  5. Spawn the harness from commandFor (model flags + prompt). On exit the Job runner POSTs /internal/runs/:id/complete.

Git credential helpers are emptied in the child env. Fetch/push goes through minted creds, not embedded PATs.

Harnesses, models, skills, assets

The catalog lives in packages/shared (defaults) and can be overlaid from Helm config.json for assets, git-sync, Bedrock, and auth — not a second control plane.

HarnessImageDefault model
session…/session:latestnone — fat root CLI image
opencode…/opencode:latestmimo-v2.5-free (also deepseek-v4-flash-free, big-pickle)
claude-code…/claude-code:latestbedrock-sonnet (needs bedrock.enabled)
codex…/codex:latestgpt-5
cursor…/cursor:latestauto (also composer-2.5)
stub…/stub:latestnone — Jobs / smoke only

Local k3s has Bedrock off. run.create with claude-code + bedrock-sonnet 409s there. Use opencode + a free Zen model, session, or stub.

Each harness lists disallowedTools: Task, Agent, subagent. Parallelism is job.spawn.

Skills are named catalog entries (or inline / URL) hashed into the prefix. Paths the runner looks at: skills, .cursor/skills, .grok/skills.

Assets — after asset.list, run.create / session.create may pass assets: ["eval-set"]. The plane checks readers, then mounts read-only. Volume kinds: pvc:…, hostPath:…, configMap:…, secret:…, emptyDir / emptyDir:Memory. Local fixture: hostPath:/var/lib/metaprompt/assets/eval-set.

Repos have readers/writers, currentSha, and currentGeneration. The default local repo is app with a fixture SHA so Jobs can start without git-sync.

Parties, coordination, rollup

A party is the shared room for a root and its descendants. Members get coord inbox, artifacts, and barriers. coord.handoff creates another run in that party. job.spawn can open a nested party for a parallel group.

When children finish, the plane rolls summaries up the tree (capped bytes). run.get with summaryTree shows the rollup. Parents should poll job.progress instead of stuffing descendant logs into the next prompt.

Kill can cascade. Share/unshare is ACL on the run (local smoke: bob is denied until run.share).

Memory and VCS

When memory.enabled, the plane talks to Postgres + pgvector. Embedding default on k3s is hash (no API key). Scopes: user, party, repo. Jobs put/search through the plane MCP; they do not mount the database.

Version control is jj-first. jj.git.fetch / jj.git.push mint short-lived creds via vcs.cred.mint. Do not embed remotes or PATs in Job specs.

Auth

Local k3s static users (not for EKS):

EKS / GKE — GitHub Enterprise OIDC JWT. User is JWT actor (or sub). Org/repo/team claims map to groups. This is not EKS IRSA and not Bedrock IRSA; those can coexist.

A Job’s identity is the run token, scoped to that run. Plane secrets stay off the CR.

Limits

Defaults in the catalog (overridable in Helm limits):

LimitDefault
Max spawn depth4
Max running per user16
Max parallel per parent8
Max attempts8
Suspend TTL86400s
Max skill / memory bytes32768
Max summary / rollup2048 / 8192
Log tail50 default, 500 max

Install

Pick a playbook from setup.md. Clanker Cloud is optional — local Apple container k8s, k3d, existing EKS, and existing GKE do not need it.

git clone https://github.com/e-jerk/metaprompt.git
cd metaprompt
bun install && bun test
make k3s-up
make cluster-smoke
kubectl --context metaprompt -n metaprompt port-forward svc/metaprompt-mcp 3333:3333

Images: Apple Silicon container build + container k8s load-image; CI docker buildx bake. See setup/build.md.

Repo layout: packages/shared (types, ACL, catalog, prefix, spawn, rollup), packages/mcp (plane), packages/runner (pod), adapters/* (images), deploy/chart (Helm), skills/ (playbooks synced onto this site).

What it is not