Extend the smoke harness with Orb execution-plane preflight probes (Docker, OpenCode, Gitea reachability/creds/PR-creation, repository writable) that report BLOCKED with exact reasons when dependencies are absent. Add scripts/orb-project-run.ts as the missing connection point between OrbProjectManager and the Orb/Git lifecycle. Write docs/DOGFOOD_V0.md covering architecture, startup order, env vars, local/server flows, demo procedure, health checks, failure recovery, cleanup, known limitations, and next milestones. The smoke harness now runs 18 preflight checks across control-plane and Orb execution-plane layers, all with stable markers and sanitized JSON reports. The orb:run script (ORB_RUN=1) drives one issue through the full Orb project-manager lifecycle.
19 KiB
Zopu Dogfood v0 — Complete Loop
Integration and operations document for the first end-to-end dogfood loop. Grounded in the merged code on
dogfood/v0. Read alongside docs/PRODUCT.md, docs/DESIGN.md, docs/TECH.md, and docs/SMOKE.md.
1. What this loop proves
A user sends a message in the web Work OS. Zopu (the global planning agent) extracts a Signal, routes it to a ProjectIssue, and the user starts work. The issue runs inside an isolated execution environment (Orb: Docker + AgentOS + OpenCode), produces a verified change on a work branch, opens a Gitea pull request, and reports the outcome back into durable state. A GLM planner/reviewer independently approves the result.
The loop touches every layer: control plane (Convex), agent service (Flue), execution plane (Orb runtime), model gateway, Git/Gitea, and the web UI.
2. Architecture summary
┌──────────────────────────────────────────────────────────────────┐
│ Web Work OS (apps/web) │
│ Conversation composer → Zopu agent → Signals → Work Units │
│ Start button → projectIssues.begin + Flue project-manager send │
└──────────────┬───────────────────────────────────────────────────┘
│
┌──────────▼──────────┐
│ Convex (control plane) │
│ projects, projectIssues, │
│ signals, events, artifacts│
└──────────┬──────────┘
│
┌──────────▼──────────────────────┐
│ Flue agent service (:3583) │
│ ┌─────────────────────────────┐ │
│ │ zopu agent │ │
│ │ (global conversation → │ │
│ │ Signal → issue routing) │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ project-manager agent │ │
│ │ (issue-scoped work, │ │
│ │ AgentOS sandbox, │ │
│ │ finalize_gitea_lifecycle) │ │
│ └─────────────────────────────┘ │
└──────────┬──────────────────────┘
│
┌──────────▼──────────────────────────────────────┐
│ Orb Runtime (Docker + AgentOS + OpenCode) │
│ OrbProjectManager orchestrates: │
│ createOrb → prepareRepository → openSession │
│ → sendTask (context pack) → model turn │
│ → WORK_COMPLETE / NEEDS_INPUT detection │
│ → Git lifecycle (commit, push, Gitea PR) │
│ Scripts: scripts/orb-proof.ts (3-stage proof) │
│ scripts/orb-project-run.ts (full issue) │
└──────────┬──────────────────────────────────────┘
│
┌──────────▼──────────┐ ┌───────────────────┐
│ Model gateway (CPA) │ │ Gitea (self-hosted)│
│ OpenAI-compatible │ │ PRs, branches │
│ minimax-m3 (worker) │ │ │
│ glm-5.2 (planner) │ │ │
└──────────────────────┘ └───────────────────┘
Two execution paths
The codebase has two ways to run issue-scoped work, both merged and functional:
-
Flue project-manager path (the web start button). The web UI calls
projectIssues.begin(Convex mutation) then sends a message to theproject-managerFlue agent (apps/web/src/hooks/use-project-workspace.ts). The agent works inside an in-process AgentOS sandbox and calls thefinalize_gitea_lifecycleaction (packages/agents/src/actions/finalize-gitea-lifecycle.ts) to open a Gitea PR. -
Orb project-manager path (Docker-isolated). The
OrbProjectManager(packages/agents/src/orb/orb-project-manager.ts) orchestrates a Docker sandbox + AgentOS VM + OpenCode session per issue. It projects raw OrbEvents into durableProjectRunEvents, detectsWORK_COMPLETE:/NEEDS_INPUT:markers, and drives the Git lifecycle viagit-adapter.ts. The entry point isscripts/orb-project-run.ts.
The Orb path provides stronger isolation (full Docker container vs. in-process VM) and is the forward path for the dedicated server. The Flue path is the current local MacBook flow.
Key files
| Component | File |
|---|---|
| Zopu agent (global routing) | packages/agents/src/agents/zopu.ts |
| Project-manager agent (Flue) | packages/agents/src/agents/project-manager.ts |
| Signal routing tools | packages/agents/src/tools/signals.ts |
| Orb runtime | packages/agents/src/orb/runtime.ts |
| Orb project-manager | packages/agents/src/orb/orb-project-manager.ts |
| Orb adapter | packages/agents/src/orb/orb-adapter.ts |
| Git adapter (Gitea lifecycle) | packages/agents/src/orb/git-adapter.ts |
| Docker sandbox | packages/agents/src/orb/docker-sandbox.ts |
| OpenCode config injection | packages/agents/src/orb/opencode-config.ts |
| Context pack assembly | packages/agents/src/orb/context-pack.ts |
| Event projection | packages/agents/src/orb/project-events.ts |
| finalize_gitea_lifecycle action | packages/agents/src/actions/finalize-gitea-lifecycle.ts |
| Smoke harness | scripts/zopu-smoke.ts |
| Orb proof fixture | scripts/orb-proof.ts |
| Orb issue driver | scripts/orb-project-run.ts |
| Web start hook | apps/web/src/hooks/use-project-workspace.ts |
| Convex projectIssues | packages/backend/convex/projectIssues.ts |
| Convex signals | packages/backend/convex/signals.ts |
| Daemon runtime | apps/daemon/src/runtime.ts |
3. Service startup order
Services must start in dependency order. The daemon owns the in-process RivetKit engine that the agent service depends on.
Local MacBook development
# 1. Convex dev server (control plane + generated API)
bun run dev:server
# 2. Agent daemon (Effect daemon + RivetKit engine + AgentOS)
bun run dev:daemon
# 3. Flue agent service (loads zopu + project-manager agents on :3583)
bun run dev:agents
# 4. Web app (Vite on :5173)
bun run dev:web
# 5. (Optional) Docker Desktop — required for the Orb path
open -a Docker
Dedicated server
See deploy/zopu-runtime/README.md. The systemd order is:
systemctl start zopu-daemon # starts RivetKit engine on :6420
sleep 3
systemctl start zopu-agent # starts Flue on :3583
The daemon must be active before the agent service because registry.start() boots the in-process RivetKit engine that the agent's createClient(RIVET_ENDPOINT) connects back to.
4. Required environment variables
All groups are documented in deploy/zopu-runtime/.env.template. Parsed by packages/env/src/agent.ts (parseAgentEnv) and packages/env/src/server.ts.
Convex (control plane)
| Variable | Required | Description |
|---|---|---|
CONVEX_URL |
Yes | Convex deployment URL |
CONVEX_SITE_URL |
Yes | Convex site URL (for actions) |
SITE_URL |
Yes | Web app origin (CORS / redirects) |
Self-hosted Git / Gitea
| Variable | Required | Description |
|---|---|---|
GITEA_URL |
For PR lifecycle | Gitea base URL (default: https://git.openputer.com) |
GITEA_TOKEN |
For PR lifecycle | Gitea API token for branch push + PR creation |
Model gateway (CPA)
| Variable | Required | Description |
|---|---|---|
AGENT_MODEL_PROVIDER |
Yes | Provider identifier (e.g. cheaptricks) |
AGENT_MODEL_NAME |
Yes | Model name (e.g. glm-5.2) |
AGENT_MODEL_API |
Yes | Must be openai-completions |
AGENT_MODEL_BASE_URL |
Yes | OpenAI-compatible /v1 endpoint |
AGENT_MODEL_API_KEY |
Yes | Gateway API key |
AGENT_MODEL_CONTEXT_WINDOW |
Yes | Context window in tokens |
AGENT_MODEL_MAX_TOKENS |
Yes | Max output tokens |
The smoke worker uses minimax-m3; the planner/reviewer uses glm-5.2. Both must be in the CPA model catalog.
AgentOS / RivetKit
| Variable | Required | Description |
|---|---|---|
RIVET_ENDPOINT |
No | RivetKit engine endpoint (default: http://localhost:6420) |
registry.start() boots an in-process engine (envoy mode) backed by a native Rust sidecar. No separate Rivet Engine process is required for single-node operation.
Zopu agent service (Flue)
| Variable | Required | Description |
|---|---|---|
FLUE_DB_TOKEN |
Yes | Flue persistence adapter token |
PORT |
Yes | Flue HTTP listen port (default: 3583) |
Daemon identity
| Variable | Required | Description |
|---|---|---|
DAEMON_ID |
Yes | Unique daemon identifier |
DAEMON_NAME |
Yes | Human-readable name |
DAEMON_VERSION |
Yes | Daemon version string |
DAEMON_HEARTBEAT_MS |
Yes | Heartbeat interval (default: 15000) |
DAEMON_COMMAND_LEASE_MS |
Yes | Command lease duration (default: 60000) |
Docker sandbox (Orb path)
| Variable | Required | Description |
|---|---|---|
ORB_DOCKER_IMAGE |
No | Docker image override (default: rivetdev/sandbox-agent:0.5.0-rc.2-full) |
ORB_DOCKER_WORKSPACE |
No | Host workspace root (default: /tmp/orb-workspaces) |
Docker socket access is via group membership on the dedicated server.
Service authentication
| Variable | Required | Description |
|---|---|---|
AUTH_SECRET |
If needed | Better Auth / Convex JWT secret |
5. Local MacBook development flow
# Clone and install
git clone <repo> zopu && cd zopu
git checkout dogfood/v0
bun install
# Start services in order (see startup order above)
bun run dev:server # Convex
bun run dev:daemon # daemon + RivetKit engine
bun run dev:agents # Flue agents
bun run dev:web # web UI
# Verify the loop is wired
bun run smoke:zopu
# Run the Orb proof (requires Docker + gateway)
ORB_PROOF=1 \
ORB_GATEWAY_API_KEY=... \
ORB_GATEWAY_BASE_URL=https://ai.example.com/v1 \
ORB_GATEWAY_MODEL=glm-5.2 \
ORB_GATEWAY_PROVIDER=cheaptricks \
bun run orb:proof
# Drive one issue through the Orb project-manager (requires Docker + gateway)
ORB_RUN=1 \
ORB_GATEWAY_API_KEY=... \
ORB_GATEWAY_BASE_URL=https://ai.example.com/v1 \
ORB_GATEWAY_MODEL=glm-5.2 \
ORB_GATEWAY_PROVIDER=cheaptricks \
bun run orb:run
6. Dedicated-server runtime flow
The dedicated server runs only the execution plane. Convex is already deployed as the control plane.
# SSH into the fresh Debian 12 server as root
export ZOPU_REPO_URL="ssh://git@git.openputer.com:2222/puter/zopu-code.git"
export ZOPU_REPO_BRANCH="dogfood/v0"
bash deploy/zopu-runtime/bootstrap.sh
# Edit .env with real values
nano /opt/zopu/.env
# Start services
systemctl start zopu-daemon
sleep 3
systemctl start zopu-agent
# Enable timers
systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer
# Verify
/opt/zopu/deploy/zopu-runtime/scripts/health-check.sh
See deploy/zopu-runtime/README.md for update, rollback, Docker cleanup, disk monitoring, firewall, and Tailscale details.
7. Demo procedure
Prerequisites: all services running, a disposable test project with a connected repository source, and the issue-scoped artifact set.
- Open the web app (
http://localhost:5173or the Tailscale hostname). - Select the test project in the Work OS.
- Type an actionable message in the composer (e.g. "Add a tiny status indicator to the dashboard"). Zopu creates a Signal and routes it to a ProjectIssue.
- The Work Unit card appears. Click Start work.
- The web UI calls
projectIssues.beginthen sends the issue to theproject-managerFlue agent. The agent works inside its sandbox. - Watch the Work Unit timeline for
run.session_opened,run.agent_message, andrun.command_executedevents. - When the agent emits
WORK_COMPLETE:, the Git lifecycle runs (commit, push, Gitea PR). - The Work Unit card shows the PR link. Click Review changes to open the Gitea PR.
- Merge remains a manual action.
For the Orb Docker path, replace step 4-5 with:
ORB_RUN=1 \
ORB_GATEWAY_API_KEY=... \
ORB_GATEWAY_BASE_URL=... \
ORB_GATEWAY_MODEL=glm-5.2 \
ORB_GATEWAY_PROVIDER=cheaptricks \
GITEA_URL=... GITEA_TOKEN=... \
ORB_RUN_REPOSITORY_URL=https://git.example.com/owner/repo.git \
ORB_RUN_REPOSITORY_PATH=owner/repo \
bun run orb:run
8. Health checks
Local
bun run smoke:zopu # full preflight (all layers)
curl -s http://localhost:5173/ # web
curl -s http://localhost:3583/ # Flue (no health endpoint by design)
Dedicated server
/opt/zopu/deploy/zopu-runtime/scripts/health-check.sh
# Probes: systemd units active, RivetKit :6420 TCP, Flue :3583 TCP, docker info
9. Logs
Local
Flue agent logs go to stdout in the bun run dev:agents terminal. Daemon logs go to the bun run dev:daemon terminal.
Dedicated server
journalctl -u zopu-daemon -f # daemon (live)
journalctl -u zopu-agent -f # agent (live)
journalctl -u zopu-daemon -n 100 # last 100 lines
journalctl -u zopu-health.service -n 50
journalctl -t zopu-daemon -t zopu-agent --since "1 hour ago"
10. Failure recovery
| Symptom | Diagnosis | Recovery |
|---|---|---|
ZOPU_SMOKE_CONTRACT_BLOCKED with convex failed |
Convex dev server not running | bun run dev:server |
ZOPU_SMOKE_CONTRACT_BLOCKED with flue failed |
Flue agent service not running | bun run dev:agents |
ZOPU_SMOKE_CONTRACT_BLOCKED with docker-daemon failed |
Docker not running | Start Docker Desktop / systemctl start docker |
ZOPU_SMOKE_CONTRACT_BLOCKED with gitea-creds-valid failed |
Missing or invalid GITEA_TOKEN |
Set GITEA_URL + GITEA_TOKEN in env |
ORB_PROOF_BLOCKED at stage 2 |
AgentOS VM creation failed | Check Docker daemon + rivetdev/sandbox-agent image |
ORB_PROOF_BLOCKED at stage 3 |
Model gateway unreachable | Check ORB_GATEWAY_* env and gateway connectivity |
Work Unit stuck in needs-input |
Agent emitted NEEDS_INPUT: |
Resolve the question and send a follow-up message |
PR not created after WORK_COMPLETE |
Gitea creds or repo path missing | Verify GITEA_URL, GITEA_TOKEN, repository path |
| Daemon offline in Convex | Daemon process crashed | systemctl restart zopu-daemon |
11. Cleanup
Local Orb artifacts
docker container prune -f --filter "label=orb"
rm -rf /tmp/orb-* /tmp/orb-proof-*
Dedicated server
/opt/zopu/deploy/zopu-runtime/scripts/docker-cleanup.sh # prune stopped containers/images
Named volumes and running containers are never removed.
12. Known limitations
-
Two parallel execution paths. The Flue
project-manageragent (in-process AgentOS sandbox) and theOrbProjectManager(Docker + AgentOS + OpenCode) both work but are not yet unified behind a single dispatcher. The web start button uses the Flue path; the Orb path is driven byscripts/orb-project-run.ts. Unifying these behind the Work Unit start button is a forward milestone. -
No auto-merge. The Git lifecycle opens a pull request and stops. Merge is always a manual human action, by design.
-
Docker not wired on the dedicated server. Docker Engine is installed and the
zopuuser is in thedockergroup, but the daemon currently uses the in-process AgentOS VM sandbox. The Orb Docker path is proven locally viascripts/orb-proof.tsbut not yet the default on the server. -
OpenCode ACP gateway wiring. The model gateway works via
OPENCODE_CONFIG_CONTENTinjected through the package manifest env + OpenAI seed model remapping (seepackages/agents/src/orb/opencode-config.tsandruntime.ts). The three-stage orb proof passes. This is a known working boundary, not a general-purpose provider configuration. -
Pre-existing check failures. The root
bun run checkhas a pre-existing unrelated failure: formatting underrepos/effectand dual Hono patch versions (4.12.30 vs 4.12.31). These are not related to the dogfood loop and should not be fixed in this lane. -
Single-node only. The dedicated server runs one node with an in-process RivetKit engine. Multi-node coordination is out of scope.
-
Web app not deployed on the server. The server runs the execution plane only; the web frontend runs locally or is deployed separately.
-
No production generated-app hosting. Only the execution plane runs on the dedicated server.
13. Next three incremental milestones
-
Unify the execution dispatcher. Route the web Work Unit start button through a single dispatcher that selects the Orb Docker path (when Docker is available) or the Flue in-process path (fallback). This eliminates the two-parallel-paths limitation and makes the Orb path the default on the dedicated server. Touch point:
apps/web/src/hooks/use-project-workspace.tsstart action + a new Convex action that invokesOrbProjectManager. -
Stream Orb events into Convex ProjectEvents. The
OrbProjectManager.onProjectEventcallback currently writes to an in-memory array. Wire it to persistProjectRunEvents into the ConvexprojectEventstable so the web Work Unit timeline shows live Orb execution progress. Touch point:packages/agents/src/orb/orb-project-manager.ts+ a new Convex mutation mirroringagentWorkspace.recordGiteaLifecycle. -
Durable run resume after daemon restart.
OrbProjectManagerholds active runs in memory. Add a Convex-backed run ledger so an active Orb run can resume after a daemon restart instead of being lost. This moves the run-state machine from in-memory to durable, matching theOrbState/RunStatetransitions inpackages/agents/src/orb/domain.ts.