kith-coord 0.1.3 → 0.1.5

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.
package/dist/cli/index.js CHANGED
@@ -6,8 +6,8 @@ import path19 from "path";
6
6
  import { spawn } from "child_process";
7
7
 
8
8
  // src/version.ts
9
- var KITH_VERSION = "0.1.3";
10
- var KITH_PROTOCOL_VERSION = "1.2.0";
9
+ var KITH_VERSION = "0.1.5";
10
+ var KITH_PROTOCOL_VERSION = "1.3.0";
11
11
 
12
12
  // src/cli/setup-ledger.ts
13
13
  import fs2 from "fs";
@@ -243,6 +243,7 @@ function uninstallKith(directory = process.cwd(), dryRun = false) {
243
243
  // src/cli/doctor.ts
244
244
  import fs5 from "fs";
245
245
  import path5 from "path";
246
+ import os2 from "os";
246
247
  import { execFileSync as execFileSync3 } from "child_process";
247
248
  import { parse as parseToml } from "smol-toml";
248
249
 
@@ -792,7 +793,7 @@ function previewIntegration(repoRoot, agent) {
792
793
 
793
794
  // src/core/observation.ts
794
795
  var { DatabaseSync: DatabaseSync2 } = createRequire2(import.meta.url)("node:sqlite");
795
- function readCoordinationState(directory = process.cwd()) {
796
+ function readCoordinationState(directory = process.cwd(), options = {}) {
796
797
  const layout = discoverRepository(directory), observedAt = Date.now();
797
798
  const empty = {
798
799
  protocolVersion: KITH_PROTOCOL_VERSION,
@@ -851,6 +852,10 @@ function readCoordinationState(directory = process.cwd()) {
851
852
  if (CoordDatabase.inspectSchema(db) !== 2) throw new Error("Offline observation requires schema 2; preserve state and upgrade with clients stopped");
852
853
  const all = (sql) => db.prepare(sql).all();
853
854
  const agents = all("SELECT * FROM agents ORDER BY registered_at DESC");
855
+ if (options.ownersOnly) {
856
+ db.exec("COMMIT");
857
+ return { ...empty, state: "ready", agents };
858
+ }
854
859
  const state = {
855
860
  ...empty,
856
861
  state: "ready",
@@ -895,7 +900,7 @@ function recoveryReport(directory = process.cwd()) {
895
900
  }
896
901
 
897
902
  // src/cli/doctor.ts
898
- function doctorReport(directory = process.cwd()) {
903
+ function doctorReport(directory = process.cwd(), home = os2.homedir()) {
899
904
  const state = readCoordinationState(directory), root = state.repository.root;
900
905
  const checks = [];
901
906
  const check = (name, ok, detail) => checks.push({ name, ok, detail });
@@ -920,24 +925,42 @@ function doctorReport(directory = process.cwd()) {
920
925
  }
921
926
  });
922
927
  };
928
+ const desktop = process.platform === "win32" ? path5.join(home === os2.homedir() ? process.env.APPDATA ?? path5.join(home, "AppData/Roaming") : path5.join(home, "AppData/Roaming"), "Claude/claude_desktop_config.json") : process.platform === "darwin" ? path5.join(home, "Library/Application Support/Claude/claude_desktop_config.json") : path5.join(home, ".config/Claude/claude_desktop_config.json");
929
+ const files = [
930
+ ["claude", path5.join(root, ".mcp.json"), false, root],
931
+ ["cursor", path5.join(root, ".cursor/mcp.json"), false, root],
932
+ ["codex", path5.join(root, ".codex/config.toml"), true, root],
933
+ ["claude-global", path5.join(home, ".claude.json"), false, home],
934
+ ["claude-desktop", desktop, false, path5.dirname(desktop)],
935
+ ["codex-global", path5.join(home, ".codex/config.toml"), true, home],
936
+ ["antigravity", path5.join(home, ".gemini/config/mcp_config.json"), false, home]
937
+ ];
923
938
  let configured = 0;
924
- for (const [client, file] of [["claude", ".mcp.json"], ["cursor", ".cursor/mcp.json"], ["codex", ".codex/config.toml"]]) {
925
- const target = path5.join(root, file);
926
- if (!fs5.existsSync(target)) continue;
939
+ const inspected = /* @__PURE__ */ new Set();
940
+ for (const [client, target, toml, base] of files) {
941
+ if (inspected.has(target) || !fs5.existsSync(target)) continue;
942
+ inspected.add(target);
927
943
  try {
928
- if (!isWithin(root, physicalPath(target))) throw new Error("Configuration is redirected outside the checkout");
944
+ if (!isWithin(physicalPath(base), physicalPath(target))) throw new Error("Configuration is redirected outside its setup root");
929
945
  const text = fs5.readFileSync(target, "utf8");
930
- const config = client === "codex" ? parseToml(text) : JSON.parse(text);
931
- const server = client === "codex" ? config.mcp_servers?.kith : config.mcpServers?.kith;
946
+ const config = toml ? parseToml(text) : JSON.parse(text);
947
+ const server = toml ? config.mcp_servers?.kith : config.mcpServers?.kith;
932
948
  if (!server) continue;
933
949
  configured++;
934
- check(`${client}-mcp`, typeof server.command === "string" && Array.isArray(server.args) && executable(server.command), "Kith stdio launcher must be executable by absolute path or PATH; native client trust and MCP discovery require client validation");
935
- } catch {
936
- check(`${client}-mcp`, false, `Cannot parse ${file}; existing configuration was preserved`);
950
+ if (typeof server.command !== "string" || !Array.isArray(server.args) || !server.args.every((a) => typeof a === "string") || !executable(server.command))
951
+ throw new Error("Kith launcher is missing or not executable; rerun setup from the installed version");
952
+ if (/^node(?:\.exe)?$/i.test(path5.basename(server.command)) && (!server.args[0] || !fs5.existsSync(server.args[0]) || !fs5.statSync(server.args[0]).isFile()))
953
+ throw new Error("Kith CLI entry is missing; rerun setup after moving or reinstalling Node/Kith");
954
+ const pin = server.args.indexOf("--repo");
955
+ if (pin >= 0 && (typeof server.args[pin + 1] !== "string" || !path5.isAbsolute(server.args[pin + 1]) || physicalPath(server.args[pin + 1]) !== root))
956
+ throw new Error("Kith launcher is pinned to a different checkout; use that checkout or review its setup");
957
+ check(`${client}-mcp`, true, "Launcher files are present; native connection, project trust and tool approval are not verified by doctor");
958
+ } catch (error) {
959
+ check(`${client}-mcp`, false, `${target}: ${error instanceof Error ? error.message : String(error)}; configuration preserved`);
937
960
  }
938
961
  }
939
- check("mcp-configuration", configured > 0, "Configure the clients you use with kith init; diagnostics do not change client settings");
940
- check("coordination-state", state.state === "ready", state.state === "ready" ? "Schema and counters validated on a private snapshot" : state.error ?? "No coordination state; run kith init before starting agents");
962
+ check("mcp-configuration", configured > 0, "Use global kith setup or project kith init for your client; configuration alone does not establish a connection");
963
+ check("coordination-state", state.state !== "unavailable", state.state === "ready" ? "Schema and counters validated on a private snapshot" : state.error ?? "No project state yet; the first owned task initializes it. No state was created by doctor");
941
964
  let nativeGuards = false;
942
965
  for (const [file, event] of [[".claude/settings.json", "PreToolUse"], [".cursor/hooks.json", "preToolUse"]]) {
943
966
  const target = path5.join(root, file);
@@ -1021,7 +1044,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
1021
1044
  "-NoProfile",
1022
1045
  "-NonInteractive",
1023
1046
  "-Command",
1024
- `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks.ToString([System.Globalization.CultureInfo]::InvariantCulture)`
1047
+ `([System.Diagnostics.Process]::GetProcessById(${pid})).StartTime.ToUniversalTime().Ticks.ToString([System.Globalization.CultureInfo]::InvariantCulture)`
1025
1048
  ],
1026
1049
  { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3, windowsHide: true }
1027
1050
  ).trim();
@@ -1201,6 +1224,37 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
1201
1224
  }
1202
1225
  };
1203
1226
 
1227
+ // src/core/claim-wait.ts
1228
+ async function waitForClaim(params) {
1229
+ const { waitTimeoutMs, signal } = params;
1230
+ if (!Number.isSafeInteger(waitTimeoutMs) || waitTimeoutMs < 0 || waitTimeoutMs > 6e5) {
1231
+ throw new Error("waitTimeoutMs must be an integer between 0 and 600000");
1232
+ }
1233
+ const started = performance.now();
1234
+ let previous;
1235
+ for (; ; ) {
1236
+ signal?.throwIfAborted();
1237
+ const elapsed = Math.round(performance.now() - started);
1238
+ if (previous && elapsed >= waitTimeoutMs) return { ...previous, waitedMs: elapsed, waitTimedOut: true };
1239
+ params.heartbeat();
1240
+ const result = params.attempt();
1241
+ const waitedMs = Math.round(performance.now() - started);
1242
+ if (result.ok || waitedMs >= waitTimeoutMs) return { ...result, waitedMs, waitTimedOut: !result.ok };
1243
+ previous = result;
1244
+ await new Promise((resolve, reject) => {
1245
+ const abort = () => {
1246
+ clearTimeout(timer);
1247
+ reject(signal.reason);
1248
+ };
1249
+ const timer = setTimeout(() => {
1250
+ signal?.removeEventListener("abort", abort);
1251
+ resolve();
1252
+ }, Math.min(100, waitTimeoutMs - waitedMs));
1253
+ signal?.addEventListener("abort", abort, { once: true });
1254
+ });
1255
+ }
1256
+ }
1257
+
1204
1258
  // src/core/engine.ts
1205
1259
  import fs15 from "fs";
1206
1260
  import path14 from "path";
@@ -2119,6 +2173,7 @@ var ResourceBroker = class _ResourceBroker {
2119
2173
  const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
2120
2174
  const now = Date.now();
2121
2175
  const expiresAt = now + leaseMs;
2176
+ if (!Number.isInteger(basePort) || basePort < 1 || basePort > 65535) throw new Error("basePort must be an integer between 1 and 65535");
2122
2177
  const resourceKey = `${params.agentId}:${params.serviceName}`;
2123
2178
  const existing = this.getResource("port", resourceKey);
2124
2179
  if (existing && existing.expires_at > now) {
@@ -2253,8 +2308,44 @@ var ResourceBroker = class _ResourceBroker {
2253
2308
  return true;
2254
2309
  });
2255
2310
  }
2311
+ /** Launch on the allocated port, without interpreting arguments as shell code. */
2312
+ async executeWithPort(params) {
2313
+ if (!params.command.length || !params.command[0]) throw new Error("A command is required");
2314
+ if (!params.serviceName || params.serviceName.length > 128) throw new Error("serviceName must contain 1 to 128 characters");
2315
+ const mutexName = `dev-port:${params.agentId}:${params.serviceName}`;
2316
+ let allocation;
2317
+ const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
2318
+ const result = await this.executeGuarded({ ...params, mutexName }, {
2319
+ prepare: async () => {
2320
+ allocation = await this.acquirePort({ ...params, basePort: params.basePort ?? 3e3 });
2321
+ params.onPort?.(allocation.port);
2322
+ return {
2323
+ command: params.command.map((arg) => arg.replaceAll("{port}", String(allocation.port))),
2324
+ env: { PORT: String(allocation.port), KITH_PORT: String(allocation.port) }
2325
+ };
2326
+ },
2327
+ renew: () => {
2328
+ if (!allocation) return;
2329
+ this.coordDb.withTransaction(() => {
2330
+ this.fencing.assertSession(params.agentId);
2331
+ const resource = this.getResource("port", `${params.agentId}:${params.serviceName}`);
2332
+ if (!resource || resource.fencing_token !== allocation.fencingToken || resource.expires_at <= Date.now()) {
2333
+ throw new Error("Dev server port lease lost");
2334
+ }
2335
+ this.coordDb.db.prepare("UPDATE resources SET expires_at=? WHERE id=?").run(Date.now() + leaseMs, resource.id);
2336
+ });
2337
+ },
2338
+ release: () => {
2339
+ if (allocation) this.coordDb.db.prepare("DELETE FROM resources WHERE type='port' AND name=? AND fencing_token=?").run(`${params.agentId}:${params.serviceName}`, allocation.fencingToken);
2340
+ }
2341
+ });
2342
+ return { ...result, port: allocation.port };
2343
+ }
2256
2344
  /** Renew leases while a detached, recorded process group runs; stop it before releasing. */
2257
- async executeWithMutex(params) {
2345
+ executeWithMutex(params) {
2346
+ return this.executeGuarded(params);
2347
+ }
2348
+ async executeGuarded(params, lifecycle) {
2258
2349
  if (process.platform === "win32") throw new Error("Supervised mutex commands currently require macOS or Linux process groups");
2259
2350
  if (!params.command.length) throw new Error("A command is required");
2260
2351
  const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
@@ -2299,6 +2390,8 @@ var ResourceBroker = class _ResourceBroker {
2299
2390
  await delay(Math.min(50, Math.max(10, leaseMs / 4)));
2300
2391
  }
2301
2392
  const waitedMs = Date.now() - started;
2393
+ const prepared = await lifecycle?.prepare();
2394
+ if (params.signal?.aborted) throw abortError();
2302
2395
  const candidates = ["./command-worker.js", "../command-worker.js", "../../dist/command-worker.js"].map((name) => fileURLToPath(new URL(name, import.meta.url)));
2303
2396
  const script = candidates.find((name) => fs10.existsSync(name));
2304
2397
  if (!script) throw new Error("Missing command worker. Build Kith before running supervised commands.");
@@ -2341,6 +2434,7 @@ var ResourceBroker = class _ResourceBroker {
2341
2434
  try {
2342
2435
  this.fencing.heartbeat(params.agentId);
2343
2436
  this.renewMutex({ ...params, fencingToken: run.fencing_token });
2437
+ lifecycle?.renew();
2344
2438
  } catch (err) {
2345
2439
  stop(err instanceof Error ? err : new Error(String(err)));
2346
2440
  }
@@ -2368,8 +2462,9 @@ var ResourceBroker = class _ResourceBroker {
2368
2462
  run.worker_start = identity;
2369
2463
  workerIdentity = identity;
2370
2464
  this.renewMutex({ ...params, fencingToken: run.fencing_token });
2465
+ lifecycle?.renew();
2371
2466
  });
2372
- worker.send({ command: params.command, cwd: params.cwd ?? this.coordDb.repoRoot, mutexName: params.mutexName });
2467
+ worker.send({ command: prepared?.command ?? params.command, env: prepared?.env, cwd: params.cwd ?? this.coordDb.repoRoot, mutexName: params.mutexName });
2373
2468
  } catch (err) {
2374
2469
  stop(err instanceof Error ? err : new Error(String(err)));
2375
2470
  }
@@ -2417,6 +2512,7 @@ var ResourceBroker = class _ResourceBroker {
2417
2512
  this.coordDb.db.prepare("DELETE FROM mutex_waiters WHERE mutex_name=? AND request_id=? AND session_epoch=?").run(params.mutexName, requestId, epoch);
2418
2513
  if (run && !this.runIsAlive(run)) this.clearRun(run);
2419
2514
  if (run && run.worker_pid === null) this.clearRun(run);
2515
+ if (!this.getRun(params.mutexName)) lifecycle?.release();
2420
2516
  });
2421
2517
  } finally {
2422
2518
  this.activeExecutions--;
@@ -3749,6 +3845,7 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
3749
3845
 
3750
3846
  // src/core/engine.ts
3751
3847
  var KithEngine = class {
3848
+ activeClaimWaits = 0;
3752
3849
  coordDb;
3753
3850
  fencing;
3754
3851
  locks;
@@ -3901,6 +3998,22 @@ ${lines.join("\n")}` : "";
3901
3998
  const worktree = intent.ok && params.isolateWorktree ? this.worktrees.spawnWorktree({ agentId: params.agentId }) : null;
3902
3999
  return { ...intent, worktree };
3903
4000
  }
4001
+ async beginWorkWhenAvailable(params) {
4002
+ return this.waitForOwnedClaim(params, () => this.beginWork(params));
4003
+ }
4004
+ async acquireLocksWhenAvailable(params) {
4005
+ return this.waitForOwnedClaim(params, () => this.locks.acquireLocks(params));
4006
+ }
4007
+ async waitForOwnedClaim(params, attempt) {
4008
+ this.activeClaimWaits++;
4009
+ try {
4010
+ return await waitForClaim({ ...params, attempt, heartbeat: () => {
4011
+ this.fencing.heartbeat(params.agentId);
4012
+ } });
4013
+ } finally {
4014
+ this.activeClaimWaits--;
4015
+ }
4016
+ }
3904
4017
  completeWork(params) {
3905
4018
  this.fencing.assertSession(params.agentId);
3906
4019
  const agent = this.fencing.getAgent(params.agentId);
@@ -3921,6 +4034,7 @@ ${lines.join("\n")}` : "";
3921
4034
  return { ok: true, completedTasks, mergeResult };
3922
4035
  }
3923
4036
  close() {
4037
+ if (this.activeClaimWaits) throw new Error("Await or cancel active file claim waits before closing Kith");
3924
4038
  if (this.broker.hasActiveCommands()) throw new Error("Await or cancel active mutex commands before closing Kith");
3925
4039
  this.coordDb.close();
3926
4040
  }
@@ -4040,17 +4154,30 @@ function buildKithMcpServer(repoRoot, pinned) {
4040
4154
  title: z.string().describe("Short title of the task or intent"),
4041
4155
  intentSummary: z.string().optional().describe("Detailed description of what will be built or modified"),
4042
4156
  scopeGlobs: z.array(z.string()).describe('File paths or glob patterns this agent will edit (e.g. ["src/auth/**"])'),
4043
- isolateWorktree: z.boolean().optional().describe("Recommended: true, giving each parallel agent a separate Git worktree")
4157
+ isolateWorktree: z.boolean().optional().describe("Recommended: true, giving each parallel agent a separate Git worktree"),
4158
+ waitTimeoutMs: z.number().int().min(0).max(6e5).optional().describe("Wait up to this many milliseconds for the whole scope; default fail-fast, not a FIFO file queue")
4044
4159
  },
4045
4160
  async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4046
4161
  const reg = register(args);
4047
- const intentRes = engine2.beginWork({
4162
+ const options = {
4048
4163
  agentId: args.agentId,
4049
4164
  title: args.title,
4050
4165
  intentSummary: args.intentSummary,
4051
4166
  scopeGlobs: args.scopeGlobs,
4052
4167
  isolateWorktree: args.isolateWorktree
4168
+ };
4169
+ const execution = args.waitTimeoutMs === void 0 ? void 0 : engine2.beginWorkWhenAvailable({
4170
+ ...options,
4171
+ waitTimeoutMs: args.waitTimeoutMs,
4172
+ signal: AbortSignal.any([disconnected.signal, extra.signal])
4053
4173
  });
4174
+ if (execution) commands.add(execution);
4175
+ let intentRes;
4176
+ try {
4177
+ intentRes = execution ? await execution : engine2.beginWork(options);
4178
+ } finally {
4179
+ if (execution) commands.delete(execution);
4180
+ }
4054
4181
  const contracts = engine2.depGraph.listContracts().slice(0, 30);
4055
4182
  return formatReply(args.agentId, {
4056
4183
  ok: intentRes.ok,
@@ -4062,6 +4189,7 @@ function buildKithMcpServer(repoRoot, pinned) {
4062
4189
  worktree: intentRes.worktree,
4063
4190
  lockConflicts: intentRes.lockConflicts,
4064
4191
  activeSiblingIntents: intentRes.activeSiblingIntents,
4192
+ ..."waitedMs" in intentRes && "waitTimedOut" in intentRes ? { waitedMs: intentRes.waitedMs, waitTimedOut: intentRes.waitTimedOut } : {},
4065
4193
  publishedContracts: contracts
4066
4194
  });
4067
4195
  }
@@ -4104,12 +4232,22 @@ function buildKithMcpServer(repoRoot, pinned) {
4104
4232
  sessionToken: z.string().optional(),
4105
4233
  patterns: z.array(z.string()),
4106
4234
  mode: z.enum(["exclusive", "shared"]).optional(),
4107
- reason: z.string().optional()
4235
+ reason: z.string().optional(),
4236
+ waitTimeoutMs: z.number().int().min(0).max(6e5).optional()
4108
4237
  },
4109
4238
  async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4110
4239
  authorize(args);
4111
- const res = engine2.locks.acquireLocks(args);
4112
- return formatReply(args.agentId, res);
4240
+ const execution = args.waitTimeoutMs === void 0 ? void 0 : engine2.acquireLocksWhenAvailable({
4241
+ ...args,
4242
+ waitTimeoutMs: args.waitTimeoutMs,
4243
+ signal: AbortSignal.any([disconnected.signal, extra.signal])
4244
+ });
4245
+ if (execution) commands.add(execution);
4246
+ try {
4247
+ return formatReply(args.agentId, execution ? await execution : engine2.locks.acquireLocks(args));
4248
+ } finally {
4249
+ if (execution) commands.delete(execution);
4250
+ }
4113
4251
  }
4114
4252
  );
4115
4253
  tool(
@@ -4206,6 +4344,45 @@ function buildKithMcpServer(repoRoot, pinned) {
4206
4344
  }
4207
4345
  }
4208
4346
  );
4347
+ tool(
4348
+ "kith_run_dev_server",
4349
+ "Run a literal argv command with an allocated port in PORT/KITH_PORT and substituted {port} arguments. Renews the lease while running; cancellation/disconnect stops the process group before release. Uses coordinator permissions. Requires macOS/Linux; no implicit shell or readiness guarantee.",
4350
+ {
4351
+ agentId: z.string(),
4352
+ sessionToken: z.string().optional(),
4353
+ serviceName: z.string().min(1).max(128),
4354
+ command: z.array(z.string()).min(1),
4355
+ basePort: z.number().int().min(1).max(65535).optional(),
4356
+ cwd: z.string().optional(),
4357
+ leaseDurationMs: z.number().int().min(100).max(864e5).optional(),
4358
+ commandTimeoutMs: z.number().int().min(1).max(6e5).optional(),
4359
+ maxOutputBytes: z.number().int().min(1).max(1048576).optional()
4360
+ },
4361
+ async (args, extra, { engine: engine2, authorize, formatReply }) => {
4362
+ authorize(args);
4363
+ const root = fs16.realpathSync(engine2.fencing.getAgent(args.agentId)?.worktree_path ?? engine2.coordDb.repoRoot);
4364
+ const cwd = fs16.realpathSync(path15.resolve(root, args.cwd ?? "."));
4365
+ const relative = path15.relative(root, cwd);
4366
+ if (relative === ".." || relative.startsWith(".." + path15.sep) || path15.isAbsolute(relative)) {
4367
+ throw new Error("Command cwd must be inside the registered worktree or repository");
4368
+ }
4369
+ const execution = engine2.broker.executeWithPort({
4370
+ ...args,
4371
+ cwd,
4372
+ basePort: args.basePort ?? 3e3,
4373
+ commandTimeoutMs: args.commandTimeoutMs ?? 6e4,
4374
+ maxOutputBytes: args.maxOutputBytes ?? 65536,
4375
+ signal: AbortSignal.any([disconnected.signal, extra.signal])
4376
+ });
4377
+ commands.add(execution);
4378
+ try {
4379
+ const result = await execution;
4380
+ return formatReply(args.agentId, { ok: result.exitCode === 0, ...result });
4381
+ } finally {
4382
+ commands.delete(execution);
4383
+ }
4384
+ }
4385
+ );
4209
4386
  tool(
4210
4387
  "kith_get_status",
4211
4388
  "Retrieve coordination state, FIFO waiters and guarded commands, plus worktree paths, actual branches, dirty-file overlaps, commit age and ahead/behind against the current integration target HEAD. Normal engine startup may reap expired ownership.",
@@ -4613,7 +4790,7 @@ Instruction protocol: ${KITH_PROTOCOL_VERSION}.
4613
4790
 
4614
4791
  Use Kith MCP to claim work, isolate changes in worktrees, share local resources and verify integration. Keep one isolated worktree per parallel agent and keep the owning MCP connection open.
4615
4792
 
4616
- 1. Call \`kith_begin_work\` with a unique \`agentId\`, an explicit \`scopeGlobs\` array and \`isolateWorktree: true\`.
4793
+ 1. Select the absolute Git repository path for this task. Include \`repository\` on calls from a global desktop connection; a pinned connection must stay in its configured checkout. Call \`kith_begin_work\` with a unique \`agentId\`, an explicit \`scopeGlobs\` array and \`isolateWorktree: true\`.
4617
4794
  2. Edit only the returned \`worktree.worktreePath\`. Report changed files through \`kith_record_file_edit\` on that connection.
4618
4795
  3. Run serialized commands with \`kith_run_with_mutex\`: pass a literal executable/argument array. Its cwd defaults to the registered worktree. Commands run with server permissions and require client approval.
4619
4796
  4. Renew idle sessions with \`kith_heartbeat\` before their lease expires (every two minutes for the default five-minute lease; sooner for a shorter lease). Inspect \`kith_get_status\`.
@@ -4628,7 +4805,7 @@ If Claude/Cursor edit hooks or CLI commands are also enabled, launch the client
4628
4805
  }
4629
4806
  content = content.includes("<!-- KITH:START -->") ? content.replace(/<!-- KITH:START -->[\s\S]*?<!-- KITH:END -->/, () => block) : content.trimEnd() + "\n\n" + block + "\n";
4630
4807
  add("AGENTS.md", content);
4631
- add(".agent/skills/kith/SKILL.md", '---\nname: kith\ndescription: Coordinate parallel coding work through Kith MCP and isolated worktrees.\n---\n\nPrefer the Kith MCP tools on one persistent connection. Begin with kith_begin_work using a unique agentId, explicit scopeGlobs and isolateWorktree:true. Edit the returned worktreePath, report files with kith_record_file_edit, run literal command arrays with kith_run_with_mutex, heartbeat before expiry and finish with kith_complete_work plus a project verificationCmd. Inspect kith_get_status. Keep capabilities private. If CLI or native hooks are enabled, first launch the client from a persistent shell with the same KITH_AGENT_ID/KITH_SESSION_TOKEN and --owner-pid "$$". Stateless shell calls cannot retain that setup. Locks require cooperating clients.\n');
4808
+ add(".agent/skills/kith/SKILL.md", '---\nname: kith\ndescription: Coordinate parallel coding work through Kith MCP and isolated worktrees.\n---\n\nPrefer the Kith MCP tools on one persistent connection. Begin with kith_begin_work using a unique agentId, explicit scopeGlobs and isolateWorktree:true. Edit the returned worktreePath, report files with kith_record_file_edit, run literal command arrays with kith_run_with_mutex, heartbeat before expiry and finish with kith_complete_work plus a project verificationCmd. Inspect kith_get_status. Keep capabilities private. If CLI or native hooks are enabled, first launch the client from a persistent shell with the same KITH_AGENT_ID/KITH_SESSION_TOKEN and --owner-pid "$$". Stateless shell calls cannot retain that setup. For global desktop connections, include the absolute Git repository in each call; never guess from the server startup directory. Configured tools do not establish ownership until begin-work succeeds. Locks require cooperating clients.\n');
4632
4809
  }
4633
4810
  if (selected.has("git")) {
4634
4811
  try {
@@ -4752,6 +4929,8 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4752
4929
  activeCollisions = /* @__PURE__ */ new Map();
4753
4930
  repositories = /* @__PURE__ */ new Map();
4754
4931
  errors = [];
4932
+ primaryRoots = /* @__PURE__ */ new Map();
4933
+ worktrees = [];
4755
4934
  static notifyMac(title, message) {
4756
4935
  if (process.platform !== "darwin") return;
4757
4936
  try {
@@ -4762,6 +4941,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4762
4941
  discoverGitRepos() {
4763
4942
  this.errors = [];
4764
4943
  this.repositories.clear();
4944
+ this.primaryRoots.clear();
4765
4945
  const candidates = [];
4766
4946
  const marker = (dir) => {
4767
4947
  try {
@@ -4790,6 +4970,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4790
4970
  const layout = discoverRepository(candidate);
4791
4971
  if (!layout.commonGitDir) throw new Error("Git repository unavailable");
4792
4972
  this.repositories.set(layout.checkoutRoot, layout.commonGitDir);
4973
+ if (!layout.isLinkedWorktree) this.primaryRoots.set(layout.commonGitDir, layout.checkoutRoot);
4793
4974
  if (inspected.has(layout.commonGitDir)) continue;
4794
4975
  inspected.add(layout.commonGitDir);
4795
4976
  const listing = execFileSync10("git", ["worktree", "list", "--porcelain", "-z"], { cwd: layout.checkoutRoot, encoding: "utf8", timeout: 4e3, maxBuffer: 4 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
@@ -4797,6 +4978,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4797
4978
  const linked = discoverRepository(record.slice(9));
4798
4979
  if (linked.commonGitDir !== layout.commonGitDir) throw new Error("Worktree repository identity changed during discovery");
4799
4980
  this.repositories.set(linked.checkoutRoot, layout.commonGitDir);
4981
+ if (!linked.isLinkedWorktree) this.primaryRoots.set(layout.commonGitDir, linked.checkoutRoot);
4800
4982
  }
4801
4983
  } catch (error) {
4802
4984
  this.errors.push({ repository: candidate, message: `Cannot inspect repository: ${error instanceof Error ? error.message : String(error)}` });
@@ -4809,10 +4991,11 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4809
4991
  const repos = this.discoverGitRepos();
4810
4992
  const now = Date.now();
4811
4993
  const seen = /* @__PURE__ */ new Set();
4994
+ const dirtyCounts = /* @__PURE__ */ new Map();
4812
4995
  const overlaps = /* @__PURE__ */ new Map();
4813
4996
  for (const repo of repos) {
4814
4997
  try {
4815
- const { stdout } = await execFileAsync("git", ["-c", "core.fsmonitor=false", "status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: repo, timeout: 4e3, maxBuffer: 4 * 1024 * 1024 });
4998
+ const { stdout } = await execFileAsync("git", ["-c", "core.fsmonitor=false", "status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: repo, timeout: 4e3, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
4816
4999
  const records = stdout.split("\0");
4817
5000
  const dirty = /* @__PURE__ */ new Map();
4818
5001
  for (let i = 0; i < records.length; i++) {
@@ -4822,20 +5005,22 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4822
5005
  dirty.set(record.slice(3), status);
4823
5006
  if (/[RC]/.test(status)) dirty.set(records[++i], status);
4824
5007
  }
5008
+ dirtyCounts.set(repo, [...dirty.keys()].filter((file) => file && file !== ".coord" && !file.startsWith(".coord/")).length);
4825
5009
  for (const [file, status] of dirty) {
4826
5010
  if (!file || file === ".coord" || file.startsWith(".coord/")) continue;
4827
5011
  const key = JSON.stringify([repo, file]);
4828
5012
  seen.add(key);
4829
- let modified = now, fingerprint = status;
5013
+ let modified, fingerprint = status;
4830
5014
  try {
4831
5015
  const stat = fs19.lstatSync(path17.join(repo, file));
4832
- modified = Math.min(now, stat.mtimeMs);
5016
+ if (Number.isFinite(stat.mtimeMs) && stat.mtimeMs <= now) modified = stat.mtimeMs;
4833
5017
  fingerprint += `:${stat.mtimeMs}:${stat.size}`;
4834
5018
  } catch (error) {
4835
5019
  if (error.code !== "ENOENT") throw error;
4836
5020
  }
4837
5021
  const previous = this.recentEdits.get(key);
4838
- if (!previous || previous.fingerprint !== fingerprint) {
5022
+ if (modified === void 0) this.recentEdits.delete(key);
5023
+ else if (!previous || previous.fingerprint !== fingerprint) {
4839
5024
  this.recentEdits.set(key, { repo, fingerprint, timestamp: modified });
4840
5025
  this.options.onFileChange?.({ repoPath: repo, filePath: file, timestamp: modified });
4841
5026
  }
@@ -4877,6 +5062,39 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4877
5062
  }
4878
5063
  }
4879
5064
  this.activeCollisions = this.errors.length ? new Map([...this.activeCollisions, ...current]) : current;
5065
+ const states = /* @__PURE__ */ new Map();
5066
+ this.worktrees = repos.map((repo) => {
5067
+ const common = this.repositories.get(repo);
5068
+ const primary = this.primaryRoots.get(common);
5069
+ const row = {
5070
+ path: repo,
5071
+ label: `${path17.basename(primary ?? path17.dirname(common))} / ${repo === primary ? "primary" : path17.basename(repo)}`,
5072
+ activity: "unknown",
5073
+ agentIds: [],
5074
+ detail: "Ownership unavailable; inspect kith status --offline before recovery.",
5075
+ dirtyFiles: dirtyCounts.get(repo) ?? null,
5076
+ sharedDirtyPaths: [...this.activeCollisions.values()].filter((alert) => alert.repos.includes(repo)).length
5077
+ };
5078
+ try {
5079
+ let state = states.get(common);
5080
+ if (!state) {
5081
+ state = readCoordinationState(primary ?? repo, { ownersOnly: true });
5082
+ states.set(common, state);
5083
+ }
5084
+ if (state.state === "unavailable") {
5085
+ row.detail += ` ${state.error}`;
5086
+ return row;
5087
+ }
5088
+ const owners = state.agents.filter((agent) => agent.worktree_path ? physicalPath(agent.worktree_path) === repo : repo === primary);
5089
+ const active = owners.filter((agent) => agent.status === "active" && Number.isFinite(agent.lease_expires_at) && agent.lease_expires_at > state.observedAt);
5090
+ row.agentIds = (active.length ? active : owners).map((agent) => agent.id);
5091
+ row.activity = active.length ? "active" : owners.length ? "retained" : "unregistered";
5092
+ row.detail = active.length ? "Current recorded Kith lease; does not identify file writers." : owners.length ? `Recorded ${[...new Set(owners.map((agent) => agent.status === "active" ? "expired lease" : agent.status))].join(", ")}; preserve work and inspect before cleanup.` : "No recorded Kith session; activity and ownership outside Kith are unknown.";
5093
+ } catch (error) {
5094
+ row.detail += ` ${error instanceof Error ? error.message : String(error)}`;
5095
+ }
5096
+ return row;
5097
+ });
4880
5098
  if (this.errors.length) this.options.onScanError?.(this.errors);
4881
5099
  return this.getActiveCollisions();
4882
5100
  }
@@ -4900,7 +5118,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4900
5118
  this.watchTimer = null;
4901
5119
  }
4902
5120
  getScanState() {
4903
- return { complete: this.errors.length === 0, errors: [...this.errors], repos: [...this.repositories.keys()] };
5121
+ return { complete: this.errors.length === 0, errors: [...this.errors], repos: [...this.repositories.keys()], ownershipComplete: this.worktrees.every((row) => row.activity !== "unknown"), worktrees: this.worktrees.map((row) => ({ ...row, agentIds: [...row.agentIds] })) };
4904
5122
  }
4905
5123
  getActiveCollisions() {
4906
5124
  return [...this.activeCollisions.values()];
@@ -4914,7 +5132,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4914
5132
  // src/cli/setup-auto.ts
4915
5133
  import fs21 from "fs";
4916
5134
  import path18 from "path";
4917
- import os2 from "os";
5135
+ import os3 from "os";
4918
5136
  import { parse as parseToml3, stringify as stringifyToml2 } from "smol-toml";
4919
5137
 
4920
5138
  // src/cli/runtime.ts
@@ -4943,7 +5161,7 @@ function ownedLauncher(value) {
4943
5161
  return false;
4944
5162
  }
4945
5163
  }
4946
- function autoConfigureAllClients(targetWorkspace = process.cwd(), home = os2.homedir(), options = {}) {
5164
+ function autoConfigureAllClients(targetWorkspace = process.cwd(), home = os3.homedir(), options = {}) {
4947
5165
  const target = physicalPath(targetWorkspace);
4948
5166
  const layout = target === path18.parse(target).root ? void 0 : discoverRepository(target);
4949
5167
  const workspace = layout?.checkoutRoot ?? target;
@@ -5064,7 +5282,7 @@ function parseFlags(argv) {
5064
5282
  } else {
5065
5283
  const key = arg.slice(2);
5066
5284
  const next = mainArgs[i + 1];
5067
- if (!["worktree", "raw", "dry-run", "help", "version", "shell", "new", "mcp-only", "offline", "once", "json", "quiet", "no-notify"].includes(key) && next && !next.startsWith("--")) {
5285
+ if (!["worktree", "wait", "raw", "dry-run", "help", "version", "shell", "new", "mcp-only", "offline", "once", "json", "quiet", "no-notify"].includes(key) && next && !next.startsWith("--")) {
5068
5286
  flags[key] = next;
5069
5287
  i++;
5070
5288
  } else {
@@ -5077,10 +5295,38 @@ function parseFlags(argv) {
5077
5295
  }
5078
5296
  return { positional, flags, passthroughCmd };
5079
5297
  }
5298
+ async function interruptible(run) {
5299
+ const controller = new AbortController();
5300
+ let interrupted;
5301
+ const sigint = () => {
5302
+ interrupted = "SIGINT";
5303
+ controller.abort();
5304
+ };
5305
+ const sigterm = () => {
5306
+ interrupted = "SIGTERM";
5307
+ controller.abort();
5308
+ };
5309
+ process.on("SIGINT", sigint);
5310
+ process.on("SIGTERM", sigterm);
5311
+ try {
5312
+ return await run(controller.signal);
5313
+ } catch (err) {
5314
+ if (!interrupted) throw err;
5315
+ process.exitCode = interrupted === "SIGINT" ? 130 : 143;
5316
+ } finally {
5317
+ process.off("SIGINT", sigint);
5318
+ process.off("SIGTERM", sigterm);
5319
+ }
5320
+ }
5080
5321
  async function main() {
5081
5322
  const { positional, flags, passthroughCmd } = parseFlags(process.argv.slice(2));
5082
5323
  const cmd = positional[0] ?? "help";
5083
5324
  const subCmd = positional[1];
5325
+ const claimWaitTimeout = () => {
5326
+ const value = flags["wait-ms"];
5327
+ if (value !== void 0 && typeof value !== "string") throw new Error("--wait-ms requires a millisecond value");
5328
+ return value === void 0 ? 6e4 : Number(value);
5329
+ };
5084
5330
  const targetFromPositional = ["init", "setup", "watch", "menubar", "bar", "tray", "doctor", "recover"].includes(cmd) && positional[1] ? path19.resolve(positional[1]) : void 0;
5085
5331
  const repoRoot = typeof flags.repo === "string" ? path19.resolve(flags.repo) : targetFromPositional ?? (cmd === "mcp" ? process.env.CLAUDE_PROJECT_DIR : void 0) ?? process.cwd();
5086
5332
  if (flags.version || cmd === "version") {
@@ -5168,19 +5414,19 @@ async function main() {
5168
5414
  notify: !isRaw && notify,
5169
5415
  onScanError: (errors) => {
5170
5416
  const message = errors.map((error) => `${error.repository}: ${error.message}`).join("\n");
5171
- if (!flags.once && message !== lastScanError) console.error(`Scan unavailable:
5417
+ if (!flags.once && message !== lastScanError) console.error(`Scan unavailable; retained overlaps may be stale:
5172
5418
  ${message}`);
5173
5419
  lastScanError = message;
5174
5420
  },
5175
5421
  onCollision: (alert) => {
5176
- if (!isRaw) {
5422
+ if (!isRaw && !flags.once) {
5177
5423
  console.log(`
5178
5424
  \u26A0\uFE0F [WORKTREE OVERLAP] ${alert.message}`);
5179
5425
  }
5180
5426
  },
5181
5427
  onFileChange: (e) => {
5182
- if (!flags.quiet && !isRaw) {
5183
- console.log(`\u{1F4DD} [EDIT] ${path19.basename(e.repoPath)}/${e.filePath}`);
5428
+ if (!flags.quiet && !isRaw && !flags.once) {
5429
+ console.log(`\u{1F4DD} [FILE MTIME] ${path19.basename(e.repoPath)}/${e.filePath}`);
5184
5430
  }
5185
5431
  }
5186
5432
  });
@@ -5198,6 +5444,12 @@ ${message}`);
5198
5444
  \u{1F4E1} Kith Passive Radar: ${targetDir}`);
5199
5445
  console.log(` Repositories (${repos.length}): ${repos.map((r) => path19.basename(r)).join(", ")}`);
5200
5446
  console.log(` Recent Modified Files: ${edits.length}`);
5447
+ for (const row of state.worktrees) {
5448
+ const activity = { active: "Active Kith lease", retained: "Retained", unregistered: "No Kith session", unknown: "Ownership unavailable" }[row.activity];
5449
+ console.log(` ${row.label} \u2014 ${activity}; ${row.dirtyFiles ?? "unknown"} Git changes; ${row.sharedDirtyPaths} shared dirty paths`);
5450
+ console.log(` ${row.path}${row.agentIds.length ? ` (${row.agentIds.join(", ")})` : ""}
5451
+ ${row.detail}`);
5452
+ }
5201
5453
  if (!state.complete) {
5202
5454
  console.error(`Scan unavailable:
5203
5455
  ${state.errors.map((error) => error.message).join("\n")}`);
@@ -5205,10 +5457,9 @@ ${state.errors.map((error) => error.message).join("\n")}`);
5205
5457
  console.log("\nNo differing or unverified shared dirty paths observed. Identical copies are excluded; compatibility is not verified.\n");
5206
5458
  } else {
5207
5459
  console.log(`
5208
- \u26A0\uFE0F Observed worktree overlaps (${alerts.length}):`);
5460
+ \u26A0\uFE0F Review before integration: ${alerts.length} shared dirty paths:`);
5209
5461
  for (const a of alerts) {
5210
5462
  console.log(` \u26A0\uFE0F ${a.message}`);
5211
- console.log(` Files: ${a.filePath}`);
5212
5463
  }
5213
5464
  console.log("");
5214
5465
  }
@@ -5223,7 +5474,7 @@ ${state.errors.map((error) => error.message).join("\n")}`);
5223
5474
  });
5224
5475
  return;
5225
5476
  }
5226
- const commands = ["session", "status", "begin", "complete", "heartbeat", "task", "lock", "contract", "worktree", "port", "mutex", "hook"];
5477
+ const commands = ["session", "status", "begin", "complete", "heartbeat", "task", "lock", "contract", "worktree", "port", "dev", "mutex", "hook"];
5227
5478
  if (!commands.includes(cmd)) throw new Error(`Unknown command '${cmd}'. Run kith help.`);
5228
5479
  const subcommands = {
5229
5480
  session: ["start", "end"],
@@ -5246,7 +5497,7 @@ ${state.errors.map((error) => error.message).join("\n")}`);
5246
5497
  const register = () => engine.fencing.registerAgent({
5247
5498
  id: agentId,
5248
5499
  client: String(flags.client ?? "cli"),
5249
- pid: typeof flags["owner-pid"] === "string" ? Number(flags["owner-pid"]) : cmd === "mutex" && !sessionToken ? process.pid : process.ppid,
5500
+ pid: typeof flags["owner-pid"] === "string" ? Number(flags["owner-pid"]) : ["mutex", "dev"].includes(cmd) && !sessionToken ? process.pid : process.ppid,
5250
5501
  sessionToken,
5251
5502
  leaseDurationMs: typeof flags["lease-ms"] === "string" ? Number(flags["lease-ms"]) : void 0
5252
5503
  });
