Add AgentGit M1: agent-native git layer over a Forgejo backend

AgentGit is a thin, DID-gated source-collaboration layer over a backend
forge (default Forgejo at git.profullstack.com, BBS-members-only) — not a
new git host. M1 implements the contract and engines:

- forge/adapter.ts: ForgeAdapter interface (only forge-specific surface)
- forge/forgejo.ts: ForgejoAdapter over Forgejo/Gitea REST v1 (injectable
  fetch, typed errors), incl. ensureUser for member provisioning
- access.ts: gateAccess DID membership gate (owner/role/visibility)
- merge-policy.ts: evaluateMergePolicy pure engine (reviews, reputation
  floor, checks, escrow, merge method, agent-merge toggle)
- service.ts: AgentGitService ties gate + policy to the adapter; refuses
  policy-failing merges; provisionMember hook for AgentBBS
- schemas: logicsrc-repo + logicsrc-pull-request, registered in
  @logicsrc/validators with fixtures
- docs/agentgit.md spec; plugin wired into root build (default/disabled)

27 vitest tests pass; full monorepo build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-14 12:45:38 +00:00
parent 14fe9d608f
commit bf046ae280
25 changed files with 1776 additions and 3 deletions

View file

@ -0,0 +1,67 @@
import type { MergeMethod, MergePolicy, PullRequest } from "./domain.js";
export interface MergeEvaluationInput {
policy: MergePolicy;
pr: Pick<PullRequest, "status" | "reviews" | "checks">;
method: MergeMethod;
actor: { did: string; isAgent: boolean };
/** Whether a funded escrow exists for the linked task; resolved by the caller. */
escrowFunded?: boolean;
}
export interface MergeEvaluation {
satisfied: boolean;
reasons: string[];
}
/**
* Pure evaluation of a repo's merge policy against a pull request. Returns
* whether the PR may be merged and, if not, every blocking reason. The actual
* merge (pr.merge) refuses unless `satisfied` is true.
*/
export function evaluateMergePolicy(input: MergeEvaluationInput): MergeEvaluation {
const { policy, pr, method, actor, escrowFunded } = input;
const reasons: string[] = [];
if (pr.status === "merged" || pr.status === "closed") {
reasons.push(`pull request is ${pr.status}`);
return { satisfied: false, reasons };
}
if (pr.status === "draft") {
reasons.push("pull request is a draft");
}
if (pr.reviews.some((review) => review.decision === "request_changes")) {
reasons.push("changes requested by a reviewer");
}
const qualifyingApprovals = pr.reviews.filter(
(review) => review.decision === "approve" && (review.reputation ?? 0) >= policy.reviewer_reputation_min
).length;
if (qualifyingApprovals < policy.min_reviews) {
reasons.push(
`needs ${policy.min_reviews} approving review(s) at reputation >= ${policy.reviewer_reputation_min}, has ${qualifyingApprovals}`
);
}
if (policy.require_passing_checks) {
const notPassing = pr.checks.filter((check) => check.status !== "passing");
if (notPassing.length > 0) {
reasons.push(`checks not passing: ${notPassing.map((check) => `${check.name}(${check.status})`).join(", ")}`);
}
}
if (policy.escrow_required && escrowFunded !== true) {
reasons.push("escrow required but not funded");
}
if (!policy.allowed_merge_methods.includes(method)) {
reasons.push(`merge method "${method}" not allowed (allowed: ${policy.allowed_merge_methods.join(", ")})`);
}
if (actor.isAgent && !policy.allow_agent_merge) {
reasons.push("agent merges are disabled for this repo");
}
return { satisfied: reasons.length === 0, reasons };
}