/*
RFC-0563: gitmesh.sync command handler. Fetches from all configured remotes,
converges on the latest signed commit using highest committer timestamp,
and advances HEAD via git merge --ff-only. Implements lock file for concurrent
execution prevention, auto-config bootstrap in Phase 1, and signature
verification when verifySignatures is true.
- Do not implement push operations — gitmesh.sync is pull-only.
- Do not implement peer discovery — that is RFC-0564 (SWIM).
- Do not implement conflict resolution — platform code conflicts are resolved by human review.
- Sync fetches all remotes then converges — it never pushes local state.
- RFC-0563: initial implementation — gitmesh.sync handler with convergence algorithm.
- RFC-0563 fix: consolidate duplicated gitLogSignatureStatus call, add diagnostics arrays (RFC-0086), add stale lock detection.
- RFC-1097: step 6 — compass.migrate codemod run
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
- RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared
*/
import { join } from "node:path";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-shared/kernel";
import type { GitMeshSyncResult } from "./types.ts";
import { loadOrCreateConfig } from "./config.ts";
import {
gitCommitTimestamp,
gitFetch,
gitFsck,
gitHasUncommittedChanges,
gitIsAncestor,
gitLogSignatureStatus,
gitMergeFfOnly,
gitRevParseHead,
gitRevParseRemote,
} from "./git-ops.ts";
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
const LOCK_FILE = ".git/gitmesh.lock";
const LAST_SYNC_FILE = ".git/gitmesh.last-sync";
const STALE_LOCK_THRESHOLD_MS = 360_000; // 6 minutes — matches git operation timeout
async function acquireLock(
workspaceRoot: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise<() => Promise> {
const lockPath = join(workspaceRoot, LOCK_FILE);
const release = async () => {
await io.rm(lockPath, { recursive: true }).catch(() => {});
};
// Atomic lock claim: `mkdir` (no -p) fails EEXIST when the lock dir exists —
// the port has no exclusive-write primitive, so the lock is a directory.
const claim = await io.exec("mkdir", [lockPath]);
if (claim.exitCode === 0) return release;
// Check if lock is stale (mtime older than 6 minutes)
const stat = await io.stat(lockPath).catch(() => null);
if (stat && Date.now() - stat.mtimeMs > STALE_LOCK_THRESHOLD_MS) {
await io.rm(lockPath, { recursive: true }).catch(() => {});
const retry = await io.exec("mkdir", [lockPath]);
if (retry.exitCode === 0) return release;
}
throw new Error("sync-in-progress: another gitmesh.sync is already running");
}
interface RemoteFetchResult {
remote: string;
success: boolean;
sha: string;
timestamp: number;
}
export async function runGitMeshSync(
_input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const { workspaceRoot } = context;
const releaseLock = await acquireLock(workspaceRoot, context.io);
try {
const config = await loadOrCreateConfig(workspaceRoot);
if (config.remotes.length === 0) {
return {
data: {
synced: false,
fromRemote: "",
commitsReceived: 0,
currentSha: await gitRevParseHead(workspaceRoot, context.io).catch(() => ""),
signaturesVerified: 0,
signaturesFailed: 0,
diagnostics: ["gitmesh.sync: no remotes configured in werkstatt.gitmesh.json"],
},
exitCode: 1,
summary: "[gitmesh.sync] no remotes configured",
nextSteps: [
{
action: "Configure remotes in werkstatt.gitmesh.json and re-run gitmesh.sync",
kind: "required",
},
],
};
}
// Integrity check
const fsckOk = await gitFsck(workspaceRoot, context.io);
if (!fsckOk) {
return {
data: {
synced: false,
fromRemote: "",
commitsReceived: 0,
currentSha: "",
signaturesVerified: 0,
signaturesFailed: 0,
diagnostics: [
"gitmesh.sync: clone corruption detected (git fsck failed) — re-clone from a trusted remote",
],
},
exitCode: 1,
summary: "[gitmesh.sync] clone corruption detected (git fsck failed)",
nextSteps: [
{ action: "Re-clone from a trusted remote, then re-run gitmesh.sync", kind: "required" },
],
};
}
// Check for uncommitted changes
const hasUncommitted = await gitHasUncommittedChanges(workspaceRoot, context.io);
if (hasUncommitted) {
return {
data: {
synced: false,
fromRemote: "",
commitsReceived: 0,
currentSha: await gitRevParseHead(workspaceRoot, context.io),
signaturesVerified: 0,
signaturesFailed: 0,
diagnostics: ["gitmesh.sync: uncommitted local changes — commit or stash before sync"],
},
exitCode: 1,
summary: "[gitmesh.sync] uncommitted local changes — commit or stash before sync",
nextSteps: [
{ action: "Commit or stash local changes, then re-run gitmesh.sync", kind: "required" },
],
};
}
// Fetch from all remotes
const fetchResults: RemoteFetchResult[] = [];
const warnings: string[] = [];
for (const remote of config.remotes) {
try {
await gitFetch(remote.name, config.trackedBranch, workspaceRoot, context.io);
const sha = await gitRevParseRemote(
remote.name,
config.trackedBranch,
workspaceRoot,
context.io,
);
const timestamp = await gitCommitTimestamp(sha, workspaceRoot, context.io);
fetchResults.push({ remote: remote.name, success: true, sha, timestamp });
} catch {
warnings.push(`remote ${remote.name} unreachable`);
fetchResults.push({ remote: remote.name, success: false, sha: "", timestamp: 0 });
}
}
const successfulFetches = fetchResults.filter((r) => r.success);
if (successfulFetches.length === 0) {
return {
data: {
synced: false,
fromRemote: "",
commitsReceived: 0,
currentSha: await gitRevParseHead(workspaceRoot, context.io),
signaturesVerified: 0,
signaturesFailed: 0,
diagnostics: warnings.map((w) => `gitmesh.sync: ${w}`),
},
exitCode: 1,
summary: `[gitmesh.sync] all remotes unreachable (${warnings.join(", ")})`,
nextSteps: [
{
action: "Check network connectivity and remote availability, then re-run gitmesh.sync",
kind: "required",
},
],
};
}
// Convergence: highest committer timestamp
const latest = successfulFetches.reduce((best, current) =>
current.timestamp > best.timestamp ? current : best,
);
const currentHead = await gitRevParseHead(workspaceRoot, context.io);
// Check for non-fast-forward
const isFastForward = await gitIsAncestor(currentHead, latest.sha, workspaceRoot, context.io);
if (!isFastForward) {
return {
data: {
synced: false,
fromRemote: latest.remote,
commitsReceived: 0,
currentSha: currentHead,
signaturesVerified: 0,
signaturesFailed: 0,
diagnostics: [
`gitmesh.sync: non-fast-forward detected (force-push on ${latest.remote}?) — manual reset required`,
],
},
exitCode: 1,
summary: `[gitmesh.sync] non-fast-forward detected (force-push on ${latest.remote}?) — manual reset required`,
nextSteps: [
{
action: `Manual reset required — force-push detected on ${latest.remote}. Reset HEAD and re-run gitmesh.sync`,
kind: "required",
},
],
};
}
// Get commits to receive (used for both signature verification and counting)
const commitsToReceive = await gitLogSignatureStatus(
`${currentHead}..${latest.sha}`,
workspaceRoot,
context.io,
);
// Signature verification
let signaturesVerified = 0;
let signaturesFailed = 0;
if (config.verifySignatures) {
for (const commit of commitsToReceive) {
if (commit.signatureStatus === "G") {
signaturesVerified++;
} else if (commit.signatureStatus === "U" || commit.signatureStatus === "N") {
// Unsigned — counts as failure in verify mode
signaturesFailed++;
} else {
// B (bad), X (expired), Y (key missing), E (expired key), R (revoked)
signaturesFailed++;
}
}
if (signaturesFailed > 0) {
return {
data: {
synced: false,
fromRemote: latest.remote,
commitsReceived: commitsToReceive.length,
currentSha: currentHead,
signaturesVerified,
signaturesFailed,
diagnostics: [
`gitmesh.sync: signature verification failed (${signaturesFailed} invalid) — HEAD not advanced`,
],
},
exitCode: 1,
summary: `[gitmesh.sync] signature verification failed (${signaturesFailed} invalid) — HEAD not advanced`,
nextSteps: [
{
action:
"Review the invalid signatures, remove untrusted commits, then re-run gitmesh.sync",
kind: "required",
},
],
};
}
}
// Advance HEAD
await gitMergeFfOnly(latest.sha, workspaceRoot, context.io);
// Write last-sync timestamp
const lastSyncPath = join(workspaceRoot, LAST_SYNC_FILE);
await context.io.writeFile(lastSyncPath, new Date().toISOString());
return {
data: {
synced: true,
fromRemote: latest.remote,
commitsReceived: commitsToReceive.length,
currentSha: latest.sha,
signaturesVerified,
signaturesFailed,
},
exitCode: 0,
summary:
`[gitmesh.sync] received ${commitsToReceive.length} commit(s) from ${latest.remote}` +
(warnings.length > 0 ? `, warnings: ${warnings.join(", ")}` : ""),
};
} finally {
await releaseLock();
}
}