@@ -5285,13 +5536,22 @@ export KITH_SESSION_TOKEN=${quote(session.sessionToken)}`);
5285
5536
  const scopeRaw = typeof flags.scope === "string" ? flags.scope : "";
5286
5537
  const scopeGlobs = scopeRaw ? scopeRaw.split(",").map((s) => s.trim()) : ["**"];
5287
5538
  const session = register();
5288
- const res = engine.beginWork({
5539
+ const options = {
5289
5540
  agentId,
5290
5541
  title,
5291
5542
  intentSummary: typeof flags.summary === "string" ? flags.summary : title,
5292
5543
  scopeGlobs,
5293
5544
  isolateWorktree: Boolean(flags.worktree)
5294
- });
5545
+ };
5546
+ const res = flags.wait || flags["wait-ms"] !== void 0 ? await interruptible((signal) => engine.beginWorkWhenAvailable({
5547
+ ...options,
5548
+ signal,
5549
+ waitTimeoutMs: claimWaitTimeout()
5550
+ })) : engine.beginWork(options);
5551
+ if (!res) {
5552
+ if (!sessionToken) engine.fencing.abandonAgentSession(agentId);
5553
+ return;
5554
+ }
5295
5555
  console.log(JSON.stringify({ ...res, sessionToken: session.sessionToken }, null, 2));
5296
5556
  process.exitCode = res.ok ? 0 : 1;
5297
5557
  return;
@@ -5337,7 +5597,16 @@ export KITH_SESSION_TOKEN=${quote(session.sessionToken)}`);
5337
5597
  if (subCmd === "acquire") {
5338
5598
  const patterns = String(flags.patterns ?? flags.scope ?? "").split(",").map((s) => s.trim()).filter(Boolean);
5339
5599
  const session = register();
5340
- const res = engine.locks.acquireLocks({ agentId, patterns });
5600
+ const res = flags.wait || flags["wait-ms"] !== void 0 ? await interruptible((signal) => engine.acquireLocksWhenAvailable({
5601
+ agentId,
5602
+ patterns,
5603
+ signal,
5604
+ waitTimeoutMs: claimWaitTimeout()
5605
+ })) : engine.locks.acquireLocks({ agentId, patterns });
5606
+ if (!res) {
5607
+ if (!sessionToken) engine.fencing.abandonAgentSession(agentId);
5608
+ return;
5609
+ }
5341
5610
  console.log(JSON.stringify({ ...res, sessionToken: session.sessionToken }, null, 2));
5342
5611
  process.exitCode = res.ok ? 0 : 1;
5343
5612
  return;
@@ -5405,9 +5674,10 @@ export KITH_SESSION_TOKEN=${quote(session.sessionToken)}`);
5405
5674
  }
5406
5675
  return;
5407
5676
  }
5408
- if (cmd === "mutex") {
5677
+ if (cmd === "mutex" || cmd === "dev") {
5409
5678
  const mutexName = String(flags.name ?? "default-mutex");
5410
5679
  const commandToRun = passthroughCmd.length ? passthroughCmd : String(flags.cmd ?? "");
5680
+ if (cmd === "dev" && (!passthroughCmd.length || !passthroughCmd[0])) throw new Error("Provide a literal command after `--` for kith dev");
5411
5681
  if (!commandToRun) {
5412
5682
  console.error("Error: provide a command after `--` for `kith mutex`");
5413
5683
  process.exitCode = 1;
@@ -5425,17 +5695,24 @@ export KITH_SESSION_TOKEN=${quote(session.sessionToken)}`);
5425
5695
  process.on("SIGINT", sigint);
