Files
zopu-code/packages/agents/src/tools/git-remote.ts
T
-Puter e744ac4687 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.
2026-07-25 03:18:23 +05:30

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);
},
}),
];