kith-coord 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -6,8 +6,8 @@ import path19 from "path";
6
6
  import { spawn } from "child_process";
7
7
 
8
8
  // src/version.ts
9
- var KITH_VERSION = "0.1.4";
10
- var KITH_PROTOCOL_VERSION = "1.3.0";
9
+ var KITH_VERSION = "0.1.6";
10
+ var KITH_PROTOCOL_VERSION = "1.4.0";
11
11
 
12
12
  // src/cli/setup-ledger.ts
13
13
  import fs2 from "fs";
@@ -343,7 +343,7 @@ var CoordDatabase = class _CoordDatabase {
343
343
  }
344
344
  if (!tables.some((table) => table.name === "meta")) throw new Error("Missing coordination schema metadata. Preserve the database before repair.");
345
345
  const row = db.prepare("SELECT value FROM meta WHERE key='schema_version'").get();
346
- if (row?.value !== "1" && row?.value !== "2") {
346
+ if (row?.value !== "1" && row?.value !== "2" && row?.value !== "3") {
347
347
  throw new Error(`Unsupported coordination schema version: ${String(row?.value)}. Preserve the database before upgrading or repairing it.`);
348
348
  }
349
349
  const version = Number(row.value);
@@ -358,7 +358,8 @@ var CoordDatabase = class _CoordDatabase {
358
358
  "resources",
359
359
  "quarantine",
360
360
  "events",
361
- ...version === 2 ? ["agent_sessions", "mutex_waiters", "mutex_runs"] : []
361
+ ...version >= 2 ? ["agent_sessions", "mutex_waiters", "mutex_runs"] : [],
362
+ ...version >= 3 ? ["mcp_operations"] : []
362
363
  ];
363
364
  if (required.some((name) => !tables.some((table) => table.name === name))) {
364
365
  throw new Error("Incomplete coordination schema. Preserve the database before repair.");
@@ -368,11 +369,12 @@ var CoordDatabase = class _CoordDatabase {
368
369
  tasks: "status,owner_agent_id,fencing_token,scope_globs_json,depends_on_json",
369
370
  locks: "owner_agent_id,pattern,mode,fencing_token,expires_at",
370
371
  resources: "type,name,owner_agent_id,fencing_token,expires_at",
371
- ...version === 2 ? {
372
+ ...version >= 2 ? {
372
373
  agent_sessions: "agent_id,token_hash,epoch",
373
374
  mutex_waiters: "ticket,mutex_name,request_id,owner_agent_id,session_epoch,waiter_pid,waiter_start,expires_at",
374
375
  mutex_runs: "mutex_name,run_id,owner_agent_id,session_epoch,fencing_token,supervisor_pid,supervisor_start,worker_pid,worker_start"
375
- } : {}
376
+ } : {},
377
+ ...version >= 3 ? { mcp_operations: "operation_key,request_hash,agent_id,session_epoch,capability_hash,owner_pid,owner_start,result_json,created_at" } : {}
376
378
  };
377
379
  for (const [table, fields] of Object.entries(columns)) {
378
380
  try {
@@ -387,7 +389,8 @@ var CoordDatabase = class _CoordDatabase {
387
389
  "SELECT fencing_token FROM tasks",
388
390
  "SELECT fencing_token FROM resources",
389
391
  "SELECT fencing_token FROM merge_queue",
390
- ...version === 2 ? ["SELECT epoch FROM agent_sessions", "SELECT session_epoch FROM mutex_waiters", "SELECT fencing_token FROM mutex_runs"] : []
392
+ ...version >= 2 ? ["SELECT epoch FROM agent_sessions", "SELECT session_epoch FROM mutex_waiters", "SELECT fencing_token FROM mutex_runs"] : [],
393
+ ...version >= 3 ? ["SELECT session_epoch FROM mcp_operations"] : []
391
394
  ];
392
395
  if (db.prepare(`SELECT 1 FROM (${tokens.join(" UNION ALL ")}) WHERE token IS NOT NULL AND (typeof(token)!='integer' OR token<0 OR token>?) LIMIT 1`).get(epoch)) {
393
396
  throw new Error("Invalid or regressed fencing counter. Preserve the database before repair.");
@@ -565,6 +568,14 @@ var CoordDatabase = class _CoordDatabase {
565
568
  worker_pid INTEGER, worker_start TEXT
566
569
  );
567
570
  `);
571
+ this.db.exec(`
572
+ CREATE TABLE IF NOT EXISTS mcp_operations (
573
+ operation_key TEXT PRIMARY KEY, request_hash TEXT NOT NULL,
574
+ agent_id TEXT NOT NULL, session_epoch INTEGER NOT NULL, capability_hash TEXT NOT NULL,
575
+ owner_pid INTEGER NOT NULL, owner_start TEXT NOT NULL,
576
+ result_json TEXT, created_at INTEGER NOT NULL
577
+ );
578
+ `);
568
579
  if (version < 2) {
569
580
  this.db.exec(`
570
581
  UPDATE tasks SET status='queued', owner_agent_id=NULL, fencing_token=NULL
@@ -576,6 +587,7 @@ var CoordDatabase = class _CoordDatabase {
576
587
  UPDATE meta SET value='2' WHERE key='schema_version';
577
588
  `);
578
589
  }
590
+ this.db.exec("UPDATE meta SET value='3' WHERE key='schema_version'");
579
591
  });
580
592
  }
581
593
  /**
@@ -793,7 +805,7 @@ function previewIntegration(repoRoot, agent) {
793
805
 
794
806
  // src/core/observation.ts
795
807
  var { DatabaseSync: DatabaseSync2 } = createRequire2(import.meta.url)("node:sqlite");
796
- function readCoordinationState(directory = process.cwd()) {
808
+ function readCoordinationState(directory = process.cwd(), options = {}) {
797
809
  const layout = discoverRepository(directory), observedAt = Date.now();
798
810
  const empty = {
799
811
  protocolVersion: KITH_PROTOCOL_VERSION,
@@ -849,9 +861,13 @@ function readCoordinationState(directory = process.cwd()) {
849
861
  if (JSON.stringify(before) !== JSON.stringify(files.map(identity))) throw new Error("Coordination state changed during snapshot; stop writers or retry");
850
862
  db = new DatabaseSync2(path4.join(scratch, "state.db"), { readOnly: true });
851
863
  db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=1000; BEGIN");
852
- if (CoordDatabase.inspectSchema(db) !== 2) throw new Error("Offline observation requires schema 2; preserve state and upgrade with clients stopped");
864
+ if (CoordDatabase.inspectSchema(db) < 2) throw new Error("Offline observation requires schema 2 or newer; preserve state and upgrade with clients stopped");
853
865
  const all = (sql) => db.prepare(sql).all();
854
866
  const agents = all("SELECT * FROM agents ORDER BY registered_at DESC");
867
+ if (options.ownersOnly) {
868
+ db.exec("COMMIT");
869
+ return { ...empty, state: "ready", agents };
870
+ }
855
871
  const state = {
856
872
  ...empty,
857
873
  state: "ready",
@@ -1618,10 +1634,10 @@ var LockEngine = class _LockEngine {
1618
1634
  static normalizePattern(repoRoot, rawPattern) {
1619
1635
  if (rawPattern.startsWith("file:")) return _LockEngine.literalScope(_LockEngine.normalizeFile(repoRoot, rawPattern.slice(5)));
1620
1636
  if (rawPattern.startsWith("symbol:")) {
1621
- const hash = rawPattern.indexOf("#");
1622
- const name = rawPattern.slice(hash + 1);
1623
- if (hash < 0 || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) throw new Error("Symbol scope must include #symbolName");
1624
- const file = _LockEngine.normalizeFile(repoRoot, rawPattern.slice(7, hash));
1637
+ const hash2 = rawPattern.indexOf("#");
1638
+ const name = rawPattern.slice(hash2 + 1);
1639
+ if (hash2 < 0 || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) throw new Error("Symbol scope must include #symbolName");
1640
+ const file = _LockEngine.normalizeFile(repoRoot, rawPattern.slice(7, hash2));
1625
1641
  return `symbol:${file}#${name}`;
1626
1642
  }
1627
1643
  if (!rawPattern || /[\\\x00-\x1f\x7f]/u.test(rawPattern)) throw new Error(`Invalid repository scope: ${rawPattern}`);
@@ -1876,9 +1892,9 @@ var LockEngine = class _LockEngine {
1876
1892
  return !this.pathCase.get(root);
1877
1893
  });
1878
1894
  const fold = (scope) => {
1879
- const hash = scope.startsWith("symbol:") ? scope.lastIndexOf("#") : -1;
1880
- const file = hash < 0 ? scope : scope.slice(0, hash);
1881
- return file.normalize("NFC").toLowerCase().replace(/ς/g, "\u03C3") + (hash < 0 ? "" : scope.slice(hash));
1895
+ const hash2 = scope.startsWith("symbol:") ? scope.lastIndexOf("#") : -1;
1896
+ const file = hash2 < 0 ? scope : scope.slice(0, hash2);
1897
+ return file.normalize("NFC").toLowerCase().replace(/ς/g, "\u03C3") + (hash2 < 0 ? "" : scope.slice(hash2));
1882
1898
  };
1883
1899
  const overlap = (a, b) => _LockEngine.patternsOverlap(a, b) || insensitive && _LockEngine.patternsOverlap(fold(a), fold(b));
1884
1900
  return overlap(requested, existing) || overlap(requested, normalized) || this.wildcardHasAliases(requested) || this.wildcardHasAliases(normalized);
@@ -2477,7 +2493,7 @@ var ResourceBroker = class _ResourceBroker {
2477
2493
  worker.once("error", reject);
2478
2494
  }), stopped]);
2479
2495
  if (params.signal?.aborted) failure ??= abortError();
2480
- if (failure) throw failure;
2496
+ if (failure) throw Object.assign(failure, { stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString() });
2481
2497
  if (!outcome) throw new Error("Command worker exited without a result");
2482
2498
  if (outcome.error) throw new Error(outcome.error);
2483
2499
  return { stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString(), exitCode: outcome.exitCode, waitedMs };
@@ -4042,25 +4058,188 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4042
4058
  import { z } from "zod";
4043
4059
  import fs16 from "fs";
4044
4060
  import path15 from "path";
4061
+
4062
+ // src/mcp/retries.ts
4063
+ import crypto11 from "crypto";
4064
+ var RetryError = class extends Error {
4065
+ constructor(code, message, retryable = false) {
4066
+ super(message);
4067
+ this.code = code;
4068
+ this.retryable = retryable;
4069
+ }
4070
+ code;
4071
+ retryable;
4072
+ };
4073
+ function failureData(error) {
4074
+ const message = error instanceof Error ? error.message : String(error);
4075
+ let code = "OPERATION_FAILED", retryable = false;
4076
+ if (error instanceof RetryError) {
4077
+ code = error.code;
4078
+ retryable = error.retryable;
4079
+ } else if (error instanceof SessionOwnershipError) code = "SESSION_OWNERSHIP";
4080
+ else if (error instanceof StaleFencingTokenError) code = "STALE_FENCING_TOKEN";
4081
+ else if (error instanceof Error && error.name === "AbortError") code = "CANCELLED";
4082
+ else if (message.startsWith("Timed out waiting for mutex ")) {
4083
+ code = "MUTEX_WAIT_TIMEOUT";
4084
+ retryable = true;
4085
+ } else if (message === "Mutex command execution timed out") code = "COMMAND_TIMEOUT";
4086
+ else if (message === "Mutex command output limit exceeded") code = "COMMAND_OUTPUT_LIMIT";
4087
+ else if (/SQLITE_BUSY|database is locked/.test(message)) code = "DATABASE_BUSY";
4088
+ else if (/schema|coordination database|fencing counter|recovery state/i.test(message)) code = "STATE_UNAVAILABLE";
4089
+ else if (/repository|checkout path|project selected|connection is pinned|Command cwd|Invalid agent ID|Lease duration|Unsupported.*pattern/i.test(message)) code = "INVALID_REQUEST";
4090
+ const details = error;
4091
+ return {
4092
+ ok: false,
4093
+ error: message,
4094
+ failure: {
4095
+ code,
4096
+ message,
4097
+ retryable,
4098
+ ...retryable ? { retryAfterMs: 100 } : {}
4099
+ },
4100
+ ...typeof details?.stdout === "string" ? { stdout: details.stdout } : {},
4101
+ ...typeof details?.stderr === "string" ? { stderr: details.stderr } : {}
4102
+ };
4103
+ }
4104
+ function classifyResult(data) {
4105
+ if (data.ok !== false || data.failure) return data;
4106
+ if (data.mergeResult) return { ...data, failure: {
4107
+ code: "INTEGRATION_FAILED",
4108
+ retryable: false,
4109
+ message: "Inspect mergeResult.diagnostics and repair or preserve the source worktree before a new completion attempt."
4110
+ } };
4111
+ if (Array.isArray(data.lockConflicts) || Array.isArray(data.conflicts)) return { ...data, failure: {
4112
+ code: data.waitTimedOut ? "CLAIM_WAIT_TIMEOUT" : "CLAIM_CONFLICT",
4113
+ retryable: true,
4114
+ retryAfterMs: 100,
4115
+ message: "Scope is owned by another session. Back off before a new attempt (with a new operationKey if used)."
4116
+ } };
4117
+ if (typeof data.exitCode === "number") return { ...data, failure: {
4118
+ code: "COMMAND_FAILED",
4119
+ retryable: false,
4120
+ message: "Inspect stdout, stderr and exitCode; commands may have partial effects. Repair before a new attempt."
4121
+ } };
4122
+ return { ...data, failure: {
4123
+ code: "OPERATION_FAILED",
4124
+ retryable: false,
4125
+ message: typeof data.error === "string" ? data.error : "Inspect the result before a new attempt."
4126
+ } };
4127
+ }
4128
+ function toolReply(data, footer = []) {
4129
+ data = classifyResult(data);
4130
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }, ...footer], structuredContent: data, isError: data.ok === false };
4131
+ }
4132
+ var hash = (text) => crypto11.createHash("sha256").update(text).digest("hex");
4133
+ function canonical(value) {
4134
+ if (Array.isArray(value)) return "[" + value.map(canonical).join(",") + "]";
4135
+ if (value && typeof value === "object") return "{" + Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => JSON.stringify(k) + ":" + canonical(v)).join(",") + "}";
4136
+ return JSON.stringify(value);
4137
+ }
4138
+ var OperationJournal = class _OperationJournal {
4139
+ constructor(db) {
4140
+ this.db = db;
4141
+ }
4142
+ db;
4143
+ static undurable = /* @__PURE__ */ new Set();
4144
+ async execute(params) {
4145
+ const { operationKey: _key, sessionToken: _token, ...input } = params.input;
4146
+ const localKey = this.db.dbPath + ":" + params.key;
4147
+ const requestHash = hash(canonical({ tool: params.tool, checkout: this.db.repoRoot, input }));
4148
+ const operation = (state, replayed) => ({ key: params.key, state, replayed });
4149
+ const previous = this.db.withTransaction(() => {
4150
+ const row = this.db.db.prepare("SELECT * FROM mcp_operations WHERE operation_key=?").get(params.key);
4151
+ if (row) {
4152
+ const token = params.token();
4153
+ if (!token || row.agent_id !== params.agentId || hash(token) !== row.capability_hash) throw new SessionOwnershipError(params.agentId ?? "");
4154
+ if (row.request_hash !== requestHash) throw new RetryError("OPERATION_KEY_REUSED", "operationKey already identifies a different tool, checkout or input. Use a new key for new work.");
4155
+ return row;
4156
+ }
4157
+ const identity = params.authorize();
4158
+ const start = FencingAndAgentManager.getProcessStartTime(process.pid);
4159
+ if (!start) throw new RetryError("STATE_UNAVAILABLE", "Cannot identify operation owner; no operation was started.");
4160
+ this.db.db.prepare(`INSERT INTO mcp_operations
4161
+ (operation_key,request_hash,agent_id,session_epoch,capability_hash,owner_pid,owner_start,result_json,created_at)
4162
+ VALUES (?,?,?,?,?,?,?,NULL,?)`).run(
4163
+ params.key,
4164
+ requestHash,
4165
+ identity.agentId,
4166
+ identity.sessionEpoch,
4167
+ hash(identity.token),
4168
+ process.pid,
4169
+ start,
4170
+ Date.now()
4171
+ );
4172
+ return void 0;
4173
+ });
4174
+ if (previous) {
4175
+ if (previous.result_json !== null) {
4176
+ let saved2;
4177
+ try {
4178
+ saved2 = JSON.parse(previous.result_json);
4179
+ if (!saved2.data || typeof saved2.data !== "object" || Array.isArray(saved2.data) || typeof saved2.grantsToken !== "boolean") throw new Error();
4180
+ } catch {
4181
+ throw new RetryError("STATE_UNAVAILABLE", "Unreadable operation result. Preserve coordination state before repair.");
4182
+ }
4183
+ return toolReply({ ...saved2.data, ...saved2.grantsToken ? { sessionToken: params.token() } : {}, operation: operation("completed", true) });
4184
+ }
4185
+ const alive = !_OperationJournal.undurable.has(localKey) && FencingAndAgentManager.isProcessAlive(previous.owner_pid, previous.owner_start);
4186
+ return toolReply({
4187
+ ...failureData(new RetryError(
4188
+ alive ? "OPERATION_IN_PROGRESS" : "OPERATION_OUTCOME_UNKNOWN",
4189
+ alive ? "Operation has not returned a durable result. Retry this same key after backoff; do not start a duplicate." : "Operation result was not recorded after its owner stopped or persistence failed. Inspect status, worktrees and external effects before deliberately starting new work with a new key.",
4190
+ alive
4191
+ )),
4192
+ operation: operation(alive ? "running" : "unknown", true)
4193
+ });
4194
+ }
4195
+ let reply;
4196
+ try {
4197
+ reply = await params.run();
4198
+ } catch (error) {
4199
+ reply = toolReply(failureData(error));
4200
+ }
4201
+ const data = reply.structuredContent;
4202
+ const { sessionToken, ...saved } = data;
4203
+ try {
4204
+ const result = this.db.db.prepare("UPDATE mcp_operations SET result_json=? WHERE operation_key=? AND result_json IS NULL").run(JSON.stringify({ data: saved, grantsToken: typeof sessionToken === "string" }), params.key);
4205
+ if (Number(result.changes) !== 1) throw new Error("Operation result was not recorded");
4206
+ } catch {
4207
+ _OperationJournal.undurable.add(localKey);
4208
+ return toolReply({
4209
+ ...failureData(new RetryError(
4210
+ "OPERATION_OUTCOME_UNKNOWN",
4211
+ "The operation may have completed but its result could not be recorded. Preserve state and inspect effects; do not repeat it automatically."
4212
+ )),
4213
+ operation: operation("unknown", false)
4214
+ });
4215
+ }
4216
+ return toolReply({ ...data, operation: operation("completed", false) }, reply.content.slice(1));
4217
+ }
4218
+ };
4219
+
4220
+ // src/mcp/server.ts
4045
4221
  function buildKithMcpServer(repoRoot, pinned) {
4046
4222
  const pinnedRoot = pinned ? discoverRepository(repoRoot).checkoutRoot : void 0;
4047
4223
  const server = new McpServer({
4048
4224
  name: "kith-coordinator",
4049
4225
  version: KITH_VERSION
4050
- }, { 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.") });
4226
+ }, { 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. For mutations, generate a unique operationKey and reuse it only to recover that exact attempt after a timeout. Replay is a historical result, never a renewed claim. Inspect failure.code and failure.retryable; an unknown command outcome requires inspection, not automatic re-execution. " + (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.") });
4051
4227
  const disconnected = new AbortController();
