{"id":49332147,"url":"https://github.com/witwave-ai/witwave","last_synced_at":"2026-05-23T03:11:08.697Z","repository":{"id":352534037,"uuid":"1201683773","full_name":"witwave-ai/witwave","owner":"witwave-ai","description":"Self-directed AI agents that work autonomously on software — including improving themselves. Multi-LLM, K8s-native, AI-operated open source.","archived":false,"fork":false,"pushed_at":"2026-04-26T22:16:18.000Z","size":6686,"stargazers_count":0,"open_issues_count":12,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-26T23:23:16.059Z","etag":null,"topics":["a2a","agent-platform","ai-agents","autonomous-agents","claude","controller-runtime","gemini","gpt","helm","kubernetes","kubernetes-operator","llm","mcp","opentelemetry","prometheus"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/witwave-ai.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-05T02:30:27.000Z","updated_at":"2026-04-26T22:16:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/witwave-ai/witwave","commit_stats":null,"previous_names":["skthomasjr/witwave","witwave-ai/witwave"],"tags_count":139,"template":false,"template_full_name":null,"purl":"pkg:github/witwave-ai/witwave","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/witwave-ai%2Fwitwave","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/witwave-ai%2Fwitwave/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/witwave-ai%2Fwitwave/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/witwave-ai%2Fwitwave/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/witwave-ai","download_url":"https://codeload.github.com/witwave-ai/witwave/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/witwave-ai%2Fwitwave/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32456168,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-29T22:27:22.272Z","status":"online","status_checked_at":"2026-04-30T02:00:05.929Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["a2a","agent-platform","ai-agents","autonomous-agents","claude","controller-runtime","gemini","gpt","helm","kubernetes","kubernetes-operator","llm","mcp","opentelemetry","prometheus"],"created_at":"2026-04-26T23:01:58.687Z","updated_at":"2026-05-23T03:11:08.684Z","avatar_url":"https://github.com/witwave-ai.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Witwave\n\nWitwave is a cloud-native autonomous agent framework for AI agent teams. Visit [witwave.ai](https://witwave.ai) for the\npublic overview, whitepapers, team roster, and Quick Start.\n\nThe project is built around agent-native engineering: named agents with durable identity, backend choice, schedules,\ntriggers, memory, observability, MCP tool access, shared workspaces, and human-governed escalation. The goal is not to\nreplace judgment with automation. The goal is to make agents behave more like managed software participants than\nisolated chat sessions.\n\nThe primary use case is autonomous software development: agents that can triage issues, implement features, fix bugs,\nevaluate their work, coordinate with other agents, and help improve the system they run on. The same framework can be\npointed at other software projects, and the same deployment model can support a single specialized agent or a team of\nagents.\n\nWhat Witwave includes:\n\n- A **harness** that routes work, schedules heartbeats/jobs/tasks, handles triggers, and chains continuations.\n- **Backend agents** for Claude, OpenAI, Codex, Gemini, and a zero-dependency `echo` stub for onboarding and smoke\n  tests.\n- Shared **MCP tools** for Kubernetes, Helm, and Prometheus.\n- A Kubernetes **operator**, Helm charts, and the `ww` CLI for installing and managing agents.\n- Persistent per-agent identity, memory, conversation logs, metrics, traces, and workspace bindings.\n\n**This project is also an experiment in AI-operated open source.** Every line of code here is written by AI. Every bug\nis diagnosed and fixed with AI. Documentation, releases, website copy, and public updates are part of the same operating\nmodel. Humans set direction, own risk, and make strategic calls; agents do the day-to-day implementation work and make\nthat work inspectable. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full model (including the\ncurrent-state-vs-target breakdown), and [`docs/product-vision.md`](docs/product-vision.md) for why this is a first-class\nproject goal rather than a convention.\n\n---\n\nBuilt on the [A2A protocol](https://a2a-protocol.org). Each named agent is a set of containers: a **harness**\ninfrastructure layer (A2A relay, heartbeat scheduler, job scheduler) and one or more **backend agent** containers that\ndo the actual LLM work (Claude Agent SDK via `claude`, OpenAI Agents SDK via `openai`, Codex-native Node backend via\n`codex`, Google Gemini SDK via `gemini`). The `echo` backend ships as a zero-dependency stub — it returns a canned\nresponse quoting the caller's prompt and is the hello-world default for `ww agent create` when no API key is configured.\n\nMultiple agents can collaborate as a team, but the named agent (harness + its backend agents) is the deployable unit.\n\n## Agent Model\n\nThree tiers to keep straight:\n\n1. **A2A agent** — any server that publishes `/.well-known/agent.json`. The protocol's unit of identity. Both the\n   harness and each backend agent qualify.\n2. **Backend agent** — the LLM-wrapping worker. One image per backend family (`claude`, `openai`, `codex`, `gemini`),\n   plus the zero-dependency `echo` stub. Each owns its own session state, memory, conversation log, and metrics, and is\n   callable standalone over A2A.\n3. **Named agent** — the deployable unit (`iris`, `nova`, `kira`, …). From outside it presents as a single A2A agent via\n   the harness's endpoint. Inside, the harness orchestrates one or more backend agents using routing rules in\n   `.witwave/backend.yaml`.\n\nNamed agents may additionally bind to one or more **workspaces** — the operator-level primitive for shared resources\nmultiple agents collaborate over (shared volumes, scoped Secrets, ConfigMap-backed files). See [Workspaces](#workspaces)\nbelow for the concept; workspaces are agent-owned via `WitwaveAgent.spec.workspaceRefs[]` and purely additive (an agent\nwith zero workspace memberships runs perfectly).\n\nA named agent is both an agent **and** an orchestrator of sub-agents. Because the harness treats any A2A URL as a valid\ndispatch target, peer named agents are reachable the same way local backend agents are — teams of named agents are just\nagents all the way down.\n\nThe split of responsibilities:\n\n- **Autonomy** (when and why work happens) lives in the harness: heartbeats, jobs, tasks, triggers, continuations,\n  webhooks.\n- **Intelligence** (what to say, what to do) lives in the backend agents: LLM SDK wrappers that turn prompts into\n  responses.\n\nRemove the harness and you have reactive LLM servers that only respond when called — not autonomous. Remove the backend\nagents and you have a scheduler with nothing to dispatch to — no intelligence. Together they form an autonomous agent.\n\n## Components\n\n| Component            | Directory                  | Type                | Description                                                                                                                                          |\n| -------------------- | -------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Harness**          | `harness/`                 | Orchestrator agent  | Scheduling, triggering, chaining, A2A relay. No LLM of its own.                                                                                      |\n| **Claude backend**   | `backends/claude/`         | Backend agent       | Executes prompts via the Claude Agent SDK.                                                                                                           |\n| **OpenAI backend**   | `backends/openai/`         | Backend agent       | Executes prompts via the OpenAI Agents SDK. Supports web search and headless browser via Playwright.                                                 |\n| **Codex backend**    | `backends/codex/`          | Backend agent       | Node.js backend reserved for Codex-optimized coding-agent execution through the Responses API.                                                       |\n| **Gemini backend**   | `backends/gemini/`         | Backend agent       | Executes prompts via the Google Gemini SDK.                                                                                                          |\n| **Echo backend**     | `backends/echo/`           | Backend agent       | Zero-dependency stub. Returns a canned response quoting the prompt. Hello-world default + reference.                                                 |\n| **MCP tools**        | `tools/`                   | Tool infrastructure | `mcp-kubernetes`, `mcp-helm`, `mcp-prometheus` — shared MCP servers backends opt into.                                                               |\n| **Dashboard**        | `clients/dashboard/`       | Web client          | Vue 3 + PrimeVue web UI.                                                                                                                             |\n| **ww CLI**           | `clients/ww/`              | Client              | Go + cobra command-line interface (`curl -fsSL https://github.com/witwave-ai/witwave/releases/latest/download/install.sh \\| sh`, or Homebrew).       |\n| **Operator**         | `operator/`                | Kubernetes operator | Go controller that reconciles `WitwaveAgent`, `WitwavePrompt`, and `WitwaveWorkspace` CRDs.                                                          |\n| **WitwaveWorkspace** | `operator/api/v1alpha1/`   | Shared-resource CRD | Operator-reconciled bundle of shared volumes, projected Secrets, and ConfigMap files that participating agents mount. See [Workspaces](#workspaces). |\n| **Agent chart**      | `charts/witwave/`          | Deployment          | Helm chart that deploys Witwave agents via templated manifests.                                                                                      |\n| **Operator chart**   | `charts/witwave-operator/` | Deployment          | Helm chart that installs the operator + CRD.                                                                                                         |\n\nThe harness routes work to backend agents but does no LLM execution itself. Client surfaces (dashboard + ww) provide\nvisibility and interaction; they don't participate in agent workflows. The operator and its chart are an alternative\ninstall path to the agent chart; both target the same per-agent deployment shape.\n\n## Workspaces\n\nA `WitwaveWorkspace` is the operator-level primitive for shared resources multiple agents collaborate over. Each\nWitwaveWorkspace declares a list of shared PVC-backed volumes, a list of pre-created Secret references, and a list of\nConfigMap-backed files; the operator provisions and projects them onto every `WitwaveAgent` whose `spec.workspaceRefs[]`\nreferences the WitwaveWorkspace. The CRD is intentionally generic — source trees, datasets, video pipelines, accumulated\nmemory pools, anything where teams of agents need the same files visible at the same paths. Membership is agent-owned:\nan agent with zero `workspaceRefs[]` runs perfectly; participation is purely additive.\n\n```yaml\napiVersion: witwave.ai/v1alpha1\nkind: WitwaveWorkspace\nmetadata:\n  name: shared\n  namespace: witwave\nspec:\n  volumes:\n    - name: source\n      size: 50Gi\n      storageClassName: efs-sc\n---\napiVersion: witwave.ai/v1alpha1\nkind: WitwaveAgent\nmetadata:\n  name: iris\n  namespace: witwave\nspec:\n  workspaceRefs:\n    - name: shared\n```\n\nMount paths default to `/workspaces/\u003cworkspace\u003e/\u003cvolume.name\u003e` so cross-agent paths line up without operator-supplied\nglue. Manage workspaces from the CLI with `ww workspace { create, list, get, status, delete, bind, unbind }` — see\n[`clients/ww/README.md`](clients/ww/README.md#witwaveworkspace-management). Full CRD schema and reconciler details live\nin [`operator/README.md`](operator/README.md#the-witwaveworkspace-resource).\n\n## How It Works\n\nOperational details that complement the Agent Model above:\n\n- Each named agent has its own identity, memory, and configuration — none baked into the image. Behavioral instructions\n  for each backend agent come from a mounted file (`CLAUDE.md` for claude, `AGENTS.md` for openai/codex, `GEMINI.md` for\n  gemini), and A2A identity comes from a mounted `agent-card.md`.\n- Every container (harness and each backend agent) exposes `/health` for probes and `/metrics` for Prometheus on a\n  dedicated port (9000 by default) alongside its A2A endpoint.\n\n## Requirements\n\n- Docker\n- A Claude Code OAuth token (`claude setup-token`) or Anthropic API key (for `claude`)\n- An OpenAI API key (for `openai`)\n- An OpenAI API key with Codex model access (for `codex`)\n- A Gemini API key (for `gemini`)\n- Nothing extra for `echo` — the stub backend runs without credentials or network access\n\n## Container Images\n\nPublished images are available on GitHub Container Registry. Every image listed below is built and pushed automatically\non every release tag.\n\n| Image            | Registry path                                     |\n| ---------------- | ------------------------------------------------- |\n| `harness`        | `ghcr.io/witwave-ai/images/harness:latest`        |\n| `claude`         | `ghcr.io/witwave-ai/images/claude:latest`         |\n| `openai`         | `ghcr.io/witwave-ai/images/openai:latest`         |\n| `codex`          | `ghcr.io/witwave-ai/images/codex:latest`          |\n| `gemini`         | `ghcr.io/witwave-ai/images/gemini:latest`         |\n| `echo`           | `ghcr.io/witwave-ai/images/echo:latest`           |\n| `backend-base`   | `ghcr.io/witwave-ai/images/backend-base:latest`   |\n| `dashboard`      | `ghcr.io/witwave-ai/images/dashboard:latest`      |\n| `operator`       | `ghcr.io/witwave-ai/images/operator:latest`       |\n| `git-sync`       | `ghcr.io/witwave-ai/images/git-sync:latest`       |\n| `mcp-kubernetes` | `ghcr.io/witwave-ai/images/mcp-kubernetes:latest` |\n| `mcp-helm`       | `ghcr.io/witwave-ai/images/mcp-helm:latest`       |\n| `mcp-prometheus` | `ghcr.io/witwave-ai/images/mcp-prometheus:latest` |\n\n`backend-base` is the shared backend base image for common runtimes and CLI/analyzer tooling (Go, Node, `kubectl`, `ww`,\n`gh`, Helm, formatters, linters, and scanners). It is image composition only: Kubernetes permissions still come from\n`WitwaveAgent.spec.kubernetesApiAccess` or an explicit ServiceAccount/RBAC binding.\n\nThe `ww` CLI ships through three install paths — pick whichever fits your environment:\n\n```bash\n# Universal (Linux + macOS) — POSIX-sh installer with SHA256 verification.\ncurl -fsSL https://github.com/witwave-ai/witwave/releases/latest/download/install.sh | sh\n\n# macOS via the witwave-ai/homebrew-ww tap (Homebrew cask).\nbrew install witwave-ai/homebrew-ww/ww\n\n# From source (developers).\ngo install github.com/witwave-ai/witwave/clients/ww@latest\n```\n\nStandalone binaries are also published on [GitHub Releases](https://github.com/witwave-ai/witwave/releases). See\n[clients/ww/README.md](clients/ww/README.md#install) for the full flag and env-var reference for the curl installer\n(version pinning, beta channel, custom prefix, cosign verification, uninstall).\n\n`ww` checks for newer releases on startup and surfaces a one-line banner (configurable via\n`ww config set update.mode ...`). To upgrade explicitly at any time:\n\n```bash\nww update              # check + upgrade if newer\nww update --check      # check only\nww update --force      # run the upgrade unconditionally\n```\n\nPull a specific image version with a semver tag, e.g. `ghcr.io/witwave-ai/images/harness:0.23.15`. The latest released\ntag is visible on the [GitHub Releases](https://github.com/witwave-ai/witwave/releases) page; substitute it for the\nversion below.\n\n## Helm Charts\n\nTwo Helm charts are published to GHCR alongside the images on every release tag. The fastest install for the\n**operator** is the `ww` CLI — it embeds the chart so you don't need `helm` on PATH or any repo configured:\n\n```bash\n# Install `ww` then use it to install the operator.\ncurl -fsSL https://github.com/witwave-ai/witwave/releases/latest/download/install.sh | sh   # or: brew install witwave-ai/homebrew-ww/ww\nww operator install                 # into witwave-system namespace\nww operator status                  # verify\n```\n\nSee [clients/ww/README.md](clients/ww/README.md#operator-management) for the full `ww operator` surface.\n\nFor direct Helm installs (GitOps workflows, non-Homebrew environments, or the main agent chart which isn't yet\nCLI-managed):\n\n```bash\n# Agent chart — deploys Witwave agents directly via templated manifests.\nhelm install witwave oci://ghcr.io/witwave-ai/charts/witwave --version 0.23.15 --namespace witwave --create-namespace\n\n# Operator chart — installs the witwave-operator controller and the WitwaveAgent CRD.\nhelm install witwave-operator oci://ghcr.io/witwave-ai/charts/witwave-operator --version 0.23.15 --namespace witwave-system --create-namespace\n```\n\nSee [charts/witwave/README.md](charts/witwave/README.md) and\n[charts/witwave-operator/README.md](charts/witwave-operator/README.md) for full installation instructions.\n\n## Getting Started\n\n### 1. Pull or build the images\n\nPull published images:\n\n```bash\ndocker pull ghcr.io/witwave-ai/images/harness:latest\ndocker pull ghcr.io/witwave-ai/images/claude:latest\ndocker pull ghcr.io/witwave-ai/images/openai:latest\ndocker pull ghcr.io/witwave-ai/images/codex:latest\ndocker pull ghcr.io/witwave-ai/images/gemini:latest\ndocker pull ghcr.io/witwave-ai/images/echo:latest\ndocker pull ghcr.io/witwave-ai/images/backend-base:latest\n```\n\nOr build locally:\n\n```bash\ndocker build -f images/backend-base/Dockerfile -t backend-base:latest .\ndocker build -f harness/Dockerfile -t harness:latest .\ndocker build --build-arg BACKEND_BASE_IMAGE=backend-base:latest -f backends/claude/Dockerfile -t claude:latest .\ndocker build --build-arg BACKEND_BASE_IMAGE=backend-base:latest -f backends/openai/Dockerfile -t openai:latest .\ndocker build --build-arg BACKEND_BASE_IMAGE=backend-base:latest -f backends/codex/Dockerfile -t codex:latest .\ndocker build --build-arg BACKEND_BASE_IMAGE=backend-base:latest -f backends/gemini/Dockerfile -t gemini:latest .\ndocker build -f backends/echo/Dockerfile -t echo:latest .\n```\n\n### 2. Configure Credentials\n\nSOPS is the committed encrypted source for repo-managed team secrets. The policy in `.sops.yaml` applies to\n`*.sops.env`, `*.sops.yaml`, `*.sops.yml`, and `*.sops.json` anywhere in the repo. A more specific\n`.secrets/*.partial.sops.yaml` rule keeps non-secret metadata readable while encrypting secret-shaped keys (`password`,\n`token`, `secret`, `client_secret`, `api_key`, `private_key`). Current encrypted mirrors:\n\n- `.agents/self/team.sops.env` — shared self-team credentials.\n- `.agents/self/\u003cagent\u003e/agent.sops.env` — per-agent GitHub identity, with in-container names like `GITHUB_TOKEN` and\n  `GITHUB_USER`.\n- `.agents/test/team.sops.env` — shared test-team credentials.\n- `.secrets/secrets.sops.yaml` — root-level encrypted holding area for non-bootstrap / in-progress secrets.\n- `.secrets/*.partial.sops.yaml` — mixed metadata/secret records where only secret-like keys are encrypted.\n\nDecrypt locally through mise/SOPS when you need to inspect one of those files:\n\n```bash\nmise exec -- sops -d .agents/self/team.sops.env\nmise exec -- sops -d .agents/self/piper/agent.sops.env\n```\n\nRun commands with SOPS dotenv files loaded into the process environment:\n\n```bash\nmise exec -- scripts/sops-exec-env.py .agents/test/team.sops.env -- \\\n  sh -lc 'test -n \"$CLAUDE_CODE_OAUTH_TOKEN\" \u0026\u0026 test -n \"$GITSYNC_USERNAME\"'\n```\n\n### 3. Start the test agents\n\n```bash\nww operator install --yes\n\n# Then follow the Bob/Fred `ww agent create` commands in:\nsed -n '1,240p' .agents/test/bootstrap.md\n```\n\n### 4. Verify\n\n```bash\n# In one terminal:\nkubectl port-forward svc/bob 8099:8000 -n witwave-test\n\n# In another terminal:\ncurl http://localhost:8099/.well-known/agent.json\ncurl http://localhost:8099/health/ready\n```\n\n## Agent Structure\n\nSelf-managing agents are defined under `.agents/self/` (the agents that maintain this repo). Each named agent has its\nown directory containing Witwave config, backend instances, logs, and memory.\n\n```text\n.agents/\n├── self/\n│   ├── iris/          # Git plumbing + releases\n│   ├── kira/          # Documentation hygiene + research\n│   ├── nova/          # Code-internal hygiene\n│   ├── evan/          # Bugs + security-oriented risk work\n│   ├── finn/          # Gap-filling work\n│   ├── felix/         # Feature work\n│   ├── piper/         # Outreach\n│   └── zora/          # Team coordination\n└── test/\n    ├── bob/           # Bob  (Claude-first smoke agent; openai/gemini fixtures parked)\n    ├── fred/          # Fred (small Claude-only second-agent sanity check)\n    ├── jack/          # OpenAI-only scaffold\n    └── luke/          # Gemini-only scaffold\n```\n\nCLI/operator-created agents use the `ww` port convention by default: harness on `8000`, backend sidecars on\n`8001..8050`, and metrics on `9000`. Ports are not hardcoded in any image. Each container reads its own port from an\nenvironment variable (`HARNESS_PORT`, `BACKEND_PORT`, `METRICS_PORT`) and can be remapped per deployment via Helm values\nor the `WitwaveAgent` CRD.\n\nEach agent directory contains:\n\n```text\n\u003cagent\u003e/\n├── agent.sops.env      # Encrypted per-agent secret mirror (when the agent has agent-specific credentials)\n├── .witwave/              # Runtime config (agent-card.md, backend.yaml, HEARTBEAT.md, jobs/)\n├── .claude/           # Claude backend config (CLAUDE.md, mcp.json, settings.json, skills/)\n├── .openai/            # OpenAI backend config (AGENTS.md, config.toml)\n├── .codex/            # Codex backend config (AGENTS.md, config.toml)\n├── .gemini/           # Gemini backend config (GEMINI.md)\n├── logs/              # harness logs (runtime, not committed)\n├── claude/         # Claude backend instance\n│   ├── logs/          # Conversation log (runtime, not committed)\n│   └── memory/        # Persistent memory (runtime, not committed)\n├── openai/          # OpenAI backend instance\n│   ├── logs/\n│   └── memory/\n├── codex/          # Codex backend instance\n│   ├── logs/\n│   └── memory/\n└── gemini/         # Gemini backend instance\n    ├── logs/\n    └── memory/\n        └── sessions/  # JSON conversation history per session\n```\n\n## Routing Configuration\n\nEach agent's `backend.yaml` (under `.witwave/`) controls where Witwave routes each type of work:\n\n```yaml\nbackend:\n  agents:\n    - id: claude\n      url: http://localhost:8010\n\n    - id: openai\n      url: http://localhost:8011\n\n    - id: codex\n      url: http://localhost:8012\n\n    - id: gemini\n      url: http://localhost:8013\n\n  routing:\n    default: claude # fallback backend when no per-concern override matches\n    a2a: claude # handles incoming A2A requests\n    heartbeat: claude # handles heartbeat-triggered work\n    job: claude # handles job execution\n    task: claude # handles task execution\n    trigger: claude # handles inbound HTTP trigger requests\n    continuation: claude # handles continuation-fired prompts\n```\n\nRouting values can be a plain agent ID string or an object with `agent:` and optional `model:` fields. Model resolution\norder: per-message override → routing entry `model:` → per-backend config `model:`.\n\n## Consensus Mode\n\nSet `consensus:` in any prompt file's frontmatter to a list of backend entries. Each entry specifies a `backend` glob\npattern and an optional `model` override. The prompt is dispatched to every matched `(backend, model)` pair in parallel,\nthen the responses are aggregated:\n\n- **Binary responses** (yes / no / agree / disagree variants): majority vote. The default backend breaks ties.\n- **Freeform responses**: a synthesis prompt is dispatched to the default backend, which merges the collected responses\n  into a single coherent answer.\n\n```yaml\nconsensus:\n  - backend: \"claude\"\n    model: \"claude-opus-4-7\"\n  - backend: \"openai*\" # glob — matches any backend whose id starts with \"openai\"\n  - backend: \"claude\"\n    model: \"claude-haiku-4-5\" # same backend, different model = two parallel calls\n```\n\nAn empty list (the default when `consensus:` is omitted) disables consensus — the prompt is dispatched to the single\nrouting target. The same backend can appear twice with different models to compare outputs from different model sizes.\n\nUse consensus mode for high-stakes decisions where you want more than one model family's perspective.\n\n## Token Budget (max-tokens)\n\nSet `max-tokens` in a job, task, or trigger frontmatter to cap cumulative token usage for that dispatch. When the\nbackend reports that usage has reached the limit, it stops processing and returns any partial response collected so far.\nA `system` entry is written to `conversation.jsonl` recording how many tokens were consumed and what the limit was.\n\n```yaml\n---\nname: daily-summary\nschedule: \"0 8 * * *\"\nmax-tokens: 4000\n---\nSummarise the day's key events.\n```\n\nThe value must be a positive integer. Invalid values are logged and ignored. The limit applies per-dispatch (not across\nsessions), so each job/task/trigger invocation gets a fresh budget. LLM-backed runtimes enforce it from their provider\nusage stream or final usage summary:\n\n| Backend  | Token source                                                  |\n| -------- | ------------------------------------------------------------- |\n| `claude` | `get_context_usage()` after each assistant turn               |\n| `openai` | `event.data.usage.total_tokens` on response events            |\n| `codex`  | Responses API `usage.total_tokens` after each response create |\n| `gemini` | `chunk.usage_metadata.total_token_count` per chunk            |\n\n## Adding an Agent\n\n1. Copy an existing agent directory:\n\n   ```bash\n   cp -r .agents/self/iris .agents/self/\u003cname\u003e\n   ```\n\n2. Update the agent's `agent-card.md` in `.witwave/` (mounted at `/home/agent/.witwave/agent-card.md`) with the agent's\n   identity and role\n\n3. Update the backend instruction files: `CLAUDE.md` (at `/home/agent/.claude/CLAUDE.md`), `AGENTS.md` (at\n   `/home/agent/.openai/AGENTS.md` or `/home/agent/.codex/AGENTS.md`), and `GEMINI.md` (at\n   `/home/agent/.gemini/GEMINI.md`) with backend-specific behavioral instructions\n\n4. Update `.agents/self/\u003cname\u003e/.witwave/backend.yaml` with the new agent's backend service names and URLs\n\n5. Deploy it with `ww agent create`, usually using `--workspace \u003cworkspace\u003e` to bind it to the right team workspace,\n   `--gitsync-bundle` to point at the agent directory, and `--with-persistence` for backend memory/session storage\n\n6. Deploy:\n\n   ```bash\n   ww agent create \u003cname\u003e --namespace \u003cnamespace\u003e --workspace \u003cworkspace\u003e --backend claude --with-persistence \\\n     --gitsync-bundle https://github.com/witwave-ai/witwave.git@main:.agents/self/\u003cname\u003e\n   ```\n\n## Communication\n\nAgents communicate over the [A2A protocol](https://a2a-protocol.org) via JSON-RPC. Each Witwave agent exposes:\n\n- `/.well-known/agent.json` — agent card (identity and capabilities)\n- `/` — A2A JSON-RPC endpoint (`message/send`)\n- `GET /health/start` — startup probe: 200 once ready, 503 while initializing\n- `GET /health/live` — liveness probe: always 200 with `{\"status\": \"ok\", \"agent\": ..., \"uptime_seconds\": ...}`\n- `GET /health/ready` — readiness probe: 200/`{\"status\": \"ready\"}`; 503/`{\"status\": \"starting\"}` while initializing;\n  503/`{\"status\": \"degraded\"}` when a backend is unhealthy\n- `GET /agents` — own agent card plus agent cards from all configured backends\n- `GET /jobs` — structured snapshot of all registered scheduled jobs (name, cron, backend, running state)\n- `GET /tasks` — structured snapshot of all registered scheduled tasks (name, days, window, running state)\n- `GET /webhooks` — structured snapshot of all registered webhook subscriptions (name, url, filters, active deliveries)\n- `GET /continuations` — structured snapshot of all registered continuation items (name, continues-after, filters,\n  active fires)\n- `GET /triggers` — structured snapshot of all registered inbound trigger endpoints (name, endpoint, description,\n  session, backend, running state)\n- `GET /heartbeat` — current heartbeat configuration from `HEARTBEAT.md`\n- `GET /conversations` — merged conversation log from all backends\n- `GET /trace` — merged trace log from all backends\n- `GET /.well-known/agent-triggers.json` — discovery array of all enabled trigger descriptors\n\nCross-agent views (`/team`, `/proxy/\u003cname\u003e`, `/conversations/\u003cname\u003e`, `/trace/\u003cname\u003e`) were retired in beta.46 — the\ndashboard pod fans out directly to each agent and owns cross-agent routing (#470).\n\nEach backend container additionally exposes:\n\n- `GET /health/start` — startup probe: 200/`{\"status\": \"ok\"}` once the process has finished initial loads (`_ready` is\n  True) and 503/`{\"status\": \"starting\"}` while still warming up. Mirrors the harness's `/health/start` so the\n  three-probe model documented in `docs/product-vision.md` holds across the platform (#1686). K8s `startupProbe` should\n  target this endpoint.\n- `GET /health` — liveness check: 200/`{\"status\": \"ok\", \"agent\": ..., \"uptime_seconds\": ...}` once the process is up.\n  Returns 200 even while initializing — does NOT flip to 503. Liveness-only by design (cycle-1 #1608, #1672); use the\n  readiness endpoint below for gating LB rotation.\n- `GET /health/ready` — readiness probe: 200 when fully ready, 503/`{\"status\": \"starting\"}` while initializing or in a\n  boot-degraded state (claude #1608, openai+gemini #1672). Operators using K8s `readinessProbe` should point at\n  `/health/ready`, not `/health`.\n- `GET /metrics` — Prometheus metrics (when `METRICS_ENABLED` is set)\n- `POST /mcp` — MCP JSON-RPC server (`initialize`, `tools/list`, `tools/call` with a backend-specific ask tool); allows\n  MCP hosts (Claude Desktop, Cursor, VS Code extensions) to invoke the agent as a tool without going through harness.\n  LLM-backed backends require a bearer token (`CONVERSATIONS_AUTH_TOKEN`) on `/mcp` (#510, #516, #518); the shared token\n  guard also gates `/conversations` and `/trace`. If the env var is left empty the backend logs a startup warning (#517)\n  or refuses protected endpoints unless `CONVERSATIONS_AUTH_DISABLED=true` is set for local/dev use. The Python SDK\n  backends bind `session_id` through `shared/session_binding.derive_session_id` with a bearer-token fingerprint before\n  lookup/insert (#867 claude, #929 openai, #935 gemini, #941 shared path); set `SESSION_ID_SECRET` in production to\n  HMAC-derive the bound ID.\n\n## Memory\n\nEach backend agent manages its own memory at `.agents/\u003cenv\u003e/\u003cname\u003e/\u003cbackend\u003e/memory/`. For `claude` and `openai`, memory\nfiles are markdown documents. The `codex` scaffold exposes bounded memory tools rooted at `CODEX_MEMORY_ROOT` (default\n`/home/agent/.codex/memory`) and persists Responses API `previous_response_id` mappings at `CODEX_SESSION_STORE_PATH`\n(default `/home/agent/.codex/sessions/responses.json`). For `gemini`, conversation history is stored as JSON in\n`memory/sessions/`. Memory files are not committed to source control. harness has no memory layer of its own.\n\nWorkspace-backed memory is a separate shared volume. When a workspace declares a `memory` volume, bound agents see it at\n`/workspaces/\u003cworkspace-name\u003e/memory`; the self team uses `witwave-self/memory`, and the test team mirrors the same\nnamespace/index contract under `witwave-test/memory`.\n\n## Authentication\n\n| Service | Method             | Environment variable                 |\n| ------- | ------------------ | ------------------------------------ |\n| claude  | Claude Max (OAuth) | `CLAUDE_CODE_OAUTH_TOKEN`            |\n| claude  | Anthropic API key  | `ANTHROPIC_API_KEY`                  |\n| openai  | OpenAI API key     | `OPENAI_API_KEY`                     |\n| codex   | OpenAI API key     | `OPENAI_API_KEY`                     |\n| gemini  | Gemini API key     | `GEMINI_API_KEY` or `GOOGLE_API_KEY` |\n\n## Security\n\nProtected endpoints use `Authorization: Bearer \u003ctoken\u003e` throughout. Two distinct harness tokens:\n\n- **`CONVERSATIONS_AUTH_TOKEN`** — read / observe endpoints (`/conversations`, `/trace`, `/mcp`, `/api/traces`,\n  `/events/stream`, `/api/sessions/\u003cid\u003e/stream`). Reused on the harness for inbound and on each backend for its own\n  protected surface.\n- **`ADHOC_RUN_AUTH_TOKEN`** — trigger-actions endpoints (`POST /jobs/\u003cname\u003e/run`, `/tasks/\u003cname\u003e/run`, `/validate`).\n\nBoth are default-closed — the server refuses requests when the token is unset. `CONVERSATIONS_AUTH_DISABLED=true` is a\ndocumented escape hatch for local dev; startup logs a loud warning when it's set.\n\nSession IDs on multi-tenant surfaces are HMAC-bound to the caller via `SESSION_ID_SECRET`. Rotation uses a probe-list\nwindow via `SESSION_ID_SECRET_PREV`: writes go to the current-secret derivation; reads probe `[current, prev]` and emit\na one-shot WARN on prev-hits so operators know when they can drop the prev secret.\n\nMCP stdio entries are gated by a per-backend command allow-list (`MCP_ALLOWED_COMMANDS`, `MCP_ALLOWED_COMMAND_PREFIXES`,\n`MCP_ALLOWED_CWD_PREFIXES`); rejections bump `backend_mcp_command_rejected_total{reason}`. Every MCP tool container\nenforces its own bearer (`MCP_TOOL_AUTH_TOKEN`) via `shared/mcp_auth.py`. Outbound webhooks go through an SSRF-resistant\nURL check that re-resolves the hostname at delivery time.\n\nThe `witwave-operator` chart runs with a split RBAC surface (`rbac.secretsWrite=false` drops Secret write verbs while\nkeeping reads). Credential Secrets are dual-checked (label + `IsControlledBy`) before any update/delete so the operator\nnever touches user-created Secrets.\n\nSee `AGENTS.md` → \"Conventions\" for the full auth / redaction / MCP / RBAC posture, `shared/redact.py` for the\nconversation-log redaction rules (idempotent merge-spans with UUID / OTel-trace shielding), and each chart's\n`values.yaml` for the full surface of security-affecting knobs.\n\n## Configuration\n\n### harness environment variables\n\n| Variable                                    | Default                             | Description                                                                                                                                                                                                                              |\n| ------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `AGENT_NAME`                                | `witwave`                           | Agent display name (e.g. `iris`)                                                                                                                                                                                                         |\n| `HARNESS_HOST`                              | `0.0.0.0`                           | Interface the harness binds to                                                                                                                                                                                                           |\n| `HARNESS_PORT`                              | `8000`                              | HTTP port the harness listens on                                                                                                                                                                                                         |\n| `HARNESS_URL`                               | `http://localhost:$HARNESS_PORT/`   | Public URL published on the A2A agent card                                                                                                                                                                                               |\n| `BACKEND_CONFIG_PATH`                       | `/home/agent/.witwave/backend.yaml` | Path to the backend routing config file                                                                                                                                                                                                  |\n| `METRICS_ENABLED`                           | _(unset)_                           | Set to any non-empty value to expose `/metrics`                                                                                                                                                                                          |\n| `METRICS_PORT`                              | `9000`                              | Dedicated port the metrics listener binds to (split from the app port so NetworkPolicy + auth can differ, #643)                                                                                                                          |\n| `METRICS_AUTH_TOKEN`                        | _(unset)_                           | Bearer token required to access `/metrics` (recommended in production)                                                                                                                                                                   |\n| `METRICS_CACHE_TTL`                         | `15`                                | Seconds to cache aggregated backend metrics between scrapes                                                                                                                                                                              |\n| `CONVERSATIONS_AUTH_TOKEN`                  | _(unset)_                           | Bearer token required to access `/conversations` and `/trace` (inbound)                                                                                                                                                                  |\n| `BACKEND_CONVERSATIONS_AUTH_TOKEN`          | _(unset)_                           | Bearer token forwarded to backend `/conversations`, `/trace`, and `/api/traces` endpoints (set if backends require auth)                                                                                                                 |\n| `TRIGGERS_AUTH_TOKEN`                       | _(unset)_                           | Bearer token required for inbound trigger requests (fallback when no per-trigger HMAC secret is set)                                                                                                                                     |\n| `HOOK_EVENTS_AUTH_TOKEN`                    | _(unset)_                           | Canonical bearer token on `/internal/events/hook-decision` (bound to the metrics listener, #924). `HARNESS_EVENTS_AUTH_TOKEN` is a back-compat alias that logs a deprecation warning when used alone (#859). Unset = refuse (#712, #933) |\n| `SESSION_ID_SECRET`                         | _(unset — permissive)_              | HMAC key for `shared/session_binding.derive_session_id` used on `/mcp` session-id binding across SDK backends (#867/#929/#935/#941). Leave unset only in single-tenant dev; set to a 256-bit random value in production                  |\n| `ADHOC_RUN_AUTH_TOKEN`                      | _(unset)_                           | Bearer token required for `POST /jobs/\u003cname\u003e/run`, `/tasks/\u003cname\u003e/run`; unset = refuse (#700)                                                                                                                                            |\n| `CORS_ALLOW_ORIGINS`                        | _(unset)_                           | Comma-separated list of allowed CORS origins; when unset, all cross-origin requests are denied (logs a warning)                                                                                                                          |\n| `CORS_ALLOW_WILDCARD`                       | `false`                             | Explicit acknowledgement for `CORS_ALLOW_ORIGINS=*`; template refuses the wildcard otherwise (#701)                                                                                                                                      |\n| `A2A_MAX_PROMPT_BYTES`                      | `1048576`                           | Reject inbound A2A prompts above this byte size at ingress; set to `0` to disable (#783)                                                                                                                                                 |\n| `CONTINUATION_MAX_CONCURRENT_FIRES_GLOBAL`  | `0` (unlimited)                     | Hard cap on in-flight continuation fires across all items; protects against fan-out storms (#781)                                                                                                                                        |\n| `TASK_STORE_PATH`                           | _(unset)_                           | Path for SQLite A2A task store; defaults to in-memory (state lost on restart)                                                                                                                                                            |\n| `WORKER_MAX_RESTARTS`                       | `5`                                 | Consecutive crash limit before a critical worker marks the agent not-ready                                                                                                                                                               |\n| `WEBHOOK_MAX_CONCURRENT_DELIVERIES`         | `50`                                | Maximum number of in-flight webhook delivery tasks across all subscriptions; deliveries beyond this cap are shed and counted                                                                                                             |\n| `WEBHOOK_MAX_CONCURRENT_DELIVERIES_PER_SUB` | `10`                                | Per-subscription cap on concurrent in-flight deliveries; also settable per webhook via `max-concurrent-deliveries` frontmatter                                                                                                           |\n| `WEBHOOK_EXTRACTION_TIMEOUT`                | `120`                               | Maximum seconds to wait for a single LLM extraction call inside a webhook delivery; prevents a slow backend from holding a delivery slot indefinitely                                                                                    |\n| `WEBHOOK_URL_ALLOWED_HOSTS`                 | _(unset)_                           | Comma-separated `host` or `host:port` entries that are allowed to override the SSRF guard on private / loopback / reserved destinations (#524)                                                                                           |\n| `JOBS_MAX_CONCURRENT`                       | `0` (unlimited)                     | Maximum number of jobs that may run concurrently; `0` disables the limit                                                                                                                                                                 |\n| `TASKS_MAX_CONCURRENT`                      | `0` (unlimited)                     | Maximum number of tasks that may run concurrently; `0` disables the limit                                                                                                                                                                |\n| `TASK_TIMEOUT_SECONDS`                      | `300`                               | Task timeout in seconds, applied to A2A backend requests                                                                                                                                                                                 |\n| `MANIFEST_PATH`                             | `/home/agent/manifest.json`         | Path to the team manifest file listing all agents by name and URL                                                                                                                                                                        |\n| `BACKENDS_READY_WARN_AFTER`                 | `120`                               | Seconds to wait before logging a warning that backends have not become healthy                                                                                                                                                           |\n| `LOG_PROMPT_MAX_BYTES`                      | `200`                               | Maximum bytes of the prompt logged at INFO level; set to `0` to suppress prompt logging entirely                                                                                                                                         |\n| `A2A_BACKEND_MAX_RETRIES`                   | `3`                                 | Maximum retry attempts for transient backend errors (429, 502, 503, 504, connection errors); must be \u003e= 1                                                                                                                                |\n| `A2A_BACKEND_RETRY_BACKOFF`                 | `1.0`                               | Base backoff in seconds for retry delay (exponential with jitter); multiplied by 2^attempt                                                                                                                                               |\n\n### Backend (claude / openai / codex / gemini) environment variables\n\n| Variable                       | Default                            | Description                                                                                                                                      |\n| ------------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `AGENT_NAME`                   | `claude`/`openai`/`codex`/`gemini` | Backend instance name (e.g. `iris-claude`)                                                                                                       |\n| `AGENT_OWNER`                  | _(same as `AGENT_NAME`)_           | Named agent this backend belongs to (e.g. `iris`); used in metric labels                                                                         |\n| `AGENT_ID`                     | `claude`/`openai`/`codex`/`gemini` | Backend slot identifier (e.g. `claude`); used in metric labels                                                                                   |\n| `AGENT_URL`                    | `http://localhost:8000/`           | Public A2A endpoint URL for the agent card                                                                                                       |\n| `BACKEND_PORT`                 | `8000`                             | HTTP port the backend listens on (internal)                                                                                                      |\n| `METRICS_ENABLED`              | _(unset)_                          | Set to any non-empty value to expose `/metrics`                                                                                                  |\n| `METRICS_PORT`                 | `9000`                             | Dedicated port the metrics listener binds to (#643; same semantics as harness)                                                                   |\n| `CONVERSATIONS_AUTH_TOKEN`     | _(unset — warn on empty)_          | Bearer token required to access `/conversations`, `/trace`, `/mcp`, and `/api/traces[/\u003cid\u003e]` on all LLM-backed backends (#510, #516, #517, #518) |\n| `CONVERSATIONS_AUTH_DISABLED`  | _(unset)_                          | Explicit escape hatch to run without the auth guard; loud startup log for visibility (#718). Intended for local dev only.                        |\n| `LOG_REDACT`                   | _(unset)_                          | When truthy, conversation and response logs redact user-prompt / agent-response content (#714)                                                   |\n| `GEMINI_MAX_HISTORY_BYTES`     | _(gemini only)_                    | Byte ceiling on the JSON session-history file gemini persists per session; older turns are truncated to fit                                      |\n| `MCP_ALLOWED_COMMANDS`         | _(per-backend default)_            | Comma-separated allow-list of basenames for stdio entries parsed from `mcp.json`                                                                 |\n| `MCP_ALLOWED_COMMAND_PREFIXES` | _(per-backend default)_            | Comma-separated allow-list of absolute-path prefixes for stdio entries                                                                           |\n| `MCP_ALLOWED_CWD_PREFIXES`     | _(per-backend default)_            | Comma-separated allow-list of working-directory prefixes for stdio entries (rejections counted on `backend_mcp_command_rejected_total`)          |\n| `TASK_STORE_PATH`              | _(unset)_                          | Path for SQLite A2A task store; defaults to in-memory (state lost on restart)                                                                    |\n| `WORKER_MAX_RESTARTS`          | `5`                                | Consecutive crash limit before a critical worker marks the backend not-ready                                                                     |\n| `LOG_PROMPT_MAX_BYTES`         | `200`                              | Maximum bytes of the prompt logged at INFO level; `0` suppresses prompt logging entirely                                                         |\n\nCodex-specific runtime knobs include `CODEX_MODEL`, `CODEX_REASONING_EFFORT`, `CODEX_STUB_MODE`, `CODEX_SHELL_ENABLED`,\n`CODEX_MEMORY_ENABLED`, `CODEX_MEMORY_ROOT`, and `CODEX_SESSION_STORE_PATH`. `CODEX_MEMORY_ROOT` bounds the Codex memory\nfile tools; paths passed to those tools are relative to that root and cannot escape it.\n\n## Metrics\n\nWhen `METRICS_ENABLED` is set, Prometheus metrics are served at `/metrics` on a **dedicated port** (9000 by default,\nconfigurable via `METRICS_PORT`) on every container. The metrics listener is split from the app listener so\nNetworkPolicy and auth posture can diverge cleanly between app traffic and monitoring scrapes.\n\nEach backend exposes `backend_*`-prefixed metrics; **claude is the superset** and peers track placeholders so\ncross-backend PromQL joins don't lose label sets. Harness exposes `harness_*`-prefixed infrastructure metrics. The\nharness `/metrics` endpoint also aggregates all backend `/metrics` endpoints with a `backend=\"\u003cid\u003e\"` label injected per\nsample, so a single scrape captures the full deployment.\n\nFor the full catalog, read each component's `metrics.py`. For the rendered view, see `charts/witwave/dashboards/`\n(Grafana sidecar) and `charts/witwave/templates/prometheusrule.yaml` (default alerts).\n\n```bash\ncurl -s http://localhost:9000/metrics | head\n```\n\n## Prompt env-var interpolation (#473)\n\nScheduler prompt bodies (`HEARTBEAT.md`, `jobs/*.md`, `tasks/*.md`, `triggers/*.md`, `continuations/*.md`) support\n`{{env.VAR}}` interpolation so the same markdown can ship across dev / staging / prod without forking:\n\n```yaml\n# jobs/daily-status.md\n---\nschedule: \"0 9 * * *\"\n---\nSend a daily status update. Environment: {{env.DEPLOYMENT_ENV}}.\nDashboard: https://{{env.DASHBOARD_HOST}}/team.\n```\n\nTwo env vars control the feature, both set on the harness container:\n\n| Variable               | Default | Description                                                                                             |\n| ---------------------- | ------- | ------------------------------------------------------------------------------------------------------- |\n| `PROMPT_ENV_ENABLED`   | unset   | Master toggle. When unset/false, prompt bodies pass through verbatim. Operators opt in.                 |\n| `PROMPT_ENV_ALLOWLIST` | empty   | Comma-separated prefixes or globs (`WITWAVE_*,DEPLOY_*`). References outside the allowlist become `\"\"`. |\n\nMissing vars (and non-allowlisted references) are substituted with an empty string and a warning is logged once per\nvariable. For triggers specifically, interpolation is applied to the operator-authored `.md` body **only** — inbound\nHTTP bodies are never interpolated, so callers who can hit the trigger endpoint cannot use the template engine to read\nlocal env vars.\n\n## Outbound Webhooks\n\nWebhooks fire after a prompt completes. Each webhook subscription is a markdown file under `.witwave/webhooks/` with\nfrontmatter fields:\n\n| Field                | Required | Description                                                                        |\n| -------------------- | -------- | ---------------------------------------------------------------------------------- |\n| `name`               | yes      | Subscription name (used in metrics labels)                                         |\n| `url`                | yes\\*    | POST target URL                                                                    |\n| `url-env-var`        | yes\\*    | Environment variable holding the URL (alternative to `url`)                        |\n| `notify-when`        | no       | `always`, `on_success` (default), or `on_error`                                    |\n| `notify-on-kind`     | no       | Glob list of prompt kinds to match (e.g. `a2a`, `job:*`, `heartbeat`); default `*` |\n| `notify-on-response` | no       | Glob list of patterns matched against the response text; default `*`               |\n| `secret`             | no       | HMAC secret — adds `X-Hub-Signature-256` header when set                           |\n| `content-type`       | no       | `Content-Type` header; default `application/json`                                  |\n\n\\* Either `url` or `url-env-var` is required.\n\n### URL safety (#524)\n\n- The `url:` template may only reference the built-in variables listed below. `{{env.VAR}}` references and\n  extraction-defined variables are **not** substituted in the URL field — env-derived URLs must be placed in a single\n  env var and read via `url-env-var`. **Migration:** any webhook previously using `url: http://{{env.FOO}}/…` must\n  switch to `url-env-var: FOO` — render fails loudly otherwise.\n- Only `http` and `https` URLs are accepted. Schemes like `file://`, `gopher://`, `ftp://` are rejected.\n- URLs whose host is a loopback / link-local / private / reserved IP literal (e.g. `127.0.0.1`, `169.254.169.254`,\n  `10.0.0.5`) are rejected to prevent SSRF to cloud metadata endpoints and internal services. Operators can opt specific\n  internal hosts into the allow-list via the `WEBHOOK_URL_ALLOWED_HOSTS` env var on harness (comma-separated `host` or\n  `host:port` entries).\n\nThe markdown body is the POST payload. Use `{{variable}}` placeholders for substitution in the body and header values\n(not in the URL — see above):\n\n| Variable               | Value                                          |\n| ---------------------- | ---------------------------------------------- |\n| `{{agent}}`            | Agent name (e.g. `iris`)                       |\n| `{{kind}}`             | Prompt kind (`a2a`, `heartbeat`, `job:\u003cname\u003e`) |\n| `{{session_id}}`       | Session/context ID                             |\n| `{{source}}`           | Source name (job name, trigger endpoint, etc.) |\n| `{{model}}`            | Model used for the prompt                      |\n| `{{success}}`          | `True` or `False`                              |\n| `{{error}}`            | Error message, or empty string on success      |\n| `{{response_preview}}` | First 2048 chars of the response text          |\n| `{{duration_seconds}}` | Prompt execution time in seconds               |\n| `{{timestamp}}`        | ISO 8601 UTC timestamp of delivery             |\n| `{{delivery_id}}`      | UUID unique to this delivery attempt           |\n\nIf the body is empty, a default JSON envelope is sent.\n\n## Observability\n\nPrometheus metrics are opt-in per-agent; see `charts/witwave` values for `metrics.*`, `serviceMonitor.*`, and\n`podMonitor.*`.\n\nDistributed tracing (OpenTelemetry) is also opt-in and spans harness + backends + operator when enabled. The pod-side\nSDK bootstraps already honour the standard OTel env vars (`shared/otel.py`, `operator/internal/tracing/otel.go`); the\nHelm charts own the wiring end-to-end (#634):\n\n- `charts/witwave` — `observability.tracing.enabled` + `observability.tracing.collector.enabled` deploys an in-cluster\n  OpenTelemetry Collector and points every agent pod at it. Set `observability.tracing.endpoint` to forward to an\n  out-of-band collector instead.\n- `charts/witwave-operator` — matching `observability.tracing.*` block; wire the same endpoint to trace the reconciler\n  alongside the agents.\n\nSee `charts/witwave/README.md` → \"Enabling distributed tracing\" for Jaeger and Tempo recipes.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwitwave-ai%2Fwitwave","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwitwave-ai%2Fwitwave","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwitwave-ai%2Fwitwave/lists"}