Add Zopu dev bootstrap: git runtimes, Codex agent-os, dev agent

A vertical slice to bootstrap the product loop on the canonical zopu-code repo.

Primitives (packages/primitives, all tested + lint/type clean):
- GitRemoteRuntime: Gitea REST client (createIssue, createBranch,
  createPullRequest, listIssues, getIssue) as an Effect context service with
  a fetch-backed transport. Live-verified against puter/zopu-code.
- GitLocalRuntime: clone, addWorktree, commit, push, currentBranch,
  setRemoteUrl over a pluggable Shell (host subprocess now, AgentOS VM exec
  later). Tested against real temp git repos.
- agent-os: Codex support via @agentos-software/codex-cli — codexSoftware
  bundle (codex+git), makeCodexAgentOsConfig(), codexSessionEnv() for the
  OpenAI base URL/key.

Env:
- GITEA_URL/GITEA_TOKEN wired into convex.ts and convex.config.ts.
- .env.example documents the self-hosted git section.

Agents (packages/agents):
- zopu-dev: development agent that creates issues on the canonical repo and
  starts autonomous work runs. git-remote tools (create/list/branch/PR) wired
  to GitRemoteRuntime; start_workflow tool enqueues a work run.
- flue run zopu-dev verified live (lists/creates issues on real Gitea).

Backend (packages/backend):
- workflows.startIssueWork mutation: fetches a Gitea issue server-side and
  admits a queued work run. The Codex VM spawn body is the next step.
This commit is contained in:
-Puter
2026-07-25 03:18:23 +05:30
parent a11ed988fb
commit e744ac4687
17 changed files with 1239 additions and 3 deletions

View File

