Chad — The Orchestrator-Agent Development Stack¶
chad-dev branch of NemoClaw. This document is the single source of truth
for how Chad is developed, how he is deployed, and how he spawns sub-agents
from inside the sandbox. Read it before you edit anything under
.github/skills/chad-*, scripts/chad-*, or the subagent-* policy
presets.
Chad is an always-on OpenClaw agent running inside a NemoClaw sandbox. He reads mail, tracks memory, grooms his own state, and — as of this branch — can delegate structured sub-tasks to typed sub-agents running under their own L7 network policies.
1. Why a Docker cluster at all¶
A naked CLI agent has no blast radius story. An interactive shell is fine for testing, but the moment you let an agent run unattended (cron, email, Slack, Discord) you need an answer to three questions:
| Question | Answer |
|---|---|
What happens if the agent runs rm -rf $HOME? |
It runs inside a container that only mounts /sandbox. |
What happens if the agent exfiltrates ~/.ssh/id_rsa? |
The container has no host filesystem access. |
| What happens if a prompt-injection rewrites his egress policy? | The policy is enforced by an L7 gateway in a separate user, and the config is hash-verified at entrypoint. |
NemoClaw wraps OpenClaw in a sandbox with three concentric rings:
┌─ Host (macOS / Linux) ────────────────────────────────────┐
│ nemoclaw CLI, credentials, launcher │
│ ┌─ Container runtime (docker / k3s on k8s) ──────────┐ │
│ │ OpenShell sandbox, gateway user, capsh drops │ │
│ │ ┌─ Sandbox user ────────────────────────────┐ │ │
│ │ │ Chad, pi, claude, gh, curl, chromium │ │ │
│ │ │ L7 policies: per-binary egress rules │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
The middle ring is where the cluster story matters. We ship two deployment flavors:
- Single-host Docker (
nemoclaw sandbox create) — one container, bind-mounted PVC, fastest iteration loop. - k8s + nested k3s (
k8s/nemoclaw-k8s.yaml) — adocker-in-dockerpod running a nested k3s node, which in turn runs the OpenShell sandbox. This is the deployment shape that scales to multiple Chads (one per user / per workspace / per model) without any of them seeing each other.
The cluster benefits we care about for Chad specifically:
- Per-Chad isolation. Each sandbox is its own network namespace, its own filesystem, its own set of L7 policies. You can run a "coding Chad" on Nemotron and a "research Chad" on Claude in the same k3s cluster without sharing memory or credentials.
- Self-healing restarts. Chad crashes? The Deployment controller
brings him back with the same PVC. His
workspace/survives because it lives on the PVC, not inside the image. - Atomic image refresh. A new Chad build is a new
ghcr.io/nvidia/nemoclaw/sandbox-baselayer plus a new per-build top layer. Rolling a new build iskubectl rollout restart— Chad comes back with new tools but same memory. - Sub-agent fan-out. Today sub-agents run inside the Chad container (budget-policed, but not isolated from each other). Phase-2 will spawn each sub-agent into its own pod in the same k3s cluster so the reviewer cannot read the writer's draft unless the parent explicitly shuttles it.
2. The Dockerfile split (and why it matters for iteration speed)¶
The sandbox image is built in two files on purpose:
| File | Builds on | Contains | Rebuilt when |
|---|---|---|---|
Dockerfile.base |
node:22-slim |
apt packages, gosu, openclaw CLI, users, .openclaw dir, gh, pyyaml, chromium |
apt or openclaw version changes (rare) |
Dockerfile |
${BASE_IMAGE} |
plugin, blueprint, chad helpers, sub-agent kinds, config | every PR (fast) |
The base image lives at ghcr.io/nvidia/nemoclaw/sandbox-base:latest and
is rebuilt on its own schedule. The thin top layer is what every
nemoclaw sandbox create or CI build touches, and it finishes in
seconds instead of minutes.
Don't put anything in Dockerfile.base unless it changes less than
once a week. Everything Chad-specific — skills, scripts, kind
manifests, orchestrator helpers — belongs in the top Dockerfile.
3. The development flow for Chad¶
3.1 One-command recovery (the happy path)¶
For a restart or host reboot of an existing sandbox, use
bash scripts/chad-ops.sh doctor and then chad-ops recover instead; see
docs/operations/chad-ops.md for the reboot
runbook (gateway shadowing on :8080, CLI certs, SSH handshake secret).
chad-setup.sh below is for a blank sandbox.
After a sandbox reset or a fresh deploy:
# Host side:
nemoclaw sandbox create --image ghcr.io/nvidia/nemoclaw/sandbox:latest
./scripts/chad-setup.sh <sandbox-name>
chad-setup.sh is idempotent and runs, in order:
- Restore — pulls
workspace/fromtantodefi/chad-stateviachad-restore-from-github. If there is no backup yet this is a no-op. - Sync skills — ships
proton-calendar,chad-bug-intake, andchad-orchestratorfrom.github/skills/to/sandbox/.openclaw-data/skills/via a tar-over-ssh pipeline. This uses an atomic tempdir-then-mv swap (see §9) so partial tar failures don't corrupt a live skill. - Deploy credentials — filters the host credentials file and writes
the runtime-safe subset to
/sandbox/.nemoclaw/credentials.json. gh auth— uses the deployedGITHUB_TOKENto authenticate the bundledghCLI.- Clone source — shallow-clones the fork into
/sandbox/sourceso Chad can self-grep ("how did I implement this?") without egress. - Register crons — workspace-backup (6h), proton-inbox, memory-groom.
After this, Chad is "alive" — his memory, credentials, skills, and cron jobs are all in place.
3.2 Inner dev loop: changing Chad¶
Most Chad changes happen in one of four places:
| What you want to change | Where | How to deploy |
|---|---|---|
| Chad's behavior / prompts / skill docs | .github/skills/chad-*/SKILL.md |
sync-skills-to-sandbox.sh (no image rebuild) |
A helper binary like chad-spawn |
.github/skills/chad-orchestrator/scripts/*.sh |
same — sync + PATH picks it up at /sandbox/.openclaw-data/skills/chad-orchestrator/scripts/ |
| Network egress for a sub-agent kind | nemoclaw-blueprint/policies/presets/subagent-*.yaml |
nemoclaw policy apply (reloads L7 gateway) |
Anything in bin/, nemoclaw/, or Dockerfile |
respective file | rebuild image → redeploy |
The skill sync path is the fast loop. You can iterate on Chad's prompts
and helper scripts without ever touching docker. chad-spawn.sh
deliberately resolves its orchestrator dir as:
1. $CHAD_ORCH_DIR (tests)
2. /sandbox/.openclaw-data/skills/chad-orchestrator (synced from host)
3. /opt/chad-orchestrator (baked into image)
Rule (2) means a freshly synced skill shadows the baked copy. Rule (3) means a brand-new sandbox (pre-sync) still has working helpers. Never delete the baked fallback — it's the safety net for emergency recovery.
3.3 Running Chad as a developer¶
# On the host
openshell ssh chad # drops you into the sandbox as sandbox user
# Inside the sandbox
openclaw agent --agent main # start Chad interactively
# or
chad-intake --from chat --task-file /tmp/task.md
The interactive path is where you prove a new skill works. The
chad-intake path is what cron calls — it's the same contract email,
Slack, or a GitHub issue would use.
4. The sub-agent contract¶
Chad's headline capability in this branch: he can delegate. Not in the
"fire a prompt" sense, but in the "spawn a typed process under a
separate L7 policy with a bounded budget and a structured return
value" sense. The contract lives in
.github/skills/chad-orchestrator/SKILL.md
and is implemented by six baked-in binaries.
4.1 The six helpers¶
| Binary | Purpose |
|---|---|
chad-route |
Classify a task into a kind (coder/researcher/writer/reviewer) deterministically. |
chad-budget |
Token budget bookkeeping with UTC day reset. show / reserve / refund / reset. |
chad-spawn |
Canonical spawner. Loads the kind manifest, checks budget, runs the sub-agent, writes result.json. |
chad-spawn-status |
Query the task ledger by id / state. |
chad-collect |
Merge recent result.json files into today's memory/<YYYY-MM-DD>.md. |
chad-intake |
Source-agnostic wrapper. --from chat\|proton\|cron\|issue, does route→spawn→collect. |
All six are baked into /usr/local/bin/ at image build time (see
Dockerfile lines ~115–133) and shipped from
.github/skills/chad-orchestrator/scripts/ via chad-setup.sh so you
can iterate on them without a rebuild.
4.2 Kinds¶
A kind is a YAML manifest under
.github/skills/chad-orchestrator/kinds/
describing how to invoke a sub-agent. Today we ship seven:
| Kind | Binary | Policy preset | Substrate | Timeout / Budget | Use case |
|---|---|---|---|---|---|
coder |
/usr/local/bin/pi |
pi-agent |
local | 600s / 50000 tok | Write/refactor code, run build + tests |
researcher |
/usr/local/bin/claude |
subagent-researcher |
local | 300s / 20000 tok | gh search, web facts, report |
writer |
/usr/local/bin/claude |
subagent-writer |
local | 600s / 25000 tok | Draft mail, docs, articles (never publishes — one spawn per article) |
reviewer |
/usr/local/bin/claude |
subagent-reviewer |
local | 300s / 25000 tok | Audit PR diff, run checklist (read-only gh) |
fitness |
/usr/local/bin/claude |
subagent-researcher |
local | 300s / 15000 tok | Strength + mobility answers from gbrain-ingested books (Rippetoe, Starrett) — brain-first, falls back to archive.org |
codex |
/usr/local/bin/codex |
subagent-codex |
gha | 600s / 50000 tok | OpenAI Codex CLI as alternate code/research backend. NVIDIA fallback when OPENAI_API_KEY absent. |
opencode |
/usr/local/bin/opencode |
subagent-opencode |
gha | 600s / 50000 tok | Multi-provider coding CLI (OpenAI/Anthropic/OpenRouter/NVIDIA). Honors OPENAI_BASE_URL for fallback routing. |
Substrates. Each kind picks where it executes:
local— sub-agent runs in-container under the kind's L7 policy preset. Existing default. Synchronous spawn.gha— sub-agent runs on a GitHub Actions runner viatantodefi/chad-state(popebot-style branch-as-job-record). Lets us add new providers (codex, opencode) without baking binaries into the Chad image. Loses L7 policy enforcement; gains real isolation per spawn. See docs/design/spawn-as-github-run.md.
Per-spawn override: chad-spawn --substrate gha (or --substrate local).
Per-spawn binary swap: chad-spawn --binary-override /usr/local/bin/codex --kind writer ... (the kind's L7 policy still applies, so the override binary must be in its allowlist).
Async (gha-only): chad-spawn --async --substrate gha ... returns task_id immediately; the chad-spawn-poll-watchdog launchd job (host-side, 5 min) reconciles when the runner commits result.json back. (Was an openclaw cron until 2026-05-14 — moved out for cost: ~12.7M tokens/day saved. See docs/operations/chad-devflow.md § Host-side watchdogs.)
The manifest shape:
kind: coder
binary: /usr/local/bin/pi
invocation: prompt-stdin # prompt-stdin | prompt-arg | openclaw-agent
network_policy_preset: pi-agent
substrate: local # local | gha (default: local)
default_timeout: 600
default_budget_tokens: 50000
prompt_template: |
You are a coding sub-agent spawned by Chad...
Task id: {{task_id}}
Task:
{{task}}
Adding a new kind is:
- Drop a
kinds/<name>.yamlfile. - Drop a matching preset under
nemoclaw-blueprint/policies/presets/subagent-<name>.yamlif the existing presets don't cover it. - For gha-substrate kinds: ensure
agent-job.ymlin chad-state knows how to install the binary (edit the workflow's "Install" step), and any new provider keys are set as chad-state secrets. - Sync skills + apply policy.
- Run
chad-spawn --kind <name> --task-file /tmp/t.md --dry-runto validate.
No recompile, no image rebuild.
4.3 The canonical flow¶
# 1. Classify (or skip and pick the kind manually)
kind="$(chad-route --task-file /tmp/task.md)" # echoes one of the five kinds
# 2. Spawn. Returns the task id on stdout.
task_id="$(chad-spawn --kind "$kind" --task-file /tmp/task.md)"
# 3. Poll (or block — chad-spawn is synchronous today; phase-2 goes async).
chad-spawn-status --id "$task_id"
# 4. Merge the result into today's memory.
chad-collect --today
Or, the source-agnostic shortcut:
chad-intake --from chat --task-file /tmp/task.md
chad-intake --from proton --message-id <id>
chad-intake --from issue --repo tantodefi/supachad --issue 42
chad-intake --from cron --task-file /sandbox/.openclaw-data/queue/cron-task.md
4.4 What each spawn writes to disk¶
/sandbox/.openclaw-data/
├── subagents/
│ └── <task-id>/
│ ├── task.json / task.txt # input (copied from --task-file)
│ ├── prompt.txt # rendered prompt (template + task body)
│ ├── stdout.log # sub-agent stdout
│ ├── stderr.log # sub-agent stderr
│ └── result.json # structured result (status, exit_code, summary, …)
├── queue/
│ └── tasks.jsonl # append-only ledger: queued → running → done|failed
└── budget.json # daily token budget with UTC day reset
All three paths are included in the chad-backup-to-github set so the
ledger and budget survive sandbox resets alongside workspace/memory.
4.5 Structured results¶
Sub-agents are expected to emit a JSON summary on the last non-empty
line of stdout. If they don't, chad-spawn synthesizes one:
{
"status": "done",
"task_id": "3e4f…",
"kind": "coder",
"exit_code": 0,
"dry_run": false,
"summary": "refactored runner.ts, tests pass",
"files_touched": ["nemoclaw/src/blueprint/runner.ts"],
"follow_ups": ["add an integration test for the snapshot path"]
}
chad-collect reads result.json files modified in the last hour (or
any window passed via --since) and appends a markdown table under
## Dispatched Tasks (<timestamp>) in memory/<today>.md. Dedupe is
based on a header signature in the tail of the file so it's safe to run
repeatedly — at the end of every spawn batch, from cron, or by hand.
4.6 Brain & workflow stack (gbrain + gstack)¶
Two auxiliary systems live alongside the orchestrator: gbrain provides shared persistent memory, gstack provides role-based reasoning frameworks.
gbrain¶
Hybrid vector + graph knowledge brain running as a local PGLite store at
/sandbox/.gbrain/brain.pglite. Wired into the sandbox in four places:
- Image build — installed from the
tantodefi/gbrainfork into/usr/local/lib/gbrain/with a shim at/usr/local/bin/gbrain. The bun-based install path is pinned so the binary resolves on Linux arm64. - Per-sandbox init —
chad-setup.shStep 3a runsgbrain initonly. gbrain is intentionally NOT registered as an MCP server. PGLite is single-process; an always-ongbrain servewould hold the file lock continuously and block every cron wrapper that uses gbrain CLI (chad-gbrain-dream,chad-issue-triage-cron, …). chad-setup.sh Step 3f actively removesmcp.servers.gbrainfrom the config on every run as defense-in-depth against wave-N plugin enables that may re-introduce it. Agents access gbrain via subprocess CLI (gbrain query,gbrain put,gbrain doctor, …); each call acquires + releases the lock atomically. See the[[pglite-single-process]]page in the memory-wiki vault for symptoms + the toggle-back-on procedure if you ever need MCP for an interactive session. - Kind prompts —
researcher,coder,reviewer, andfitnesssub-agents are instructed tomcp_gbrain_searchbefore any external API call, and to write findings back withmcp_gbrain_put_pageso the next spawn doesn't pay the same research cost twice. - Backup/restore —
chad-backup-to-github.shexports all pages as per-page.mdfiles (sha-diff-checked). Restore:gbrain import brain/ --no-embed— embeddings rebuild lazily on the first query.
Known gotcha — PGLite root-ownership. Any
gbrain importorgbrain exportrun that executes as root (e.g. during a chad-setup.sh restore step run viakubectl exec) writes WAL segments and lock files asroot:root. On the next session startupgbrain servecannot acquire the lock and aborts withTimed out waiting for PGLite lockorPGlite failed to initialize properly. Thechad-setup.shgbrain-init step now runschown -R sandbox:sandboxand removes stalepostmaster.pid/.gbrain-lockviakubectl execbeforegbrain init.
Memory maintenance pipeline¶
gbrain has three scheduled jobs that keep it current and bounded. All three write summaries to today's workspace memory file and produce feedback artifacts the signal-detector and self-improve loops consume.
| Job | Schedule | Where it runs | What it does |
|---|---|---|---|
| chad-gbrain-dream | Daily 03:30 UTC (sandbox cron) | sandbox | Ingest workspace docs + memory + events → gbrain put; extract timeline; doctor; detach gbrain embed --stale and gbrain extract links. Writes dream-digest-<date>.md and (if doctor flags [WARN/FAIL/ERROR]) feedback_brain_health_<date>.md. Also surfaces Embedded/Chunks ratio in the digest; writes feedback_embed_staleness_<date>.md if < 95% (catches a silently-stalled embed job). |
| chad-webui-ingest | Daily 04:30 UTC (host launchd) | host | Reads ~/.nemoclaw/openwebui/data/webui.db, extracts chats updated in the last 25h from the chat.chat JSON column, formats as markdown with frontmatter, SSHes content into the sandbox via gbrain put chat/<chat-id>. Idempotent. Without this, OpenWebUI conversation history is invisible to long-term memory. |
| chad-gbrain-prune | Weekly Sundays 02:00 UTC (sandbox cron) | sandbox | Retention sweep. Deletes memory/<date> and events/<date> >365d from gbrain; deletes workspace dream-digest-*.md, feedback_*.md, prune-log-*.md >30d. Protects system/*, agent/*, and the workspace <date>.md journal (source-of-truth). Default DRY_RUN=1 — set DRY_RUN=0 in the cron env to actually delete. |
The three together implement the convergence loop: openwebui chats →
gbrain (webui-ingest) → dream consolidation (dream) → bounded
storage (prune). Both CLI-originated and OpenWebUI-originated turns
end up in the same brain.
Host-side launchd inventory: dev.nemoclaw.{nvidia-proxy,
nvidia-liveness, chad-webui-ingest, chad-tunnel}.plist. Sandbox-side
cron inventory: openclaw cron list. Memory-plugin inventory: the
gateway loads 9 plugins (acpx, active-memory, browser, device-pair,
memory-lancedb, memory-wiki, phone-control, talk-voice, tokenjuice)
— memory-lancedb actively injects ~3 contextual memories per agent
turn alongside gbrain queries; it is not orphaned and must not be
deleted.
gstack¶
gstack ships two distinct tiers; only the first works inside Chad's sandbox.
Tier 1 — openclaw reasoning skills (sandbox-safe, synced by chad-setup.sh)¶
The full canonical 40-skill OpenClaw-adapter bundle from
garrytan/gstack's .openclaw/skills/ directory — no browser daemon, no
Bun binary, no Chromium. Structured reasoning frameworks the agent can
invoke in-session. Names dropped the gstack-openclaw- prefix and use
the canonical gstack-<verb> shape (e.g. gstack-investigate,
gstack-ceo-review, gstack-qa, gstack-ship, gstack-review,
gstack-retro, gstack-canary, gstack-cso, gstack-design-review,
gstack-plan-eng-review, gstack-land-and-deploy, gstack-health,
gstack-context-save/-restore, gstack-freeze/-unfreeze,
gstack-document-release, gstack-make-pdf, gstack-codex, …). The
full list is enumerated in
docs/operations/chad-skills.md.
chad-setup.sh syncs these from ~/.claude/skills/gstack/.openclaw/skills/
(note the dot-prefix — that's the canonical host-adapter dir, alongside
.cursor, .opencode, .agents, etc., kept current by
gstack-upgrade) into /sandbox/.openclaw-data/skills/ in the sandbox.
The setup script also prunes any legacy gstack-openclaw-* dirs from a
previous 4-skill sync so they don't ship as duplicates of the canonical
names. If gstack is not installed on the host the step warns and skips —
it is not a hard dependency.
Install gstack on the host once:
git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git \
~/.claude/skills/gstack
cd ~/.claude/skills/gstack && ./setup
Tier 2 — full gstack with browse daemon (host-only, not in sandbox)¶
Skills like /review, /qa, /ship, /browse, /gstack-ceo rely on a
persistent headless Chromium daemon (bun build --compile). This tier
runs on the developer's host machine (tantodefi's Mac), driven by Claude
Code, and does not belong inside the Chad sandbox — the sandbox has no
Bun, no Chromium, and the L7 policy does not allow arbitrary browser
egress.
The two tiers combine cleanly: when tantodefi asks Chad to review code,
Chad spawns a reviewer sub-agent (tier 1, in-sandbox); when tantodefi
runs /review himself in Claude Code on the host, that's tier 2 with full
diff analysis and browser-based verification.
4.7 Fitness RAG kind¶
The fitness kind is the first application-specific sub-agent and the
worked example of the brain-first pattern:
- Two books ingested once into gbrain by
chad-ingest-fitness-books.sh: Starting Strength (Rippetoe, 3rd ed., 313 chunks) and Supple Leopard (Starrett, 677 chunks). The ingest script pulls the OCR text from archive.org, chunks on paragraph boundaries with a 1-paragraph overlap, and callsgbrain putper chunk. - The kind manifest
kinds/fitness.yamlinstructs the sub-agent to run twomcp_gbrain_searchcalls, build the answer only from retrieved chunks, cite chunk titles, and emit aNOT_FOUNDsentinel if the books don't cover the topic — no fallback to general training knowledge, no web search. - Network egress is
subagent-researcherplus the archive.org rules in the preset (see §5) — enough to re-fetch a book if the brain gets wiped, nothing more.
After a one-time ingest, answers are essentially free: keyword search against PGLite, no inference unless the agent needs to synthesize across chunks.
5. Network policy presets per kind¶
Each sub-agent kind runs under a dedicated set of L7 policies pinned to
specific binary paths. This is the single most important piece of the
contract — a rogue writer cannot call GitHub at all, a rogue
reviewer cannot POST to GitHub, a rogue researcher cannot push to
a git remote.
| Preset file | Binds | Egress scope |
|---|---|---|
presets/pi-agent.yaml |
/usr/local/bin/pi |
NVIDIA inference only |
presets/subagent-researcher.yaml |
claude, openclaw, node, gh, curl, python3 |
NVIDIA inference + full github.com read + GET-only archive.org / *.archive.org for book/paper ingest |
presets/subagent-writer.yaml |
claude, openclaw, node |
NVIDIA inference only — no github, no messaging |
presets/subagent-reviewer.yaml |
claude, openclaw, node, gh, curl |
NVIDIA inference + GET-only api.github.com / github.com / raw.githubusercontent.com |
presets/gbrain.yaml |
/usr/local/bin/gbrain, bun |
loopback (host MCP) + NVIDIA inference for embeddings |
The base openclaw-sandbox policy also adds an internet_archive rule
(GET-only) pinned to python3 + curl so the top-level
chad-ingest-fitness-books.sh can run from the sandbox without
inheriting researcher privileges.
The reviewer's GET-only rule is the key asymmetry: it can fetch PR diffs but it cannot post a review or a comment. Writing verdicts back to GitHub is always the parent agent's job.
The contract we follow when adding a kind: sub-agents draft, parents
publish. If the sub-agent needs to change external state, the design
is wrong — have it write a proposed_actions array into result.json
and let Chad execute them.
6. Budget protection¶
Budget is the single safety net that makes delegation sane. Without it, a Chad loop that spawns a sub-agent that spawns a sub-agent will drain your NVIDIA key in minutes.
State lives at /sandbox/.openclaw-data/budget.json:
{
"date_utc": "2026-04-08",
"daily_limit": 500000,
"remaining_tokens": 450000,
"spent_by_kind": { "coder": 30000, "researcher": 20000 }
}
Rules:
- First spawn of any UTC day rewrites the file with
remaining_tokens = daily_limit. chad-spawncallschad-budget reserve $N $kindbefore running the sub-agent. On insufficient budget the spawn exits 77.--dry-runskips the budget check entirely — use this freely.- Override the limit with
CHAD_DAILY_TOKEN_LIMITat container start, or by editing the JSON by hand for one-off tests. - Manual reset:
chad-budget reset(typically only needed to recover from a botched manual edit).
Budget is honor-based inside the sandbox. It is not enforced by the kernel or by the L7 gateway — a sub-agent that lies about its token count will get away with it. The real kill-switch is the NVIDIA API key itself and the OpenShell inference rate limiter. Budget is there to catch honest bugs, not attackers.
7. Cron integration (token-optimized)¶
Chad runs twelve standing openclaw cron jobs plus three host-side
launchd watchdogs, all registered by chad-setup.sh (crons) and one-shot
plist loads (watchdogs). The schedules are deliberately conservative
— every cron fire tokenizes instructions and spawns a model call, so the
rule is: fewer, cheaper runs + a budget guard at the top of each one.
| Job | Cadence | Budget guard | What it does |
|---|---|---|---|
email-check |
0 2,6-23 * * * (19×/day) |
skip if remaining_tokens < 30000 |
Reads mail via proton-tool, follows EMAIL-POLICY.md rules in the workspace, logs to memory/<today>.md |
workspace-backup |
every 6h | n/a (no model call) | chad-backup-to-github with the §12 diff-check |
issue-triage |
daily 10:00 UTC | skip if remaining_tokens < 3×N×70k |
chad-issue-triage — scores open issues, routes top 2 through a researcher (see §12) |
gbrain-dream |
nightly 03:30 UTC | skip if remaining_tokens < 50k |
chad-gbrain-dream — runs gbrain dream to consolidate links and surface orphans |
self-improve |
weekly Sun 03:00 UTC | skip if remaining_tokens < 2×budget |
chad-self-improve --detach (v2, 2026-06-10) — wrapper detaches, collects last week's signal deterministically, ONE single-turn LLM call drafts prose + machine-readable proposals into feedback-proposals.md (see §13) |
chad-budget-audit |
weekly Mon 04:00 UTC | n/a (audits, no model call) | Compares last-50-runs telemetry against task-profiles.json, rolls up premium spend from /tmp/chad-premium.jsonl, appends recommendations to memory/feedback-proposals.md |
chad-proposal-apply |
daily 04:30 UTC | n/a (applies safe-list) | Reads structured proposals from feedback-proposals.md, applies bounded timeoutSeconds/maxOutputTokens edits via openclaw cron edit, appends ## Applied block. |
chad-skill-watch |
daily 09:00 UTC | n/a (diff only) | Diffs openclaw skills list --json against state/skills-snapshot.json, surfaces added/removed/changed skills under ## Skill catalog diff in today's memory. |
memory-curator |
weekly Sat 04:00 UTC | inactivity-gated (≥7d since last + ≥1h idle) + budget guard | chad-memory-curator — Hermes-style consolidation pass over memory-lancedb captures + workspace MEMORY.md. Draft-only: writes proposals to curator-runs/<utc>/proposals.json. |
spawn-gc |
weekly Mon 02:30 UTC | n/a (gh API only) | chad-spawn-gc — phase 1 reconciles orphaned ledger entries (queued/running >24h → failed/done); phase 2 prunes chad-spawn/* branches on chad-state (done=7d, failed=30d, in-flight kept; skips itself if gh is unauthenticated). |
nightly-experiments |
nightly 02:00 UTC | skip if remaining_tokens < 20000 (in-wrapper) |
(v2 2026-06-10) chad-experiment-cron — deterministic observe → evaluate → design driver; experiment verdicts and new designs come from single-turn no-tools LLM calls, never a multi-turn cron loop. Per-operator concurrent budget + regression auto-retire. See docs/operations/chad-experiments.md. |
gbrain-prune |
weekly Sun 02:00 UTC | n/a (no model call) | chad-gbrain-prune 0 — drops stale gbrain pages older than retention windows (cron passes 0 = DRY_RUN off; bare invocations still default to dry-run). |
Plus three host-side launchd watchdogs (no agent overhead, run every 5 min via SSH):
| Plist | Supervises | Why |
|---|---|---|
dev.nemoclaw.chad-gateway-watchdog |
openclaw gateway run on port 18789 |
OOM recovery with 4 GB heap |
dev.nemoclaw.chad-shim-watchdog |
chad-shim.py on port 8901 (the chad model bridge) |
Restart on death (BrokenPipeError etc.) |
dev.nemoclaw.chad-spawn-poll |
Runs chad-spawn-poll itself, instead of via an openclaw cron |
Reclaimed ~12.7M tokens/day vs. the prior agent-turn cron. See docs/operations/chad-devflow.md § Host-side watchdogs. |
What changed from the prior schedule:
email-checkused to run every 30 min (48×/day) with a ~1400-char instruction string tokenized on every fire. The new 19×/day schedule- terse message (points at
EMAIL-POLICY.mdinstead of inlining rules) cuts email-related token spend by roughly 60% without noticeably slower responses — admins get a reply within an hour instead of within 30 min, which is still well under human-scale latency for Chad's use case. - Every new cron has a budget guard at the top of its message.
The agent calls
chad-budget show --field remaining_tokensand short-circuits before hitting the model if the reserve is too low. This is how a runaway Monday doesn't drain Tuesday's pool. issue-triageandself-improveare the new feedback loop — see §12 and §13.
The orchestrator plugs into this naturally: email-check can classify
a reply as "needs-research + draft-reply" and spawn both a researcher
and a writer in one shot. issue-triage is how a human-curated GitHub
issue turns into a triage plan without Chad having to poll all day.
7.1 Wrapper-only invariant (legacy K2.5 caveat) and the hybrid Phase-2 inference path¶
openclaw cron runs in isolated sessions that re-tokenize the full
prompt every fire and don't inherit the interactive shell's env. Two
consequences:
- Cron prompts must be one-line wrapper invocations. Originally this
was forced by Kimi K2.5's multi-turn tool-call regression (550k input
tokens / 691s on a single fire). Kimi K2.5 was deprecated 2026-04-29;
the default now is
nvidia/nemotron-3-super-120b-a12b, which isreasoningSafe=true. The wrapper-only invariant still stands because nemotron-3-super on the embedded path is slow, and re-tokenizing a long instruction every cron tick is expensive regardless of model. Every cron message looks like: "Run<wrapper>. Confirm it printed<sentinel line>, then exit. Do not …" - Slow work goes via
nohup … & disowninside the wrapper. The wrapper returns within 1–60s; the actual work writes its result intomemory/<today>.mdfor the next cron tick to read.chad-workspace-backupis the canonical example. - Cron jobs are registered with
--no-deliver --best-effort-deliver. The chad sandbox'smessagingChannelslist is empty (no Telegram / Discord bound), so the gateway's default delivery path returns an error per fire and gates the run asfailed. Disable delivery globally — the wrapper's stdout sentinel is the cron's ack, not a deliverable.chad-setup.shregisters every chad-owned cron with these flags and patches pre-existing crons on every run (idempotent).
The wrappers themselves live at scripts/chad-cron-wrappers/ and are
deployed to /usr/local/bin/ by chad-setup.sh. Each wrapper is its own
SPDX-headered script — diffs are reviewable, and individual wrappers can
be hot-patched on a running sandbox via kubectl cp without rerunning
the whole setup.
Phase-1 / Phase-2 hybrid¶
The wrapper-only invariant gives correctness but loses one nice property the old "full inference" cron prompts had: actual thinking about the inbox or the issue queue. Chad recovers it with a two-phase design:
- Phase 1 — deterministic shell. The wrapper does the boring,
reliable work: parse
proton-tool inbox, classify by sender/flags, batchmark-read, drop AuthContext blobs, write the memory block. Pure shell + Python regex. Always runs. - Phase 2 — single-turn, no-tools LLM draft. When the wrapper has
parked items that warrant thought, it shells out to
chad-phase2-draft-repliesfor one assistant turn: - No MCP servers attached, no tools defined, prompt explicitly
forbids tool use → there's nothing to round-trip, so any model's
multi-turn tool-call regression (K2.5 was the original hazard;
Nemotron 3 Super 120B is
reasoningSafe=truebut the no-tools invariant survives as cheap insurance) cannot fire. - Reasoning ON for max intelligence (per profile, currently
high). - Output is a strict JSON object validated by a tolerant balanced-brace extractor; failure mode is a no-op.
- Premium routes to Sonnet (or Opus) when the parked item came from a sender / GitHub mention that holds a valid AuthContext blob.
- Drafts are NEVER sent / posted — they append to today's memory under "### Draft replies (review before sending)" or "### Issue triage drafts (review before posting)" for human gating.
Phase 2 is opt-in per task profile via a phase2: { … } block in
scripts/task-profiles.json (currently wired for email-check and
issue-triage). The block names model, thinking, max-tokens, timeout,
min-budget floor, and whether premium routing is allowed.
This is how the system gets the "full inference quality" feel back without re-introducing the multi-turn regression: the cron payload stays a dumb harness call (thinking off, tools on, reasoning-safe model), and the heavy thinking happens in the single-turn helper (thinking high, tools off, no round-trip surface to fail on).
7.2 Premium escalation (Anthropic outsource)¶
Chad's primary inference is nvidia/nemotron-3-super-120b-a12b via the
NVIDIA Endpoints free tier (Kimi K2.5 was the previous default; deprecated
2026-04-29 — see docs/inference/providers.md
for the migration note). For tasks where Nemotron's review or reasoning
depth feels shallow (multi-turn coding, deep architecture sketches), Chad
can escalate to Claude Sonnet/Opus through a tightly gated wrapper.
| Component | Path | Purpose |
|---|---|---|
chad-premium-client |
scripts/chad-cron-wrappers/chad-premium-client |
Python helper that POSTs to api.anthropic.com/v1/messages. Shells the actual HTTP call out to curl so OPA can pin a real binary identity (Python's /proc/self/exe is /usr/bin/python3 — too broad). Logs every call to /tmp/chad-premium.jsonl (model, source, identity, in/out tokens, latency). |
chad-auth-context |
scripts/chad-cron-wrappers/chad-auth-context |
AuthContext drop/show — {source, verifiedIdentity, allowsPremium, createdAt, scope}. Premium calls require allowsPremium=true. |
chad-premium |
scripts/chad-cron-wrappers/chad-premium |
User-facing wrapper. Auto-detects NEMOCLAW_INVOKER_TOKEN for terminal use; reads $CHAD_AUTH_CONTEXT_PATH for cron use. |
chad-route-prompt |
scripts/chad-cron-wrappers/chad-route-prompt |
Dashboard /premium <prompt> prefix → drops AuthContext → calls chad-premium. |
nemoclaw-blueprint/policies/presets/chad-premium.yaml |
policy preset | L7 policy: only /usr/local/bin/chad-premium-client and /usr/bin/curl may POST /v1/messages. |
Authorized invocation paths (each carries an AuthContext):
- Dashboard
/premium <prompt>→chad-route-prompt→chad-premium - Terminal
chad-premium(auto-detectsNEMOCLAW_INVOKER_TOKEN) email-checkcron when From: matchestantodefi@proton.me/supachad@proton.meissue-triagecron when an open issue mentions@supachad/@tantodefi
Cron ticks with no inbound trigger have no AuthContext — chad-premium-client
fails closed at the application layer. Even if a future bug allowed an
unauthorized python script to fabricate an AuthContext, the L7 proxy still
blocks the call because only chad-premium-client is in the binaries
allowlist.
chad-budget-audit rolls up /tmp/chad-premium.jsonl weekly (model ×
source × identity × calls × tokens × p95 latency) into the audit report.
8. Backup and recovery¶
Chad's state lives in three places, in order of criticality:
| Tier | Location | Contents | Restored by |
|---|---|---|---|
| 1. GitHub | tantodefi/chad-state (private) |
workspace/ including MEMORY.md, memory/, subagents/, queue/, budget.json; brain/ (every gbrain page as Markdown); cron/jobs.json |
chad-restore-from-github; brain-only and cron-only restores in docs/operations/gbrain.md and chad-ops cron-reload |
| 2. Host tarball | ~/.nemoclaw/backups/sandbox-state-*.tar.gz |
Full /sandbox snapshot streamed via SSH tar |
tar -xzf + kubectl cp |
| 3. Pod ephemeral fs | container writable layer (NOT a PVC) | Live filesystem | NOTHING — see warning below |
Tier 1 is the "nuclear survival" path — if every tier below is lost,
chad-setup.sh on a blank sandbox recovers everything but the running
processes.
⚠ CRITICAL —
/sandboxis NOT on a PVC. The chad pod mounts onlyopenshell-client-tls(secret),openshell-supervisor-bin, and the defaultkube-api-accessservice-account volume. All of/sandbox(~1.2 GB: gbrain pglite, openclaw config, workspace memory, lancedb, credentials) lives in the container's writable layer.Running
kubectl delete pod chad -n openshell(or anything that recreates the pod) WIPES THE BRAIN. Verify the mount situation withdocker exec openshell-cluster-nemoclaw kubectl get pod chad -n openshell -o jsonpath='{.spec.volumes[*].name}'.Before any pod-recreating operation, take a Tier-2 backup:
TS=$(date -u +%Y%m%dT%H%M%SZ) ssh openshell-chad 'cd / && tar -cf - sandbox' \ | gzip > ~/.nemoclaw/backups/sandbox-state-${TS}.tar.gzRestore is
kubectl cpthe un-tarred tree back into the new pod pluschmod +xon the wrappers. The proper fix is adding a PVC for/sandboxin the nemoclaw-blueprint StatefulSet — until that lands, treat the running pod as a single live copy.
chad-backup-to-github was updated in this branch to diff-check
before PUT: it computes the local blob sha (using the same algorithm
as git hash-object) and compares it to the remote sha before
uploading. Unchanged files are skipped. With ~20 files and a 6h cron
this saves ~400 GitHub API calls per day.
The backup set also grew on this branch:
- Workspace top-level now pushes
HEARTBEAT.mdandTOOLS.mdalongsideSOUL.md,USER.md,IDENTITY.md,AGENTS.md,MEMORY.md. - Brain pages are exported as a directory of per-page
.mdfiles (viagbrain export --dir) instead of a singlepages.ndjson. The backup script briefly stopsgbrain serveto release the PGLite lock, exports, pushes each markdown file through the sha-diff path (so unchanged pages skip), then restartsgbrain serve. The restore mirror (chad-restore-from-github.sh) usesgbrain import <dir> --no-embed, which re-indexes lazily at first query.
9. Small fixes bundled into this branch¶
- Backup diff-check.
scripts/chad-backup-to-github.shnow computes a local blob sha and skips the PUT if the remote already matches. Log line reportsPushed N files, skipped M unchanged, K errors. - Atomic skill sync.
.github/skills/chad-bug-intake/scripts/sync-skills-to-sandbox.shno longer doesrm -rf $remote_dir/$skill; tar xf -— it now extracts into a tempdir next to the live skill and atomicallymvs the live copy out and the new copy in. If tar or ssh die mid-stream, the live skill is untouched and the sync rolls back. This prevents the "half-populated skill directory" failure mode that could leavechad-spawn.shunable to find itskinds/tree. - Dockerfile wiring. The six orchestrator helpers and the kind
manifests are now baked into the image (
/usr/local/bin/chad-*and/opt/chad-orchestrator/kinds/) as a last-resort fallback when the synced skill directory is missing. - chad-setup.sh. The default skill sync list now includes
chad-orchestratoralongsideproton-calendarandchad-bug-intake. - Python UTC handling. All the Python snippets in the orchestrator
use
datetime.now(_UTC)where_UTC = getattr(datetime, "UTC", datetime.timezone.utc)so they don't emit deprecation warnings on Python 3.12+. - YAML fallback.
chad-spawn.shships a stdlib-only YAML loader so the helpers work on hosts without PyYAML (matters for local iteration — the sandbox base image always has it). - gbrain MCP auto-register.
chad-setup.shnow runsopenclaw mcp set gbrain …aftergbrain init, momentarily making/sandbox/.openclawwritable for the edit and restoring the stricter444perms onopenclaw.jsonafterwards. Sub-agents get the gbrain tools without any per-spawn flag. - Spawner cleanup.
chad-spawn.shdropped the--localflag and the inline--mcp-server gbrainwiring — both are incompatible with openclaw 2026.4.x. Gbrain lives inopenclaw.jsoninstead, and the spawner setsHOME=/sandboxsoopenclaw agentresolves its config regardless of the invoking uid. - Gbrain install path. The image now installs gbrain into a fixed
project dir (
/usr/local/lib/gbrain) from thetantodefi/gbrainfork and ships a small wrapper at/usr/local/bin/gbrain.bun install -ghad unpredictable bin-link paths on Linux arm64; this is deterministic./opt/gbrainis also whitelisted for read + execute in the base sandbox policy so bun can run it. - Sub-agent
fitness+ archive.org egress. New kind + routing rule inchad-route; newinternet_archivepolicy inopenclaw-sandbox.yamland matching rule insubagent-researcher.yaml. GET-only, pinned topython3+curl. - Router additions.
chad-routenow matches fitness vocab (squat/deadlift/mobility/Rippetoe/Starrett/…) before the genericcoderregex, so "how do I fix my squat" stops getting classified as a code task. - Onboarding typing fix.
src/lib/onboard.tsgained a localisChannelConfiguredhelper so the non-interactive messaging-setup path stops crashing when the symbol is referenced before the interactive branch defines it. - Open WebUI chat front-end.
scripts/openwebui/ships a docker-compose stack exposing open-webui via a Cloudflare Tunnel. Two modes:--mode=quick(ephemeral*.trycloudflare.com, email/password auth, MVP) and tunnel mode (managed tunnel + Cloudflare Access on a real domain, header-trusted SSO). Seedocs/operations/openwebui.md. - Chad-as-a-model.
scripts/openwebui/chad-shim.pyis a stdlib-only HTTP shim listening on127.0.0.1:8901inside the sandbox. It exposes an OpenAI-compat/v1/chat/completionsendpoint and translates each turn into oneopenclaw agent --json --agent main --session-id <hash>invocation. open-webui sees a model calledchad; under the hood, every reply benefits from gbrain context, network policies, the action-gate, and premium routing. The shim is reached from the host vianpm run webui:chad:up(one-shot SSH port-forward) orwebui:chad:install(persistent launchd LaunchAgent withKeepAlive=true).chad-setup.shdeploys the shim to/usr/local/bin/chad-shim.pyand starts it;chad-restore-from-githubandchad-backup-to-githubeach self-heal a crashed shim, so any cron pulse keeps it alive.
10. Capabilities & deliberate non-goals¶
What this branch does today.
- Nested-spawn isolation (
ghasubstrate). Sub-agents spawn onto a per-job GitHub Actions runner instead of sharing Chad's container. See docs/design/spawn-as-github-run.md. - Async spawn.
chad-spawn --async --substrate ghareturns immediately; the ledger carriessubstrate+async;chad-spawn-poll(every 5min) reconcilesresult.json;chad-spawn-gcretires branches weekly. - Two orchestrators, bridged. chad-spawn (imperative one-shot
sub-agents) plus chad-Smithers (durable JSX workflows) with the runs
IDE at
runs.supachad.com.scripts/chad-smithers/lib/spawn.js'srunSpawn()lets a workflow offload a heavy step tochad-spawn(no parallel GHA system was built). Multi-step / durable / inspectable flows run as workflows (issue-triage,content-pipeline,self-improve,memory-curator,log-digest), each crash-resumable withApprovalgates and shadow-safe by default; mechanical one-shot crons stay plain wrappers. - Nemotron 3 Ultra 550B + reasoning by default on hosted profiles (self-hosted stays Super 120B); model fusion + a daily NVIDIA model refresh keep the roster current.
- Agent backends: nemotron / claudecode / codex / opencode
(
opencode/big-pickle, via Smithers' built-in adapter) / anthropic / local, auto-routed bypickAgent(role).
Deliberate non-goals (permanent by design, not gaps):
github_pr_merge: autostays blocked — a merge is irreversible and synthesizes CI signal a human should gate.chad_self_modify_identity: autostays operator-only —SOUL.md,IDENTITY.md,USER.mdare operator-owned; Chad proposes, the operator applies.- Chad-as-a-product / multi-tenant. Single-operator agent against a private state repo, by design.
Engineering items parked with real blockers (operator infra / security review / no second user) are tracked in the operator's task list, not advertised here.
11. Safety rules¶
These are the rules I will reject PRs over:
- Never let a sub-agent spawn another sub-agent without explicit parent approval. Budget is honor-based — the contract is the only enforcement.
- Never route a task to
coderwithout Chad reading the task file first. A malicious task body can hide prompt injection. - Never commit secrets into
result.json,stdout.log, ormemory/. The backup pipeline pushes these to a private repo but the same rule applies as with any git history. - Never grant a new sub-agent kind write access to GitHub without writing the "why" into its preset header. Writes belong to the parent, draft-only belongs to the sub-agent.
- Always
--dry-runthe first invocation of a new kind or after editing anykinds/*.yamlmanifest. - Always let
chad-collectrun after a spawn batch so the ledger converges withmemory/YYYY-MM-DD.mdbefore the next cron fire. - Always keep the
/opt/chad-orchestrator/kinds/baked fallback in the Dockerfile. It's the only thing standing between a half-synced sandbox and a silent "kind not found" failure mode.
11.5 Known deploy gaps¶
A running sandbox can lag behind chad-dev if the image was built before
a wrapper was added. chad-setup.sh only runs at image build time, so
wrappers added to source after the most recent build won't appear in
/usr/local/bin/ until a rebuild.
Verify what's actually installed in a live sandbox:
for w in chad-spawn-poll chad-spawn-gc chad-memory-curator; do
ssh openshell-chad "[ -x /usr/local/bin/$w ] && echo PRESENT || echo MISSING"
done
If anything reports MISSING:
- Stopgap — deploy to a sandbox-writable path:
ssh openshell-chad 'mkdir -p /sandbox/.openclaw-data/bin'
for w in chad-spawn-poll chad-spawn-gc chad-memory-curator; do
cat scripts/chad-cron-wrappers/$w | ssh openshell-chad \
"cat > /sandbox/.openclaw-data/bin/$w && chmod +x /sandbox/.openclaw-data/bin/$w"
done
# Point cron prompts at the new path:
ssh openshell-chad "openclaw cron edit <id> --message 'Run \`/sandbox/.openclaw-data/bin/<wrapper> ...\`'"
- Permanent fix — rebuild the image.
chad-setup.shline ~354 installs every wrapper underscripts/chad-cron-wrappers/viainstall_to_usrlocal.
Symptom of running without these wrappers: matching crons spend 8–22 min
per tick with the agent grinding through which/find/ls/npx looking
for the missing binary, burning 277k–666k input tokens before idle-timeout.
The 2026-05-11 incident traced 100% of the spawn-poll / spawn-gc /
memory-curator errors to this gap. Always check this before chasing
other cron-error hypotheses.
12. Where to look next¶
- SKILL.md —
/.github/skills/chad-orchestrator/SKILL.mdis the operator-facing doc Chad reads at invocation time. - Spawner —
/.github/skills/chad-orchestrator/scripts/chad-spawn.shis the canonical implementation of the contract. - Setup script —
/scripts/chad-setup.shis the one-command-recovery path. - Devflow catalog —
/docs/operations/chad-devflow.mdlists every wrapper, its schedule, and which symptoms point at it. - Workflow scenarios —
/docs/operations/chad-workflows.mdnames the 10 expected inbound email scenarios and how each flows through the pipeline. - Open WebUI front-end —
/docs/operations/openwebui.mdcovers the chat UI, dual-provider setup, and chad-as-a-model persistence. - Policies —
/nemoclaw-blueprint/policies/presets/subagent-*.yamlare the kind-specific L7 rules. - Dockerfile —
/Dockerfilelines ~115–133 wire the orchestrator helpers into the image.
For anything NemoClaw-wide (CLI, blueprint, plugin, tests), read
CLAUDE.md and AGENTS.md.