feat(dogfood): integrate and document complete v0 loop

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.
This commit is contained in:
-Puter
2026-07-25 02:11:35 +05:30
parent 360f0d4829
commit a11ed988fb
6 changed files with 954 additions and 4 deletions

379
docs/DOGFOOD_V0.md Normal file
View File

@@ -0,0 +1,379 @@
# 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](./PRODUCT.md), [docs/DESIGN.md](./DESIGN.md), [docs/TECH.md](./TECH.md), and [docs/SMOKE.md](./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:
1. **Flue project-manager path** (the web start button). The web UI calls `projectIssues.begin` (Convex mutation) then sends a message to the `project-manager` Flue agent (`apps/web/src/hooks/use-project-workspace.ts`). The agent works inside an in-process AgentOS sandbox and calls the `finalize_gitea_lifecycle` action (`packages/agents/src/actions/finalize-gitea-lifecycle.ts`) to open a Gitea PR.
2. **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 durable `ProjectRunEvent`s, detects `WORK_COMPLETE:` / `NEEDS_INPUT:` markers, and drives the Git lifecycle via `git-adapter.ts`. The entry point is `scripts/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
```bash
# 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](../deploy/zopu-runtime/README.md). The systemd order is:
```bash
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](../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
```bash
# 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.
```bash
# 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](../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.
1. Open the web app (`http://localhost:5173` or the Tailscale hostname).
2. Select the test project in the Work OS.
3. 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.
4. The Work Unit card appears. Click **Start work**.
5. The web UI calls `projectIssues.begin` then sends the issue to the `project-manager` Flue agent. The agent works inside its sandbox.
6. Watch the Work Unit timeline for `run.session_opened`, `run.agent_message`, and `run.command_executed` events.
7. When the agent emits `WORK_COMPLETE:`, the Git lifecycle runs (commit, push, Gitea PR).
8. The Work Unit card shows the PR link. Click **Review changes** to open the Gitea PR.
9. Merge remains a manual action.
For the Orb Docker path, replace step 4-5 with:
```bash
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
```bash
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
```bash
/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
```bash
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
```bash
docker container prune -f --filter "label=orb"
rm -rf /tmp/orb-* /tmp/orb-proof-*
```
### Dedicated server
```bash
/opt/zopu/deploy/zopu-runtime/scripts/docker-cleanup.sh # prune stopped containers/images
```
Named volumes and running containers are never removed.
## 12. Known limitations
1. **Two parallel execution paths.** The Flue `project-manager` agent (in-process AgentOS sandbox) and the `OrbProjectManager` (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 by `scripts/orb-project-run.ts`. Unifying these behind the Work Unit start button is a forward milestone.
2. **No auto-merge.** The Git lifecycle opens a pull request and stops. Merge is always a manual human action, by design.
3. **Docker not wired on the dedicated server.** Docker Engine is installed and the `zopu` user is in the `docker` group, but the daemon currently uses the in-process AgentOS VM sandbox. The Orb Docker path is proven locally via `scripts/orb-proof.ts` but not yet the default on the server.
4. **OpenCode ACP gateway wiring.** The model gateway works via `OPENCODE_CONFIG_CONTENT` injected through the package manifest env + OpenAI seed model remapping (see `packages/agents/src/orb/opencode-config.ts` and `runtime.ts`). The three-stage orb proof passes. This is a known working boundary, not a general-purpose provider configuration.
5. **Pre-existing check failures.** The root `bun run check` has a pre-existing unrelated failure: formatting under `repos/effect` and 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.
6. **Single-node only.** The dedicated server runs one node with an in-process RivetKit engine. Multi-node coordination is out of scope.
7. **Web app not deployed on the server.** The server runs the execution plane only; the web frontend runs locally or is deployed separately.
8. **No production generated-app hosting.** Only the execution plane runs on the dedicated server.
## 13. Next three incremental milestones
1. **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.ts` start action + a new Convex action that invokes `OrbProjectManager`.
2. **Stream Orb events into Convex ProjectEvents.** The `OrbProjectManager.onProjectEvent` callback currently writes to an in-memory array. Wire it to persist `ProjectRunEvent`s into the Convex `projectEvents` table 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 mirroring `agentWorkspace.recordGiteaLifecycle`.
3. **Durable run resume after daemon restart.** `OrbProjectManager` holds 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 the `OrbState` / `RunState` transitions in `packages/agents/src/orb/domain.ts`.

View File

@@ -1,9 +1,34 @@
# Zopu Integration Smoke # Zopu Integration Smoke
The smoke harness is a repeatable integration lane for the thin Zopu MVP. It probes the local web app, authenticated Convex control plane, Flue `project-manager` route, AgentOS daemon/workspace contract, and the CPA OpenAI-compatible gateway before it creates any project issue. The smoke harness is a repeatable integration lane for the thin Zopu MVP. It probes the local web app, authenticated Convex control plane, Flue `project-manager` route, AgentOS daemon/workspace contract, the CPA OpenAI-compatible gateway, and the Orb execution-plane dependencies (Docker, OpenCode, Gitea, repository writability) before it creates any project issue.
The harness does not edit CPA configuration, restart services, print bearer tokens/API keys, or run parallel worker calls. `minimax-m3` is the serial worker-loop model; `glm-5.2` performs the plan and the independent review. The harness does not edit CPA configuration, restart services, print bearer tokens/API keys, or run parallel worker calls. `minimax-m3` is the serial worker-loop model; `glm-5.2` performs the plan and the independent review.
## Preflight checks
| Check ID | Layer | What it probes |
| --- | --- | --- |
| `web` | Control plane | Web app HTTP reachability |
| `convex` | Control plane | Convex health query returns OK |
| `organization` | Control plane | Authenticated personal organization |
| `project-contract` | Control plane | Disposable project exists with a connected source |
| `agentos-workspace-contract` | Control plane | Issue-scoped artifacts present |
| `agentos-daemon` | Control plane | Online local daemon |
| `flue` | Agent service | project-manager Flue route reachable |
| `cpa-model-catalog` | Model gateway | minimax-m3 and glm-5.2 in catalog |
| `cpa-completion-minimax-m3` | Model gateway | minimax-m3 accepts a no-tools completion |
| `cpa-completion-glm-5.2` | Model gateway | glm-5.2 accepts a no-tools completion |
| `model-roles` | Model gateway | Role selection contract (worker/planner-reviewer) |
| `worker-gateway-config` | Model gateway | AGENT_MODEL_BASE_URL matches the CPA base URL |
| `docker-daemon` | Orb execution plane | Docker daemon responds to `docker version` |
| `opencode-package` | Orb execution plane | `@agentos-software/opencode` is resolvable |
| `gitea-reachable` | Orb execution plane | Gitea `/api/v1/version` endpoint reachable |
| `gitea-creds-valid` | Orb execution plane | Gitea token authenticates against `/api/v1/user` |
| `gitea-pr-creation` | Orb execution plane | Target repository accessible for PR creation |
| `repository-writable` | Orb execution plane | Orb workspace root is writable |
A missing or unreachable dependency reports the check as failed with the exact reason. The harness never fakes success.
## Setup ## Setup
Use a disposable/test project that already has a connected repository source and the issue-scoped artifact set. Export a Better Auth/Convex access token for the signed-in user, the local daemon id, and the CPA key/base URL. The CPA URL must include its OpenAI-compatible `/v1` path. Use a disposable/test project that already has a connected repository source and the issue-scoped artifact set. Export a Better Auth/Convex access token for the signed-in user, the local daemon id, and the CPA key/base URL. The CPA URL must include its OpenAI-compatible `/v1` path.
@@ -16,6 +41,14 @@ export ZOPU_SMOKE_CPA_BASE_URL='https://ai.example.invalid/v1'
export ZOPU_SMOKE_CPA_API_KEY='...' export ZOPU_SMOKE_CPA_API_KEY='...'
``` ```
For the Orb execution-plane checks (Docker, Gitea PR creation), also export:
```bash
export GITEA_URL='https://git.example.com'
export GITEA_TOKEN='...'
export ZOPU_SMOKE_REPOSITORY_PATH='owner/repo'
```
The harness reuses `AGENT_MODEL_*`, `CONVEX_URL`, `DAEMON_ID`, `SITE_URL`, and `VITE_FLUE_URL` when their `ZOPU_SMOKE_*` equivalents are absent. It never rewrites those values. Override the tiny feature request with `ZOPU_SMOKE_FEATURE_REQUEST` when the disposable project needs a more specific target. The harness reuses `AGENT_MODEL_*`, `CONVEX_URL`, `DAEMON_ID`, `SITE_URL`, and `VITE_FLUE_URL` when their `ZOPU_SMOKE_*` equivalents are absent. It never rewrites those values. Override the tiny feature request with `ZOPU_SMOKE_FEATURE_REQUEST` when the disposable project needs a more specific target.
## Commands ## Commands
@@ -41,6 +74,15 @@ The command prints one of these stable markers:
Exit codes are `0` for a passing preflight or completed run, `2` for a contract block, and `1` for a runtime failure. Reports contain only endpoint labels, statuses, counts, ids, and sanitized failure text; model transcripts and credentials are intentionally omitted. Exit codes are `0` for a passing preflight or completed run, `2` for a contract block, and `1` for a runtime failure. Reports contain only endpoint labels, statuses, counts, ids, and sanitized failure text; model transcripts and credentials are intentionally omitted.
## Cleanup
The harness does not create Docker containers or Orb workspaces in preflight mode. In run mode, the Flue `project-manager` agent owns its sandbox lifecycle. To clean up Orb proof fixtures or run artifacts:
```bash
docker container prune -f --filter "label=orb"
rm -rf /tmp/orb-* /tmp/orb-proof-*
```
## Current lane boundary ## Current lane boundary
The smoke intentionally stops before mutation when the project source, issue-scoped AgentOS artifacts, or online daemon contract is absent. Those checks are the integration boundary for the project-loop and daemon lanes; do not paper over them in this harness or make a live gateway change to satisfy a failed probe. The smoke intentionally stops before mutation when the project source, issue-scoped AgentOS artifacts, or online daemon contract is absent. The Orb execution-plane checks (Docker, OpenCode, Gitea) report BLOCKED when those external dependencies are not available on the local MacBook; this is expected and does not indicate an implementation failure. See [docs/DOGFOOD_V0.md](./DOGFOOD_V0.md) for the full lifecycle.

View File

@@ -60,7 +60,8 @@
"docs:update": "bun run scripts/update-docs.ts", "docs:update": "bun run scripts/update-docs.ts",
"subtree": "bun run scripts/subtree.ts", "subtree": "bun run scripts/subtree.ts",
"fix": "ultracite fix", "fix": "ultracite fix",
"orb:proof": "bun run scripts/orb-proof.ts" "orb:proof": "bun run scripts/orb-proof.ts",
"orb:run": "bun run scripts/orb-project-run.ts"
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {

View File

@@ -9,9 +9,13 @@ const smokeEnvSchema = z.object({
cpaApiKey: z.string().min(1), cpaApiKey: z.string().min(1),
cpaBaseUrl: z.url(), cpaBaseUrl: z.url(),
daemonId: z.string().min(1), daemonId: z.string().min(1),
dockerImage: z.string().min(1).optional(),
featureRequest: z.string().min(10), featureRequest: z.string().min(10),
flueUrl: z.url(), flueUrl: z.url(),
giteaToken: z.string().min(1).optional(),
giteaUrl: z.url().optional(),
projectId: z.string().min(1).optional(), projectId: z.string().min(1).optional(),
repositoryPath: z.string().min(1).optional(),
webUrl: z.url(), webUrl: z.url(),
}); });
@@ -31,6 +35,7 @@ export const parseSmokeEnv = (
cpaBaseUrl: cpaBaseUrl:
runtimeEnv.ZOPU_SMOKE_CPA_BASE_URL ?? runtimeEnv.AGENT_MODEL_BASE_URL, runtimeEnv.ZOPU_SMOKE_CPA_BASE_URL ?? runtimeEnv.AGENT_MODEL_BASE_URL,
daemonId: runtimeEnv.ZOPU_SMOKE_DAEMON_ID ?? runtimeEnv.DAEMON_ID, daemonId: runtimeEnv.ZOPU_SMOKE_DAEMON_ID ?? runtimeEnv.DAEMON_ID,
dockerImage: runtimeEnv.ORB_DOCKER_IMAGE,
featureRequest: featureRequest:
runtimeEnv.ZOPU_SMOKE_FEATURE_REQUEST ?? runtimeEnv.ZOPU_SMOKE_FEATURE_REQUEST ??
"Add a tiny accessible status control to the existing web project's primary page, keep existing behavior unchanged, and add a focused test for it.", "Add a tiny accessible status control to the existing web project's primary page, keep existing behavior unchanged, and add a focused test for it.",
@@ -38,7 +43,11 @@ export const parseSmokeEnv = (
runtimeEnv.ZOPU_SMOKE_FLUE_URL ?? runtimeEnv.ZOPU_SMOKE_FLUE_URL ??
runtimeEnv.VITE_FLUE_URL ?? runtimeEnv.VITE_FLUE_URL ??
"http://localhost:3583", "http://localhost:3583",
giteaToken: runtimeEnv.GITEA_TOKEN ?? runtimeEnv.ZOPU_SMOKE_GITEA_TOKEN,
giteaUrl: runtimeEnv.GITEA_URL ?? runtimeEnv.ZOPU_SMOKE_GITEA_URL,
projectId: runtimeEnv.ZOPU_SMOKE_PROJECT_ID, projectId: runtimeEnv.ZOPU_SMOKE_PROJECT_ID,
repositoryPath:
runtimeEnv.ZOPU_SMOKE_REPOSITORY_PATH ?? runtimeEnv.GITEA_REPOSITORY_PATH,
webUrl: webUrl:
runtimeEnv.ZOPU_SMOKE_WEB_URL ?? runtimeEnv.ZOPU_SMOKE_WEB_URL ??
runtimeEnv.SITE_URL ?? runtimeEnv.SITE_URL ??

268
scripts/orb-project-run.ts Normal file
View File

@@ -0,0 +1,268 @@
/**
* Orb Project Run — Integration Glue Entry Point
*
* Connects OrbProjectManager (packages/agents/src/orb/orb-project-manager.ts)
* to the Orb runtime (Docker + AgentOS + OpenCode) and the Git/Gitea lifecycle
* (packages/agents/src/orb/git-adapter.ts). This is the missing connection
* point: OrbProjectManager was fully implemented and live-tested but had no
* production entry point — no Convex action, no script invocation.
*
* Usage:
* 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 \
GITEA_URL=https://git.example.com \
GITEA_TOKEN=... \
ORB_RUN_REPOSITORY_URL=https://git.example.com/owner/repo.git \
ORB_RUN_REPOSITORY_PATH=owner/repo \
bun run scripts/orb-project-run.ts
*
* Stable markers: ORB_RUN_COMPLETED (exit 0), ORB_RUN_BLOCKED (exit 2),
* ORB_RUN_FAILED (exit 1). The script never prints gateway secrets; only
* provider/model identifiers and sanitized status text.
*
* See docs/DOGFOOD_V0.md for the full lifecycle and the relationship between
* this entry point, the Flue project-manager agent, and the web start button.
*/
/* eslint-disable no-console -- CLI tool */
import { createGiteaHttpTransport } from "../packages/agents/src/git/gitea";
import type { OrbModelGatewayConfig } from "../packages/agents/src/orb/domain";
import { createGitLifecyclePort } from "../packages/agents/src/orb/git-adapter";
import { createOrbAdapter } from "../packages/agents/src/orb/orb-adapter";
import { OrbProjectManager } from "../packages/agents/src/orb/orb-project-manager";
const MARKER = {
blocked: "ORB_RUN_BLOCKED",
completed: "ORB_RUN_COMPLETED",
failed: "ORB_RUN_FAILED",
} as const;
interface RunConfig {
readonly gateway: OrbModelGatewayConfig;
readonly giteaToken: string | undefined;
readonly giteaUrl: string | undefined;
readonly issueBody: string;
readonly issueNumber: number;
readonly issueTitle: string;
readonly repositoryPath: string | undefined;
readonly repositoryUrl: string | undefined;
readonly baseBranch: string;
readonly workspaceRoot: string;
}
const envValue = (key: string): string | undefined => process.env[key];
const resolveGateway = (): OrbModelGatewayConfig | null => {
const apiKey =
envValue("ORB_GATEWAY_API_KEY") ?? envValue("AGENT_MODEL_API_KEY");
const baseUrl =
envValue("ORB_GATEWAY_BASE_URL") ?? envValue("AGENT_MODEL_BASE_URL");
const model = envValue("ORB_GATEWAY_MODEL") ?? envValue("AGENT_MODEL_NAME");
const provider =
envValue("ORB_GATEWAY_PROVIDER") ?? envValue("AGENT_MODEL_PROVIDER");
if (
!apiKey?.trim() ||
!baseUrl?.trim() ||
!model?.trim() ||
!provider?.trim()
) {
return null;
}
return { apiKey, baseUrl, model, provider };
};
const resolveConfig = (): RunConfig | null => {
const gateway = resolveGateway();
if (!gateway) {
return null;
}
const issueTitle = envValue("ORB_RUN_ISSUE_TITLE") ?? "Orb run: echo test";
const issueBody =
envValue("ORB_RUN_ISSUE_BODY") ??
"Reply with WORK_COMPLETE: orb project run succeeded";
return {
baseBranch: envValue("ORB_RUN_BASE_BRANCH") ?? "main",
gateway,
giteaToken: envValue("GITEA_TOKEN") ?? envValue("ORB_RUN_GITEA_TOKEN"),
giteaUrl: envValue("GITEA_URL") ?? envValue("ORB_RUN_GITEA_URL"),
issueBody,
issueNumber: Number(envValue("ORB_RUN_ISSUE_NUMBER") ?? "1"),
issueTitle,
repositoryPath:
envValue("ORB_RUN_REPOSITORY_PATH") ??
envValue("ZOPU_SMOKE_REPOSITORY_PATH"),
repositoryUrl: envValue("ORB_RUN_REPOSITORY_URL"),
workspaceRoot:
envValue("ORB_DOCKER_WORKSPACE") ?? `/tmp/orb-project-run-${Date.now()}`,
};
};
const runComplete = async (): Promise<number> => {
const config = resolveConfig();
if (!config) {
console.error(
"[orb-run] missing gateway config: set ORB_GATEWAY_* (or AGENT_MODEL_* fallbacks)."
);
console.log(`\n${MARKER.blocked}`);
return 2;
}
const {
baseBranch,
gateway,
giteaToken,
giteaUrl,
issueBody,
issueNumber,
issueTitle,
repositoryPath,
repositoryUrl,
workspaceRoot,
} = config;
console.log(
`[orb-run] gateway provider=${gateway.provider} model=${gateway.model}`
);
// The Orb adapter wraps the OrbRuntime (Docker + AgentOS + OpenCode).
const orbAdapter = createOrbAdapter();
const events: { type: string; text?: string }[] = [];
// The Git lifecycle adapter connects Orb sandbox commands to Gitea PR
// creation. If Gitea is not configured, the Git lifecycle is skipped but
// the Orb run still completes the model turn and repository preparation.
const createGitLifecycle = (
orb: Parameters<typeof createGitLifecyclePort>[0]
) => {
if (!giteaUrl || !giteaToken) {
throw new Error(
"GITEA_URL and GITEA_TOKEN are required for the Git publish lifecycle"
);
}
return createGitLifecyclePort(
orb,
createGiteaHttpTransport({ baseUrl: giteaUrl, token: giteaToken })
);
};
const pm = new OrbProjectManager({
createGitLifecycle,
onProjectEvent: (e) => {
events.push({ text: e.text, type: e.type });
const summary = (e.text ?? e.type).slice(0, 120);
console.log(` [event] ${e.type}: ${summary}`);
},
orbAdapter,
});
const issueId = `orb-run-${Date.now()}`;
const branchName = `work/orb-run-${issueNumber}-${Date.now()}`;
let result;
try {
result = await pm.startIssue({
baseBranch,
branchName,
context: {
artifacts: [],
contextFiles: [],
issueBody,
issueTitle,
repositoryUrl,
},
contextPack: {
artifacts: [],
contextFiles: [],
evidence: [],
issueBody,
issueNumber,
issueTitle,
repositoryMetadata: {
baseBranch,
repositoryName: repositoryPath ?? "orb-run",
repositoryUrl: repositoryUrl ?? "local",
},
},
docker: {
hostWorkspacePath: workspaceRoot,
},
gateway,
issueId,
issueNumber,
issueTitle,
projectId: "orb-run",
runId: `run-${Date.now()}`,
});
console.log(
`[orb-run] session opened: ${result.sessionId ?? "?"} (status=${result.status})`
);
// If the run completed with a WORK_COMPLETE marker, drive the Git
// lifecycle. If Gitea is not configured, the model turn + repository
// preparation still constitutes a successful Orb run.
if (result.status === "completing" || result.status === "completed") {
if (giteaUrl && giteaToken && repositoryPath) {
console.log("[orb-run] driving Git/Gitea lifecycle...");
try {
const gitResult = await pm.complete({ issueId });
console.log(
`[orb-run] Git lifecycle: ${gitResult.status} branch=${gitResult.branch}`
);
} catch (error) {
console.log(
`[orb-run] Git lifecycle failed: ${error instanceof Error ? error.message : String(error)}`
);
}
} else {
console.log(
"[orb-run] Gitea not configured; skipping Git lifecycle (model turn completed)"
);
}
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[orb-run] failed: ${message}`);
await pm.cancel(issueId).catch((cancelError: unknown) => void cancelError);
console.log(`\n${MARKER.failed}`);
return 1;
}
await pm.cancel(issueId).catch((cancelError: unknown) => void cancelError);
const eventTypes = events.map((e) => e.type);
console.log(
`[orb-run] events: ${[...new Set(eventTypes)].join(", ")} (${events.length} total)`
);
const hasSessionOpened = eventTypes.includes("run.session_opened");
if (hasSessionOpened) {
console.log(`\n${MARKER.completed}`);
return 0;
}
console.log(`\n${MARKER.failed}`);
return 1;
};
if (envValue("ORB_RUN") !== "1") {
console.error(
"Set ORB_RUN=1 to drive one issue through the Orb project-manager (requires Docker + model gateway)."
);
console.error(MARKER.blocked);
process.exit(2);
}
(async () => {
try {
const code = await runComplete();
process.exit(code);
} catch (error) {
console.error("[orb-run] unexpected failure:", error);
console.error(MARKER.failed);
process.exit(1);
}
})();

