kith-coord 0.1.2 → 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.2";
5
- var KITH_PROTOCOL_VERSION = "1.1.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");
@@ -3917,73 +3932,105 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3917
3932
  import { z } from "zod";
3918
3933
  import fs16 from "fs";
3919
3934
  import path15 from "path";
3920
- function createKithMcpServer(repoRoot = process.cwd()) {
3921
- const engine = new KithEngine(repoRoot);
3935
+ function buildKithMcpServer(repoRoot, pinned) {
3936
+ const pinnedRoot = pinned ? discoverRepository(repoRoot).checkoutRoot : void 0;
3922
3937
  const server = new McpServer({
3923
3938
  name: "kith-coordinator",
3924
3939
  version: KITH_VERSION
3925
- }, { 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. 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.") });
3926
- const tokenFor = (args) => args.sessionToken ?? (process.env.KITH_AGENT_ID === args.agentId ? process.env.KITH_SESSION_TOKEN : void 0);
3927
- const owned = /* @__PURE__ */ new Set();
3928
- const register = (args) => {
3929
- const result = engine.fencing.registerAgent({
3930
- id: args.agentId,
3931
- client: args.client ?? "mcp",
3932
- sessionToken: tokenFor(args),
3933
- leaseDurationMs: args.leaseDurationMs
3934
- });
3935
- if (result.agent.pid === process.pid) owned.add(args.agentId);
3936
- return result;
3937
- };
3938
- const authorize = (args) => {
3939
- const token = tokenFor(args);
3940
- if (token) engine.fencing.resumeSession(args.agentId, token);
3941
- engine.fencing.heartbeat(args.agentId);
3942
- };
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.") });
3943
3941
  const disconnected = new AbortController();
3944
3942
  const commands = /* @__PURE__ */ new Set();
3945
- const cleanup = () => {
3946
- for (const id of owned) {
3947
- try {
3948
- engine.fencing.abandonAgentSession(id);
3949
- } 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
+ }
3950
3976
  }
3951
- }
3952
- 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
+ );
3953
4008
  };
3954
4009
  let closing = false;
3955
4010
  server.server.onclose = () => {
3956
4011
  if (closing) return;
3957
4012
  closing = true;
3958
4013
  disconnected.abort();
4014
+ const cleanup = () => {
4015
+ for (const context of contexts.values()) context.close();
4016
+ contexts.clear();
4017
+ };
3959
4018
  if (commands.size) void Promise.allSettled([...commands]).then(cleanup);
3960
4019
  else cleanup();
3961
4020
  };
3962
- const formatReply = (agentId, payload) => {
3963
- const delta = agentId ? engine.consumeAgentDelta(agentId).formattedFooter : "";
3964
- const data = payload;
3965
- return {
3966
- content: [
3967
- { type: "text", text: JSON.stringify(data, null, 2) },
3968
- ...delta ? [{ type: "text", text: delta }] : []
3969
- ],
3970
- structuredContent: data,
3971
- isError: data.ok === false
3972
- };
3973
- };
3974
- 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.", {
3975
4022
  agentId: z.string(),
3976
4023
  sessionToken: z.string().optional(),
3977
4024
  leaseDurationMs: z.number().optional()
3978
- }, async (args) => formatReply(void 0, { ok: true, ...register(args) }));
3979
- 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.", {
3980
4027
  agentId: z.string(),
3981
4028
  sessionToken: z.string().optional()
3982
- }, async (args) => {
4029
+ }, async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
3983
4030
  authorize(args);
3984
- 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) });
3985
4032
  });
