kith-coord 0.1.1 → 0.1.3

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
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/cli/index.ts
4
+ import fs22 from "fs";
5
+ import path19 from "path";
6
+ import { spawn } from "child_process";
7
+
3
8
  // src/version.ts
4
- var KITH_VERSION = "0.1.1";
5
- var KITH_PROTOCOL_VERSION = "1.0.0";
9
+ var KITH_VERSION = "0.1.3";
10
+ var KITH_PROTOCOL_VERSION = "1.2.0";
6
11
 
7
12
  // src/cli/setup-ledger.ts
8
13
  import fs2 from "fs";
@@ -55,6 +60,9 @@ function repositoryFile(root, file) {
55
60
  }
56
61
  function discoverRepository(input) {
57
62
  const directory = fs.realpathSync.native(input);
63
+ if (directory === path.parse(directory).root) {
64
+ throw new Error("Cannot coordinate a filesystem root; select an explicit project with --repo.");
65
+ }
58
66
  if (!fs.statSync(directory).isDirectory()) throw new Error(`Not a directory: ${input}`);
59
67
  const git2 = (...args) => execFileSync("git", ["rev-parse", ...args], {
60
68
  cwd: directory,
@@ -988,6 +996,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
988
996
  static DEFAULT_LEASE_MS = 5 * 60 * 1e3;
989
997
  capabilities = /* @__PURE__ */ new Map();
990
998
  static ownWindowsStart = null;
999
+ static inspectionFailure = null;
991
1000
  static validateLease(duration) {
992
1001
  if (!Number.isSafeInteger(duration) || duration < 100 || duration > 864e5) {
993
1002
  throw new Error("Lease duration must be an integer between 100 and 86400000 milliseconds");
@@ -996,6 +1005,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
996
1005
  }
997
1006
  /** A real kernel process identity; tests must not supply liveness sentinels. */
998
1007
  static getProcessStartTime(pid) {
1008
+ this.inspectionFailure = null;
999
1009
  if (!Number.isSafeInteger(pid) || pid <= 0) return null;
1000
1010
  try {
1001
1011
  if (process.platform === "linux") {
@@ -1013,9 +1023,12 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
1013
1023
  "-Command",
1014
1024
  `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks.ToString([System.Globalization.CultureInfo]::InvariantCulture)`
1015
1025
  ],
1016
- { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3, windowsHide: true }
1026
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3, windowsHide: true }
1017
1027
  ).trim();
1018
- if (!/^\d+$/.test(ticks)) return null;
1028
+ if (!/^\d+$/.test(ticks)) {
1029
+ this.inspectionFailure = "invalid kernel creation-time response";
1030
+ return null;
1031
+ }
1019
1032
  const identity = `windows:${ticks}`;
1020
1033
  if (pid === process.pid) this.ownWindowsStart = identity;
1021
1034
  return identity;
@@ -1027,7 +1040,9 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
1027
1040
  env: { ...process.env, LC_ALL: "C" }
1028
1041
  }).trim();
1029
1042
  return out || null;
1030
- } catch {
1043
+ } catch (error) {
1044
+ const failure = error;
1045
+ this.inspectionFailure = [failure.code, failure.status === void 0 ? null : `exit ${failure.status}`, failure.signal].filter(Boolean).join(", ") || "inspection unavailable";
1031
1046
  return null;
1032
1047
  }
1033
1048
  }
@@ -1057,7 +1072,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
1057
1072
  if (params.sessionToken !== void 0) throw new SessionOwnershipError(params.id);
1058
1073
  const pid = params.pid ?? process.pid;
1059
1074
  const start = _FencingAndAgentManager.getProcessStartTime(pid);
1060
- if (!start) throw new Error(`Cannot determine start time of live session owner PID ${pid}`);
1075
+ if (!start) throw new Error(`Cannot determine start time of live session owner PID ${pid} (${_FencingAndAgentManager.inspectionFailure ?? "inspection unavailable"})`);
1061
1076
  const now = Date.now();
1062
1077
  const epoch = this.coordDb.nextFencingToken();
1063
1078
  const token = crypto2.randomBytes(32).toString("base64url");
@@ -2589,6 +2604,7 @@ var DependencyAndContractEngine = class {
2589
2604
  }
