From a11ed988fb12f729b202ea8f63bb2e2304444ac3 Mon Sep 17 00:00:00 2001 From: -Puter <22245429+puterhimself@users.noreply.github.com> Date: Sat, 25 Jul 2026 02:11:35 +0530 Subject: [PATCH] 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. --- docs/DOGFOOD_V0.md | 379 +++++++++++++++++++++++++++++++++++++ docs/SMOKE.md | 46 ++++- package.json | 3 +- packages/env/src/smoke.ts | 9 + scripts/orb-project-run.ts | 268 ++++++++++++++++++++++++++ scripts/zopu-smoke.ts | 253 ++++++++++++++++++++++++- 6 files changed, 954 insertions(+), 4 deletions(-) create mode 100644 docs/DOGFOOD_V0.md create mode 100644 scripts/orb-project-run.ts diff --git a/docs/DOGFOOD_V0.md b/docs/DOGFOOD_V0.md new file mode 100644 index 0000000..0f92b5d --- /dev/null +++ b/docs/DOGFOOD_V0.md @@ -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 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`. diff --git a/docs/SMOKE.md b/docs/SMOKE.md index 469223f..f3b3ec8 100644 --- a/docs/SMOKE.md +++ b/docs/SMOKE.md @@ -1,9 +1,34 @@ # 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. +## 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 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='...' ``` +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. ## 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. +## 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 -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. diff --git a/package.json b/package.json index bbafa16..e9d6066 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,8 @@ "docs:update": "bun run scripts/update-docs.ts", "subtree": "bun run scripts/subtree.ts", "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": {}, "devDependencies": { diff --git a/packages/env/src/smoke.ts b/packages/env/src/smoke.ts index 6df3abe..85db3c6 100644 --- a/packages/env/src/smoke.ts +++ b/packages/env/src/smoke.ts @@ -9,9 +9,13 @@ const smokeEnvSchema = z.object({ cpaApiKey: z.string().min(1), cpaBaseUrl: z.url(), daemonId: z.string().min(1), + dockerImage: z.string().min(1).optional(), featureRequest: z.string().min(10), flueUrl: z.url(), + giteaToken: z.string().min(1).optional(), + giteaUrl: z.url().optional(), projectId: z.string().min(1).optional(), + repositoryPath: z.string().min(1).optional(), webUrl: z.url(), }); @@ -31,6 +35,7 @@ export const parseSmokeEnv = ( cpaBaseUrl: runtimeEnv.ZOPU_SMOKE_CPA_BASE_URL ?? runtimeEnv.AGENT_MODEL_BASE_URL, daemonId: runtimeEnv.ZOPU_SMOKE_DAEMON_ID ?? runtimeEnv.DAEMON_ID, + dockerImage: runtimeEnv.ORB_DOCKER_IMAGE, featureRequest: 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.", @@ -38,7 +43,11 @@ export const parseSmokeEnv = ( runtimeEnv.ZOPU_SMOKE_FLUE_URL ?? runtimeEnv.VITE_FLUE_URL ?? "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, + repositoryPath: + runtimeEnv.ZOPU_SMOKE_REPOSITORY_PATH ?? runtimeEnv.GITEA_REPOSITORY_PATH, webUrl: runtimeEnv.ZOPU_SMOKE_WEB_URL ?? runtimeEnv.SITE_URL ?? diff --git a/scripts/orb-project-run.ts b/scripts/orb-project-run.ts new file mode 100644 index 0000000..8f957e4 --- /dev/null +++ b/scripts/orb-project-run.ts @@ -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 => { + 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[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); + } +})(); diff --git a/scripts/zopu-smoke.ts b/scripts/zopu-smoke.ts index 49f60c4..fe85700 100644 --- a/scripts/zopu-smoke.ts +++ b/scripts/zopu-smoke.ts @@ -78,7 +78,8 @@ const { values, positionals } = parseArgs({ const usage = `Usage: bun run smoke:zopu [--run] [--report ] [--timeout-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. Required environment: @@ -93,6 +94,13 @@ Optional environment: ZOPU_SMOKE_FEATURE_REQUEST override the tiny feature request ZOPU_SMOKE_WEB_URL default: SITE_URL or http://localhost:5173 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) { @@ -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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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: { readonly baseReport: Omit; readonly convex: ConvexHttpClient; @@ -772,6 +1017,12 @@ const main = async (): Promise => { timeoutMs, }); + await probeOrbCapabilities({ + checks, + env, + secrets, + timeoutMs, + }); const initialLoop = initialSmokeLoopState(); let loop: SmokeLoopState; try {