3986
- server.tool(
4033
+ tool(
3987
4034
  "kith_begin_work",
3988
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.",
3989
4036
  {
@@ -3995,19 +4042,19 @@ function createKithMcpServer(repoRoot = process.cwd()) {
3995
4042
  scopeGlobs: z.array(z.string()).describe('File paths or glob patterns this agent will edit (e.g. ["src/auth/**"])'),
3996
4043
  isolateWorktree: z.boolean().optional().describe("Recommended: true, giving each parallel agent a separate Git worktree")
3997
4044
  },
3998
- async (args) => {
4045
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
3999
4046
  const reg = register(args);
4000
- const intentRes = engine.beginWork({
4047
+ const intentRes = engine2.beginWork({
4001
4048
  agentId: args.agentId,
4002
4049
  title: args.title,
4003
4050
  intentSummary: args.intentSummary,
4004
4051
  scopeGlobs: args.scopeGlobs,
4005
4052
  isolateWorktree: args.isolateWorktree
4006
4053
  });
4007
- const contracts = engine.depGraph.listContracts().slice(0, 30);
4054
+ const contracts = engine2.depGraph.listContracts().slice(0, 30);
4008
4055
  return formatReply(args.agentId, {
4009
4056
  ok: intentRes.ok,
4010
- agent: engine.fencing.getAgent(args.agentId),
4057
+ agent: engine2.fencing.getAgent(args.agentId),
4011
4058
  sessionToken: reg.sessionToken,
4012
4059
  fencingToken: intentRes.fencingToken,
4013
4060
  task: intentRes.task,
@@ -4019,7 +4066,7 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4019
4066
  });
4020
4067
  }
4021
4068
  );
4022
- server.tool(
4069
+ tool(
4023
4070
  "kith_claim_next_task",
4024
4071
  "Atomically claim the highest-priority unblocked task from the DAG task queue and lock its declared file scope.",
4025
4072
  {
@@ -4027,13 +4074,13 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4027
4074
  sessionToken: z.string().optional(),
4028
4075
  client: z.string().optional()
4029
4076
  },
4030
- async (args) => {
4077
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4031
4078
  const session = register(args);
4032
- const res = engine.intents.claimNextTask(args.agentId);
4079
+ const res = engine2.intents.claimNextTask(args.agentId);
4033
4080
  return formatReply(args.agentId, { ...res, sessionToken: session.sessionToken });
4034
4081
  }
4035
4082
  );
4036
- server.tool(
4083
+ tool(
4037
4084
  "kith_add_task",
4038
4085
  "Add a task to the orchestrated DAG queue with optional task dependencies and file scope globs.",
4039
4086
  {
@@ -4044,12 +4091,12 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4044
4091
  dependsOn: z.array(z.string()).optional(),
4045
4092
  scopeGlobs: z.array(z.string()).optional()
4046
4093
  },
4047
- async (args) => {
4048
- const task = engine.intents.addTask(args);
4094
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4095
+ const task = engine2.intents.addTask(args);
4049
4096
  return formatReply(void 0, { ok: true, task });
4050
4097
  }
4051
4098
  );
4052
- server.tool(
4099
+ tool(
4053
4100
  "kith_check_and_lock_files",
4054
4101
  "Acquire exclusive or shared fencing-token locks on file paths, globs, or symbols.",
4055
4102
  {
@@ -4059,13 +4106,13 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4059
4106
  mode: z.enum(["exclusive", "shared"]).optional(),
4060
4107
  reason: z.string().optional()
4061
4108
  },
4062
- async (args) => {
4109
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4063
4110
  authorize(args);
4064
- const res = engine.locks.acquireLocks(args);
4111
+ const res = engine2.locks.acquireLocks(args);
4065
4112
  return formatReply(args.agentId, res);
4066
4113
  }
4067
4114
  );
4068
- server.tool(
4115
+ tool(
4069
4116
  "kith_record_file_edit",
4070
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.",
4071
4118
  {
@@ -4074,9 +4121,9 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4074
4121
  filePath: z.string(),
4075
4122
  content: z.string().optional()
4076
4123
  },
4077
- async (args) => {
4124
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4078
4125
  authorize(args);
4079
- const res = engine.processPostFileEdit({
4126
+ const res = engine2.processPostFileEdit({
4080
4127
  agentId: args.agentId,
4081
4128
  filePath: args.filePath,
4082
4129
  content: args.content
@@ -4084,7 +4131,7 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4084
4131
  return formatReply(args.agentId, res);
4085
4132
  }
4086
4133
  );
4087
- server.tool(
4134
+ tool(
4088
4135
  "kith_publish_contract",
4089
4136
  "Explicitly publish a shared interface, function signature, API route, or schema to the Contract Blackboard.",
4090
4137
  {
@@ -4096,13 +4143,13 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4096
4143
  signature: z.string(),
4097
4144
  docComment: z.string().optional()
4098
4145
  },
4099
- async (args) => {
4146
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4100
4147
  authorize(args);
4101
- const contract = engine.depGraph.publishExplicitContract(args);
4148
+ const contract = engine2.depGraph.publishExplicitContract(args);
4102
4149
  return formatReply(args.agentId, { ok: true, contract });
4103
4150
  }
4104
4151
  );
4105
- server.tool(
4152
+ tool(
4106
4153
  "kith_acquire_port",
4107
4154
  "Reserve a local TCP port among cooperating Kith clients; start the service promptly and handle unrelated bind races.",
4108
4155
  {
@@ -4111,13 +4158,13 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4111
4158
  serviceName: z.string(),
4112
4159
  basePort: z.number().optional()
4113
4160
  },
4114
- async (args) => {
4161
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4115
4162
  authorize(args);
4116
- const portRes = await engine.broker.acquirePort(args);
4163
+ const portRes = await engine2.broker.acquirePort(args);
4117
4164
  return formatReply(args.agentId, portRes);
4118
4165
  }
4119
4166
  );
4120
- server.tool(
4167
+ tool(
4121
4168
  "kith_run_with_mutex",
4122
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.",
4123
4170
  {
@@ -4131,15 +4178,15 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4131
4178
  commandTimeoutMs: z.number().int().min(1).max(6e5).optional(),
4132
4179
  maxOutputBytes: z.number().int().min(1).max(1048576).optional()
4133
4180
  },
4134
- async (args, extra) => {
4181
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4135
4182
  authorize(args);
4136
- 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);
4137
4184
  const cwd = fs16.realpathSync(path15.resolve(root, args.cwd ?? "."));
4138
4185
  const relative = path15.relative(root, cwd);
4139
4186
  if (relative === ".." || relative.startsWith(".." + path15.sep) || path15.isAbsolute(relative)) {
4140
4187
  throw new Error("Command cwd must be inside the registered worktree or repository");
4141
4188
  }
4142
- const execution = engine.broker.executeWithMutex({
4189
+ const execution = engine2.broker.executeWithMutex({
4143
4190
  agentId: args.agentId,
4144
4191
  mutexName: args.mutexName,
4145
4192
  command: args.command,
@@ -4159,7 +4206,7 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4159
4206
  }
4160
4207
  }
4161
4208
  );
4162
- server.tool(
4209
+ tool(
4163
4210
  "kith_get_status",
4164
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.",
4165
4212
  {
@@ -4167,9 +4214,9 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4167
4214
  sessionToken: z.string().optional(),
4168
4215
  includeContracts: z.boolean().optional().describe("Include the full contract registry. Default false keeps frequent status polls compact; contractCount always reports its size.")
4169
4216
  },
4170
- async (args) => {
4217
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4171
4218
  if (args.agentId) authorize({ ...args, agentId: args.agentId });
4172
- const state = engine.getStatus();
4219
+ const state = engine2.getStatus();
4173
4220
  return formatReply(args.agentId, {
4174
4221
  ...state,
4175
4222
  contracts: args.includeContracts ? state.contracts : [],
@@ -4178,16 +4225,16 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4178
4225
  });
4179
4226
  }
4180
4227
  );
4181
- server.tool(
4228
+ tool(
4182
4229
  "kith_preview_integrate",
4183
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.",
4184
4231
  { agentId: z.string(), sessionToken: z.string().optional() },
4185
- async (args) => {
4232
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4186
4233
  authorize(args);
4187
- return formatReply(args.agentId, engine.previewIntegration(args.agentId));
4234
+ return formatReply(args.agentId, engine2.previewIntegration(args.agentId));
4188
4235
  }
4189
4236
  );
4190
- server.tool(
4237
+ tool(
4191
4238
  "kith_complete_work",
4192
4239
  "Complete an agent task, optionally run the Speculative Merge Train to verify and merge its worktree branch, and release all locks and resources.",
4193
4240
  {
@@ -4198,16 +4245,25 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4198
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."),
4199
4246
  verificationCmd: z.string().optional().describe('Command to run in speculative merge tree (e.g. "npm run typecheck")')
4200
4247
  },
4201
- async (args) => {
4248
+ async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4202
4249
  authorize(args);
4203
- const result = engine.completeWork(args);
4250
+ const result = engine2.completeWork(args);
4204
4251
  return formatReply(result.ok ? void 0 : args.agentId, result);
4205
4252
  }
4206
4253
  );
4207
4254
  return { server, engine };
4208
4255
  }
4209
- async function runKithMcpStdioServer(repoRoot = process.cwd()) {
4210
- 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);
4211
4267
  const transport = new StdioServerTransport();
4212
4268
  let closing = false;
4213
4269
  const close = () => {
@@ -4642,6 +4698,356 @@ esac
4642
4698
  return report;
4643
4699
  }
4644
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
+
4645
5051
  // src/cli/index.ts
4646
5052
  function parseFlags(argv) {
4647
5053
  const dashDashIdx = argv.indexOf("--");
@@ -4658,7 +5064,7 @@ function parseFlags(argv) {
4658
5064
  } else {
4659
5065
  const key = arg.slice(2);
4660
5066
  const next = mainArgs[i + 1];
4661
- 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("--")) {
4662
5068
  flags[key] = next;
4663
5069
  i++;
4664
5070
  } else {
@@ -4675,7 +5081,8 @@ async function main() {
4675
5081
  const { positional, flags, passthroughCmd } = parseFlags(process.argv.slice(2));
4676
5082
  const cmd = positional[0] ?? "help";
4677
5083
  const subCmd = positional[1];
4678
- 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();
4679
5086
  if (flags.version || cmd === "version") {
4680
5087
  console.log(KITH_VERSION);
4681
5088
  return;
@@ -4699,7 +5106,7 @@ async function main() {
4699
5106
  return;
4700
5107
  }
4701
5108
  if (cmd === "mcp") {
4702
- await runKithMcpStdioServer(repoRoot);
5109
+ await runKithMcpStdioServer(repoRoot, typeof flags.repo === "string");
4703
5110
  return;
4704
5111
  }
4705
5112
  if (cmd === "uninstall") {
@@ -4720,6 +5127,102 @@ async function main() {
4720
5127
  process.exitCode = state.state === "unavailable" ? 1 : 0;
4721
5128
  return;
4722
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
+ }
4723
5226
  const commands = ["session", "status", "begin", "complete", "heartbeat", "task", "lock", "contract", "worktree", "port", "mutex", "hook"];
4724
5227
  if (!commands.includes(cmd)) throw new Error(`Unknown command '${cmd}'. Run kith help.`);
4725
5228
  const subcommands = {
@@ -5036,7 +5539,10 @@ Usage:
5036
5539
  kith uninstall [--dry-run] Restore unchanged tracked setup files; retain work and recovery state
5037
5540
  kith doctor Check runtime, PATH, configuration, sessions and worktree health without writes
5038
5541
  kith recover Inspect retained work, expired claims and recovery records offline
5039
- 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
5040
5546
  kith begin --agent <id> --title <t> --scope <g> Declare intent, check overlaps, lock scope
5041
5547
  kith complete --agent <id> [--merge <branch>] Run Speculative Merge Train & release locks
5042
5548
  kith task add|claim|list Manage orchestrated DAG task queue