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.
120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
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);
|
|
},
|
|
}),
|
|
];
|