AkurAI Build
Menu

popagent

public

Latest change fa391bb84299efc38430196d6f7156e2c978d1b7 - Respect the autonomy pause in follow-ups and refresh scheduled workspaces by AkurAI Build

import { agentWorkspaces, type AgentWorkspaceStore } from "./agent-workspaces";
import { appLogger } from "./observability";

/**
 * Why a checkout was or was not advanced. Everything except `synced` leaves the
 * working tree exactly as it was.
 */
export type WorkspaceSyncResult = "synced" | "dirty" | "diverged" | "skipped";

async function git(path: string, args: string[]): Promise<{ ok: boolean; output: string }> {
  const process = Bun.spawn(["git", "-C", path, ...args], { stdout: "pipe", stderr: "pipe" });
  const [output, code] = await Promise.all([new Response(process.stdout).text(), process.exited]);
  return { ok: code === 0, output: output.trim() };
}

/**
 * Fast-forward one registered workspace checkout onto its tracked remote so
 * agents read current source instead of a clone that has silently fallen
 * behind. Only ever fast-forwards: uncommitted work, a checkout without an
 * upstream, and local commits the remote does not have are all left untouched.
 */
export async function syncWorkspaceCheckout(path: string): Promise<WorkspaceSyncResult> {
  if (!(await git(path, ["rev-parse", "--is-inside-work-tree"])).ok) return "skipped";
  const upstream = await git(path, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
  if (!upstream.ok || !upstream.output) return "skipped";
  const status = await git(path, ["status", "--porcelain=v1"]);
  if (!status.ok) return "skipped";
  if (status.output) return "dirty";

  const [remote, ...branch] = upstream.output.split("/");
  if (!remote || !branch.length) return "skipped";
  if (!(await git(path, ["fetch", "--quiet", remote, branch.join("/")])).ok) return "skipped";
  if (!(await git(path, ["merge", "--ff-only", upstream.output])).ok) return "diverged";
  return "synced";
}

/**
 * Resolves a workspace id to its checkout and refreshes it. Failures are logged
 * and swallowed: a stale checkout must never stop the work that requested it.
 */
export function createWorkspaceSync(
  workspaces: Pick<AgentWorkspaceStore, "resolveRepository"> = agentWorkspaces,
): (workspaceId: string) => Promise<void> {
  return async (workspaceId: string) => {
    try {
      const { path } = await workspaces.resolveRepository(workspaceId);
      const result = await syncWorkspaceCheckout(path);
      if (result !== "skipped") appLogger().info("workspace.sync", { workspaceId, path, result });
    } catch (error) {
      appLogger().warn("workspace.sync.failed", { workspaceId, error: error instanceof Error ? error.message : String(error) });
    }
  };
}