View File

@@ -78,7 +78,8 @@ const { values, positionals } = parseArgs({
const usage = `Usage: bun run smoke:zopu [--run] [--report <path>] [--timeout-ms <ms>] const usage = `Usage: bun run smoke:zopu [--run] [--report <path>] [--timeout-ms <ms>]
Without --run, the command only probes the local web/Convex/Flue/AgentOS and CPA contracts. Without --run, the command probes the control-plane (web/Convex/Flue/AgentOS/CPA)
and the Orb execution-plane (Docker/OpenCode/Gitea/repository) contracts.
With --run, it creates one issue in the explicit smoke project, drives the worker, and sends the result to the GLM planner/reviewer. With --run, it creates one issue in the explicit smoke project, drives the worker, and sends the result to the GLM planner/reviewer.
Required environment: Required environment:
@@ -93,6 +94,13 @@ Optional environment:
ZOPU_SMOKE_FEATURE_REQUEST override the tiny feature request ZOPU_SMOKE_FEATURE_REQUEST override the tiny feature request
ZOPU_SMOKE_WEB_URL default: SITE_URL or http://localhost:5173 ZOPU_SMOKE_WEB_URL default: SITE_URL or http://localhost:5173
ZOPU_SMOKE_FLUE_URL default: VITE_FLUE_URL or http://localhost:3583 ZOPU_SMOKE_FLUE_URL default: VITE_FLUE_URL or http://localhost:3583
Orb execution-plane environment (probed and reported as BLOCKED when absent):
GITEA_URL self-hosted Gitea base URL
GITEA_TOKEN Gitea API token for PR creation
ZOPU_SMOKE_REPOSITORY_PATH owner/repo slug for the PR-creation probe
ORB_DOCKER_IMAGE optional Docker image override
ORB_DOCKER_WORKSPACE optional Orb workspace root (default /tmp/orb-*)
`; `;
if (values.help || positionals.length > 0) { if (values.help || positionals.length > 0) {
@@ -580,6 +588,243 @@ const probeGateway = async (input: {
} }
}; };
// ---------------------------------------------------------------------------
// Orb-path preflight probes — Docker, OpenCode, Git/Gitea, repository writable
//
// These probes check the Orb execution-plane dependencies that the
// OrbProjectManager relies on. They are intentionally separate from the
// control-plane/gateway probes: a missing Docker daemon or Gitea token
// reports as a BLOCKED preflight item, never a fake success.
// ---------------------------------------------------------------------------
const probeDocker = async (checks: SmokeCapabilityCheck[]): Promise<void> => {
try {
const proc = Bun.spawn(
["docker", "version", "--format", "{{.Server.Version}}"],
{ stderr: "pipe", stdout: "pipe" }
);
const stdout = await new Response(proc.stdout).text();
const exitCode = await proc.exited;
const version = stdout.trim();
if (exitCode === 0 && version.length > 0) {
addCheck(
checks,
"docker-daemon",
true,
`Docker server version ${version}`
);
} else {
addCheck(
checks,
"docker-daemon",
false,
"Docker daemon is not running; start Docker Desktop or dockerd"
);
}
} catch (error) {
addCheck(
checks,
"docker-daemon",
false,
`Docker CLI unavailable: ${errorMessage(error, [])}`
);
}
};
const probeOpenCodePackage = async (
checks: SmokeCapabilityCheck[]
): Promise<void> => {
try {
await import("@agentos-software/opencode");
addCheck(
checks,
"opencode-package",
true,
"@agentos-software/opencode is resolvable"
);
} catch {
addCheck(
checks,
"opencode-package",
false,
"@agentos-software/opencode is not installed; Orb AgentOS VM cannot link OpenCode"
);
}
};
const probeGitea = async (input: {
readonly checks: SmokeCapabilityCheck[];
readonly giteaToken: string | undefined;
readonly giteaUrl: string | undefined;
readonly repositoryPath: string | undefined;
readonly secrets: readonly string[];
readonly timeoutMs: number;
}): Promise<void> => {
const { checks, giteaToken, giteaUrl, repositoryPath, secrets, timeoutMs } =
input;
if (giteaUrl === undefined) {
addCheck(
checks,
"gitea-reachable",
false,
"GITEA_URL is not set; set it to the self-hosted Gitea base URL for the Orb Git lifecycle"
);
} else {
try {
const response = await fetch(
`${giteaUrl.replace(/\/$/u, "")}/api/v1/version`,
{
headers: giteaToken ? { Authorization: `token ${giteaToken}` } : {},
signal: withTimeout(timeoutMs),
}
);
addCheck(
checks,
"gitea-reachable",
response.ok,
response.ok
? "Gitea API version endpoint is reachable"
: `Gitea API returned HTTP ${response.status}`
);
} catch (error) {
addCheck(
checks,
"gitea-reachable",
false,
`Gitea API is unreachable: ${errorMessage(error, secrets)}`
);
}
}
if (giteaUrl === undefined || giteaToken === undefined) {
addCheck(
checks,
"gitea-creds-valid",
false,
giteaToken === undefined
? "GITEA_TOKEN is not set; the Orb Git lifecycle needs it to create pull requests"
: "Gitea URL is not configured; cannot validate token"
);
} else {
try {
const response = await fetch(
`${giteaUrl.replace(/\/$/u, "")}/api/v1/user`,
{
headers: { Authorization: `token ${giteaToken}` },
signal: withTimeout(timeoutMs),
}
);
addCheck(
checks,
"gitea-creds-valid",
response.ok,
response.ok
? "Gitea token is valid"
: `Gitea token returned HTTP ${response.status}`
);
} catch (error) {
addCheck(
checks,
"gitea-creds-valid",
false,
`Gitea credential probe failed: ${errorMessage(error, secrets)}`
);
}
}
if (
repositoryPath === undefined ||
giteaToken === undefined ||
giteaUrl === undefined
) {
addCheck(
checks,
"gitea-pr-creation",
false,
repositoryPath === undefined
? "ZOPU_SMOKE_REPOSITORY_PATH (owner/repo) is not set; cannot probe PR creation"
: "Gitea URL or token is missing; cannot probe PR creation"
);
} else {
try {
const path = repositoryPath
.split("/")
.map((segment) => encodeURIComponent(segment))
.join("/");
const response = await fetch(
`${giteaUrl.replace(/\/$/u, "")}/api/v1/repos/${path}`,
{
headers: { Authorization: `token ${giteaToken}` },
signal: withTimeout(timeoutMs),
}
);
addCheck(
checks,
"gitea-pr-creation",
response.ok,
response.ok
? `Repository ${repositoryPath} is accessible for PR creation`
: `Repository ${repositoryPath} returned HTTP ${response.status}`
);
} catch (error) {
addCheck(
checks,
"gitea-pr-creation",
false,
`Gitea repository probe failed: ${errorMessage(error, secrets)}`
);
}
}
};
const probeRepositoryWritable = async (
checks: SmokeCapabilityCheck[]
): Promise<void> => {
const { mkdir, rm, writeFile } = await import("node:fs/promises");
const workspace =
process.env.ORB_DOCKER_WORKSPACE ?? "/tmp/orb-smoke-workspace-probe";
try {
await mkdir(workspace, { recursive: true });
const probe = `${workspace}/.orb-write-probe`;
await writeFile(probe, "ok");
await rm(probe);
addCheck(
checks,
"repository-writable",
true,
`Orb workspace root ${workspace} is writable`
);
} catch (error) {
addCheck(
checks,
"repository-writable",
false,
`Orb workspace root is not writable: ${errorMessage(error, [])}`
);
}
};
const probeOrbCapabilities = async (input: {
readonly checks: SmokeCapabilityCheck[];
readonly env: SmokeEnv;
readonly secrets: readonly string[];
readonly timeoutMs: number;
}): Promise<void> => {
const { checks, env, secrets, timeoutMs } = input;
await probeDocker(checks);
await probeOpenCodePackage(checks);
await probeGitea({
checks,
giteaToken: env.giteaToken,
giteaUrl: env.giteaUrl,
repositoryPath: env.repositoryPath,
secrets,
timeoutMs,
});
await probeRepositoryWritable(checks);
};
const runSmoke = async (input: { const runSmoke = async (input: {
readonly baseReport: Omit<SmokeReport, "status">; readonly baseReport: Omit<SmokeReport, "status">;
readonly convex: ConvexHttpClient; readonly convex: ConvexHttpClient;
@@ -772,6 +1017,12 @@ const main = async (): Promise<number> => {
timeoutMs, timeoutMs,
}); });
await probeOrbCapabilities({
checks,
env,
secrets,
timeoutMs,
});
const initialLoop = initialSmokeLoopState(); const initialLoop = initialSmokeLoopState();
let loop: SmokeLoopState; let loop: SmokeLoopState;
try { try {