kith-coord 0.1.2 → 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
@@ -74,6 +74,7 @@ declare class FencingAndAgentManager {
74
74
  static readonly DEFAULT_LEASE_MS: number;
75
75
  private readonly capabilities;
76
76
  private static ownWindowsStart;
77
+ private static inspectionFailure;
77
78
  constructor(coordDb: CoordDatabase);
78
79
  static validateLease(duration: number): number;
79
80
  /** A real kernel process identity; tests must not supply liveness sentinels. */
@@ -542,6 +543,19 @@ interface ResourceRecord {
542
543
  acquired_at: number;
543
544
  expires_at: number;
544
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
+ }
545
559
  declare class ResourceBroker {
546
560
  private readonly coordDb;
547
561
  private readonly fencing;
@@ -586,26 +600,29 @@ declare class ResourceBroker {
586
600
  mutexName: string;
587
601
  fencingToken: number;
588
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
+ }>;
589
616
  /** Renew leases while a detached, recorded process group runs; stop it before releasing. */
590
- executeWithMutex(params: {
591
- agentId: string;
617
+ executeWithMutex(params: SupervisedCommandOptions & {
592
618
  mutexName: string;
593
- command: string | string[];
594
- cwd?: string;
595
- timeoutMs?: number;
596
- commandTimeoutMs?: number;
597
- leaseDurationMs?: number;
598
- killGraceMs?: number;
599
- signal?: AbortSignal;
600
- maxOutputBytes?: number;
601
- onStdout?: (chunk: Buffer) => void;
602
- onStderr?: (chunk: Buffer) => void;
603
619
  }): Promise<{
604
620
  stdout: string;
605
621
  stderr: string;
606
622
  exitCode: number;
607
623
  waitedMs: number;
608
624
  }>;
625
+ private executeGuarded;
609
626
  private getRun;
610
627
  private runIsAlive;
611
628
  private clearRun;
@@ -698,6 +715,7 @@ interface PostFileEditHookResult {
698
715
  warnings: string[];
699
716
  }
700
717
  declare class KithEngine {
718
+ private activeClaimWaits;
701
719
  readonly coordDb: CoordDatabase;
702
720
  readonly fencing: FencingAndAgentManager;
703
721
  readonly locks: LockEngine;
@@ -785,6 +803,29 @@ declare class KithEngine {
785
803
  lockConflicts: LockConflict[];
786
804
  activeSiblingIntents: TaskRecord[];
787
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;
788
829
  completeWork(params: {
789
830
  agentId: string;
790
831
  taskId?: string;
@@ -978,7 +1019,7 @@ declare function recoveryReport(directory?: string): {
978
1019
  error: string | undefined;
979
1020
  };
980
1021
 
981
- declare const KITH_VERSION = "0.1.2";
982
- declare const KITH_PROTOCOL_VERSION = "1.1.0";
1022
+ declare const KITH_VERSION = "0.1.4";
1023
+ declare const KITH_PROTOCOL_VERSION = "1.3.0";
983
1024
 
984
- 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.2";
3
- var KITH_PROTOCOL_VERSION = "1.1.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";
@@ -51,6 +82,9 @@ function repositoryFile(root, file) {
51
82
  }
52
83
  function discoverRepository(input) {
53
84
  const directory = fs.realpathSync.native(input);
85
+ if (directory === path.parse(directory).root) {
86
+ throw new Error("Cannot coordinate a filesystem root; select an explicit project with --repo.");
87
+ }
54
88
  if (!fs.statSync(directory).isDirectory()) throw new Error(`Not a directory: ${input}`);
55
89
  const git2 = (...args) => execFileSync("git", ["rev-parse", ...args], {
56
90
  cwd: directory,
@@ -615,6 +649,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
615
649
  static DEFAULT_LEASE_MS = 5 * 60 * 1e3;
616
650
  capabilities = /* @__PURE__ */ new Map();
617
651
  static ownWindowsStart = null;
652
+ static inspectionFailure = null;
618
653
  static validateLease(duration) {
619
654
  if (!Number.isSafeInteger(duration) || duration < 100 || duration > 864e5) {
620
655
  throw new Error("Lease duration must be an integer between 100 and 86400000 milliseconds");
@@ -623,6 +658,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
623
658
  }
624
659
  /** A real kernel process identity; tests must not supply liveness sentinels. */
625
660
  static getProcessStartTime(pid) {
661
+ this.inspectionFailure = null;
626
662
  if (!Number.isSafeInteger(pid) || pid <= 0) return null;
627
663
  try {
628
664
  if (process.platform === "linux") {
@@ -638,11 +674,14 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
638
674
  "-NoProfile",
639
675
  "-NonInteractive",
640
676
  "-Command",
641
- `(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)`
642
678
  ],
643
- { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3, windowsHide: true }
679
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3, windowsHide: true }
644
680
  ).trim();
645
- if (!/^\d+$/.test(ticks)) return null;
681
+ if (!/^\d+$/.test(ticks)) {
682
+ this.inspectionFailure = "invalid kernel creation-time response";
683
+ return null;
684
+ }
646
685
  const identity = `windows:${ticks}`;
647
686
  if (pid === process.pid) this.ownWindowsStart = identity;
648
687
  return identity;
@@ -654,7 +693,9 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
654
693
  env: { ...process.env, LC_ALL: "C" }
655
694
  }).trim();
656
695
  return out || null;
657
- } catch {
696
+ } catch (error) {
697
+ const failure = error;
698
+ this.inspectionFailure = [failure.code, failure.status === void 0 ? null : `exit ${failure.status}`, failure.signal].filter(Boolean).join(", ") || "inspection unavailable";
658
699
  return null;
659
700
  }
660
701
  }
@@ -684,7 +725,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
684
725
  if (params.sessionToken !== void 0) throw new SessionOwnershipError(params.id);
685
726
  const pid = params.pid ?? process.pid;
686
727
  const start = _FencingAndAgentManager.getProcessStartTime(pid);
687
- if (!start) throw new Error(`Cannot determine start time of live session owner PID ${pid}`);
728
+ if (!start) throw new Error(`Cannot determine start time of live session owner PID ${pid} (${_FencingAndAgentManager.inspectionFailure ?? "inspection unavailable"})`);
688
729
  const now = Date.now();
689
730
  const epoch = this.coordDb.nextFencingToken();
690
731
  const token = crypto2.randomBytes(32).toString("base64url");
@@ -1376,6 +1417,7 @@ var ResourceBroker = class _ResourceBroker {
1376
1417
  const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
1377
1418
  const now = Date.now();
1378
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");
1379
1421
  const resourceKey = `${params.agentId}:${params.serviceName}`;
1380
1422
  const existing = this.getResource("port", resourceKey);
1381
1423
  if (existing && existing.expires_at > now) {
@@ -1510,8 +1552,44 @@ var ResourceBroker = class _ResourceBroker {
1510
1552
  return true;
1511
1553
  });
1512
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
+ }
1513
1588
  /** Renew leases while a detached, recorded process group runs; stop it before releasing. */
1514
- async executeWithMutex(params) {
1589
+ executeWithMutex(params) {
1590
+ return this.executeGuarded(params);
1591
+ }
1592
+ async executeGuarded(params, lifecycle) {
1515
1593
  if (process.platform === "win32") throw new Error("Supervised mutex commands currently require macOS or Linux process groups");
1516
1594
  if (!params.command.length) throw new Error("A command is required");
1517
1595
  const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
@@ -1556,6 +1634,8 @@ var ResourceBroker = class _ResourceBroker {
1556
1634
  await delay(Math.min(50, Math.max(10, leaseMs / 4)));
1557
1635
  }
1558
1636
  const waitedMs = Date.now() - started;
1637
+ const prepared = await lifecycle?.prepare();
1638
+ if (params.signal?.aborted) throw abortError();
1559
1639
  const candidates = ["./command-worker.js", "../command-worker.js", "../../dist/command-worker.js"].map((name) => fileURLToPath(new URL(name, import.meta.url)));
1560
1640
  const script = candidates.find((name) => fs6.existsSync(name));
1561
1641
  if (!script) throw new Error("Missing command worker. Build Kith before running supervised commands.");
@@ -1598,6 +1678,7 @@ var ResourceBroker = class _ResourceBroker {
1598
1678
  try {
1599
1679
  this.fencing.heartbeat(params.agentId);
1600
1680
  this.renewMutex({ ...params, fencingToken: run.fencing_token });
1681
+ lifecycle?.renew();
1601
1682
  } catch (err) {
1602
1683
  stop(err instanceof Error ? err : new Error(String(err)));
1603
1684
  }
@@ -1625,8 +1706,9 @@ var ResourceBroker = class _ResourceBroker {
1625
1706
  run.worker_start = identity;
1626
1707
  workerIdentity = identity;
1627
1708
  this.renewMutex({ ...params, fencingToken: run.fencing_token });
1709
+ lifecycle?.renew();
1628
1710
  });
1629
- 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 });
1630
1712
  } catch (err) {
1631
1713
  stop(err instanceof Error ? err : new Error(String(err)));
1632
1714
  }
@@ -1674,6 +1756,7 @@ var ResourceBroker = class _ResourceBroker {
1674
1756
  this.coordDb.db.prepare("DELETE FROM mutex_waiters WHERE mutex_name=? AND request_id=? AND session_epoch=?").run(params.mutexName, requestId, epoch);
1675
1757
  if (run && !this.runIsAlive(run)) this.clearRun(run);
1676
1758
  if (run && run.worker_pid === null) this.clearRun(run);
1759
+ if (!this.getRun(params.mutexName)) lifecycle?.release();
1677
1760
  });
1678
1761
  } finally {
1679
1762
  this.activeExecutions--;
@@ -3415,6 +3498,7 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
3415
3498
 
3416
3499
  // src/core/engine.ts
3417
3500
  var KithEngine = class {
3501
+ activeClaimWaits = 0;
3418
3502
  coordDb;
3419
3503
  fencing;
3420
3504
  locks;
@@ -3567,6 +3651,22 @@ ${lines.join("\n")}` : "";
3567
3651
  const worktree = intent.ok && params.isolateWorktree ? this.worktrees.spawnWorktree({ agentId: params.agentId }) : null;
3568
3652
  return { ...intent, worktree };
3569
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
+ }
3570
3670
  completeWork(params) {
3571
3671
  this.fencing.assertSession(params.agentId);
3572
3672
  const agent = this.fencing.getAgent(params.agentId);
@@ -3587,6 +3687,7 @@ ${lines.join("\n")}` : "";
3587
3687
  return { ok: true, completedTasks, mergeResult };
3588
3688
  }
3589
3689
  close() {
3690
+ if (this.activeClaimWaits) throw new Error("Await or cancel active file claim waits before closing Kith");
3590
3691
  if (this.broker.hasActiveCommands()) throw new Error("Await or cancel active mutex commands before closing Kith");
3591
3692
  this.coordDb.close();
3592
3693
  }