/* 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(); } }