Menu
popagent
publicLatest 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) });
}
};
}