kith-coord 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@ process.once("message", (message) => {
13
13
  if (started) return;
14
14
  started = true;
15
15
  let finished = false;
16
- const env = { ...process.env, ...message.mutexName === "package-install" ? { KITH_PACKAGE_INSTALL_GUARDED: "1" } : {} };
16
+ const env = { ...process.env, ...message.env, ...message.mutexName === "package-install" ? { KITH_PACKAGE_INSTALL_GUARDED: "1" } : {} };
17
17
  const child = Array.isArray(message.command) ? spawn(message.command[0], message.command.slice(1), { cwd: message.cwd, env, stdio: ["ignore", "inherit", "inherit"] }) : spawn(message.command, { cwd: message.cwd, env, stdio: ["ignore", "inherit", "inherit"], shell: true });
18
18
  child.once("error", (err) => {
19
19
  if (finished) return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/command-worker.ts"],"sourcesContent":["// A detached process-group leader. The command cannot start until its supervisor\n// has durably recorded this group and sent the start message over private IPC.\nimport os from 'node:os';\nimport { spawn } from 'node:child_process';\n\nlet started = false;\nprocess.on('disconnect', () => {\n try { process.kill(-process.pid, 'SIGKILL'); } catch { process.exit(1); }\n});\nprocess.once('message', (message: { command: string | string[]; cwd: string; mutexName: string }) => {\n if (started) return;\n started = true;\n let finished = false;\n const env = { ...process.env, ...(message.mutexName === 'package-install' ? { KITH_PACKAGE_INSTALL_GUARDED: '1' } : {}) };\n const child = Array.isArray(message.command)\n ? spawn(message.command[0], message.command.slice(1), { cwd: message.cwd, env, stdio: ['ignore', 'inherit', 'inherit'] })\n : spawn(message.command, { cwd: message.cwd, env, stdio: ['ignore', 'inherit', 'inherit'], shell: true });\n child.once('error', (err) => {\n if (finished) return;\n finished = true;\n process.send?.({ error: err.message }, () => process.exit(1));\n });\n child.once('close', (code, signal) => {\n if (finished) return;\n finished = true;\n if (process.connected) process.send?.({ exitCode: code ?? (signal ? 128 + (os.constants.signals[signal] ?? 1) : 1), signal }, () => process.exit(0));\n });\n});\nprocess.send?.({ ready: true });\n"],"mappings":";AAEA,OAAO,QAAQ;AACf,SAAS,aAAa;AAEtB,IAAI,UAAU;AACd,QAAQ,GAAG,cAAc,MAAM;AAC7B,MAAI;AAAE,YAAQ,KAAK,CAAC,QAAQ,KAAK,SAAS;AAAA,EAAG,QAAQ;AAAE,YAAQ,KAAK,CAAC;AAAA,EAAG;AAC1E,CAAC;AACD,QAAQ,KAAK,WAAW,CAAC,YAA4E;AACnG,MAAI,QAAS;AACb,YAAU;AACV,MAAI,WAAW;AACf,QAAM,MAAM,EAAE,GAAG,QAAQ,KAAK,GAAI,QAAQ,cAAc,oBAAoB,EAAE,8BAA8B,IAAI,IAAI,CAAC,EAAG;AACxH,QAAM,QAAQ,MAAM,QAAQ,QAAQ,OAAO,IACvC,MAAM,QAAQ,QAAQ,CAAC,GAAG,QAAQ,QAAQ,MAAM,CAAC,GAAG,EAAE,KAAK,QAAQ,KAAK,KAAK,OAAO,CAAC,UAAU,WAAW,SAAS,EAAE,CAAC,IACtH,MAAM,QAAQ,SAAS,EAAE,KAAK,QAAQ,KAAK,KAAK,OAAO,CAAC,UAAU,WAAW,SAAS,GAAG,OAAO,KAAK,CAAC;AAC1G,QAAM,KAAK,SAAS,CAAC,QAAQ;AAC3B,QAAI,SAAU;AACd,eAAW;AACX,YAAQ,OAAO,EAAE,OAAO,IAAI,QAAQ,GAAG,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA,EAC9D,CAAC;AACD,QAAM,KAAK,SAAS,CAAC,MAAM,WAAW;AACpC,QAAI,SAAU;AACd,eAAW;AACX,QAAI,QAAQ,UAAW,SAAQ,OAAO,EAAE,UAAU,SAAS,SAAS,OAAO,GAAG,UAAU,QAAQ,MAAM,KAAK,KAAK,IAAI,OAAO,GAAG,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA,EACrJ,CAAC;AACH,CAAC;AACD,QAAQ,OAAO,EAAE,OAAO,KAAK,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/core/command-worker.ts"],"sourcesContent":["// A detached process-group leader. The command cannot start until its supervisor\n// has durably recorded this group and sent the start message over private IPC.\nimport os from 'node:os';\nimport { spawn } from 'node:child_process';\n\nlet started = false;\nprocess.on('disconnect', () => {\n try { process.kill(-process.pid, 'SIGKILL'); } catch { process.exit(1); }\n});\nprocess.once('message', (message: { command: string | string[]; cwd: string; mutexName: string; env?: Record<string, string> }) => {\n if (started) return;\n started = true;\n let finished = false;\n const env = { ...process.env, ...message.env, ...(message.mutexName === 'package-install' ? { KITH_PACKAGE_INSTALL_GUARDED: '1' } : {}) };\n const child = Array.isArray(message.command)\n ? spawn(message.command[0], message.command.slice(1), { cwd: message.cwd, env, stdio: ['ignore', 'inherit', 'inherit'] })\n : spawn(message.command, { cwd: message.cwd, env, stdio: ['ignore', 'inherit', 'inherit'], shell: true });\n child.once('error', (err) => {\n if (finished) return;\n finished = true;\n process.send?.({ error: err.message }, () => process.exit(1));\n });\n child.once('close', (code, signal) => {\n if (finished) return;\n finished = true;\n if (process.connected) process.send?.({ exitCode: code ?? (signal ? 128 + (os.constants.signals[signal] ?? 1) : 1), signal }, () => process.exit(0));\n });\n});\nprocess.send?.({ ready: true });\n"],"mappings":";AAEA,OAAO,QAAQ;AACf,SAAS,aAAa;AAEtB,IAAI,UAAU;AACd,QAAQ,GAAG,cAAc,MAAM;AAC7B,MAAI;AAAE,YAAQ,KAAK,CAAC,QAAQ,KAAK,SAAS;AAAA,EAAG,QAAQ;AAAE,YAAQ,KAAK,CAAC;AAAA,EAAG;AAC1E,CAAC;AACD,QAAQ,KAAK,WAAW,CAAC,YAA0G;AACjI,MAAI,QAAS;AACb,YAAU;AACV,MAAI,WAAW;AACf,QAAM,MAAM,EAAE,GAAG,QAAQ,KAAK,GAAG,QAAQ,KAAK,GAAI,QAAQ,cAAc,oBAAoB,EAAE,8BAA8B,IAAI,IAAI,CAAC,EAAG;AACxI,QAAM,QAAQ,MAAM,QAAQ,QAAQ,OAAO,IACvC,MAAM,QAAQ,QAAQ,CAAC,GAAG,QAAQ,QAAQ,MAAM,CAAC,GAAG,EAAE,KAAK,QAAQ,KAAK,KAAK,OAAO,CAAC,UAAU,WAAW,SAAS,EAAE,CAAC,IACtH,MAAM,QAAQ,SAAS,EAAE,KAAK,QAAQ,KAAK,KAAK,OAAO,CAAC,UAAU,WAAW,SAAS,GAAG,OAAO,KAAK,CAAC;AAC1G,QAAM,KAAK,SAAS,CAAC,QAAQ;AAC3B,QAAI,SAAU;AACd,eAAW;AACX,YAAQ,OAAO,EAAE,OAAO,IAAI,QAAQ,GAAG,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA,EAC9D,CAAC;AACD,QAAM,KAAK,SAAS,CAAC,MAAM,WAAW;AACpC,QAAI,SAAU;AACd,eAAW;AACX,QAAI,QAAQ,UAAW,SAAQ,OAAO,EAAE,UAAU,SAAS,SAAS,OAAO,GAAG,UAAU,QAAQ,MAAM,KAAK,KAAK,IAAI,OAAO,GAAG,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA,EACrJ,CAAC;AACH,CAAC;AACD,QAAQ,OAAO,EAAE,OAAO,KAAK,CAAC;","names":[]}
package/dist/index.d.ts CHANGED
@@ -543,6 +543,19 @@ interface ResourceRecord {
543
543
  acquired_at: number;
544
544
  expires_at: number;
545
545
  }
546
+ interface SupervisedCommandOptions {
547
+ agentId: string;
548
+ command: string | string[];
549
+ cwd?: string;
550
+ timeoutMs?: number;
551
+ commandTimeoutMs?: number;
552
+ leaseDurationMs?: number;
553
+ killGraceMs?: number;
554
+ signal?: AbortSignal;
555
+ maxOutputBytes?: number;
556
+ onStdout?: (chunk: Buffer) => void;
557
+ onStderr?: (chunk: Buffer) => void;
558
+ }
546
559
  declare class ResourceBroker {
547
560
  private readonly coordDb;
548
561
  private readonly fencing;
@@ -587,26 +600,29 @@ declare class ResourceBroker {
587
600
  mutexName: string;
588
601
  fencingToken: number;
589
602
  }): boolean;
603
+ /** Launch on the allocated port, without interpreting arguments as shell code. */
604
+ executeWithPort(params: Omit<SupervisedCommandOptions, 'command'> & {
605
+ command: string[];
606
+ serviceName: string;
607
+ basePort?: number;
608
+ onPort?: (port: number) => void;
609
+ }): Promise<{
610
+ port: number;
611
+ stdout: string;
612
+ stderr: string;
613
+ exitCode: number;
614
+ waitedMs: number;
615
+ }>;
590
616
  /** Renew leases while a detached, recorded process group runs; stop it before releasing. */
591
- executeWithMutex(params: {
592
- agentId: string;
617
+ executeWithMutex(params: SupervisedCommandOptions & {
593
618
  mutexName: string;
594
- command: string | string[];
595
- cwd?: string;
596
- timeoutMs?: number;
597
- commandTimeoutMs?: number;
598
- leaseDurationMs?: number;
599
- killGraceMs?: number;
600
- signal?: AbortSignal;
601
- maxOutputBytes?: number;
602
- onStdout?: (chunk: Buffer) => void;
603
- onStderr?: (chunk: Buffer) => void;
604
619
  }): Promise<{
605
620
  stdout: string;
606
621
  stderr: string;
607
622
  exitCode: number;
608
623
  waitedMs: number;
609
624
  }>;
625
+ private executeGuarded;
610
626
  private getRun;
611
627
  private runIsAlive;
612
628
  private clearRun;
@@ -699,6 +715,7 @@ interface PostFileEditHookResult {
699
715
  warnings: string[];
700
716
  }
701
717
  declare class KithEngine {
718
+ private activeClaimWaits;
702
719
  readonly coordDb: CoordDatabase;
703
720
  readonly fencing: FencingAndAgentManager;
704
721
  readonly locks: LockEngine;
@@ -786,6 +803,29 @@ declare class KithEngine {
786
803
  lockConflicts: LockConflict[];
787
804
  activeSiblingIntents: TaskRecord[];
788
805
  };
806
+ beginWorkWhenAvailable(params: Parameters<KithEngine['beginWork']>[0] & {
807
+ waitTimeoutMs: number;
808
+ signal?: AbortSignal;
809
+ }): Promise<{
810
+ worktree: SpawnedWorktree | null;
811
+ ok: boolean;
812
+ task: TaskRecord | null;
813
+ fencingToken: number;
814
+ locks: LockRecord[];
815
+ lockConflicts: LockConflict[];
816
+ activeSiblingIntents: TaskRecord[];
817
+ } & {
818
+ waitedMs: number;
819
+ waitTimedOut: boolean;
820
+ }>;
821
+ acquireLocksWhenAvailable(params: Parameters<LockEngine['acquireLocks']>[0] & {
822
+ waitTimeoutMs: number;
823
+ signal?: AbortSignal;
824
+ }): Promise<AcquireLockResult & {
825
+ waitedMs: number;
826
+ waitTimedOut: boolean;
827
+ }>;
828
+ private waitForOwnedClaim;
789
829
  completeWork(params: {
790
830
  agentId: string;
791
831
  taskId?: string;
@@ -979,7 +1019,7 @@ declare function recoveryReport(directory?: string): {
979
1019
  error: string | undefined;
980
1020
  };
981
1021
 
982
- declare const KITH_VERSION = "0.1.3";
983
- declare const KITH_PROTOCOL_VERSION = "1.2.0";
1022
+ declare const KITH_VERSION = "0.1.4";
1023
+ declare const KITH_PROTOCOL_VERSION = "1.3.0";
984
1024
 
985
- export { type AcquireLockResult, type AgentRecord, type AgentUnreadDelta, AstAnalyzer, type BashGuardPostReport, BashMerkleGuard, type BlastRadiusAlert, CoWWorktreeManager, type ConflictRecord, type ContractRecord, CoordDatabase, type CoordEvent, type DeclareIntentResult, DependencyAndContractEngine, type DirtyMergeProbeResult, type ExtractedImport, type ExtractedSymbol, FencingAndAgentManager, type FileAstAnalysis, type FileModificationReport, type FileSnapshotEntry, IntentAndTaskEngine, KITH_PROTOCOL_VERSION, KITH_VERSION, KithEngine, type LockConflict, LockEngine, type LockRecord, type MergeTrainEntry, type PostFileEditHookResult, type QuarantineRecord, type RepositoryLayout, ResourceBroker, type ResourceRecord, SessionOwnershipError, type SessionRegistration, ShadowGitEngine, type SpawnedWorktree, type SpeculativeMergeResult, SpeculativeMergeTrain, StaleFencingTokenError, type SymbolKind, type TaskRecord, type WorkspaceObservation, type WorktreeChange, caseSensitivePaths, discoverRepository, isWithin, observeWorkspaces, physicalPath, previewIntegration, readCoordinationState, recoveryReport, repositoryFile, worktreeChanges };
1025
+ export { type AcquireLockResult, type AgentRecord, type AgentUnreadDelta, AstAnalyzer, type BashGuardPostReport, BashMerkleGuard, type BlastRadiusAlert, CoWWorktreeManager, type ConflictRecord, type ContractRecord, CoordDatabase, type CoordEvent, type DeclareIntentResult, DependencyAndContractEngine, type DirtyMergeProbeResult, type ExtractedImport, type ExtractedSymbol, FencingAndAgentManager, type FileAstAnalysis, type FileModificationReport, type FileSnapshotEntry, IntentAndTaskEngine, KITH_PROTOCOL_VERSION, KITH_VERSION, KithEngine, type LockConflict, LockEngine, type LockRecord, type MergeTrainEntry, type PostFileEditHookResult, type QuarantineRecord, type RepositoryLayout, ResourceBroker, type ResourceRecord, SessionOwnershipError, type SessionRegistration, ShadowGitEngine, type SpawnedWorktree, type SpeculativeMergeResult, SpeculativeMergeTrain, StaleFencingTokenError, type SupervisedCommandOptions, type SymbolKind, type TaskRecord, type WorkspaceObservation, type WorktreeChange, caseSensitivePaths, discoverRepository, isWithin, observeWorkspaces, physicalPath, previewIntegration, readCoordinationState, recoveryReport, repositoryFile, worktreeChanges };
package/dist/index.js CHANGED
@@ -1,6 +1,37 @@
1
+ // src/core/claim-wait.ts
2
+ async function waitForClaim(params) {
3
+ const { waitTimeoutMs, signal } = params;
4
+ if (!Number.isSafeInteger(waitTimeoutMs) || waitTimeoutMs < 0 || waitTimeoutMs > 6e5) {
5
+ throw new Error("waitTimeoutMs must be an integer between 0 and 600000");
6
+ }
7
+ const started = performance.now();
8
+ let previous;
9
+ for (; ; ) {
10
+ signal?.throwIfAborted();
11
+ const elapsed = Math.round(performance.now() - started);
12
+ if (previous && elapsed >= waitTimeoutMs) return { ...previous, waitedMs: elapsed, waitTimedOut: true };
13
+ params.heartbeat();
14
+ const result = params.attempt();
15
+ const waitedMs = Math.round(performance.now() - started);
16
+ if (result.ok || waitedMs >= waitTimeoutMs) return { ...result, waitedMs, waitTimedOut: !result.ok };
17
+ previous = result;
18
+ await new Promise((resolve, reject) => {
19
+ const abort = () => {
20
+ clearTimeout(timer);
21
+ reject(signal.reason);
22
+ };
23
+ const timer = setTimeout(() => {
24
+ signal?.removeEventListener("abort", abort);
25
+ resolve();
26
+ }, Math.min(100, waitTimeoutMs - waitedMs));
27
+ signal?.addEventListener("abort", abort, { once: true });
28
+ });
29
+ }
30
+ }
31
+
1
32
  // src/version.ts
2
- var KITH_VERSION = "0.1.3";
3
- var KITH_PROTOCOL_VERSION = "1.2.0";
33
+ var KITH_VERSION = "0.1.4";
34
+ var KITH_PROTOCOL_VERSION = "1.3.0";
4
35
 
5
36
  // src/core/workspace-observation.ts
6
37
  import { execFileSync as execFileSync2, spawnSync } from "child_process";
@@ -643,7 +674,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
643
674
  "-NoProfile",
644
675
  "-NonInteractive",
645
676
  "-Command",
646
- `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks.ToString([System.Globalization.CultureInfo]::InvariantCulture)`
677
+ `([System.Diagnostics.Process]::GetProcessById(${pid})).StartTime.ToUniversalTime().Ticks.ToString([System.Globalization.CultureInfo]::InvariantCulture)`
647
678
  ],
648
679
  { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3, windowsHide: true }
649
680
  ).trim();
@@ -1386,6 +1417,7 @@ var ResourceBroker = class _ResourceBroker {
1386
1417
  const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
1387
1418
  const now = Date.now();
1388
1419
  const expiresAt = now + leaseMs;
1420
+ if (!Number.isInteger(basePort) || basePort < 1 || basePort > 65535) throw new Error("basePort must be an integer between 1 and 65535");
1389
1421
  const resourceKey = `${params.agentId}:${params.serviceName}`;
1390
1422
  const existing = this.getResource("port", resourceKey);
1391
1423
  if (existing && existing.expires_at > now) {
@@ -1520,8 +1552,44 @@ var ResourceBroker = class _ResourceBroker {
1520
1552
  return true;
1521
1553
  });
1522
1554
  }
1555
+ /** Launch on the allocated port, without interpreting arguments as shell code. */
1556
+ async executeWithPort(params) {
1557
+ if (!params.command.length || !params.command[0]) throw new Error("A command is required");
1558
+ if (!params.serviceName || params.serviceName.length > 128) throw new Error("serviceName must contain 1 to 128 characters");
1559
+ const mutexName = `dev-port:${params.agentId}:${params.serviceName}`;
1560
+ let allocation;
1561
+ const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
1562
+ const result = await this.executeGuarded({ ...params, mutexName }, {
1563
+ prepare: async () => {
1564
+ allocation = await this.acquirePort({ ...params, basePort: params.basePort ?? 3e3 });
1565
+ params.onPort?.(allocation.port);
1566
+ return {
1567
+ command: params.command.map((arg) => arg.replaceAll("{port}", String(allocation.port))),
1568
+ env: { PORT: String(allocation.port), KITH_PORT: String(allocation.port) }
1569
+ };
1570
+ },
1571
+ renew: () => {
1572
+ if (!allocation) return;
1573
+ this.coordDb.withTransaction(() => {
1574
+ this.fencing.assertSession(params.agentId);
1575
+ const resource = this.getResource("port", `${params.agentId}:${params.serviceName}`);
1576
+ if (!resource || resource.fencing_token !== allocation.fencingToken || resource.expires_at <= Date.now()) {
1577
+ throw new Error("Dev server port lease lost");
1578
+ }
1579
+ this.coordDb.db.prepare("UPDATE resources SET expires_at=? WHERE id=?").run(Date.now() + leaseMs, resource.id);
1580
+ });
1581
+ },
1582
+ release: () => {
1583
+ if (allocation) this.coordDb.db.prepare("DELETE FROM resources WHERE type='port' AND name=? AND fencing_token=?").run(`${params.agentId}:${params.serviceName}`, allocation.fencingToken);
1584
+ }
1585
+ });
1586
+ return { ...result, port: allocation.port };
1587
+ }
1523
1588
  /** Renew leases while a detached, recorded process group runs; stop it before releasing. */
1524
- async executeWithMutex(params) {
1589
+ executeWithMutex(params) {
1590
+ return this.executeGuarded(params);
1591
+ }
1592
+ async executeGuarded(params, lifecycle) {
1525
1593
  if (process.platform === "win32") throw new Error("Supervised mutex commands currently require macOS or Linux process groups");
1526
1594
  if (!params.command.length) throw new Error("A command is required");
1527
1595
  const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
@@ -1566,6 +1634,8 @@ var ResourceBroker = class _ResourceBroker {
1566
1634
  await delay(Math.min(50, Math.max(10, leaseMs / 4)));
1567
1635
  }
1568
1636
  const waitedMs = Date.now() - started;
1637
+ const prepared = await lifecycle?.prepare();
1638
+ if (params.signal?.aborted) throw abortError();
1569
1639
  const candidates = ["./command-worker.js", "../command-worker.js", "../../dist/command-worker.js"].map((name) => fileURLToPath(new URL(name, import.meta.url)));
1570
1640
  const script = candidates.find((name) => fs6.existsSync(name));
1571
1641
  if (!script) throw new Error("Missing command worker. Build Kith before running supervised commands.");
@@ -1608,6 +1678,7 @@ var ResourceBroker = class _ResourceBroker {
1608
1678
  try {
1609
1679
  this.fencing.heartbeat(params.agentId);
1610
1680
  this.renewMutex({ ...params, fencingToken: run.fencing_token });
1681
+ lifecycle?.renew();
1611
1682
  } catch (err) {
1612
1683
  stop(err instanceof Error ? err : new Error(String(err)));
1613
1684
  }
@@ -1635,8 +1706,9 @@ var ResourceBroker = class _ResourceBroker {
1635
1706
  run.worker_start = identity;
1636
1707
  workerIdentity = identity;
1637
1708
  this.renewMutex({ ...params, fencingToken: run.fencing_token });
1709
+ lifecycle?.renew();
1638
1710
  });
1639
- worker.send({ command: params.command, cwd: params.cwd ?? this.coordDb.repoRoot, mutexName: params.mutexName });
1711
+ worker.send({ command: prepared?.command ?? params.command, env: prepared?.env, cwd: params.cwd ?? this.coordDb.repoRoot, mutexName: params.mutexName });
1640
1712
  } catch (err) {
1641
1713
  stop(err instanceof Error ? err : new Error(String(err)));
1642
1714
  }
@@ -1684,6 +1756,7 @@ var ResourceBroker = class _ResourceBroker {
1684
1756
  this.coordDb.db.prepare("DELETE FROM mutex_waiters WHERE mutex_name=? AND request_id=? AND session_epoch=?").run(params.mutexName, requestId, epoch);
1685
1757
  if (run && !this.runIsAlive(run)) this.clearRun(run);
1686
1758
  if (run && run.worker_pid === null) this.clearRun(run);
1759
+ if (!this.getRun(params.mutexName)) lifecycle?.release();
1687
1760
  });
1688
1761
  } finally {
1689
1762
  this.activeExecutions--;
@@ -3425,6 +3498,7 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
3425
3498
 
3426
3499
  // src/core/engine.ts
3427
3500
  var KithEngine = class {
3501
+ activeClaimWaits = 0;
3428
3502
  coordDb;
3429
3503
  fencing;
3430
3504
  locks;
@@ -3577,6 +3651,22 @@ ${lines.join("\n")}` : "";
3577
3651
  const worktree = intent.ok && params.isolateWorktree ? this.worktrees.spawnWorktree({ agentId: params.agentId }) : null;
3578
3652
  return { ...intent, worktree };
3579
3653
  }
3654
+ async beginWorkWhenAvailable(params) {
3655
+ return this.waitForOwnedClaim(params, () => this.beginWork(params));
3656
+ }
3657
+ async acquireLocksWhenAvailable(params) {
3658
+ return this.waitForOwnedClaim(params, () => this.locks.acquireLocks(params));
3659
+ }
3660
+ async waitForOwnedClaim(params, attempt) {
3661
+ this.activeClaimWaits++;
3662
+ try {
3663
+ return await waitForClaim({ ...params, attempt, heartbeat: () => {
3664
+ this.fencing.heartbeat(params.agentId);
3665
+ } });
3666
+ } finally {
3667
+ this.activeClaimWaits--;
3668
+ }
3669
+ }
3580
3670
  completeWork(params) {
3581
3671
  this.fencing.assertSession(params.agentId);
3582
3672
  const agent = this.fencing.getAgent(params.agentId);
@@ -3597,6 +3687,7 @@ ${lines.join("\n")}` : "";
3597
3687
  return { ok: true, completedTasks, mergeResult };
3598
3688
  }
3599
3689
  close() {
3690
+ if (this.activeClaimWaits) throw new Error("Await or cancel active file claim waits before closing Kith");
3600
3691
  if (this.broker.hasActiveCommands()) throw new Error("Await or cancel active mutex commands before closing Kith");
3601
3692
  this.coordDb.close();
3602
3693
  }