@kontextmind/kxm
Version:
KXM local-first multi-agent orchestration and operator dashboard
1,008 lines (928 loc) • 34.1 kB
text/typescript
/**
* KXM External Side-Effect Idempotency (Slice C)
* Ensures deterministic branching, preflight CAS checks, and immutable receipts for external mutations.
*/
import { DatabaseSync } from "./sqlite.ts";
import { createHash } from "node:crypto";
import {
mkdirSync,
openSync,
closeSync,
writeFileSync,
readFileSync,
unlinkSync,
existsSync,
lstatSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { openDatabase } from "./database.ts";
import { spawnSync } from "node:child_process";
export const EXTERNAL_EFFECT_SCHEMA = "kxm.external-effect-receipt.v1" as const;
export const DEFAULT_LEASE_TIMEOUT_MS = 300_000; // 300s (5 minutes) per Decision Q6
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000; // 30s per Decision Q6
export type ExternalActionKind =
| "git-branch"
| "git-commit"
| "git-push"
| "pr-create"
| "tracker-issue"
| "webhook";
export type ExternalEffectStatus = "in-flight" | "committed" | "failed" | "aborted" | "uncertain";
export interface ExternalEffectReceipt {
schema: typeof EXTERNAL_EFFECT_SCHEMA;
effectKey: string;
runId: string;
stepId: string;
attemptId: string;
actionKind: ExternalActionKind;
targetRef: string;
status: ExternalEffectStatus;
payloadHash: string;
receiptPayload: Record<string, unknown>;
executedAt: string;
lastHeartbeatAt?: string | undefined;
completedAt?: string | undefined;
/** The hub lease this effect executes under, for shared kinds only. The
* ledger's own CAS is per run/step; this pair is what fences the effect
* against a writer on another box. */
leaseResource?: string | undefined;
fencingToken?: number | undefined;
}
export interface RunBranchOptions {
description?: string | undefined;
workflowId?: string | undefined;
issueKey?: string | undefined;
format?: "prefix-run" | "run-suffix" | undefined;
}
export function slugifyBranchPart(text: string, maxLength: number = 40): string {
const cleaned = text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
if (cleaned.length <= maxLength) return cleaned;
return cleaned.slice(0, maxLength).replace(/-+$/, "");
}
export function deterministicRunBranch(
runId: string,
descriptionOrOptions?: string | RunBranchOptions,
): string {
const cleanId = runId.replace(/^run_/, "");
if (!descriptionOrOptions) {
return `kxm/run-${cleanId}`;
}
const options: RunBranchOptions =
typeof descriptionOrOptions === "string"
? { description: descriptionOrOptions }
: descriptionOrOptions;
const parts: string[] = [];
if (options.workflowId) {
const wf = slugifyBranchPart(options.workflowId, 25);
if (wf) parts.push(wf);
}
if (options.issueKey) {
const issue = slugifyBranchPart(options.issueKey, 20);
if (issue && !parts.some((p) => p.includes(issue))) parts.push(issue);
}
if (options.description) {
const desc = slugifyBranchPart(options.description, 35);
if (desc && !parts.some((p) => p.includes(desc))) parts.push(desc);
}
const slug = parts.join("-").slice(0, 50).replace(/-+$/, "");
if (!slug) {
return `kxm/run-${cleanId}`;
}
if (options.format === "prefix-run") {
return `kxm/${slug}-run-${cleanId}`;
}
return `kxm/run-${cleanId}-${slug}`;
}
export function computeEffectKey(
runId: string,
stepId: string,
actionKind: ExternalActionKind,
targetRef: string,
): string {
const raw = `${runId}:${stepId}:${actionKind}:${targetRef}`;
return `eff_${createHash("sha256").update(raw).digest("hex").slice(0, 16)}`;
}
// v1 → v2 adds the hub lease identity a shared effect executes under. There is no
// migration lane here either: an older ledger file is refused at open, not reshaped.
const EXTERNAL_EFFECTS_SCHEMA_VERSION = 2;
const EXTERNAL_EFFECTS_DDL = `
CREATE TABLE IF NOT EXISTS external_effects (
effect_key TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
step_id TEXT NOT NULL,
attempt_id TEXT NOT NULL,
action_kind TEXT NOT NULL,
target_ref TEXT NOT NULL,
status TEXT NOT NULL,
payload_hash TEXT NOT NULL,
receipt_payload TEXT NOT NULL,
executed_at TEXT NOT NULL,
last_heartbeat_at TEXT,
completed_at TEXT,
lease_resource TEXT,
fencing_token INTEGER
);
CREATE INDEX IF NOT EXISTS idx_ext_effects_run ON external_effects(run_id);
`;
const EXTERNAL_EFFECTS_SHAPE = {
external_effects: [
"effect_key", "run_id", "step_id", "attempt_id", "action_kind", "target_ref",
"status", "payload_hash", "receipt_payload", "executed_at", "last_heartbeat_at", "completed_at",
"lease_resource", "fencing_token",
],
} as const;
interface EffectRow {
effect_key: string;
run_id: string;
step_id: string;
attempt_id: string;
action_kind: ExternalActionKind;
target_ref: string;
status: ExternalEffectStatus;
payload_hash: string;
receipt_payload: string;
executed_at: string;
last_heartbeat_at: string | null;
completed_at: string | null;
lease_resource: string | null;
fencing_token: number | null;
}
function receiptFromRow(row: EffectRow): ExternalEffectReceipt {
return {
schema: EXTERNAL_EFFECT_SCHEMA,
effectKey: row.effect_key,
runId: row.run_id,
stepId: row.step_id,
attemptId: row.attempt_id,
actionKind: row.action_kind,
targetRef: row.target_ref,
status: row.status,
payloadHash: row.payload_hash,
receiptPayload: JSON.parse(row.receipt_payload) as Record<string, unknown>,
executedAt: row.executed_at,
lastHeartbeatAt: row.last_heartbeat_at ?? undefined,
completedAt: row.completed_at ?? undefined,
leaseResource: row.lease_resource ?? undefined,
fencingToken: row.fencing_token ?? undefined,
};
}
export class ExternalEffectsLedger {
private db: DatabaseSync;
constructor(dbPath: string = ":memory:") {
if (dbPath !== ":memory:") {
mkdirSync(dirname(dbPath), { recursive: true });
}
// Goes through the shared opener, not a bare `new DatabaseSync`: that is what gives this
// store the same contract as every other one — path checks, WAL, the `user_version` gate
// that refuses an older stamp outright, and the column-shape check that turns a missing
// column into `runtime_schema_shape_invalid` here instead of a bare SQL error on first use.
this.db = openDatabase(dbPath, "external effects ledger", {
schema: EXTERNAL_EFFECTS_DDL,
version: EXTERNAL_EFFECTS_SCHEMA_VERSION,
tables: EXTERNAL_EFFECTS_SHAPE,
});
}
/**
* Preflight Check-And-Set (CAS):
* Guarantees that an external mutation is only initiated if not already committed or currently in-flight.
* Auto-reclaims stale leases after timeoutMs (default 300s per Decision Q6).
*/
claimEffect(input: {
runId: string;
stepId: string;
attemptId: string;
actionKind: ExternalActionKind;
targetRef: string;
payload?: Record<string, unknown> | undefined;
timeoutMs?: number | undefined;
/** The hub lease this effect runs under. Shared kinds carry it; unique
* namespaces (`git-branch`, `git-commit`) leave it unset. */
leaseResource?: string | undefined;
fencingToken?: number | undefined;
}): { ok: true; effectKey: string } | { ok: false; error: string; existing?: ExternalEffectReceipt } {
const effectKey = computeEffectKey(input.runId, input.stepId, input.actionKind, input.targetRef);
const now = new Date().toISOString();
const payloadStr = JSON.stringify(input.payload ?? {});
const payloadHash = createHash("sha256").update(payloadStr).digest("hex");
const timeout = input.timeoutMs ?? DEFAULT_LEASE_TIMEOUT_MS;
const existing = this.getReceipt(effectKey);
if (existing) {
if (existing.status === "uncertain") {
return {
ok: false,
error: `effect_uncertain: ${input.actionKind} on ${input.targetRef} is parked as uncertain after a fencing conflict and requires explicit recovery`,
existing,
};
}
if (existing.status === "committed") {
return {
ok: false,
error: `effect_already_committed: ${input.actionKind} on ${input.targetRef} was already committed`,
existing,
};
}
if (existing.status === "in-flight") {
const lastActivity = existing.lastHeartbeatAt ?? existing.executedAt;
const elapsed = Date.now() - Date.parse(lastActivity);
if (elapsed < timeout) {
return {
ok: false,
error: `effect_in_flight: ${input.actionKind} on ${input.targetRef} is currently executing`,
existing,
};
}
// If timed out, allow reclaim by updating status and timestamps
}
}
const stmt = this.db.prepare(`
INSERT INTO external_effects (
effect_key, run_id, step_id, attempt_id, action_kind, target_ref,
status, payload_hash, receipt_payload, executed_at, last_heartbeat_at,
lease_resource, fencing_token
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(effect_key) DO UPDATE SET
attempt_id = excluded.attempt_id,
status = 'in-flight',
executed_at = excluded.executed_at,
last_heartbeat_at = excluded.last_heartbeat_at,
payload_hash = excluded.payload_hash,
receipt_payload = excluded.receipt_payload,
lease_resource = excluded.lease_resource,
fencing_token = excluded.fencing_token
`);
stmt.run(
effectKey,
input.runId,
input.stepId,
input.attemptId,
input.actionKind,
input.targetRef,
"in-flight",
payloadHash,
payloadStr,
now,
now,
input.leaseResource ?? null,
input.fencingToken ?? null,
);
return { ok: true, effectKey };
}
/**
* Refreshes the lease heartbeat for an in-flight effect (Decision Q6).
* Workers invoke this periodically (default 30s) while performing side-effects.
*/
heartbeatEffect(effectKey: string): { ok: true; lastHeartbeatAt: string } | { ok: false; error: string } {
const existing = this.getReceipt(effectKey);
if (!existing) {
return { ok: false, error: `effect_not_found: effect ${effectKey} does not exist` };
}
if (existing.status !== "in-flight") {
return {
ok: false,
error: `effect_not_in_flight: cannot heartbeat effect in status '${existing.status}'`,
};
}
const now = new Date().toISOString();
const stmt = this.db.prepare(`
UPDATE external_effects
SET last_heartbeat_at = ?
WHERE effect_key = ? AND status = 'in-flight'
`);
stmt.run(now, effectKey);
return { ok: true, lastHeartbeatAt: now };
}
/** Park an effect as uncertain: a fencing conflict means the outside world
* may have been touched, and only explicit recovery can unblock it. */
markUncertain(effectKey: string): void {
this.db.prepare("UPDATE external_effects SET status = 'uncertain' WHERE effect_key = ? AND status = 'in-flight'").run(effectKey);
}
commitEffect(
effectKey: string,
receiptPayload: Record<string, unknown>,
): void {
const now = new Date().toISOString();
const stmt = this.db.prepare(`
UPDATE external_effects
SET status = 'committed', receipt_payload = ?, completed_at = ?
WHERE effect_key = ?
`);
stmt.run(JSON.stringify(receiptPayload), now, effectKey);
}
abortEffect(effectKey: string, reason: string): void {
const now = new Date().toISOString();
const stmt = this.db.prepare(`
UPDATE external_effects
SET status = 'aborted', receipt_payload = ?, completed_at = ?
WHERE effect_key = ?
`);
stmt.run(JSON.stringify({ error: reason }), now, effectKey);
}
getReceipt(effectKey: string): ExternalEffectReceipt | undefined {
const stmt = this.db.prepare(`
SELECT * FROM external_effects WHERE effect_key = ?
`);
const row = stmt.get(effectKey) as EffectRow | undefined;
if (!row) return undefined;
return receiptFromRow(row);
}
listRunEffects(runId: string): ExternalEffectReceipt[] {
const stmt = this.db.prepare(`
SELECT * FROM external_effects WHERE run_id = ? ORDER BY executed_at ASC
`);
const rows = stmt.all(runId) as unknown as EffectRow[];
return rows.map(receiptFromRow);
}
close(): void {
this.db.close();
}
}
// ---------------------------------------------------------------------------
// Shared effects under a fenced hub lease (P3)
// ---------------------------------------------------------------------------
/**
* The lease surface a shared effect needs. Declared structurally so this module
* stays a leaf: `HubClient` satisfies it as written, and a test can stand in a
* stub without a hub.
*/
export interface EffectLeaseGateway {
acquireLease(resource: string, ttlMs?: number): Promise<{ lease: EffectLease; renewed: boolean }>;
renewLease(resource: string, fencingToken: number, ttlMs?: number): Promise<{ lease: EffectLease }>;
releaseLease(resource: string, fencingToken: number): Promise<{ released: boolean }>;
}
export interface EffectLease {
resource: string;
fencingToken: number;
expiresAt: string;
holderAgentId?: string | undefined;
holderAgentName?: string | undefined;
}
export type SharedEffectRefusalCode =
/** No lease could be obtained: no gateway was bound, or the hub was unreachable. */
| "effect_lease_unavailable"
/** Another agent holds the resource right now. */
| "effect_lease_held"
/** The hub has moved past the token this effect holds. */
| "effect_lease_superseded"
| "effect_already_committed"
| "effect_in_flight"
/** The effect is parked as uncertain after a fencing conflict. */
| "effect_uncertain"
| "effect_not_in_flight"
| "effect_not_found";
/** The engine's attempt state for an effect whose outcome cannot be proven. */
export type EffectAttemptState = "blocked_uncertain";
export interface SharedEffectRefusal {
ok: false;
code: SharedEffectRefusalCode;
error: string;
/** Present when the effect may already have touched the outside world. The
* attempt parks here and nothing retries it. */
attemptState?: EffectAttemptState | undefined;
existing?: ExternalEffectReceipt | undefined;
}
export interface SharedEffectClaim {
ok: true;
effectKey: string;
leaseResource?: string | undefined;
fencingToken?: number | undefined;
}
/** Kinds whose target is shared with every other box: two runs naming the same
* `targetRef` mean the same real thing, so exactly one may execute. */
export const SHARED_EFFECT_KINDS: readonly ExternalActionKind[] = Object.freeze([
"git-push",
"pr-create",
"tracker-issue",
"webhook",
]);
/** Does `targetRef` name a branch this run owns? A run branch is a unique
* namespace — `deterministicRunBranch` derives it from the run id in all three
* of its formats — so pushing it contends with nobody. */
export function isRunBranchRef(runId: string, targetRef: string): boolean {
const cleanId = runId.replace(/^run_/, "");
if (!cleanId) return false;
const branch = targetRef.replace(/^refs\/heads\//, "");
if (!branch.startsWith("kxm/")) return false;
return branch === `kxm/run-${cleanId}`
|| branch.startsWith(`kxm/run-${cleanId}-`)
|| branch.endsWith(`-run-${cleanId}`);
}
/**
* Whether an effect must hold a hub lease before it executes.
*
* `git-branch` and `git-commit` write a namespace this run already owns, so
* they stay lease-free. A `git-push` to this run's own branch is the same case;
* a push to any other ref is shared and is fenced.
*/
export function effectRequiresHubLease(
actionKind: ExternalActionKind,
targetRef: string,
runId: string,
): boolean {
if (!SHARED_EFFECT_KINDS.includes(actionKind)) return false;
if (actionKind === "git-push") return !isRunBranchRef(runId, targetRef);
return true;
}
function errorCodeOf(error: unknown): string | undefined {
const code = (error as { code?: unknown } | undefined)?.code;
return typeof code === "string" ? code : undefined;
}
function errorText(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
/** A refusal the hub stated about the lease itself, as opposed to not reaching
* the hub at all. Any of these means the token is gone for good. */
function isLeaseLost(code: string | undefined): boolean {
return code === "lease_superseded" || code === "lease_expired" || code === "lease_not_found";
}
/**
* Claim an external effect, taking its hub lease first when the kind is shared.
*
* The lease comes before the ledger row on purpose: the receipt records the
* token it will have to present at commit, and a claim that cannot be fenced
* never becomes a claim at all. A shared effect with an unreachable hub is
* refused with `effect_lease_unavailable` and executes nothing — running it
* unfenced is exactly the two-writer failure the lease exists to prevent.
*/
export async function claimSharedEffect(input: {
ledger: ExternalEffectsLedger;
lease?: EffectLeaseGateway | undefined;
runId: string;
stepId: string;
attemptId: string;
actionKind: ExternalActionKind;
targetRef: string;
payload?: Record<string, unknown> | undefined;
timeoutMs?: number | undefined;
/** Lease TTL. Defaults to the Q6 lease timeout so the existing heartbeat
* interval renews it well inside its deadline. */
leaseTtlMs?: number | undefined;
}): Promise<SharedEffectClaim | SharedEffectRefusal> {
const needsLease = effectRequiresHubLease(input.actionKind, input.targetRef, input.runId);
let lease: EffectLease | undefined;
let leaseRenewed = false;
if (needsLease) {
if (!input.lease) {
return {
ok: false,
code: "effect_lease_unavailable",
error: `effect_lease_unavailable: ${input.actionKind} on ${input.targetRef} is shared and no hub lease is bound`,
};
}
try {
const acquired = await input.lease.acquireLease(input.targetRef, input.leaseTtlMs ?? DEFAULT_LEASE_TIMEOUT_MS);
lease = acquired.lease;
leaseRenewed = acquired.renewed === true;
} catch (error) {
const code = errorCodeOf(error);
if (code === "lease_held") {
return {
ok: false,
code: "effect_lease_held",
error: `effect_lease_held: ${input.targetRef} is leased by another agent: ${errorText(error)}`,
};
}
return {
ok: false,
code: "effect_lease_unavailable",
error: `effect_lease_unavailable: could not reach the bound hub for ${input.targetRef}: ${errorText(error)}`,
};
}
}
const claimed = input.ledger.claimEffect({
runId: input.runId,
stepId: input.stepId,
attemptId: input.attemptId,
actionKind: input.actionKind,
targetRef: input.targetRef,
payload: input.payload,
timeoutMs: input.timeoutMs,
...(lease ? { leaseResource: lease.resource, fencingToken: lease.fencingToken } : {}),
});
if (!claimed.ok) {
// Only hand back a lease this caller newly acquired. A renewal of an
// existing holder's lease must survive a claim refusal: the original
// effect is still in-flight and still needs the fence.
if (lease && input.lease && !leaseRenewed) {
await releaseQuietly(input.lease, input.targetRef, lease.fencingToken);
}
return {
ok: false,
code: claimed.error.startsWith("effect_already_committed")
? "effect_already_committed"
: claimed.error.startsWith("effect_uncertain") ? "effect_uncertain" : "effect_in_flight",
error: claimed.error,
existing: claimed.existing,
};
}
return {
ok: true,
effectKey: claimed.effectKey,
...(lease ? { leaseResource: lease.resource, fencingToken: lease.fencingToken } : {}),
};
}
/**
* The Q6 heartbeat for a shared effect: renew the hub lease under the same
* token, then refresh the ledger's own lease timestamp.
*
* A hub that has moved past the token parks the attempt — the effect may
* already have touched the outside world, and the resource now belongs to
* someone else.
*/
export async function heartbeatSharedEffect(input: {
ledger: ExternalEffectsLedger;
lease?: EffectLeaseGateway | undefined;
effectKey: string;
leaseTtlMs?: number | undefined;
}): Promise<{ ok: true; lastHeartbeatAt: string; leaseExpiresAt?: string } | SharedEffectRefusal> {
const receipt = input.ledger.getReceipt(input.effectKey);
if (!receipt) {
return { ok: false, code: "effect_not_found", error: `effect_not_found: effect ${input.effectKey} does not exist` };
}
let leaseExpiresAt: string | undefined;
if (receipt.leaseResource !== undefined && receipt.fencingToken !== undefined) {
if (!input.lease) {
return {
ok: false,
code: "effect_lease_unavailable",
error: `effect_lease_unavailable: ${receipt.targetRef} holds a hub lease but no gateway is bound`,
};
}
try {
const renewed = await input.lease.renewLease(
receipt.targetRef,
receipt.fencingToken,
input.leaseTtlMs ?? DEFAULT_LEASE_TIMEOUT_MS,
);
leaseExpiresAt = renewed.lease.expiresAt;
} catch (error) {
const code = errorCodeOf(error);
if (isLeaseLost(code)) {
return {
ok: false,
code: "effect_lease_superseded",
error: `effect_lease_superseded: the hub no longer recognises token ${receipt.fencingToken} on ${receipt.targetRef}: ${errorText(error)}`,
attemptState: "blocked_uncertain",
existing: receipt,
};
}
return {
ok: false,
code: "effect_lease_unavailable",
error: `effect_lease_unavailable: could not renew the lease on ${receipt.targetRef}: ${errorText(error)}`,
existing: receipt,
};
}
}
const beat = input.ledger.heartbeatEffect(input.effectKey);
if (!beat.ok) {
// The row exists — `getReceipt` just read it — so the only remaining refusal
// is a status that no longer accepts a heartbeat.
return { ok: false, code: "effect_not_in_flight", error: beat.error, existing: receipt };
}
return { ok: true, lastHeartbeatAt: beat.lastHeartbeatAt, ...(leaseExpiresAt ? { leaseExpiresAt } : {}) };
}
/**
* Commit a shared effect, but only while the hub still recognises its token.
*
* The token is re-presented before the receipt is written. If the hub has
* superseded it — or cannot be reached to say either way — the effect is left
* `in-flight` and the attempt parks `blocked_uncertain`. Nothing retries: the
* write may or may not have landed, and re-running it is how a second writer
* lands the same change twice.
*/
export async function commitSharedEffect(input: {
ledger: ExternalEffectsLedger;
lease?: EffectLeaseGateway | undefined;
effectKey: string;
receiptPayload: Record<string, unknown>;
leaseTtlMs?: number | undefined;
}): Promise<{ ok: true; receipt: ExternalEffectReceipt } | SharedEffectRefusal> {
const receipt = input.ledger.getReceipt(input.effectKey);
if (!receipt) {
return { ok: false, code: "effect_not_found", error: `effect_not_found: effect ${input.effectKey} does not exist` };
}
const fenced = receipt.leaseResource !== undefined && receipt.fencingToken !== undefined;
if (fenced) {
if (!input.lease) {
return {
ok: false,
code: "effect_lease_unavailable",
error: `effect_lease_unavailable: ${receipt.targetRef} holds a hub lease but no gateway is bound`,
attemptState: "blocked_uncertain",
existing: receipt,
};
}
try {
await input.lease.renewLease(receipt.targetRef, receipt.fencingToken!, input.leaseTtlMs ?? DEFAULT_LEASE_TIMEOUT_MS);
} catch (error) {
const code = errorCodeOf(error);
if (isLeaseLost(code)) {
input.ledger.markUncertain(input.effectKey);
}
return {
ok: false,
code: isLeaseLost(code) ? "effect_lease_superseded" : "effect_lease_unavailable",
error: isLeaseLost(code)
? `effect_lease_superseded: token ${receipt.fencingToken} on ${receipt.targetRef} was superseded before commit: ${errorText(error)}`
: `effect_lease_unavailable: the lease on ${receipt.targetRef} could not be confirmed before commit: ${errorText(error)}`,
attemptState: "blocked_uncertain",
existing: input.ledger.getReceipt(input.effectKey),
};
}
}
input.ledger.commitEffect(input.effectKey, input.receiptPayload);
if (fenced && input.lease) {
await releaseQuietly(input.lease, receipt.targetRef, receipt.fencingToken!);
}
return { ok: true, receipt: input.ledger.getReceipt(input.effectKey)! };
}
/** Hand the resource back. `resource` is always the name the caller asked for,
* never the hub's project-scoped form — the hub applies that prefix itself, and
* passing it back would name a second resource. A failed release is not an
* error the caller can act on: the lease falls to its deadline on the hub
* clock. */
async function releaseQuietly(gateway: EffectLeaseGateway, resource: string, fencingToken: number): Promise<void> {
try {
await gateway.releaseLease(resource, fencingToken);
} catch {
// Deliberately swallowed: the TTL frees it.
}
}
export interface WorktreeLock {
lockPath: string;
release: () => void;
}
/**
* Resolves the lockfile location for concurrent git worktree modifications.
* By default targets .git/kxm-worktree.lock, or if in a linked worktree, targets the main git directory.
*/
export function resolveWorktreeLockPath(repoRoot: string): string {
const gitPath = join(repoRoot, ".git");
if (existsSync(gitPath)) {
try {
const stat = lstatSync(gitPath);
if (stat.isDirectory()) {
return join(gitPath, "kxm-worktree.lock");
}
if (stat.isFile()) {
const content = readFileSync(gitPath, "utf8").trim();
const match = content.match(/^gitdir:\s*(.+)$/i);
if (match && match[1]) {
const resolvedGitDir = resolve(repoRoot, match[1]);
return join(resolvedGitDir, "kxm-worktree.lock");
}
}
} catch {
// Fall back
}
}
return join(repoRoot, ".kxm-worktree.lock");
}
/**
* Acquires an exclusive file-based lock for worktree mutations.
* Supports auto-breaking stale locks if the holding process has died or timed out.
*/
export function acquireWorktreeLock(
repoRoot: string,
options?: {
timeoutMs?: number | undefined;
staleTimeoutMs?: number | undefined;
pollIntervalMs?: number | undefined;
},
): { ok: true; lock: WorktreeLock } | { ok: false; error: string } {
const lockPath = resolveWorktreeLockPath(repoRoot);
const timeoutMs = options?.timeoutMs ?? 5_000;
const staleTimeoutMs = options?.staleTimeoutMs ?? 30_000;
const pollIntervalMs = options?.pollIntervalMs ?? 50;
const startTime = Date.now();
mkdirSync(dirname(lockPath), { recursive: true });
while (true) {
try {
const fd = openSync(lockPath, "wx");
const metadata = JSON.stringify({
pid: process.pid,
acquiredAt: new Date().toISOString(),
repoRoot,
});
writeFileSync(fd, metadata, "utf8");
closeSync(fd);
return {
ok: true,
lock: {
lockPath,
release: () => {
try {
unlinkSync(lockPath);
} catch {
// Ignore if already unlinked
}
},
},
};
} catch (err: unknown) {
const code = (err as { code?: string }).code;
if (code !== "EEXIST") {
return { ok: false, error: `lock_io_error: ${(err as Error).message}` };
}
// Existing lock found - check for staleness
try {
const content = readFileSync(lockPath, "utf8");
const parsed = JSON.parse(content) as { pid?: number; acquiredAt?: string };
let isStale = false;
if (parsed.acquiredAt) {
const age = Date.now() - Date.parse(parsed.acquiredAt);
if (age > staleTimeoutMs) {
isStale = true;
}
}
if (parsed.pid && typeof parsed.pid === "number") {
try {
process.kill(parsed.pid, 0);
} catch (e: unknown) {
if ((e as { code?: string }).code === "ESRCH") {
isStale = true; // Process dead
}
}
}
if (isStale) {
try {
unlinkSync(lockPath);
continue; // Immediately retry lock
} catch {
// Already unlinked or contention
}
}
} catch {
// Corrupt lock file
try {
unlinkSync(lockPath);
continue;
} catch {
// Ignore
}
}
if (Date.now() - startTime >= timeoutMs) {
return {
ok: false,
error: `lock_timeout: timed out after ${timeoutMs}ms waiting for worktree lock at ${lockPath}`,
};
}
if (pollIntervalMs > 0) {
const waitBuf = new Int32Array(new SharedArrayBuffer(4));
Atomics.wait(waitBuf, 0, 0, pollIntervalMs);
}
}
}
}
/**
* Runs a function inside an exclusive worktree lock.
*/
export async function withWorktreeLock<T>(
repoRoot: string,
fn: () => T | Promise<T>,
options?: {
timeoutMs?: number | undefined;
staleTimeoutMs?: number | undefined;
pollIntervalMs?: number | undefined;
},
): Promise<T> {
const res = acquireWorktreeLock(repoRoot, options);
if (!res.ok) {
throw new Error(`worktree_lock_failed: ${res.error}`);
}
try {
return await fn();
} finally {
res.lock.release();
}
}
export interface BranchCleanupOptions {
removeWorktree?: boolean | undefined;
deleteRemote?: boolean | undefined;
remoteName?: string | undefined;
execFn?: ((cmd: string, args: string[]) => { status: number; stdout: string; stderr: string }) | undefined;
}
export interface BranchCleanupResult {
ok: boolean;
branchName: string;
deletedLocalBranch: boolean;
deletedRemoteBranch: boolean;
removedWorktreePath?: string | undefined;
error?: string | undefined;
}
/**
* Deterministic Branch Cleanup (Decision Q7).
* Removes associated git worktrees and deletes the local (and optionally remote) run branch.
*/
export function cleanupMergedRunBranch(
repoRoot: string,
branchName: string,
options?: BranchCleanupOptions,
): BranchCleanupResult {
const runner = options?.execFn ?? ((cmd: string, args: string[]) => {
const res = spawnSync(cmd, args, {
cwd: repoRoot,
encoding: "utf8",
windowsHide: true,
env: Object.fromEntries(
Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("GIT_")),
),
});
return {
status: res.status ?? 1,
stdout: res.stdout || "",
stderr: res.stderr || "",
};
});
const shouldRemoveWorktree = options?.removeWorktree ?? true;
let removedWorktreePath: string | undefined;
let deletedLocalBranch = false;
let deletedRemoteBranch = false;
// 1. Remove worktree if one is attached to this branch
if (shouldRemoveWorktree) {
try {
const wtList = runner("git", ["worktree", "list", "--porcelain"]);
if (wtList.status === 0) {
const blocks = wtList.stdout.split(/\n\n+/);
for (const block of blocks) {
const lines = block.trim().split("\n");
let wtPath: string | undefined;
let wtBranch: string | undefined;
for (const line of lines) {
if (line.startsWith("worktree ")) {
wtPath = line.substring("worktree ".length).trim();
} else if (line.startsWith("branch ")) {
wtBranch = line.substring("branch ".length).trim();
}
}
if (wtPath && wtBranch && (wtBranch === `refs/heads/${branchName}` || wtBranch === branchName)) {
const rmRes = runner("git", ["worktree", "remove", "--force", wtPath]);
if (rmRes.status === 0) {
removedWorktreePath = wtPath;
} else {
return {
ok: false,
branchName,
deletedLocalBranch: false,
deletedRemoteBranch: false,
error: `failed_to_remove_worktree: ${rmRes.stderr.trim() || rmRes.stdout.trim()}`,
};
}
}
}
}
} catch (err: unknown) {
return {
ok: false,
branchName,
deletedLocalBranch: false,
deletedRemoteBranch: false,
error: `worktree_inspection_failed: ${(err as Error).message}`,
};
}
}
// 2. Delete local branch if it exists
const checkBranch = runner("git", ["rev-parse", "--verify", `refs/heads/${branchName}`]);
if (checkBranch.status === 0) {
const delRes = runner("git", ["branch", "-D", branchName]);
if (delRes.status === 0) {
deletedLocalBranch = true;
} else {
return {
ok: false,
branchName,
deletedLocalBranch: false,
deletedRemoteBranch: false,
...(removedWorktreePath ? { removedWorktreePath } : {}),
error: `failed_to_delete_local_branch: ${delRes.stderr.trim() || delRes.stdout.trim()}`,
};
}
}
// 3. Delete remote branch if requested
if (options?.deleteRemote) {
const remote = options.remoteName || "origin";
const remoteDelRes = runner("git", ["push", remote, "--delete", branchName]);
if (remoteDelRes.status === 0) {
deletedRemoteBranch = true;
} else {
return {
ok: false,
branchName,
deletedLocalBranch,
deletedRemoteBranch: false,
...(removedWorktreePath ? { removedWorktreePath } : {}),
error: `failed_to_delete_remote_branch: ${remoteDelRes.stderr.trim() || remoteDelRes.stdout.trim()}`,
};
}
}
return {
ok: true,
branchName,
deletedLocalBranch,
deletedRemoteBranch,
...(removedWorktreePath ? { removedWorktreePath } : {}),
};
}