2590
2605
  for (const exp of analysis.exports) {
2591
2606
  const prev = existingBySymbol.get(exp.symbolName);
2607
+ if (prev && prev.kind === exp.kind && prev.signature === exp.signature && prev.ast_hash === exp.astHash && prev.params_fingerprint === exp.paramsFingerprint && prev.doc_comment === exp.docComment) continue;
2592
2608
  const isBreaking = prev !== void 0 && prev.ast_hash !== exp.astHash;
2593
2609
  if (isBreaking && prev) {
2594
2610
  changedSymbols.push({
@@ -3916,73 +3932,105 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3916
3932
  import { z } from "zod";
3917
3933
  import fs16 from "fs";
3918
3934
  import path15 from "path";
3919
- function createKithMcpServer(repoRoot = process.cwd()) {
3920
- const engine = new KithEngine(repoRoot);
3935
+ function buildKithMcpServer(repoRoot, pinned) {
3936
+ const pinnedRoot = pinned ? discoverRepository(repoRoot).checkoutRoot : void 0;
3921
3937
  const server = new McpServer({
3922
3938
  name: "kith-coordinator",
3923
3939
  version: KITH_VERSION
3924
- }, { instructions: "Kith coordinates work claims, isolated worktrees, local resources and verified integration for cooperating coding agents. State stays local; no hosted service or model API key is required. Recommended workflow: one isolated worktree per parallel agent. Call kith_begin_work with isolateWorktree:true and explicit scopeGlobs; edit only the returned worktreePath, call kith_record_file_edit, run literal argv with kith_run_with_mutex, and finish with kith_complete_work plus a project verificationCmd. Heartbeat before the session expires. " + (process.env.KITH_AGENT_ID ? `This server shares a session with CLI/hooks. Use agentId '${process.env.KITH_AGENT_ID}'. Its inherited capability is handled internally; do not request or log it.` : "Start with kith_begin_work or kith_start_session and a unique agentId. This connection retains its granted capability. Another connection needs the session token to resume that ID. Keep tokens private.") });
3925
- const tokenFor = (args) => args.sessionToken ?? (process.env.KITH_AGENT_ID === args.agentId ? process.env.KITH_SESSION_TOKEN : void 0);
3926
- const owned = /* @__PURE__ */ new Set();
3927
- const register = (args) => {
3928
- const result = engine.fencing.registerAgent({
3929
- id: args.agentId,
3930
- client: args.client ?? "mcp",
3931
- sessionToken: tokenFor(args),
3932
- leaseDurationMs: args.leaseDurationMs
3933
- });
3934
- if (result.agent.pid === process.pid) owned.add(args.agentId);
3935
- return result;
3936
- };
3937
- const authorize = (args) => {
3938
- const token = tokenFor(args);
3939
- if (token) engine.fencing.resumeSession(args.agentId, token);
3940
- engine.fencing.heartbeat(args.agentId);
3941
- };
3940
+ }, { instructions: "Kith coordinates work claims, isolated worktrees, local resources and verified integration for cooperating coding agents. State stays local; no hosted service or model API key is required. The optional repository argument selects an absolute Git checkout for each call on a global connection. Keep passing the original checkout, even while editing its returned worktree. If no project context is available, ask for a repository path; never substitute HOME or a filesystem root. Each call is routed independently; there is no shared current project. Recommended workflow: one isolated worktree per parallel agent. Call kith_begin_work with isolateWorktree:true and explicit scopeGlobs; edit only the returned worktreePath, call kith_record_file_edit, run literal argv with kith_run_with_mutex, and finish with kith_complete_work plus a project verificationCmd. Heartbeat before the session expires. " + (repoRoot && process.env.KITH_AGENT_ID ? `This server shares a session with CLI/hooks. Use agentId '${process.env.KITH_AGENT_ID}'. Its inherited capability is handled internally; do not request or log it.` : "Start with kith_begin_work or kith_start_session and a unique agentId. This connection retains its granted capability. Omit sessionToken on subsequent calls on this connection; only another connection needs it to resume that ID. Initial grant/resume replies contain the token and client transcripts may record it. Keep transcripts private.") });
3942
3941
  const disconnected = new AbortController();
3943
3942
  const commands = /* @__PURE__ */ new Set();
3944
- const cleanup = () => {
3945
- for (const id of owned) {
3946
- try {
3947
- engine.fencing.abandonAgentSession(id);
3948
- } catch {
3943
+ const createContext = (root, inheritCapability) => {
3944
+ const engine2 = new KithEngine(root);
3945
+ const owned = /* @__PURE__ */ new Set();
3946
+ const tokenFor = (args) => args.sessionToken ?? (inheritCapability && process.env.KITH_AGENT_ID === args.agentId ? process.env.KITH_SESSION_TOKEN : void 0);
3947
+ const register = (args) => {
3948
+ const result = engine2.fencing.registerAgent({
3949
+ id: args.agentId,
3950
+ client: args.client ?? "mcp",
3951
+ sessionToken: tokenFor(args),
3952
+ leaseDurationMs: args.leaseDurationMs
3953
+ });
3954
+ if (result.agent.pid === process.pid) owned.add(args.agentId);
3955
+ return result;
3956
+ };
3957
+ const authorize = (args) => {
3958
+ const token = tokenFor(args);
3959
+ if (token) engine2.fencing.resumeSession(args.agentId, token);
3960
+ engine2.fencing.heartbeat(args.agentId);
3961
+ };
3962
+ const formatReply = (agentId, payload) => {
3963
+ const delta = agentId ? engine2.consumeAgentDelta(agentId).formattedFooter : "";
3964
+ const data = payload;
3965
+ return { content: [
3966
+ { type: "text", text: JSON.stringify(data, null, 2) },
3967
+ ...delta ? [{ type: "text", text: delta }] : []
3968
+ ], structuredContent: data, isError: data.ok === false };
3969
+ };
3970
+ const close = () => {
3971
+ for (const id of owned) {
3972
+ try {
3973
+ engine2.fencing.abandonAgentSession(id);
3974
+ } catch {
3975
+ }
3949
3976
  }
3950
- }
3951
- engine.close();
3977
+ engine2.close();
3978
+ };
3979
+ return { engine: engine2, register, authorize, formatReply, close };
3980
+ };
3981
+ const contexts = /* @__PURE__ */ new Map();
3982
+ const contextFor = (repository) => {
3983
+ if (repository !== void 0 && !path15.isAbsolute(repository)) throw new Error("repository must be an absolute Git checkout path.");
3984
+ const target = repository ?? repoRoot;
3985
+ if (!target) throw new Error("No project selected. Pass repository with the absolute Git checkout path for this task.");
3986
+ const layout = discoverRepository(target);
3987
+ if (pinnedRoot && layout.checkoutRoot !== pinnedRoot) throw new Error("This connection is pinned with --repo. Use a global connection to select another repository.");
3988
+ if (!pinned && !layout.commonGitDir) throw new Error("Global connections require a Git checkout; do not use HOME or a parent directory as a project.");
3989
+ let context = contexts.get(layout.checkoutRoot);
3990
+ if (!context) {
3991
+ context = createContext(layout.checkoutRoot, layout.checkoutRoot === (pinnedRoot ?? repoRoot));
3992
+ contexts.set(layout.checkoutRoot, context);
3993
+ }
3994
+ return context;
3995
+ };
3996
+ const engine = pinned ? contextFor().engine : void 0;
3997
+ const tool = (name, description, schema, handler) => {
3998
+ const inputSchema = { ...schema, repository: z.string().optional().describe("Absolute Git checkout path. Required on global connections without project context. Always use the same original checkout for a task.") };
3999
+ server.tool(
4000
+ name,
4001
+ description,
4002
+ inputSchema,
4003
+ async (args, extra) => {
4004
+ const { repository, ...input } = args;
4005
+ return handler(input, extra, contextFor(repository));
4006
+ }
4007
+ );
3952
4008
  };
3953
4009
  let closing = false;
3954
4010
  server.server.onclose = () => {
3955
4011
  if (closing) return;
3956
4012
  closing = true;
3957
4013
  disconnected.abort();
4014
+ const cleanup = () => {
4015
+ for (const context of contexts.values()) context.close();
4016
+ contexts.clear();
4017
+ };
3958
4018
  if (commands.size) void Promise.allSettled([...commands]).then(cleanup);
3959
4019
  else cleanup();
3960
4020
  };
3961
- const formatReply = (agentId, payload) => {
3962
- const delta = agentId ? engine.consumeAgentDelta(agentId).formattedFooter : "";
3963
- const data = payload;
3964
- return {
3965
- content: [
3966
- { type: "text", text: JSON.stringify(data, null, 2) },
3967
- ...delta ? [{ type: "text", text: delta }] : []
3968
- ],
3969
- structuredContent: data,
3970
- isError: data.ok === false
3971
- };
3972
- };
3973
- server.tool("kith_start_session", "Create a capability-owned session or resume one with its token.", {
4021
+ tool("kith_start_session", "Create a capability-owned session or resume one with its token.", {
3974
4022
  agentId: z.string(),
3975
4023
  sessionToken: z.string().optional(),
3976
4024
  leaseDurationMs: z.number().optional()
3977
- }, async (args) => formatReply(void 0, { ok: true, ...register(args) }));
3978
- server.tool("kith_heartbeat", "Renew an active, owned session. Expired sessions cannot be revived.", {
4025
+ }, async (args, extra, { engine: engine2, register, authorize, formatReply }) => formatReply(void 0, { ok: true, ...register(args) }));
4026
+ tool("kith_heartbeat", "Renew an active, owned session. Expired sessions cannot be revived.", {
3979
4027
  agentId: z.string(),
3980
4028
  sessionToken: z.string().optional()
3981
- }, async (args) => {
4029
+ }, async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
3982
4030
  authorize(args);
3983
- return formatReply(args.agentId, { ok: true, agent: engine.fencing.getAgent(args.agentId) });
4031
+ return formatReply(args.agentId, { ok: true, agent: engine2.fencing.getAgent(args.agentId) });
3984
4032
  });
3985
- server.tool(
4033
+ tool(
3986
4034
  "kith_begin_work",
3987
4035
  "Register an agent session, verify no scope lock conflicts exist with sibling agents, acquire monotonic fencing-token locks, and optionally create an isolated Git worktree.",
3988
4036
  {
@@ -3994,19 +4042,19 @@ function createKithMcpServer(repoRoot = process.cwd()) {
3994
4042
  scopeGlobs: z.array(z.string()).describe('File paths or glob patterns this agent will edit (e.g. ["src/auth/**"])'),
3995
4043
  isolateWorktree: z.boolean().optional().describe("Recommended: true, giving each parallel agent a separate Git worktree")
3996
4044
  },
3997
- async (args) => {
4045
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
3998
4046
  const reg = register(args);
3999
- const intentRes = engine.beginWork({
4047
+ const intentRes = engine2.beginWork({
4000
4048
  agentId: args.agentId,
4001
4049
  title: args.title,
4002
4050
  intentSummary: args.intentSummary,
4003
4051
  scopeGlobs: args.scopeGlobs,
4004
4052
  isolateWorktree: args.isolateWorktree
4005
4053
  });
4006
- const contracts = engine.depGraph.listContracts().slice(0, 30);
4054
+ const contracts = engine2.depGraph.listContracts().slice(0, 30);
4007
4055
  return formatReply(args.agentId, {
4008
4056
  ok: intentRes.ok,
4009
- agent: reg.agent,
4057
+ agent: engine2.fencing.getAgent(args.agentId),
4010
4058
  sessionToken: reg.sessionToken,
4011
4059
  fencingToken: intentRes.fencingToken,
4012
4060
  task: intentRes.task,
@@ -4018,7 +4066,7 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4018
4066
  });
4019
4067
  }
4020
4068
  );
4021
- server.tool(
4069
+ tool(
4022
4070
  "kith_claim_next_task",
4023
4071
  "Atomically claim the highest-priority unblocked task from the DAG task queue and lock its declared file scope.",
4024
4072
  {
@@ -4026,13 +4074,13 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4026
4074
  sessionToken: z.string().optional(),
4027
4075
  client: z.string().optional()
4028
4076
  },
4029
- async (args) => {
4077
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4030
4078
  const session = register(args);
4031
- const res = engine.intents.claimNextTask(args.agentId);
4079
+ const res = engine2.intents.claimNextTask(args.agentId);
4032
4080
  return formatReply(args.agentId, { ...res, sessionToken: session.sessionToken });
4033
4081
  }
4034
4082
  );
4035
- server.tool(
4083
+ tool(
4036
4084
  "kith_add_task",
4037
4085
  "Add a task to the orchestrated DAG queue with optional task dependencies and file scope globs.",
4038
4086
  {
@@ -4043,12 +4091,12 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4043
4091
  dependsOn: z.array(z.string()).optional(),
4044
4092
  scopeGlobs: z.array(z.string()).optional()
4045
4093
  },
4046
- async (args) => {
4047
- const task = engine.intents.addTask(args);
4094
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4095
+ const task = engine2.intents.addTask(args);
4048
4096
  return formatReply(void 0, { ok: true, task });
4049
4097
  }
4050
4098
  );
4051
- server.tool(
4099
+ tool(
4052
4100
  "kith_check_and_lock_files",
4053
4101
  "Acquire exclusive or shared fencing-token locks on file paths, globs, or symbols.",
4054
4102
  {
@@ -4058,13 +4106,13 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4058
4106
  mode: z.enum(["exclusive", "shared"]).optional(),
4059
4107
  reason: z.string().optional()
4060
4108
  },
4061
- async (args) => {
4109
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4062
4110
  authorize(args);
4063
- const res = engine.locks.acquireLocks(args);
4111
+ const res = engine2.locks.acquireLocks(args);
4064
4112
  return formatReply(args.agentId, res);
4065
4113
  }
4066
4114
  );
4067
- server.tool(
4115
+ tool(
4068
4116
  "kith_record_file_edit",
4069
4117
  "Analyze a modified file for supported contract signatures, flag exact name/kind matches in other files as non-blocking reuse hints, check transitive import blast radius, and run a live dirty-tree git merge-tree conflict check.",
4070
4118
  {
@@ -4073,9 +4121,9 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4073
4121
  filePath: z.string(),
4074
4122
  content: z.string().optional()
4075
4123
  },
4076
- async (args) => {
4124
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4077
4125
  authorize(args);
4078
- const res = engine.processPostFileEdit({
4126
+ const res = engine2.processPostFileEdit({
4079
4127
  agentId: args.agentId,
4080
4128
  filePath: args.filePath,
4081
4129
  content: args.content
@@ -4083,7 +4131,7 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4083
4131
  return formatReply(args.agentId, res);
4084
4132
  }
4085
4133
  );
4086
- server.tool(
4134
+ tool(
4087
4135
  "kith_publish_contract",
4088
4136
  "Explicitly publish a shared interface, function signature, API route, or schema to the Contract Blackboard.",
4089
4137
  {
@@ -4095,13 +4143,13 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4095
4143
  signature: z.string(),
4096
4144
  docComment: z.string().optional()
4097
4145
  },
4098
- async (args) => {
4146
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4099
4147
  authorize(args);
4100
- const contract = engine.depGraph.publishExplicitContract(args);
4148
+ const contract = engine2.depGraph.publishExplicitContract(args);
4101
4149
  return formatReply(args.agentId, { ok: true, contract });
4102
4150
  }
4103
4151
  );
4104
- server.tool(
4152
+ tool(
4105
4153
  "kith_acquire_port",
4106
4154
  "Reserve a local TCP port among cooperating Kith clients; start the service promptly and handle unrelated bind races.",
4107
4155
  {
@@ -4110,13 +4158,13 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4110
4158
  serviceName: z.string(),
4111
4159
  basePort: z.number().optional()
4112
4160
  },
4113
- async (args) => {
4161
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4114
4162
  authorize(args);
4115
- const portRes = await engine.broker.acquirePort(args);
4163
+ const portRes = await engine2.broker.acquirePort(args);
4116
4164
  return formatReply(args.agentId, portRes);
4117
4165
  }
4118
4166
  );
4119
- server.tool(
4167
+ tool(
4120
4168
  "kith_run_with_mutex",
4121
4169
  "Run an argv command under a renewable named mutex, using the MCP server process permissions. Useful when a sandboxed client shell cannot inspect process identities. Requires an owned session. Cancellation or connection loss stops the supervised command before release. This is command execution, not a filesystem sandbox.",
4122
4170
  {
@@ -4130,15 +4178,15 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4130
4178
  commandTimeoutMs: z.number().int().min(1).max(6e5).optional(),
4131
4179
  maxOutputBytes: z.number().int().min(1).max(1048576).optional()
4132
4180
  },
4133
- async (args, extra) => {
4181
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4134
4182
  authorize(args);
4135
- const root = fs16.realpathSync(engine.fencing.getAgent(args.agentId)?.worktree_path ?? engine.coordDb.repoRoot);
4183
+ const root = fs16.realpathSync(engine2.fencing.getAgent(args.agentId)?.worktree_path ?? engine2.coordDb.repoRoot);
4136
4184
  const cwd = fs16.realpathSync(path15.resolve(root, args.cwd ?? "."));
4137
4185
  const relative = path15.relative(root, cwd);
4138
4186
  if (relative === ".." || relative.startsWith(".." + path15.sep) || path15.isAbsolute(relative)) {
4139
4187
  throw new Error("Command cwd must be inside the registered worktree or repository");
4140
4188
  }
4141
- const execution = engine.broker.executeWithMutex({
4189
+ const execution = engine2.broker.executeWithMutex({
4142
4190
  agentId: args.agentId,
4143
4191
  mutexName: args.mutexName,
4144
4192
  command: args.command,
@@ -4158,29 +4206,35 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4158
4206
  }
4159
4207
  }
4160
4208
  );
4161
- server.tool(
4209
+ tool(
4162
4210
  "kith_get_status",
4163
4211
  "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.",
4164
4212
  {
4165
4213
  agentId: z.string().optional(),
4166
- sessionToken: z.string().optional()
4214
+ sessionToken: z.string().optional(),
4215
+ includeContracts: z.boolean().optional().describe("Include the full contract registry. Default false keeps frequent status polls compact; contractCount always reports its size.")
4167
4216
  },
4168
- async (args) => {
4217
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4169
4218
  if (args.agentId) authorize({ ...args, agentId: args.agentId });
4170
- const state = engine.getStatus();
4171
- return formatReply(args.agentId, state);
4219
+ const state = engine2.getStatus();
4220
+ return formatReply(args.agentId, {
4221
+ ...state,
4222
+ contracts: args.includeContracts ? state.contracts : [],
4223
+ contractCount: state.contracts.length,
4224
+ contractsIncluded: args.includeContracts ?? false
4225
+ });
4172
4226
  }
4173
4227
  );
4174
- server.tool(
4228
+ tool(
4175
4229
  "kith_preview_integrate",
4176
4230
  "Preview exact Git conflict paths for a committed registered worktree against the current integration target. Refuses dirty inputs; never checkpoints or changes source/target files. A clean preview does not run project verification.",
4177
4231
  { agentId: z.string(), sessionToken: z.string().optional() },
4178
- async (args) => {
4232
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4179
4233
  authorize(args);
4180
- return formatReply(args.agentId, engine.previewIntegration(args.agentId));
4234
+ return formatReply(args.agentId, engine2.previewIntegration(args.agentId));
4181
4235
  }
4182
4236
  );
4183
- server.tool(
4237
+ tool(
4184
4238
  "kith_complete_work",
4185
4239
  "Complete an agent task, optionally run the Speculative Merge Train to verify and merge its worktree branch, and release all locks and resources.",
4186
4240
  {
@@ -4188,19 +4242,28 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4188
4242
  sessionToken: z.string().optional(),
4189
4243
  taskId: z.string().optional(),
4190
4244
  fencingToken: z.number().optional(),
4191
- mergeBranch: z.string().optional().describe("If work was done on a worktree/branch, run Speculative Merge Train to integrate it"),
4245
+ mergeBranch: z.string().optional().describe("Source branch to integrate, not the destination. Defaults to the registered agent branch; destination is the selected repository checkout current branch. Omit for claims-only work without a registered branch."),
4192
4246
  verificationCmd: z.string().optional().describe('Command to run in speculative merge tree (e.g. "npm run typecheck")')
4193
4247
  },
4194
- async (args) => {
4248
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4195
4249
  authorize(args);
4196
- const result = engine.completeWork(args);
4250
+ const result = engine2.completeWork(args);
4197
4251
  return formatReply(result.ok ? void 0 : args.agentId, result);
4198
4252
  }
4199
4253
  );
4200
4254
  return { server, engine };
4201
4255
  }
4202
- async function runKithMcpStdioServer(repoRoot = process.cwd()) {
4203
- const { server } = createKithMcpServer(repoRoot);
4256
+ async function runKithMcpStdioServer(repoRoot = process.cwd(), pinned = true) {
4257
+ let defaultRoot = repoRoot;
4258
+ if (!pinned) {
4259
+ try {
4260
+ const layout = discoverRepository(repoRoot);
4261
+ defaultRoot = layout.commonGitDir ? layout.checkoutRoot : void 0;
4262
+ } catch {
4263
+ defaultRoot = void 0;
4264
+ }
4265
+ }
4266
+ const { server } = buildKithMcpServer(defaultRoot, pinned);
4204
4267
  const transport = new StdioServerTransport();
4205
4268
  let closing = false;
4206
4269
  const close = () => {
@@ -4556,7 +4619,7 @@ Use Kith MCP to claim work, isolate changes in worktrees, share local resources
4556
4619
  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\`.
4557
4620
  5. Complete with \`kith_complete_work\` and a project-specific \`verificationCmd\`. Failed verification keeps the integration target unchanged. Successful worktrees are retained for inspection.
4558
4621
 
4559
- The MCP connection retains its capability; do not copy or log session tokens. A new connection needs a valid capability to resume an existing ID. Locks coordinate cooperating writers; hooks are not an OS security sandbox.
4622
+ The MCP connection retains its capability; omit \`sessionToken\` on subsequent calls on that connection. Initial grant/resume replies contain the token, so keep client transcripts private. Do not copy tokens into shell commands or public reports. A new connection needs a valid capability to resume an existing ID. Locks coordinate cooperating writers; hooks are not an OS security sandbox.
4560
4623
 
4561
4624
  If Claude/Cursor edit hooks or CLI commands are also enabled, launch the client from a persistent shell after \`eval "$(kith session start --agent <id> --owner-pid "$$" --shell)"\`. CLI, hooks and MCP must inherit the same KITH_AGENT_ID/KITH_SESSION_TOKEN and use the same agent ID. Separate stateless shell calls do not preserve these exports or the owning shell. MCP-only setup skips new native edit hooks and preserves existing ones.
4562
4625
  <!-- KITH:END -->`;
@@ -4635,6 +4698,356 @@ esac
4635
4698
  return report;
4636
4699
  }
4637
4700
 
4701
+ // src/core/watcher.ts
4702
+ import fs19 from "fs";
4703
+ import path17 from "path";
4704
+ import { createHash } from "crypto";
4705
+ import { execFile, execFileSync as execFileSync10 } from "child_process";
4706
+ import { promisify } from "util";
4707
+ var execFileAsync = promisify(execFile);
4708
+ function contentIdentity(repo, file) {
4709
+ const target = path17.join(repo, file);
4710
+ let stat;
4711
+ try {
4712
+ stat = fs19.lstatSync(target);
4713
+ } catch (error) {
4714
+ if (error.code === "ENOENT") return "absent";
4715
+ throw error;
4716
+ }
4717
+ const parent = fs19.realpathSync(path17.dirname(target));
4718
+ const relative = path17.relative(repo, parent);
4719
+ if (relative === ".." || relative.startsWith(`..${path17.sep}`) || path17.isAbsolute(relative)) throw new Error(`Path redirected outside checkout: ${file}`);
4720
+ if (stat.isSymbolicLink()) return `link:${fs19.readlinkSync(target)}`;
4721
+ if (!stat.isFile() || stat.size > 8 * 1024 * 1024) return null;
4722
+ const fd = fs19.openSync(target, fs19.constants.O_RDONLY | (fs19.constants.O_NOFOLLOW ?? 0));
4723
+ try {
4724
+ const before = fs19.fstatSync(fd);
4725
+ if (before.ino !== stat.ino || before.dev !== stat.dev || before.size !== stat.size || before.mtimeMs !== stat.mtimeMs) throw new Error(`File changed during comparison: ${file}`);
4726
+ const hash = createHash("sha256");
4727
+ const buffer = Buffer.allocUnsafe(64 * 1024);
4728
+ for (let offset = 0; offset < before.size; ) {
4729
+ const count = fs19.readSync(fd, buffer, 0, Math.min(buffer.length, before.size - offset), offset);
4730
+ if (!count) throw new Error(`File changed during comparison: ${file}`);
4731
+ hash.update(buffer.subarray(0, count));
4732
+ offset += count;
4733
+ }
4734
+ const after = fs19.fstatSync(fd);
4735
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) throw new Error(`File changed during comparison: ${file}`);
4736
+ return `file:${before.mode & 73}:${hash.digest("hex")}`;
4737
+ } finally {
4738
+ fs19.closeSync(fd);
4739
+ }
4740
+ }
4741
+ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4742
+ constructor(rootDir, options = {}) {
4743
+ this.rootDir = rootDir;
4744
+ this.options = options;
4745
+ if (options.intervalMs !== void 0 && (!Number.isFinite(options.intervalMs) || options.intervalMs < 100)) throw new Error("Watch interval must be at least 100ms.");
4746
+ }
4747
+ rootDir;
4748
+ options;
4749
+ isWatching = false;
4750
+ watchTimer = null;
4751
+ recentEdits = /* @__PURE__ */ new Map();
4752
+ activeCollisions = /* @__PURE__ */ new Map();
4753
+ repositories = /* @__PURE__ */ new Map();
4754
+ errors = [];
4755
+ static notifyMac(title, message) {
4756
+ if (process.platform !== "darwin") return;
4757
+ try {
4758
+ execFileSync10("osascript", ["-e", "on run argv\ndisplay notification (item 2 of argv) with title (item 1 of argv)\nend run", title, message], { stdio: "ignore", timeout: 2e3 });
4759
+ } catch {
4760
+ }
4761
+ }
4762
+ discoverGitRepos() {
4763
+ this.errors = [];
4764
+ this.repositories.clear();
4765
+ const candidates = [];
4766
+ const marker = (dir) => {
4767
+ try {
4768
+ fs19.lstatSync(path17.join(dir, ".git"));
4769
+ return true;
4770
+ } catch (error) {
4771
+ if (error.code === "ENOENT") return false;
4772
+ throw error;
4773
+ }
4774
+ };
4775
+ try {
4776
+ const root = fs19.realpathSync(this.rootDir);
4777
+ if (marker(root)) candidates.push(root);
4778
+ for (const entry of fs19.readdirSync(root, { withFileTypes: true })) {
4779
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
4780
+ const sub = path17.join(root, entry.name);
4781
+ if (marker(sub)) candidates.push(sub);
4782
+ }
4783
+ }
4784
+ } catch (error) {
4785
+ this.errors.push({ repository: this.rootDir, message: `Cannot discover repositories: ${error instanceof Error ? error.message : String(error)}` });
4786
+ }
4787
+ const inspected = /* @__PURE__ */ new Set();
4788
+ for (const candidate of candidates) {
4789
+ try {
4790
+ const layout = discoverRepository(candidate);
4791
+ if (!layout.commonGitDir) throw new Error("Git repository unavailable");
4792
+ this.repositories.set(layout.checkoutRoot, layout.commonGitDir);
4793
+ if (inspected.has(layout.commonGitDir)) continue;
4794
+ inspected.add(layout.commonGitDir);
4795
+ const listing = execFileSync10("git", ["worktree", "list", "--porcelain", "-z"], { cwd: layout.checkoutRoot, encoding: "utf8", timeout: 4e3, maxBuffer: 4 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
4796
+ for (const record of listing.split("\0")) if (record.startsWith("worktree ")) {
4797
+ const linked = discoverRepository(record.slice(9));
4798
+ if (linked.commonGitDir !== layout.commonGitDir) throw new Error("Worktree repository identity changed during discovery");
4799
+ this.repositories.set(linked.checkoutRoot, layout.commonGitDir);
4800
+ }
4801
+ } catch (error) {
4802
+ this.errors.push({ repository: candidate, message: `Cannot inspect repository: ${error instanceof Error ? error.message : String(error)}` });
4803
+ }
4804
+ }
4805
+ if (!candidates.length && !this.errors.length) this.errors.push({ repository: this.rootDir, message: "No Git repositories found in this directory or its immediate children." });
4806
+ return [...this.repositories.keys()];
4807
+ }
4808
+ async scanOnce() {
4809
+ const repos = this.discoverGitRepos();
4810
+ const now = Date.now();
4811
+ const seen = /* @__PURE__ */ new Set();
4812
+ const overlaps = /* @__PURE__ */ new Map();
4813
+ for (const repo of repos) {
4814
+ 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 });
4816
+ const records = stdout.split("\0");
4817
+ const dirty = /* @__PURE__ */ new Map();
4818
+ for (let i = 0; i < records.length; i++) {
4819
+ const record = records[i];
4820
+ if (!record) continue;
4821
+ const status = record.slice(0, 2);
4822
+ dirty.set(record.slice(3), status);
4823
+ if (/[RC]/.test(status)) dirty.set(records[++i], status);
4824
+ }
4825
+ for (const [file, status] of dirty) {
4826
+ if (!file || file === ".coord" || file.startsWith(".coord/")) continue;
4827
+ const key = JSON.stringify([repo, file]);
4828
+ seen.add(key);
4829
+ let modified = now, fingerprint = status;
4830
+ try {
4831
+ const stat = fs19.lstatSync(path17.join(repo, file));
4832
+ modified = Math.min(now, stat.mtimeMs);
4833
+ fingerprint += `:${stat.mtimeMs}:${stat.size}`;
4834
+ } catch (error) {
4835
+ if (error.code !== "ENOENT") throw error;
4836
+ }
4837
+ const previous = this.recentEdits.get(key);
4838
+ if (!previous || previous.fingerprint !== fingerprint) {
4839
+ this.recentEdits.set(key, { repo, fingerprint, timestamp: modified });
4840
+ this.options.onFileChange?.({ repoPath: repo, filePath: file, timestamp: modified });
4841
+ }
4842
+ const identity = JSON.stringify([this.repositories.get(repo), file]);
4843
+ const overlap = overlaps.get(identity) ?? { file, repos: [] };
4844
+ overlap.repos.push(repo);
4845
+ overlaps.set(identity, overlap);
4846
+ }
4847
+ } catch (error) {
4848
+ this.errors.push({ repository: repo, message: `Git scan unavailable: ${error instanceof Error ? error.message : String(error)}` });
4849
+ }
4850
+ }
4851
+ if (!this.errors.length) {
4852
+ for (const key of this.recentEdits.keys()) if (!seen.has(key)) this.recentEdits.delete(key);
4853
+ }
4854
+ const current = /* @__PURE__ */ new Map();
4855
+ for (const [id, overlap] of overlaps) if (overlap.repos.length > 1) {
4856
+ let identical = false;
4857
+ try {
4858
+ const identities = overlap.repos.map((repo) => contentIdentity(repo, overlap.file));
4859
+ identical = identities.every((value) => value !== null && value === identities[0]);
4860
+ } catch (error) {
4861
+ this.errors.push({ repository: overlap.repos.join(", "), message: `Cannot compare ${overlap.file}: ${error instanceof Error ? error.message : String(error)}` });
4862
+ }
4863
+ if (identical) continue;
4864
+ const previous = this.activeCollisions.get(id);
4865
+ const alert = {
4866
+ id,
4867
+ filePath: overlap.file,
4868
+ repos: overlap.repos,
4869
+ severity: "collision",
4870
+ timestamp: previous?.timestamp ?? now,
4871
+ message: `Shared dirty path '${overlap.file}' has different or unverified contents in linked worktrees: ${overlap.repos.join(", ")}. This is not a proven merge conflict.`
4872
+ };
4873
+ current.set(id, alert);
4874
+ if (!previous) {
4875
+ this.options.onCollision?.(alert);
4876
+ if (this.options.notify) _PassiveConflictWatcher.notifyMac("Kith worktree overlap", alert.message);
4877
+ }
4878
+ }
4879
+ this.activeCollisions = this.errors.length ? new Map([...this.activeCollisions, ...current]) : current;
4880
+ if (this.errors.length) this.options.onScanError?.(this.errors);
4881
+ return this.getActiveCollisions();
4882
+ }
4883
+ start() {
4884
+ if (this.isWatching) return;
4885
+ this.isWatching = true;
4886
+ const tick = async () => {
4887
+ try {
4888
+ await this.scanOnce();
4889
+ } catch (error) {
4890
+ this.errors = [{ repository: this.rootDir, message: String(error) }];
4891
+ this.options.onScanError?.(this.errors);
4892
+ }
4893
+ if (this.isWatching) this.watchTimer = setTimeout(tick, this.options.intervalMs ?? 3e3);
4894
+ };
4895
+ void tick();
4896
+ }
4897
+ stop() {
4898
+ this.isWatching = false;
4899
+ if (this.watchTimer) clearTimeout(this.watchTimer);
4900
+ this.watchTimer = null;
4901
+ }
4902
+ getScanState() {
4903
+ return { complete: this.errors.length === 0, errors: [...this.errors], repos: [...this.repositories.keys()] };
4904
+ }
4905
+ getActiveCollisions() {
4906
+ return [...this.activeCollisions.values()];
4907
+ }
4908
+ getRecentEdits() {
4909
+ const now = Date.now();
4910
+ return [...this.recentEdits].map(([key, value]) => ({ key: `${value.repo}/${JSON.parse(key)[1]}`, repo: value.repo, ageSec: Math.max(0, Math.round((now - value.timestamp) / 1e3)) })).filter((value) => value.ageSec < 300).sort((a, b) => a.ageSec - b.ageSec);
4911
+ }
4912
+ };
4913
+
4914
+ // src/cli/setup-auto.ts
4915
+ import fs21 from "fs";
4916
+ import path18 from "path";
4917
+ import os2 from "os";
4918
+ import { parse as parseToml3, stringify as stringifyToml2 } from "smol-toml";
4919
+
4920
+ // src/cli/runtime.ts
4921
+ import fs20 from "fs";
4922
+ import { fileURLToPath as fileURLToPath3 } from "url";
4923
+ function installedCliPath() {
4924
+ for (const relative of ["./index.js", "../../dist/cli/index.js"]) {
4925
+ const file = fileURLToPath3(new URL(relative, import.meta.url));
4926
+ if (fs20.existsSync(file)) return fs20.realpathSync(file);
4927
+ }
4928
+ throw new Error("Build or install Kith before configuring clients.");
4929
+ }
4930
+
4931
+ // src/cli/setup-auto.ts
4932
+ function object2(value, label) {
4933
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object; no configuration was written.`);
4934
+ return value;
4935
+ }
4936
+ function ownedLauncher(value) {
4937
+ if (value.type !== void 0 && value.type !== "stdio") return false;
4938
+ if (typeof value.command !== "string" || !/^node(?:\.exe)?$/i.test(path18.basename(value.command)) || !Array.isArray(value.args) || value.args[1] !== "mcp" || ![2, 4].includes(value.args.length) || value.args.length === 4 && value.args[2] !== "--repo") return false;
4939
+ try {
4940
+ const entry = fs21.realpathSync(value.args[0]);
4941
+ return path18.basename(entry) === "index.js" && path18.basename(path18.dirname(entry)) === "cli" && JSON.parse(fs21.readFileSync(path18.resolve(entry, "../../../package.json"), "utf8")).name === "kith-coord";
4942
+ } catch {
4943
+ return false;
4944
+ }
4945
+ }
4946
+ function autoConfigureAllClients(targetWorkspace = process.cwd(), home = os2.homedir(), options = {}) {
4947
+ const target = physicalPath(targetWorkspace);
4948
+ const layout = target === path18.parse(target).root ? void 0 : discoverRepository(target);
4949
+ const workspace = layout?.checkoutRoot ?? target;
4950
+ home = physicalPath(home);
4951
+ const server = { command: process.execPath, args: [installedCliPath(), "mcp"] };
4952
+ const projectServer = { ...server, args: [...server.args, "--repo", workspace] };
4953
+ const results = [];
4954
+ const groups = /* @__PURE__ */ new Map();
4955
+ const warnings = [];
4956
+ const queue = (root, file, content) => {
4957
+ if (!isWithin(root, file)) throw new Error(`Configuration is outside its setup root: ${file}`);
4958
+ if (fs21.existsSync(file) && fs21.readFileSync(file, "utf8") === content) return;
4959
+ const group = groups.get(root) ?? { coordDir: root === workspace && layout ? layout.coordDir : path18.join(root, ".coord"), plan: /* @__PURE__ */ new Map() };
4960
+ if (physicalPath(group.coordDir) !== group.coordDir) throw new Error("Setup backup directory must not be redirected.");
4961
+ group.plan.set(path18.relative(root, file), { content, mode: 384 });
4962
+ groups.set(root, group);
4963
+ };
4964
+ const safePath = (file) => {
4965
+ if (physicalPath(file) !== file) throw new Error(`Configuration path is redirected: ${file}; no configuration was written.`);
4966
+ };
4967
+ const jsonClient = (clientName, file, root, found, projectScoped = false) => {
4968
+ const selectedServer = projectScoped ? projectServer : server;
4969
+ if (!found) {
4970
+ results.push({ clientName, configPath: file, found: false, configured: false, notes: "No existing client configuration found" });
4971
+ return;
4972
+ }
4973
+ safePath(file);
4974
+ const cfg = fs21.existsSync(file) ? object2(JSON.parse(fs21.readFileSync(file, "utf8")), file) : {};
4975
+ const servers = object2(cfg.mcpServers ?? {}, `${file} mcpServers`);
4976
+ const previous = servers.kith === void 0 ? void 0 : object2(servers.kith, `${file} kith`);
4977
+ const exact = previous?.command === selectedServer.command && JSON.stringify(previous.args) === JSON.stringify(selectedServer.args);
4978
+ if (previous && !exact && !ownedLauncher(previous)) {
4979
+ results.push({ clientName, configPath: file, found: true, configured: false, notes: "Custom Kith server preserved; review its repository and launcher manually" });
4980
+ return;
4981
+ }
4982
+ cfg.mcpServers = { ...servers, kith: { ...previous, ...selectedServer } };
4983
+ queue(root, file, JSON.stringify(cfg, null, 2) + "\n");
4984
+ results.push({ clientName, configPath: file, found: true, configured: true, notes: `${options.dryRun ? "Would configure" : "Configured"} Kith ${projectScoped ? `for ${workspace}` : "for per-task repository selection"}; existing settings preserved` });
4985
+ };
4986
+ const desktop = process.platform === "win32" ? path18.join(process.env.APPDATA ?? path18.join(home, "AppData/Roaming"), "Claude/claude_desktop_config.json") : process.platform === "darwin" ? path18.join(home, "Library/Application Support/Claude/claude_desktop_config.json") : path18.join(home, ".config/Claude/claude_desktop_config.json");
4987
+ jsonClient("Claude Desktop App", desktop, home, fs21.existsSync(desktop));
4988
+ jsonClient(
4989
+ "Claude Code CLI",
4990
+ path18.join(home, ".claude.json"),
4991
+ home,
4992
+ fs21.existsSync(path18.join(home, ".claude.json")) || fs21.existsSync(path18.join(home, ".claude"))
4993
+ );
4994
+ const codex = path18.join(home, ".codex/config.toml");
4995
+ if (fs21.existsSync(path18.dirname(codex))) {
4996
+ safePath(codex);
4997
+ const original = fs21.existsSync(codex) ? fs21.readFileSync(codex, "utf8") : "";
4998
+ const cfg = parseToml3(original), servers = object2(cfg.mcp_servers ?? {}, "Codex MCP servers");
4999
+ const previous = servers.kith === void 0 ? void 0 : object2(servers.kith, "Codex Kith server");
5000
+ let content = original;
5001
+ let configured = true;
5002
+ const exact = previous?.command === server.command && JSON.stringify(previous.args) === JSON.stringify(server.args);
5003
+ if (!previous) {
5004
+ content += (content && !content.endsWith("\n") ? "\n" : "") + "\n" + stringifyToml2({ mcp_servers: { kith: { ...server, env_vars: ["KITH_AGENT_ID", "KITH_SESSION_TOKEN"] } } });
5005
+ } else if (!exact) {
5006
+ const block = /(^\[mcp_servers\.kith\][^\S\r\n]*\r?\n)([\s\S]*?)(?=^\[|$(?![\s\S]))/m;
5007
+ const match = original.match(block);
5008
+ if (ownedLauncher(previous) && match && /^command\s*=\s*"(?:[^"\\\r\n]|\\.)*"\s*$/m.test(match[2]) && /^args\s*=\s*\[[^\r\n]*\]\s*$/m.test(match[2])) {
5009
+ const fields = [`command = ${JSON.stringify(server.command)}`, `args = ${JSON.stringify(server.args)}`];
5010
+ const updated = match[2].replace(/^command\s*=.*$/m, fields[0]).replace(/^args\s*=.*$/m, fields[1]);
5011
+ content = original.replace(block, () => match[1] + updated);
5012
+ } else configured = false;
5013
+ }
5014
+ parseToml3(content);
5015
+ if (configured) queue(home, codex, content);
5016
+ results.push({
5017
+ clientName: "Codex",
5018
+ found: true,
5019
+ configPath: codex,
5020
+ configured,
5021
+ notes: configured ? `${options.dryRun ? "Would configure" : "Configured"} Kith for per-task repository selection; other TOML preserved` : "Custom Kith server preserved; review its repository and launcher manually"
5022
+ });
5023
+ }
5024
+ jsonClient("Cursor", path18.join(workspace, ".cursor/mcp.json"), workspace, Boolean(layout?.commonGitDir), true);
5025
+ const antigravity = path18.join(home, ".gemini/config/mcp_config.json");
5026
+ jsonClient("Antigravity", antigravity, home, fs21.existsSync(path18.dirname(antigravity)));
5027
+ for (const [root] of groups) {
5028
+ if (!fs21.existsSync(path18.join(root, ".git"))) continue;
5029
+ const file = path18.join(root, ".gitignore");
5030
+ safePath(file);
5031
+ const original = fs21.existsSync(file) ? fs21.readFileSync(file, "utf8") : "";
5032
+ if (!original.split(/\r?\n/).some((line) => [".coord/", "/.coord/"].includes(line.trim()))) {
5033
+ queue(root, file, original + (original && !original.endsWith("\n") ? "\n" : "") + "\n# Kith private setup backups\n.coord/\n");
5034
+ }
5035
+ }
5036
+ if (!options.dryRun && groups.size) {
5037
+ const engine = layout?.commonGitDir ? new KithEngine(workspace) : void 0;
5038
+ try {
5039
+ for (const [root, group] of groups) {
5040
+ fs21.mkdirSync(group.coordDir, { recursive: true, mode: 448 });
5041
+ applySetupPlan(root, group.coordDir, group.plan, warnings);
5042
+ }
5043
+ } finally {
5044
+ engine?.close();
5045
+ }
5046
+ }
5047
+ for (const warning of warnings) results.push({ clientName: "Setup backup", found: true, configured: false, configPath: "", notes: warning });
5048
+ return results;
5049
+ }
5050
+
4638
5051
  // src/cli/index.ts
4639
5052
  function parseFlags(argv) {
4640
5053
  const dashDashIdx = argv.indexOf("--");
@@ -4651,7 +5064,7 @@ function parseFlags(argv) {
4651
5064
  } else {
4652
5065
  const key = arg.slice(2);
4653
5066
  const next = mainArgs[i + 1];
4654
- if (!["worktree", "raw", "dry-run", "help", "version", "shell", "new", "mcp-only", "offline"].includes(key) && next && !next.startsWith("--")) {
5067
+ if (!["worktree", "raw", "dry-run", "help", "version", "shell", "new", "mcp-only", "offline", "once", "json", "quiet", "no-notify"].includes(key) && next && !next.startsWith("--")) {
4655
5068
  flags[key] = next;
4656
5069
  i++;
4657
5070
  } else {
@@ -4668,7 +5081,8 @@ async function main() {
4668
5081
  const { positional, flags, passthroughCmd } = parseFlags(process.argv.slice(2));
4669
5082
  const cmd = positional[0] ?? "help";
4670
5083
  const subCmd = positional[1];
4671
- const repoRoot = typeof flags.repo === "string" ? flags.repo : process.cwd();
5084
+ const targetFromPositional = ["init", "setup", "watch", "menubar", "bar", "tray", "doctor", "recover"].includes(cmd) && positional[1] ? path19.resolve(positional[1]) : void 0;
5085
+ const repoRoot = typeof flags.repo === "string" ? path19.resolve(flags.repo) : targetFromPositional ?? (cmd === "mcp" ? process.env.CLAUDE_PROJECT_DIR : void 0) ?? process.cwd();
4672
5086
  if (flags.version || cmd === "version") {
4673
5087
  console.log(KITH_VERSION);
4674
5088
  return;
@@ -4692,7 +5106,7 @@ async function main() {
4692
5106
  return;
4693
5107
  }
4694
5108
  if (cmd === "mcp") {
4695
- await runKithMcpStdioServer(repoRoot);
5109
+ await runKithMcpStdioServer(repoRoot, typeof flags.repo === "string");
4696
5110
  return;
4697
5111
  }
4698
5112
  if (cmd === "uninstall") {
@@ -4713,6 +5127,102 @@ async function main() {
4713
5127
  process.exitCode = state.state === "unavailable" ? 1 : 0;
4714
5128
  return;
4715
5129
  }
5130
+ if (cmd === "setup" || cmd === "auto-detect") {
5131
+ const targetDir = positional[1] ? path19.resolve(positional[1]) : repoRoot;
5132
+ const results = autoConfigureAllClients(targetDir, void 0, { dryRun: Boolean(flags["dry-run"]) });
5133
+ console.log(`
5134
+ \u2728 Kith Auto-Detect & Setup for ${targetDir}
5135
+ `);
5136
+ for (const r of results) {
5137
+ const statusIcon = r.configured ? "\u2713" : r.found ? "!" : "\u2717";
5138
+ console.log(` ${statusIcon} ${r.clientName.padEnd(24)} -> ${r.notes}`);
5139
+ }
5140
+ if (results.some((r) => r.found && !r.configured)) process.exitCode = 1;
5141
+ console.log("\nExisting hooks and permissions are unchanged. Restart configured clients; review any skipped entries.\n");
5142
+ return;
5143
+ }
5144
+ if (cmd === "menubar" || cmd === "bar" || cmd === "tray") {
5145
+ const targetDir = positional[1] ? path19.resolve(positional[1]) : repoRoot;
5146
+ if (process.platform !== "darwin") throw new Error("The menu-bar view requires macOS. Use kith watch on this platform.");
5147
+ const menubarBin = path19.resolve(path19.dirname(installedCliPath()), "../../bin/kith-menubar");
5148
+ if (!fs22.existsSync(menubarBin)) throw new Error("Menu-bar binary is missing; reinstall the package or run pnpm build:menubar in the source checkout.");
5149
+ await new Promise((resolve, reject) => {
5150
+ const child = spawn(menubarBin, [fs22.realpathSync(targetDir), process.execPath, installedCliPath()], { detached: true, stdio: "ignore" });
5151
+ child.once("error", reject);
5152
+ child.once("spawn", () => {
5153
+ child.unref();
5154
+ resolve();
5155
+ });
5156
+ });
5157
+ console.log(`Kith menu-bar view launched for ${targetDir}.`);
5158
+ return;
5159
+ }
5160
+ if (cmd === "watch" || cmd === "monitor") {
5161
+ const targetDir = positional[1] ? path19.resolve(positional[1]) : repoRoot;
5162
+ const notify = flags.notify !== "false" && !flags["no-notify"];
5163
+ const intervalMs = typeof flags.interval === "string" ? Number(flags.interval) : 2500;
5164
+ const isRaw = Boolean(flags.raw || flags.json);
5165
+ let lastScanError = "";
5166
+ const watcher = new PassiveConflictWatcher(targetDir, {
5167
+ intervalMs,
5168
+ notify: !isRaw && notify,
5169
+ onScanError: (errors) => {
5170
+ const message = errors.map((error) => `${error.repository}: ${error.message}`).join("\n");
5171
+ if (!flags.once && message !== lastScanError) console.error(`Scan unavailable:
5172
+ ${message}`);
5173
+ lastScanError = message;
5174
+ },
5175
+ onCollision: (alert) => {
5176
+ if (!isRaw) {
5177
+ console.log(`
5178
+ \u26A0\uFE0F [WORKTREE OVERLAP] ${alert.message}`);
5179
+ }
5180
+ },
5181
+ onFileChange: (e) => {
5182
+ if (!flags.quiet && !isRaw) {
5183
+ console.log(`\u{1F4DD} [EDIT] ${path19.basename(e.repoPath)}/${e.filePath}`);
5184
+ }
5185
+ }
5186
+ });
5187
+ if (flags.once) {
5188
+ const alerts = await watcher.scanOnce();
5189
+ const state = watcher.getScanState();
5190
+ const repos = state.repos;
5191
+ const edits = watcher.getRecentEdits();
5192
+ if (!state.complete) process.exitCode = 1;
5193
+ if (flags.raw || flags.json) {
5194
+ console.log(JSON.stringify({ ...state, recentEdits: edits, collisions: alerts }, null, 2));
5195
+ return;
5196
+ }
5197
+ console.log(`
5198
+ \u{1F4E1} Kith Passive Radar: ${targetDir}`);
5199
+ console.log(` Repositories (${repos.length}): ${repos.map((r) => path19.basename(r)).join(", ")}`);
5200
+ console.log(` Recent Modified Files: ${edits.length}`);
5201
+ if (!state.complete) {
5202
+ console.error(`Scan unavailable:
5203
+ ${state.errors.map((error) => error.message).join("\n")}`);
5204
+ } else if (alerts.length === 0) {
5205
+ console.log("\nNo differing or unverified shared dirty paths observed. Identical copies are excluded; compatibility is not verified.\n");
5206
+ } else {
5207
+ console.log(`
5208
+ \u26A0\uFE0F Observed worktree overlaps (${alerts.length}):`);
5209
+ for (const a of alerts) {
5210
+ console.log(` \u26A0\uFE0F ${a.message}`);
5211
+ console.log(` Files: ${a.filePath}`);
5212
+ }
5213
+ console.log("");
5214
+ }
5215
+ return;
5216
+ }
5217
+ console.log(`
5218
+ \u{1F440} Kith Passive Conflict Radar watching: ${targetDir}`);
5219
+ console.log(` Read-only overlap observations; this does not identify agents or verify compatibility. Press Ctrl+C to stop.
5220
+ `);
5221
+ watcher.start();
5222
+ await new Promise(() => {
5223
+ });
5224
+ return;
5225
+ }
4716
5226
  const commands = ["session", "status", "begin", "complete", "heartbeat", "task", "lock", "contract", "worktree", "port", "mutex", "hook"];
4717
5227
  if (!commands.includes(cmd)) throw new Error(`Unknown command '${cmd}'. Run kith help.`);
4718
5228
  const subcommands = {
@@ -5029,7 +5539,10 @@ Usage:
5029
5539
  kith uninstall [--dry-run] Restore unchanged tracked setup files; retain work and recovery state
5030
5540
  kith doctor Check runtime, PATH, configuration, sessions and worktree health without writes
5031
5541
  kith recover Inspect retained work, expired claims and recovery records offline
5032
- kith mcp Start stdio MCP server
5542
+ kith setup [checkout] [--dry-run] Configure detected clients; preserve hooks and custom settings
5543
+ kith watch [checkout] [--once --json] Observe linked-worktree path overlaps without enforcement
5544
+ kith menubar [checkout] Optional read-only macOS view
5545
+ kith mcp --repo <checkout> Start stdio MCP server bound to a project
5033
5546
  kith begin --agent <id> --title <t> --scope <g> Declare intent, check overlaps, lock scope
5034
5547
  kith complete --agent <id> [--merge <branch>] Run Speculative Merge Train & release locks
5035
5548
  kith task add|claim|list Manage orchestrated DAG task queue