@@ -9,7 +9,8 @@
"dev": "bun --env-file=../../.env flue dev",
"dev:tailscale": "bun --env-file=../../.env flue dev",
"run": "bun --env-file=../../.env flue run",
"run:zopu": "bun --env-file=../../.env flue run zopu"
"run:zopu": "bun --env-file=../../.env flue run zopu",
"run:zopu-dev": "bun --env-file=../../.env flue run zopu-dev"
},
"dependencies": {
"@agentos-software/opencode": "0.2.7",

View File

@@ -0,0 +1,51 @@
import { parseAgentEnv } from "@code/env/agent";
import { defineAgent } from "@flue/runtime";
import { local } from "@flue/runtime/node";
import { createGitRemoteTools, makeGitRemoteConfig } from "../tools/git-remote";
import { createWorkflowTools } from "../tools/workflow";
const INSTRUCTIONS = `You are Zopu Dev, the development agent for the canonical zopu-code repository (self-hosted Gitea: puter/zopu-code).
## Your job
You talk with the user and turn agreed-upon work into tracked issues, then kick off autonomous implementation. You operate on exactly one repository.
## Loop
1. Understand the request. Ask one focused clarifying question only when genuinely ambiguous. Do not create work from casual chat.
2. Check for duplicates. Call list_issues to see what already exists.
3. Create the issue. Call create_issue with a clear title and a body that captures the acceptance criteria. Report the issue number and URL back to the user.
4. Start work only on explicit confirmation. When the user says to start (e.g. "go", "start", "work on it"), call start_workflow with the issue number. This spins up a Codex agent in an isolated worktree that implements, verifies, commits, and opens a pull request.
5. Report outcomes plainly: "Created issue #N: <title> — <url>" and "Started run <id> (status <status>)".
## Rules
- Never invent issue numbers. Always create or list first.
- Never start work without explicit user confirmation.
- Titles are concise; bodies carry the acceptance criteria.
- If a tool returns an error with a reason, surface it and suggest next steps instead of retrying blindly.`;
export default defineAgent(({ env }) => {
const agentEnv = parseAgentEnv(env);
const gitConfig = makeGitRemoteConfig(agentEnv);
return {
description:
"Development agent that creates issues on the canonical zopu-code repo and starts autonomous Codex work runs.",
instructions: INSTRUCTIONS,
model: `${agentEnv.AGENT_MODEL_PROVIDER}/${agentEnv.AGENT_MODEL_NAME}`,
sandbox: local({ cwd: process.cwd() }),
tools: [
...createGitRemoteTools(gitConfig),
...createWorkflowTools({
convexUrl: agentEnv.CONVEX_URL,
token: agentEnv.FLUE_DB_TOKEN,
}),
],
};
});

View File

@@ -0,0 +1,119 @@
import {
createBranch,
createIssue,
createPullRequest,
fetchTransport,
listIssues,
} from "@code/primitives/git-remote-runtime";
import type {
GitRemoteConfig,
GitRemoteError,
} from "@code/primitives/git-remote-runtime";
import { defineTool } from "@flue/runtime";
import { Effect } from "effect";
import * as v from "valibot";
/**
* Canonical repo for the bootstrap: one self-hosted Gitea repo hosts all work.
* Hard-coded owner/repo keeps the agent off credential-shaped free input.
*/
export const CANONICAL_REPO = { owner: "puter", repo: "zopu-code" } as const;
export const makeGitRemoteConfig = (runtimeEnv: {
readonly GITEA_TOKEN?: string;
readonly GITEA_URL: string;
}): GitRemoteConfig => {
if (!runtimeEnv.GITEA_TOKEN) {
throw new Error(
"GITEA_TOKEN is required to configure the git remote tools"
);
}
return {
baseUrl: runtimeEnv.GITEA_URL,
owner: CANONICAL_REPO.owner,
repo: CANONICAL_REPO.repo,
token: runtimeEnv.GITEA_TOKEN,
};
};
const run = <A, E>(eff: Effect.Effect<A, E>): Promise<A> =>
Effect.runPromise(eff);
const describeError = (error: GitRemoteError) => ({
error: error.message,
reason: error.reason,
});
// Tool returns are serialized to the model; round-trip through JSON so the
// Effect-branded/readonly value objects become plain mutable JsonValue.
// oxlint-disable-next-line unicorn/prefer-structured-clone -- structuredClone keeps readonly modifiers; JSON erases them for JsonValue
const json = (value: unknown) => JSON.parse(JSON.stringify(value));
/**
* Git-remote tools bound to the canonical repo. The agent creates issues,
* branches, and pull requests on puter/zopu-code via the Gitea API using the
* server-side token — it never accepts credentials or repo paths as input.
*/
export const createGitRemoteTools = (config: GitRemoteConfig) => [
defineTool({
description:
"Create an issue on the canonical zopu-code repository. Use this to turn an agreed-upon piece of work into a tracked issue. Returns the issue number and URL. Always call list_issues first to avoid duplicates.",
input: v.object({
body: v.string(),
title: v.string(),
}),
name: "create_issue",
async run({ input }) {
const result = await run(
createIssue(fetchTransport, config, input)
).catch(describeError);
return json(result);
},
}),
defineTool({
description:
"List open issues on the canonical zopu-code repository. Call this before creating an issue to check for duplicates and understand existing work.",
name: "list_issues",
async run() {
const result = await run(listIssues(fetchTransport, config)).catch(
describeError
);
return json(result);
},
}),
defineTool({
description:
"Create a remote branch on the canonical zopu-code repository. Usually you do not need this directly — start_workflow handles branching for a work run. Use only when explicitly setting up a branch by hand.",
input: v.object({
from: v.optional(v.string()),
name: v.string(),
}),
name: "create_branch",
async run({ input }) {
const result = await run(
createBranch(fetchTransport, config, input)
).catch(describeError);
return json(result);
},
}),
defineTool({
description:
"Open a pull request on the canonical zopu-code repository. Provide the work branch, the base branch (usually main), a title, and a body. Returns the PR number and URL.",
input: v.object({
baseBranch: v.string(),
body: v.string(),
branch: v.string(),
title: v.string(),
}),
name: "create_pull_request",
async run({ input }) {
const result = await run(
createPullRequest(fetchTransport, config, input)
).catch(describeError);
return json(result);
},
}),
];

View File

@@ -0,0 +1,37 @@
import { defineTool } from "@flue/runtime";
import { ConvexHttpClient } from "convex/browser";
import { makeFunctionReference } from "convex/server";
import * as v from "valibot";
// The control-plane mutation that enqueues a work run for an issue. Implemented
// in the backend Convex workflow module; referenced by name so the agent never
// imports generated bindings.
const startIssueWorkRef = makeFunctionReference<
"mutation",
{ readonly issueNumber: number; readonly token: string },
{ readonly runId: string; readonly status: string }
>("workflows:startIssueWork");
export const createWorkflowTools = (clientOptions: {
readonly convexUrl: string;
readonly token: string;
}) => {
const client = new ConvexHttpClient(clientOptions.convexUrl);
return [
defineTool({
description:
"Start the autonomous work workflow for an existing zopu-code issue. This spawns a Codex agent in an isolated worktree of the canonical repo, gives it the issue, lets it implement and verify the change, then commits and opens a pull request. Use it after create_issue (or for an existing issue number) once the user confirms they want work to begin. Returns the run id and initial status.",
input: v.object({
issueNumber: v.number(),
}),
name: "start_workflow",
async run({ input }) {
return await client.mutation(startIssueWorkRef, {
issueNumber: input.issueNumber,
token: clientOptions.token,
});
},
}),
];
};