5426
5696
  process.on("SIGTERM", sigterm);
5427
5697
  try {
5428
- const res = await engine.broker.executeWithMutex({
5698
+ const shared = {
5429
5699
  agentId,
5430
- mutexName,
5431
- command: commandToRun,
5432
5700
  signal: controller.signal,
5433
5701
  leaseDurationMs: typeof flags["lease-ms"] === "string" ? Number(flags["lease-ms"]) : void 0,
5434
5702
  timeoutMs: typeof flags["wait-ms"] === "string" ? Number(flags["wait-ms"]) : void 0,
5435
5703
  commandTimeoutMs: typeof flags["timeout-ms"] === "string" ? Number(flags["timeout-ms"]) : void 0,
5436
5704
  onStdout: (chunk) => process.stdout.write(chunk),
5437
5705
  onStderr: (chunk) => process.stderr.write(chunk)
5438
- });
5706
+ };
5707
+ const res = cmd === "dev" ? await engine.broker.executeWithPort({
5708
+ ...shared,
5709
+ command: passthroughCmd,
5710
+ serviceName: String(flags.service ?? "web"),
5711
+ basePort: flags.base === void 0 ? 3e3 : Number(flags.base),
5712
+ cwd: path19.resolve(engine.fencing.getAgent(agentId)?.worktree_path ?? engine.coordDb.repoRoot, typeof flags.cwd === "string" ? flags.cwd : "."),
5713
+ onPort: (port) => process.stderr.write(`Kith: ${String(flags.service ?? "web")} allocated port ${port} (PORT, KITH_PORT, {port})
5714
+ `)
5715
+ }) : await engine.broker.executeWithMutex({ ...shared, mutexName, command: commandToRun });
5439
5716
  process.exitCode = res.exitCode;
5440
5717
  } catch (err) {
5441
5718
  if (interrupted) process.exitCode = interrupted === "SIGINT" ? 130 : 143;
@@ -5549,7 +5826,9 @@ Usage:
5549
5826
  kith lock acquire|release|list Manage fencing-token file/glob locks
5550
5827
  kith contract publish|list Manage Contract Blackboard
5551
5828
  kith worktree spawn|remove|list|preview Manage isolated Git worktrees
5552
- kith port --agent <id> --service <name> Lease collision-free TCP port
5829
+ kith port --agent <id> --service <name> Reserve an available TCP port
5830
+ kith dev --agent <id> -- <cmd> Run with PORT and {port} argument substitution
5831
+ kith begin/lock acquire ... --wait --wait-ms 60000 Wait for a scope claim (bounded, cancellable)
5553
5832
  kith mutex --agent <id> --name <m> -- <cmd> Serialize commands via mutex
5554
5833
  kith hook pre|post|pre-commit Tool & git hook handlers
5555
5834
  `);