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:
@@ -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",
|
||||
|
||||
51
packages/agents/src/agents/zopu-dev.ts
Normal file
51
packages/agents/src/agents/zopu-dev.ts
Normal 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,
|
||||
}),
|
||||
],
|
||||
};
|
||||
});
|
||||
119
packages/agents/src/tools/git-remote.ts
Normal file
119
packages/agents/src/tools/git-remote.ts
Normal 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);
|
||||
},
|
||||
}),
|
||||
];
|
||||
37
packages/agents/src/tools/workflow.ts
Normal file
37
packages/agents/src/tools/workflow.ts
Normal 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,
|
||||
});
|
||||
},
|
||||
}),
|
||||
];
|
||||
};
|
||||
Reference in New Issue
Block a user