4052
4228
  const commands = /* @__PURE__ */ new Set();
4053
4229
  const createContext = (root, inheritCapability) => {
4054
4230
  const engine2 = new KithEngine(root);
4055
4231
  const owned = /* @__PURE__ */ new Set();
4056
- const tokenFor = (args) => args.sessionToken ?? (inheritCapability && process.env.KITH_AGENT_ID === args.agentId ? process.env.KITH_SESSION_TOKEN : void 0);
4232
+ const capabilities = /* @__PURE__ */ new Map();
4233
+ const journal = new OperationJournal(engine2.coordDb);
4234
+ const tokenFor = (args) => args.sessionToken ?? (inheritCapability && process.env.KITH_AGENT_ID === args.agentId ? process.env.KITH_SESSION_TOKEN : capabilities.get(args.agentId));
4057
4235
  const register = (args) => {
4058
4236
  const result = engine2.fencing.registerAgent({
4059
4237
  id: args.agentId,
4060
4238
  client: args.client ?? "mcp",
4061
- sessionToken: tokenFor(args),
4239
+ sessionToken: args.sessionToken ?? (inheritCapability && process.env.KITH_AGENT_ID === args.agentId ? process.env.KITH_SESSION_TOKEN : void 0),
4062
4240
  leaseDurationMs: args.leaseDurationMs
4063
4241
  });
4242
+ capabilities.set(args.agentId, result.sessionToken);
4064
4243
  if (result.agent.pid === process.pid) owned.add(args.agentId);
4065
4244
  return result;
4066
4245
  };
@@ -4072,10 +4251,7 @@ function buildKithMcpServer(repoRoot, pinned) {
4072
4251
  const formatReply = (agentId, payload) => {
4073
4252
  const delta = agentId ? engine2.consumeAgentDelta(agentId).formattedFooter : "";
4074
4253
  const data = payload;
4075
- return { content: [
4076
- { type: "text", text: JSON.stringify(data, null, 2) },
4077
- ...delta ? [{ type: "text", text: delta }] : []
4078
- ], structuredContent: data, isError: data.ok === false };
4254
+ return toolReply(data, delta ? [{ type: "text", text: delta }] : []);
4079
4255
  };
4080
4256
  const close = () => {
4081
4257
  for (const id of owned) {
@@ -4086,7 +4262,7 @@ function buildKithMcpServer(repoRoot, pinned) {
4086
4262
  }
4087
4263
  engine2.close();
4088
4264
  };
4089
- return { engine: engine2, register, authorize, formatReply, close };
4265
+ return { engine: engine2, register, authorize, formatReply, close, journal, tokenFor };
4090
4266
  };
4091
4267
  const contexts = /* @__PURE__ */ new Map();
4092
4268
  const contextFor = (repository) => {
@@ -4105,14 +4281,48 @@ function buildKithMcpServer(repoRoot, pinned) {
4105
4281
  };
4106
4282
  const engine = pinned ? contextFor().engine : void 0;
4107
4283
  const tool = (name, description, schema, handler) => {
4108
- 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.") };
4284
+ const keyed = !["kith_get_status", "kith_preview_integrate", "kith_heartbeat"].includes(name);
4285
+ const inputSchema = { ...schema, ...keyed ? { operationKey: z.string().min(1).max(128).optional().describe("Unique client-generated key for this exact mutation. Reuse only to recover its result; never an ownership credential. Retried failures are snapshots; use a new key after repair or backoff.") } : {}, 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.") };
4109
4286
  server.tool(
4110
4287
  name,
4111
4288
  description,
4112
4289
  inputSchema,
4113
4290
  async (args, extra) => {
4114
4291
  const { repository, ...input } = args;
4115
- return handler(input, extra, contextFor(repository));
4292
+ const execution = (async () => {
4293
+ try {
4294
+ const context = contextFor(repository);
4295
+ const run = async () => {
4296
+ extra.signal.throwIfAborted();
4297
+ return handler(input, extra, context);
4298
+ };
4299
+ if (!keyed || input.operationKey === void 0) return await run();
4300
+ const identity = input;
4301
+ return await context.journal.execute({
4302
+ key: input.operationKey,
4303
+ tool: name,
4304
+ input,
4305
+ agentId: identity.agentId,
4306
+ token: () => identity.agentId ? context.tokenFor({ ...identity, agentId: identity.agentId }) : void 0,
4307
+ authorize: () => {
4308
+ if (!identity.agentId) throw new RetryError("INVALID_REQUEST", "Keyed mutations require agentId and an owned session. Start a session first.");
4309
+ const args2 = { ...identity, agentId: identity.agentId };
4310
+ if (["kith_start_session", "kith_begin_work", "kith_claim_next_task"].includes(name)) context.register(args2);
4311
+ else context.authorize(args2);
4312
+ return { agentId: identity.agentId, sessionEpoch: context.engine.fencing.assertSession(identity.agentId), token: context.tokenFor(args2) };
4313
+ },
4314
+ run
4315
+ });
4316
+ } catch (error) {
4317
+ return toolReply(failureData(error));
4318
+ }
4319
+ })();
4320
+ commands.add(execution);
4321
+ try {
4322
+ return await execution;
4323
+ } finally {
4324
+ commands.delete(execution);
4325
+ }
4116
4326
  }
4117
4327
  );
4118
4328
  };
@@ -4209,6 +4419,8 @@ function buildKithMcpServer(repoRoot, pinned) {
4209
4419
  "Add a task to the orchestrated DAG queue with optional task dependencies and file scope globs.",
4210
4420
  {
4211
4421
  id: z.string().optional(),
4422
+ agentId: z.string().optional().describe("Required for operationKey; identifies an already owned session."),
4423
+ sessionToken: z.string().optional(),
4212
4424
  title: z.string(),
4213
4425
  intentSummary: z.string().optional(),
4214
4426
  priority: z.number().optional(),
@@ -4216,6 +4428,7 @@ function buildKithMcpServer(repoRoot, pinned) {
4216
4428
  scopeGlobs: z.array(z.string()).optional()
4217
4429
  },
4218
4430
  async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
4431
+ if (args.agentId) authorize({ ...args, agentId: args.agentId });
4219
4432
  const task = engine2.intents.addTask(args);
4220
4433
  return formatReply(void 0, { ok: true, task });
4221
4434
  }
@@ -4896,17 +5109,17 @@ function contentIdentity(repo, file) {
4896
5109
  try {
4897
5110
  const before = fs19.fstatSync(fd);
4898
5111
  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}`);
4899
- const hash = createHash("sha256");
5112
+ const hash2 = createHash("sha256");
4900
5113
  const buffer = Buffer.allocUnsafe(64 * 1024);
4901
5114
  for (let offset = 0; offset < before.size; ) {
4902
5115
  const count = fs19.readSync(fd, buffer, 0, Math.min(buffer.length, before.size - offset), offset);
4903
5116
  if (!count) throw new Error(`File changed during comparison: ${file}`);
4904
- hash.update(buffer.subarray(0, count));
5117
+ hash2.update(buffer.subarray(0, count));
4905
5118
  offset += count;
4906
5119
  }
4907
5120
  const after = fs19.fstatSync(fd);
4908
5121
  if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) throw new Error(`File changed during comparison: ${file}`);
4909
- return `file:${before.mode & 73}:${hash.digest("hex")}`;
5122
+ return `file:${before.mode & 73}:${hash2.digest("hex")}`;
4910
5123
  } finally {
4911
5124
  fs19.closeSync(fd);
4912
5125
  }
@@ -4925,6 +5138,8 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4925
5138
  activeCollisions = /* @__PURE__ */ new Map();
4926
5139
  repositories = /* @__PURE__ */ new Map();
4927
5140
  errors = [];
5141
+ primaryRoots = /* @__PURE__ */ new Map();
5142
+ worktrees = [];
4928
5143
  static notifyMac(title, message) {
4929
5144
  if (process.platform !== "darwin") return;
4930
5145
  try {
@@ -4935,6 +5150,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4935
5150
  discoverGitRepos() {
4936
5151
  this.errors = [];
4937
5152
  this.repositories.clear();
5153
+ this.primaryRoots.clear();
4938
5154
  const candidates = [];
4939
5155
  const marker = (dir) => {
4940
5156
  try {
@@ -4963,6 +5179,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4963
5179
  const layout = discoverRepository(candidate);
4964
5180
  if (!layout.commonGitDir) throw new Error("Git repository unavailable");
4965
5181
  this.repositories.set(layout.checkoutRoot, layout.commonGitDir);
5182
+ if (!layout.isLinkedWorktree) this.primaryRoots.set(layout.commonGitDir, layout.checkoutRoot);
4966
5183
  if (inspected.has(layout.commonGitDir)) continue;
4967
5184
  inspected.add(layout.commonGitDir);
4968
5185
  const listing = execFileSync10("git", ["worktree", "list", "--porcelain", "-z"], { cwd: layout.checkoutRoot, encoding: "utf8", timeout: 4e3, maxBuffer: 4 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
@@ -4970,6 +5187,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4970
5187
  const linked = discoverRepository(record.slice(9));
4971
5188
  if (linked.commonGitDir !== layout.commonGitDir) throw new Error("Worktree repository identity changed during discovery");
4972
5189
  this.repositories.set(linked.checkoutRoot, layout.commonGitDir);
5190
+ if (!linked.isLinkedWorktree) this.primaryRoots.set(layout.commonGitDir, linked.checkoutRoot);
4973
5191
  }
4974
5192
  } catch (error) {
4975
5193
  this.errors.push({ repository: candidate, message: `Cannot inspect repository: ${error instanceof Error ? error.message : String(error)}` });
@@ -4982,10 +5200,11 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4982
5200
  const repos = this.discoverGitRepos();
4983
5201
  const now = Date.now();
4984
5202
  const seen = /* @__PURE__ */ new Set();
5203
+ const dirtyCounts = /* @__PURE__ */ new Map();
4985
5204
  const overlaps = /* @__PURE__ */ new Map();
4986
5205
  for (const repo of repos) {
4987
5206
  try {
4988
- const { stdout } = await execFileAsync("git", ["-c", "core.fsmonitor=false", "status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: repo, timeout: 4e3, maxBuffer: 4 * 1024 * 1024 });
5207
+ const { stdout } = await execFileAsync("git", ["-c", "core.fsmonitor=false", "status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: repo, timeout: 4e3, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
4989
5208
  const records = stdout.split("\0");
4990
5209
  const dirty = /* @__PURE__ */ new Map();
4991
5210
  for (let i = 0; i < records.length; i++) {
@@ -4995,6 +5214,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
4995
5214
  dirty.set(record.slice(3), status);
4996
5215
  if (/[RC]/.test(status)) dirty.set(records[++i], status);
4997
5216
  }
5217
+ dirtyCounts.set(repo, [...dirty.keys()].filter((file) => file && file !== ".coord" && !file.startsWith(".coord/")).length);
4998
5218
  for (const [file, status] of dirty) {
4999
5219
  if (!file || file === ".coord" || file.startsWith(".coord/")) continue;
5000
5220
  const key = JSON.stringify([repo, file]);
@@ -5051,6 +5271,39 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
5051
5271
  }
5052
5272
  }
5053
5273
  this.activeCollisions = this.errors.length ? new Map([...this.activeCollisions, ...current]) : current;
5274
+ const states = /* @__PURE__ */ new Map();
5275
+ this.worktrees = repos.map((repo) => {
5276
+ const common = this.repositories.get(repo);
5277
+ const primary = this.primaryRoots.get(common);
5278
+ const row = {
5279
+ path: repo,
5280
+ label: `${path17.basename(primary ?? path17.dirname(common))} / ${repo === primary ? "primary" : path17.basename(repo)}`,
5281
+ activity: "unknown",
5282
+ agentIds: [],
5283
+ detail: "Ownership unavailable; inspect kith status --offline before recovery.",
5284
+ dirtyFiles: dirtyCounts.get(repo) ?? null,
5285
+ sharedDirtyPaths: [...this.activeCollisions.values()].filter((alert) => alert.repos.includes(repo)).length
5286
+ };
5287
+ try {
5288
+ let state = states.get(common);
5289
+ if (!state) {
5290
+ state = readCoordinationState(primary ?? repo, { ownersOnly: true });
5291
+ states.set(common, state);
5292
+ }
5293
+ if (state.state === "unavailable") {
5294
+ row.detail += ` ${state.error}`;
5295
+ return row;
5296
+ }
5297
+ const owners = state.agents.filter((agent) => agent.worktree_path ? physicalPath(agent.worktree_path) === repo : repo === primary);
5298
+ const active = owners.filter((agent) => agent.status === "active" && Number.isFinite(agent.lease_expires_at) && agent.lease_expires_at > state.observedAt);
5299
+ row.agentIds = (active.length ? active : owners).map((agent) => agent.id);
5300
+ row.activity = active.length ? "active" : owners.length ? "retained" : "unregistered";
5301
+ row.detail = active.length ? "Current recorded Kith lease; does not identify file writers." : owners.length ? `Recorded ${[...new Set(owners.map((agent) => agent.status === "active" ? "expired lease" : agent.status))].join(", ")}; preserve work and inspect before cleanup.` : "No recorded Kith session; activity and ownership outside Kith are unknown.";
5302
+ } catch (error) {
5303
+ row.detail += ` ${error instanceof Error ? error.message : String(error)}`;
5304
+ }
5305
+ return row;
5306
+ });
5054
5307
  if (this.errors.length) this.options.onScanError?.(this.errors);
5055
5308
  return this.getActiveCollisions();
5056
5309
  }
@@ -5074,7 +5327,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
5074
5327
  this.watchTimer = null;
5075
5328
  }
5076
5329
  getScanState() {
5077
- return { complete: this.errors.length === 0, errors: [...this.errors], repos: [...this.repositories.keys()] };
5330
+ return { complete: this.errors.length === 0, errors: [...this.errors], repos: [...this.repositories.keys()], ownershipComplete: this.worktrees.every((row) => row.activity !== "unknown"), worktrees: this.worktrees.map((row) => ({ ...row, agentIds: [...row.agentIds] })) };
5078
5331
  }
5079
5332
  getActiveCollisions() {
5080
5333
  return [...this.activeCollisions.values()];
@@ -5375,13 +5628,13 @@ ${message}`);
5375
5628
  lastScanError = message;
5376
5629
  },
5377
5630
  onCollision: (alert) => {
5378
- if (!isRaw) {
5631
+ if (!isRaw && !flags.once) {
5379
5632
  console.log(`
5380
5633
  \u26A0\uFE0F [WORKTREE OVERLAP] ${alert.message}`);
5381
5634
  }
5382
5635
  },
5383
5636
  onFileChange: (e) => {
5384
- if (!flags.quiet && !isRaw) {
5637
+ if (!flags.quiet && !isRaw && !flags.once) {
5385
5638
  console.log(`\u{1F4DD} [FILE MTIME] ${path19.basename(e.repoPath)}/${e.filePath}`);
5386
5639
  }
5387
5640
  }
@@ -5400,6 +5653,12 @@ ${message}`);
5400
5653
  \u{1F4E1} Kith Passive Radar: ${targetDir}`);
5401
5654
  console.log(` Repositories (${repos.length}): ${repos.map((r) => path19.basename(r)).join(", ")}`);
5402
5655
  console.log(` Recent Modified Files: ${edits.length}`);
5656
+ for (const row of state.worktrees) {
5657
+ const activity = { active: "Active Kith lease", retained: "Retained", unregistered: "No Kith session", unknown: "Ownership unavailable" }[row.activity];
5658
+ console.log(` ${row.label} \u2014 ${activity}; ${row.dirtyFiles ?? "unknown"} Git changes; ${row.sharedDirtyPaths} shared dirty paths`);
5659
+ console.log(` ${row.path}${row.agentIds.length ? ` (${row.agentIds.join(", ")})` : ""}
5660
+ ${row.detail}`);
5661
+ }
5403
5662
  if (!state.complete) {
5404
5663
  console.error(`Scan unavailable:
5405
5664
  ${state.errors.map((error) => error.message).join("\n")}`);
@@ -5407,10 +5666,9 @@ ${state.errors.map((error) => error.message).join("\n")}`);
5407
5666
  console.log("\nNo differing or unverified shared dirty paths observed. Identical copies are excluded; compatibility is not verified.\n");
5408
5667
  } else {
5409
5668
  console.log(`
5410
- \u26A0\uFE0F Observed worktree overlaps (${alerts.length}):`);
5669
+ \u26A0\uFE0F Review before integration: ${alerts.length} shared dirty paths:`);
5411
5670
  for (const a of alerts) {
5412
5671
  console.log(` \u26A0\uFE0F ${a.message}`);
5413
- console.log(` Files: ${a.filePath}`);
5414
5672
  }
5415
5673
  console.log("");
5416
5674
  }