kith-coord 0.1.0 → 0.1.2

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,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/version.ts
4
- var KITH_VERSION = "0.1.0";
5
- var KITH_PROTOCOL_VERSION = "1.0.0";
4
+ var KITH_VERSION = "0.1.2";
5
+ var KITH_PROTOCOL_VERSION = "1.1.0";
6
6
 
7
7
  // src/cli/setup-ledger.ts
8
8
  import fs2 from "fs";
@@ -14,6 +14,7 @@ import { execFileSync } from "child_process";
14
14
  import fs from "fs";
15
15
  import path from "path";
16
16
  function physicalPath(input) {
17
+ if (process.platform === "win32") input = input.replaceAll("/", path.sep);
17
18
  const absolute = path.isAbsolute(input) ? input : process.cwd() + path.sep + input;
18
19
  let current = path.parse(absolute).root;
19
20
  for (const component of absolute.slice(current.length).split(path.sep)) {
@@ -44,7 +45,7 @@ function isWithin(root, target) {
44
45
  return relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
45
46
  }
46
47
  function repositoryFile(root, file) {
47
- if (!file || /[\\\x00-\x1f\x7f]/u.test(file)) throw new Error(`Invalid file path: ${file}`);
48
+ if (!file || /[\x00-\x1f\x7f]/u.test(file) || process.platform !== "win32" && file.includes("\\")) throw new Error(`Invalid file path: ${file}`);
48
49
  const canonicalRoot = physicalPath(root);
49
50
  const target = physicalPath(path.isAbsolute(file) ? file : canonicalRoot + path.sep + file);
50
51
  if (!isWithin(canonicalRoot, target) || target === canonicalRoot) {
@@ -140,7 +141,7 @@ function persist(root, coordDir, ledger) {
140
141
  fs2.closeSync(fd);
141
142
  }
142
143
  fs2.renameSync(temp, file);
143
- for (const parent of [path2.dirname(file), coordDir]) {
144
+ for (const parent of process.platform === "win32" ? [] : [path2.dirname(file), coordDir]) {
144
145
  const dir = fs2.openSync(parent, "r");
145
146
  try {
146
147
  fs2.fsyncSync(dir);
@@ -174,7 +175,7 @@ function applySetupPlan(root, coordDir, plan, warnings) {
174
175
  before: previous ? previous.before : current,
175
176
  after: Buffer.from(planned.content).toString("base64"),
176
177
  beforeMode: previous ? previous.beforeMode : mode,
177
- afterMode: mode ?? (planned.mode ?? 438) & ~process.umask()
178
+ afterMode: mode ?? (process.platform === "win32" ? 438 : (planned.mode ?? 438) & ~process.umask())
178
179
  };
179
180
  }
180
181
  persist(root, coordDir, ledger);
@@ -892,7 +893,7 @@ function doctorReport(directory = process.cwd()) {
892
893
  const check = (name, ok, detail) => checks.push({ name, ok, detail });
893
894
  const [major, minor] = process.versions.node.split(".").map(Number);
894
895
  check("node", major === 22 && minor >= 13 || major >= 24, process.version);
895
- check("platform", process.platform === "darwin" || process.platform === "linux", process.platform === "win32" ? "Windows ownership and process-group supervision are not validated" : process.platform);
896
+ check("platform", process.platform === "darwin" || process.platform === "linux", process.platform === "win32" ? "Native Windows is limited to MCP coordination; supervised mutex commands and shell hooks require macOS/Linux or WSL" : process.platform);
896
897
  try {
897
898
  const version = execFileSync3("git", ["--version"], { encoding: "utf8", timeout: 2e3 }).trim();
898
899
  const parts = /\b(\d+)\.(\d+)/.exec(version);
@@ -960,6 +961,7 @@ function doctorReport(directory = process.cwd()) {
960
961
  import { execFileSync as execFileSync4 } from "child_process";
961
962
  import crypto2 from "crypto";
962
963
  import fs6 from "fs";
964
+ import path6 from "path";
963
965
  var SessionOwnershipError = class extends Error {
964
966
  constructor(agentId) {
965
967
  super(`No valid active session capability for '${agentId}'. Start a session, or resume it with its session token.`);
@@ -985,6 +987,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
985
987
  coordDb;
986
988
  static DEFAULT_LEASE_MS = 5 * 60 * 1e3;
987
989
  capabilities = /* @__PURE__ */ new Map();
990
+ static ownWindowsStart = null;
988
991
  static validateLease(duration) {
989
992
  if (!Number.isSafeInteger(duration) || duration < 100 || duration > 864e5) {
990
993
  throw new Error("Lease duration must be an integer between 100 and 86400000 milliseconds");
@@ -999,6 +1002,24 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
999
1002
  const stat = fs6.readFileSync(`/proc/${pid}/stat`, "utf8");
1000
1003
  return `linux:${stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]}`;
1001
1004
  }
1005
+ if (process.platform === "win32") {
1006
+ if (pid === process.pid && this.ownWindowsStart) return this.ownWindowsStart;
1007
+ const powershell = path6.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
1008
+ const ticks = execFileSync4(
1009
+ powershell,
1010
+ [
1011
+ "-NoProfile",
1012
+ "-NonInteractive",
1013
+ "-Command",
1014
+ `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks.ToString([System.Globalization.CultureInfo]::InvariantCulture)`
1015
+ ],
1016
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3, windowsHide: true }
1017
+ ).trim();
1018
+ if (!/^\d+$/.test(ticks)) return null;
1019
+ const identity = `windows:${ticks}`;
1020
+ if (pid === process.pid) this.ownWindowsStart = identity;
1021
+ return identity;
1022
+ }
1002
1023
  const out = execFileSync4("ps", ["-o", "lstart=", "-p", String(pid)], {
1003
1024
  encoding: "utf8",
1004
1025
  stdio: ["ignore", "pipe", "ignore"],
@@ -1167,12 +1188,12 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
1167
1188
 
1168
1189
  // src/core/engine.ts
1169
1190
  import fs15 from "fs";
1170
- import path13 from "path";
1191
+ import path14 from "path";
1171
1192
 
1172
1193
  // src/core/ast-analyzer.ts
1173
1194
  import crypto3 from "crypto";
1174
1195
  import fs7 from "fs";
1175
- import path6 from "path";
1196
+ import path7 from "path";
1176
1197
  var AstAnalyzer = class _AstAnalyzer {
1177
1198
  /**
1178
1199
  * Analyzes a source file's content to extract exported contracts (functions, types,
@@ -1180,7 +1201,7 @@ var AstAnalyzer = class _AstAnalyzer {
1180
1201
  */
1181
1202
  static analyzeSource(repoRoot, filePath, sourceContent) {
1182
1203
  const relPath = repositoryFile(repoRoot, filePath);
1183
- const absPath = path6.join(repoRoot, relPath);
1204
+ const absPath = path7.join(repoRoot, relPath);
1184
1205
  const content = sourceContent ?? (fs7.existsSync(absPath) ? fs7.readFileSync(absPath, "utf8") : "");
1185
1206
  const fileHash = crypto3.createHash("sha256").update(content).digest("hex").slice(0, 16);
1186
1207
  const exports = _AstAnalyzer.extractExports(content);
@@ -1373,17 +1394,17 @@ var AstAnalyzer = class _AstAnalyzer {
1373
1394
  if (!specifier.startsWith(".")) {
1374
1395
  return null;
1375
1396
  }
1376
- const sourceDir = path6.dirname(path6.join(repoRoot, sourceRelPath));
1377
- const baseTarget = path6.resolve(sourceDir, specifier.replace(/\.(js|jsx|mjs|cjs)$/, ""));
1397
+ const sourceDir = path7.dirname(path7.join(repoRoot, sourceRelPath));
1398
+ const baseTarget = path7.resolve(sourceDir, specifier.replace(/\.(js|jsx|mjs|cjs)$/, ""));
1378
1399
  const candidates = [
1379
- path6.resolve(sourceDir, specifier),
1400
+ path7.resolve(sourceDir, specifier),
1380
1401
  `${baseTarget}.ts`,
1381
1402
  `${baseTarget}.tsx`,
1382
1403
  `${baseTarget}.js`,
1383
1404
  `${baseTarget}.mjs`,
1384
- path6.join(baseTarget, "index.ts"),
1385
- path6.join(baseTarget, "index.tsx"),
1386
- path6.join(baseTarget, "index.js")
1405
+ path7.join(baseTarget, "index.ts"),
1406
+ path7.join(baseTarget, "index.tsx"),
1407
+ path7.join(baseTarget, "index.js")
1387
1408
  ];
1388
1409
  for (const candidate of candidates) {
1389
1410
  if (fs7.existsSync(candidate) && fs7.statSync(candidate).isFile()) {
@@ -1448,12 +1469,12 @@ var AstAnalyzer = class _AstAnalyzer {
1448
1469
  // src/core/bash-guard.ts
1449
1470
  import crypto5 from "crypto";
1450
1471
  import fs9 from "fs";
1451
- import path8 from "path";
1472
+ import path9 from "path";
1452
1473
 
1453
1474
  // src/core/locks.ts
1454
1475
  import crypto4 from "crypto";
1455
1476
  import fs8 from "fs";
1456
- import path7 from "path";
1477
+ import path8 from "path";
1457
1478
 
1458
1479
  // src/core/glob.ts
1459
1480
  function scopeSegments(pattern, literal = false) {
@@ -1543,9 +1564,9 @@ var LockEngine = class _LockEngine {
1543
1564
  const parts = rawPattern.split("/");
1544
1565
  const magic = parts.findIndex((p) => /[?*]/u.test(p));
1545
1566
  const prefix = magic < 0 ? rawPattern : parts.slice(0, magic).join("/") || ".";
1546
- const absolute = physicalPath(path7.isAbsolute(prefix) ? prefix : root + "/" + prefix);
1567
+ const absolute = physicalPath(path8.isAbsolute(prefix) ? prefix : root + "/" + prefix);
1547
1568
  if (!isWithin(root, absolute)) throw new Error(`Scope is outside repository: ${rawPattern}`);
1548
- const relative = path7.relative(root, absolute).split(path7.sep).join("/");
1569
+ const relative = path8.relative(root, absolute).split(path8.sep).join("/");
1549
1570
  const suffix = magic < 0 ? "" : parts.slice(magic).join("/");
1550
1571
  const normalized = [relative, suffix].filter(Boolean).join("/");
1551
1572
  scopeSegments(normalized);
@@ -1580,7 +1601,7 @@ var LockEngine = class _LockEngine {
1580
1601
  let budget = 1e4;
1581
1602
  while (pending.length) {
1582
1603
  const { root, relative } = pending.pop();
1583
- const dir = path7.join(root, relative);
1604
+ const dir = path8.join(root, relative);
1584
1605
  let entries;
1585
1606
  try {
1586
1607
  entries = fs8.readdirSync(dir, { withFileTypes: true });
@@ -1839,7 +1860,7 @@ var BashMerkleGuard = class {
1839
1860
  getSnapshotFilePath(agentId) {
1840
1861
  const epoch = this.fencing.assertSession(agentId);
1841
1862
  const safeId = crypto5.createHash("sha256").update(`${agentId}:${epoch}`).digest("hex");
1842
- return path8.join(this.coordDb.coordDir, "snapshots", `${safeId}.json`);
1863
+ return path9.join(this.coordDb.coordDir, "snapshots", `${safeId}.json`);
1843
1864
  }
1844
1865
  /**
1845
1866
  * Captures a Merkle snapshot of workspace files before a Bash tool execution.
@@ -1851,7 +1872,7 @@ var BashMerkleGuard = class {
1851
1872
  const entries = {};
1852
1873
  const files = this.walkWorkspaceFiles(rootDir);
1853
1874
  for (const absPath of files) {
1854
- const relPath = path8.relative(rootDir, absPath).replace(/\\/g, "/");
1875
+ const relPath = path9.relative(rootDir, absPath).replace(/\\/g, "/");
1855
1876
  const stat = fs9.statSync(absPath);
1856
1877
  if (stat.size > 5 * 1024 * 1024) continue;
1857
1878
  const buf = fs9.readFileSync(absPath);
@@ -1894,7 +1915,7 @@ var BashMerkleGuard = class {
1894
1915
  const authorizedModifiedFiles = [];
1895
1916
  const quarantinedViolations = [];
1896
1917
  for (const absPath of currentFiles) {
1897
- const relPath = path8.relative(rootDir, absPath).replace(/\\/g, "/");
1918
+ const relPath = path9.relative(rootDir, absPath).replace(/\\/g, "/");
1898
1919
  seenCurrentRelPaths.add(relPath);
1899
1920
  const stat = fs9.statSync(absPath);
1900
1921
  if (stat.size > 5 * 1024 * 1024) continue;
@@ -1956,7 +1977,7 @@ var BashMerkleGuard = class {
1956
1977
  }
1957
1978
  if (authorizedModifiedFiles.length > 0) {
1958
1979
  for (const relFile of authorizedModifiedFiles) {
1959
- const absFile = path8.join(rootDir, relFile);
1980
+ const absFile = path9.join(rootDir, relFile);
1960
1981
  if (fs9.existsSync(absFile) && /\.(ts|tsx|js|jsx|mjs|py)$/.test(relFile)) {
1961
1982
  this.depGraph.recordFileModification(params.agentId, relFile, fs9.readFileSync(absFile, "utf8"));
1962
1983
  }
@@ -1977,7 +1998,7 @@ var BashMerkleGuard = class {
1977
1998
  const id = `quar_${crypto5.randomBytes(6).toString("hex")}`;
1978
1999
  const now = Date.now();
1979
2000
  const safeName = params.relPath.replace(/[^a-zA-Z0-9._-]/g, "_");
1980
- const patchPath = path8.join(
2001
+ const patchPath = path9.join(
1981
2002
  this.coordDb.coordDir,
1982
2003
  "quarantine",
1983
2004
  `${id}_${safeName}.quarantine`
@@ -1991,13 +2012,13 @@ var BashMerkleGuard = class {
1991
2012
  if (params.prevEntry?.backupContentBase64 !== void 0) {
1992
2013
  fs9.writeFileSync(patchPath + ".before", Buffer.from(params.prevEntry.backupContentBase64, "base64"), { mode: 384 });
1993
2014
  }
1994
- const absTarget = path8.join(params.rootDir, params.relPath);
2015
+ const absTarget = path9.join(params.rootDir, params.relPath);
1995
2016
  if (params.restoreLockedFiles && params.prevEntry === null) {
1996
2017
  if (fs9.existsSync(absTarget)) {
1997
2018
  fs9.unlinkSync(absTarget);
1998
2019
  }
1999
2020
  } else if (params.restoreLockedFiles && params.prevEntry?.backupContentBase64 !== void 0) {
2000
- fs9.mkdirSync(path8.dirname(absTarget), { recursive: true });
2021
+ fs9.mkdirSync(path9.dirname(absTarget), { recursive: true });
2001
2022
  fs9.writeFileSync(
2002
2023
  absTarget,
2003
2024
  Buffer.from(params.prevEntry.backupContentBase64, "base64")
@@ -2041,7 +2062,7 @@ var BashMerkleGuard = class {
2041
2062
  if (entry.name === ".git" || entry.name === ".coord" || entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage" || entry.name === ".next" || entry.name === "target") {
2042
2063
  continue;
2043
2064
  }
2044
- const fullPath = path8.join(dir, entry.name);
2065
+ const fullPath = path9.join(dir, entry.name);
2045
2066
  if (entry.isDirectory()) {
2046
2067
  this.walkWorkspaceFiles(fullPath, acc);
2047
2068
  } else if (entry.isFile()) {
@@ -2478,7 +2499,7 @@ function delay(ms) {
2478
2499
  // src/core/dep-graph.ts
2479
2500
  import crypto7 from "crypto";
2480
2501
  import fs11 from "fs";
2481
- import path9 from "path";
2502
+ import path10 from "path";
2482
2503
  var DependencyAndContractEngine = class {
2483
2504
  constructor(coordDb, lockEngine, fencing) {
2484
2505
  this.coordDb = coordDb;
@@ -2501,13 +2522,13 @@ var DependencyAndContractEngine = class {
2501
2522
  return indexedCount;
2502
2523
  }
2503
2524
  walkSourceFiles(dir, acc = []) {
2504
- if (!fs11.existsSync(dir) || dir !== this.coordDb.repoRoot && fs11.existsSync(path9.join(dir, ".git"))) return acc;
2525
+ if (!fs11.existsSync(dir) || dir !== this.coordDb.repoRoot && fs11.existsSync(path10.join(dir, ".git"))) return acc;
2505
2526
  const entries = fs11.readdirSync(dir, { withFileTypes: true });
2506
2527
  for (const entry of entries) {
2507
2528
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "target" || entry.name === "coverage") {
2508
2529
  continue;
2509
2530
  }
2510
- const fullPath = path9.join(dir, entry.name);
2531
+ const fullPath = path10.join(dir, entry.name);
2511
2532
  if (entry.isDirectory()) {
2512
2533
  this.walkSourceFiles(fullPath, acc);
2513
2534
  } else if (entry.isFile() && /\.(ts|tsx|js|jsx|mjs|py)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
@@ -2568,6 +2589,7 @@ var DependencyAndContractEngine = class {
2568
2589
  }
2569
2590
  for (const exp of analysis.exports) {
2570
2591
  const prev = existingBySymbol.get(exp.symbolName);
2592
+ 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;
2571
2593
  const isBreaking = prev !== void 0 && prev.ast_hash !== exp.astHash;
2572
2594
  if (isBreaking && prev) {
2573
2595
  changedSymbols.push({
@@ -3039,7 +3061,7 @@ var IntentAndTaskEngine = class {
3039
3061
  import { execFileSync as execFileSync5, execSync } from "child_process";
3040
3062
  import crypto9 from "crypto";
3041
3063
  import fs12 from "fs";
3042
- import path10 from "path";
3064
+ import path11 from "path";
3043
3065
  var SpeculativeMergeTrain = class {
3044
3066
  constructor(coordDb, fencing) {
3045
3067
  this.coordDb = coordDb;
@@ -3078,8 +3100,8 @@ var SpeculativeMergeTrain = class {
3078
3100
  now
3079
3101
  );
3080
3102
  });
3081
- const stageDir = path10.join(this.coordDb.coordDir, "worktrees", `_merge_train_${id}`);
3082
- const lockPath = path10.join(this.coordDb.coordDir, "merge-train.lock");
3103
+ const stageDir = path11.join(this.coordDb.coordDir, "worktrees", `_merge_train_${id}`);
3104
+ const lockPath = path11.join(this.coordDb.coordDir, "merge-train.lock");
3083
3105
  let lockFd;
3084
3106
  try {
3085
3107
  try {
@@ -3130,8 +3152,8 @@ var SpeculativeMergeTrain = class {
3130
3152
  cwd: this.coordDb.repoRoot,
3131
3153
  stdio: "ignore"
3132
3154
  });
3133
- const rootNodeModules = path10.join(this.coordDb.repoRoot, "node_modules");
3134
- const stageNodeModules = path10.join(stageDir, "node_modules");
3155
+ const rootNodeModules = path11.join(this.coordDb.repoRoot, "node_modules");
3156
+ const stageNodeModules = path11.join(stageDir, "node_modules");
3135
3157
  const hydratedNodeModules = fs12.existsSync(rootNodeModules) && !fs12.existsSync(stageNodeModules);
3136
3158
  if (hydratedNodeModules) {
3137
3159
  fs12.symlinkSync(rootNodeModules, stageNodeModules, "dir");
@@ -3296,7 +3318,7 @@ ${combined}`
3296
3318
  import { execFileSync as execFileSync6 } from "child_process";
3297
3319
  import crypto10 from "crypto";
3298
3320
  import fs13 from "fs";
3299
- import path11 from "path";
3321
+ import path12 from "path";
3300
3322
  var ShadowGitEngine = class {
3301
3323
  constructor(coordDb, fencing) {
3302
3324
  this.coordDb = coordDb;
@@ -3329,7 +3351,7 @@ var ShadowGitEngine = class {
3329
3351
  getShadowIndexPath(agentId) {
3330
3352
  const safeId = agentId.replace(/[^a-zA-Z0-9_-]/g, "_");
3331
3353
  const epoch = this.fencing.assertSession(agentId);
3332
- return path11.join(this.coordDb.coordDir, "shadow-index", `${safeId}-${epoch}.index`);
3354
+ return path12.join(this.coordDb.coordDir, "shadow-index", `${safeId}-${epoch}.index`);
3333
3355
  }
3334
3356
  /**
3335
3357
  * Updates an agent's ephemeral shadow Git index (`.coord/shadow-index/<agent-id>.index`)
@@ -3366,7 +3388,7 @@ var ShadowGitEngine = class {
3366
3388
  } else if (params.modifiedFiles && params.modifiedFiles.length > 0) {
3367
3389
  for (const mod of params.modifiedFiles) {
3368
3390
  const relPath = LockEngine.normalizeFile(targetDir, mod.filePath);
3369
- const absPath = path11.join(targetDir, relPath);
3391
+ const absPath = path12.join(targetDir, relPath);
3370
3392
  const content = mod.content ?? (fs13.existsSync(absPath) ? fs13.readFileSync(absPath, "utf8") : null);
3371
3393
  if (content === null) {
3372
3394
  {
@@ -3567,7 +3589,7 @@ var ShadowGitEngine = class {
3567
3589
  // src/core/worktree-cow.ts
3568
3590
  import { execFileSync as execFileSync7 } from "child_process";
3569
3591
  import fs14 from "fs";
3570
- import path12 from "path";
3592
+ import path13 from "path";
3571
3593
  var CoWWorktreeManager = class _CoWWorktreeManager {
3572
3594
  constructor(coordDb, fencing) {
3573
3595
  this.coordDb = coordDb;
@@ -3587,13 +3609,13 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
3587
3609
  const safeId = params.agentId.replace(/[^a-zA-Z0-9_-]/g, "-");
3588
3610
  const branch = params.branch ?? `kith/${safeId}`;
3589
3611
  const baseRef = params.baseRef ?? "HEAD";
3590
- const worktreePath = path12.join(this.coordDb.coordDir, "worktrees", safeId);
3612
+ const worktreePath = path13.join(this.coordDb.coordDir, "worktrees", safeId);
3591
3613
  if (!this.fencing.getAgent(params.agentId)) throw new Error(`Unknown agent: ${params.agentId}`);
3592
3614
  if (!safeId || safeId === "." || safeId === "..") throw new Error("Invalid agent ID");
3593
3615
  if (fs14.existsSync(worktreePath)) {
3594
3616
  throw new Error(`Worktree already exists: ${worktreePath}. Commit or preserve its work before removing it.`);
3595
3617
  }
3596
- fs14.mkdirSync(path12.dirname(worktreePath), { recursive: true });
3618
+ fs14.mkdirSync(path13.dirname(worktreePath), { recursive: true });
3597
3619
  execFileSync7("git", ["worktree", "add", "-b", branch, worktreePath, baseRef], {
3598
3620
  cwd: this.coordDb.repoRoot,
3599
3621
  stdio: "ignore"
@@ -3601,16 +3623,16 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
3601
3623
  const hydratedDirs = [];
3602
3624
  let hydrationMethod = "none";
3603
3625
  for (const dirName of _CoWWorktreeManager.HYDRATABLE_DIRS) {
3604
- const srcDir = path12.join(this.coordDb.repoRoot, dirName);
3605
- const destDir = path12.join(worktreePath, dirName);
3626
+ const srcDir = path13.join(this.coordDb.repoRoot, dirName);
3627
+ const destDir = path13.join(worktreePath, dirName);
3606
3628
  if (!fs14.existsSync(srcDir) || fs14.existsSync(destDir)) continue;
3607
3629
  const method = this.cloneDirectoryCoW(srcDir, destDir);
3608
3630
  hydrationMethod = method;
3609
3631
  hydratedDirs.push(dirName);
3610
3632
  }
3611
3633
  for (const envName of _CoWWorktreeManager.ENV_FILES) {
3612
- const srcEnv = path12.join(this.coordDb.repoRoot, envName);
3613
- const destEnv = path12.join(worktreePath, envName);
3634
+ const srcEnv = path13.join(this.coordDb.repoRoot, envName);
3635
+ const destEnv = path13.join(worktreePath, envName);
3614
3636
  if (fs14.existsSync(srcEnv) && !fs14.existsSync(destEnv)) {
3615
3637
  fs14.copyFileSync(srcEnv, destEnv);
3616
3638
  }
@@ -3661,7 +3683,7 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
3661
3683
  removeWorktree(agentId) {
3662
3684
  this.fencing.assertSession(agentId);
3663
3685
  const safeId = agentId.replace(/[^a-zA-Z0-9_-]/g, "-");
3664
- const worktreePath = path12.join(this.coordDb.coordDir, "worktrees", safeId);
3686
+ const worktreePath = path13.join(this.coordDb.coordDir, "worktrees", safeId);
3665
3687
  if (!fs14.existsSync(worktreePath)) return false;
3666
3688
  const registered = this.fencing.getAgent(agentId)?.worktree_path;
3667
3689
  if (!registered || physicalPath(registered) !== physicalPath(worktreePath) || this.fencing.listAgents(true).some((agent) => agent.id !== agentId && agent.worktree_path && physicalPath(agent.worktree_path) === physicalPath(worktreePath))) {
@@ -3752,7 +3774,7 @@ var KithEngine = class {
3752
3774
  const file = LockEngine.normalizeFile(targetRoot, params.filePath);
3753
3775
  const access = this.locks.checkFileWriteAccess({ agentId: params.agentId, filePath: file });
3754
3776
  if (!access.allowed) throw new Error(access.reason);
3755
- const content = params.content ?? (fs15.existsSync(path13.join(targetRoot, file)) ? fs15.readFileSync(path13.join(targetRoot, file), "utf8") : "");
3777
+ const content = params.content ?? (fs15.existsSync(path14.join(targetRoot, file)) ? fs15.readFileSync(path14.join(targetRoot, file), "utf8") : "");
3756
3778
  const contractReport = this.depGraph.recordFileModification(
3757
3779
  params.agentId,
3758
3780
  file,
@@ -3894,13 +3916,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3894
3916
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3895
3917
  import { z } from "zod";
3896
3918
  import fs16 from "fs";
3897
- import path14 from "path";
3919
+ import path15 from "path";
3898
3920
  function createKithMcpServer(repoRoot = process.cwd()) {
3899
3921
  const engine = new KithEngine(repoRoot);
3900
3922
  const server = new McpServer({
3901
3923
  name: "kith-coordinator",
3902
3924
  version: KITH_VERSION
3903
- }, { 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
+ }, { 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.") });
3904
3926
  const tokenFor = (args) => args.sessionToken ?? (process.env.KITH_AGENT_ID === args.agentId ? process.env.KITH_SESSION_TOKEN : void 0);
3905
3927
  const owned = /* @__PURE__ */ new Set();
3906
3928
  const register = (args) => {
@@ -3985,7 +4007,7 @@ function createKithMcpServer(repoRoot = process.cwd()) {
3985
4007
  const contracts = engine.depGraph.listContracts().slice(0, 30);
3986
4008
  return formatReply(args.agentId, {
3987
4009
  ok: intentRes.ok,
3988
- agent: reg.agent,
4010
+ agent: engine.fencing.getAgent(args.agentId),
3989
4011
  sessionToken: reg.sessionToken,
3990
4012
  fencingToken: intentRes.fencingToken,
3991
4013
  task: intentRes.task,
@@ -4112,9 +4134,9 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4112
4134
  async (args, extra) => {
4113
4135
  authorize(args);
4114
4136
  const root = fs16.realpathSync(engine.fencing.getAgent(args.agentId)?.worktree_path ?? engine.coordDb.repoRoot);
4115
- const cwd = fs16.realpathSync(path14.resolve(root, args.cwd ?? "."));
4116
- const relative = path14.relative(root, cwd);
4117
- if (relative === ".." || relative.startsWith(".." + path14.sep) || path14.isAbsolute(relative)) {
4137
+ const cwd = fs16.realpathSync(path15.resolve(root, args.cwd ?? "."));
4138
+ const relative = path15.relative(root, cwd);
4139
+ if (relative === ".." || relative.startsWith(".." + path15.sep) || path15.isAbsolute(relative)) {
4118
4140
  throw new Error("Command cwd must be inside the registered worktree or repository");
4119
4141
  }
4120
4142
  const execution = engine.broker.executeWithMutex({
@@ -4142,12 +4164,18 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4142
4164
  "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.",
4143
4165
  {
4144
4166
  agentId: z.string().optional(),
4145
- sessionToken: z.string().optional()
4167
+ sessionToken: z.string().optional(),
4168
+ includeContracts: z.boolean().optional().describe("Include the full contract registry. Default false keeps frequent status polls compact; contractCount always reports its size.")
4146
4169
  },
4147
4170
  async (args) => {
4148
4171
  if (args.agentId) authorize({ ...args, agentId: args.agentId });
4149
4172
  const state = engine.getStatus();
4150
- return formatReply(args.agentId, state);
4173
+ return formatReply(args.agentId, {
4174
+ ...state,
4175
+ contracts: args.includeContracts ? state.contracts : [],
4176
+ contractCount: state.contracts.length,
4177
+ contractsIncluded: args.includeContracts ?? false
4178
+ });
4151
4179
  }
4152
4180
  );
4153
4181
  server.tool(
@@ -4167,7 +4195,7 @@ function createKithMcpServer(repoRoot = process.cwd()) {
4167
4195
  sessionToken: z.string().optional(),
4168
4196
  taskId: z.string().optional(),
4169
4197
  fencingToken: z.number().optional(),
4170
- mergeBranch: z.string().optional().describe("If work was done on a worktree/branch, run Speculative Merge Train to integrate it"),
4198
+ 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."),
4171
4199
  verificationCmd: z.string().optional().describe('Command to run in speculative merge tree (e.g. "npm run typecheck")')
4172
4200
  },
4173
4201
  async (args) => {
@@ -4384,7 +4412,8 @@ var HookHandler = class {
4384
4412
  // src/cli/init.ts
4385
4413
  import { execFileSync as execFileSync9 } from "child_process";
4386
4414
  import fs18 from "fs";
4387
- import path15 from "path";
4415
+ import path16 from "path";
4416
+ import { fileURLToPath as fileURLToPath2 } from "url";
4388
4417
  import { parse as parseToml2, stringify as stringifyToml } from "smol-toml";
4389
4418
  var INTEGRATIONS = ["claude", "codex", "cursor", "agents", "git", "shims"];
4390
4419
  function object(value, label) {
@@ -4416,11 +4445,14 @@ function initializeKithRepository(repoRoot = process.cwd(), cliBinaryCommand = "
4416
4445
  for (const integration of selected) {
4417
4446
  if (!INTEGRATIONS.includes(integration)) throw new Error(`Unknown integration: ${integration}`);
4418
4447
  }
4448
+ if (process.platform === "win32" && !mcpOnly && ["claude", "cursor", "git", "shims"].some((name) => selected.has(name))) {
4449
+ throw new Error("Native Windows setup supports MCP-only configuration; use --mcp-only or WSL for shell hooks and shims.");
4450
+ }
4419
4451
  if (!fs18.statSync(absRoot).isDirectory()) throw new Error(`Not a directory: ${absRoot}`);
4420
4452
  const plan = /* @__PURE__ */ new Map();
4421
4453
  const warnings = [];
4422
4454
  const add = (name, content, mode2) => {
4423
- const target = path15.isAbsolute(name) ? name : path15.join(absRoot, name);
4455
+ const target = path16.isAbsolute(name) ? name : path16.join(absRoot, name);
4424
4456
  if (!isWithin(absRoot, physicalPath(target))) throw new Error(`Configuration path leaves repository: ${name}`);
4425
4457
  if (!fs18.existsSync(target) || fs18.readFileSync(target, "utf8") !== content) {
4426
4458
  plan.set(name, { content, mode: mode2 });
@@ -4428,14 +4460,16 @@ function initializeKithRepository(repoRoot = process.cwd(), cliBinaryCommand = "
4428
4460
  };
4429
4461
  const json = (name, value) => add(name, JSON.stringify(value, null, 2) + "\n");
4430
4462
  const hookCommand = `${cliBinaryCommand} --repo ${shellQuote(absRoot)} hook`;
4431
- const server = { command: "kith", args: ["mcp", "--repo", absRoot] };
4432
- const ignorePath = path15.join(absRoot, ".gitignore");
4463
+ const windowsCli = process.platform === "win32" ? ["./cli/index.js", "./index.js", "../../dist/cli/index.js"].map((relative) => fileURLToPath2(new URL(relative, import.meta.url))).find((file) => fs18.existsSync(file)) : void 0;
4464
+ if (process.platform === "win32" && !windowsCli) throw new Error("Build Kith before generating native Windows MCP configuration");
4465
+ const server = windowsCli ? { command: process.execPath, args: [windowsCli, "mcp", "--repo", absRoot] } : { command: "kith", args: ["mcp", "--repo", absRoot] };
4466
+ const ignorePath = path16.join(absRoot, ".gitignore");
4433
4467
  const ignore = fs18.existsSync(ignorePath) ? fs18.readFileSync(ignorePath, "utf8") : "";
4434
4468
  if (!ignore.split(/\r?\n/).some((line) => [".coord/", "/.coord/"].includes(line.trim()))) {
4435
4469
  add(".gitignore", ignore + (ignore && !ignore.endsWith("\n") ? "\n" : "") + "\n# Kith local state and worktrees\n.coord/\n");
4436
4470
  }
4437
4471
  if (selected.has("claude")) {
4438
- const settings = readConfig(path15.join(absRoot, ".claude/settings.json"));
4472
+ const settings = readConfig(path16.join(absRoot, ".claude/settings.json"));
4439
4473
  const hooks = object(settings.hooks ?? {}, "Claude hooks");
4440
4474
  if (mcpOnly && ["PreToolUse", "PostToolUse"].some((event) => Array.isArray(hooks[event]) && hooks[event].length)) {
4441
4475
  warnings.push("Existing Claude tool hooks preserved. Review whether they require inherited KITH credentials; --mcp-only does not disable guards.");
@@ -4462,12 +4496,12 @@ function initializeKithRepository(repoRoot = process.cwd(), cliBinaryCommand = "
4462
4496
  settings.hooks = hooks;
4463
4497
  json(".claude/settings.json", settings);
4464
4498
  }
4465
- const mcp = readConfig(path15.join(absRoot, ".mcp.json"));
4499
+ const mcp = readConfig(path16.join(absRoot, ".mcp.json"));
4466
4500
  mcp.mcpServers = { ...object(mcp.mcpServers ?? {}, "Claude MCP servers"), kith: server };
4467
4501
  json(".mcp.json", mcp);
4468
4502
  }
4469
4503
  if (selected.has("codex")) {
4470
- const file = path15.join(absRoot, ".codex/config.toml");
4504
+ const file = path16.join(absRoot, ".codex/config.toml");
4471
4505
  const original = fs18.existsSync(file) ? fs18.readFileSync(file, "utf8") : "";
4472
4506
  let config;
4473
4507
  try {
@@ -4492,10 +4526,10 @@ function initializeKithRepository(repoRoot = process.cwd(), cliBinaryCommand = "
4492
4526
  }
4493
4527
  }
4494
4528
  if (selected.has("cursor")) {
4495
- const mcp = readConfig(path15.join(absRoot, ".cursor/mcp.json"));
4529
+ const mcp = readConfig(path16.join(absRoot, ".cursor/mcp.json"));
4496
4530
  mcp.mcpServers = { ...object(mcp.mcpServers ?? {}, "Cursor MCP servers"), kith: server };
4497
4531
  json(".cursor/mcp.json", mcp);
4498
- const existingCursorHooks = readConfig(path15.join(absRoot, ".cursor/hooks.json"));
4532
+ const existingCursorHooks = readConfig(path16.join(absRoot, ".cursor/hooks.json"));
4499
4533
  if (mcpOnly && ["preToolUse", "postToolUse"].some((event) => Array.isArray(existingCursorHooks.hooks?.[event]) && existingCursorHooks.hooks[event].length)) warnings.push("Existing Cursor hooks preserved; review their inherited session requirements.");
4500
4534
  if (!mcpOnly) {
4501
4535
  const config = existingCursorHooks;
@@ -4514,7 +4548,7 @@ function initializeKithRepository(repoRoot = process.cwd(), cliBinaryCommand = "
4514
4548
  }
4515
4549
  }
4516
4550
  if (selected.has("agents")) {
4517
- const file = path15.join(absRoot, "AGENTS.md");
4551
+ const file = path16.join(absRoot, "AGENTS.md");
4518
4552
  let content = fs18.existsSync(file) ? fs18.readFileSync(file, "utf8") : "# Repository Agent Instructions\n";
4519
4553
  const block = `<!-- KITH:START -->
4520
4554
  ## Parallel Agent Coordination (kith)
@@ -4529,7 +4563,7 @@ Use Kith MCP to claim work, isolate changes in worktrees, share local resources
4529
4563
  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\`.
4530
4564
  5. Complete with \`kith_complete_work\` and a project-specific \`verificationCmd\`. Failed verification keeps the integration target unchanged. Successful worktrees are retained for inspection.
4531
4565
 
4532
- 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.
4566
+ 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.
4533
4567
 
4534
4568
  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.
4535
4569
  <!-- KITH:END -->`;
@@ -4547,10 +4581,10 @@ If Claude/Cursor edit hooks or CLI commands are also enabled, launch the client
4547
4581
  encoding: "utf8",
4548
4582
  stdio: ["ignore", "pipe", "ignore"]
4549
4583
  }).trim();
4550
- const hook = path15.join(hooksDir, "pre-commit");
4584
+ const hook = path16.join(hooksDir, "pre-commit");
4551
4585
  const physicalHooks = fs18.existsSync(hooksDir) ? fs18.realpathSync(hooksDir) : hooksDir;
4552
- const relativeHooks = path15.relative(fs18.realpathSync(absRoot), physicalHooks);
4553
- if (relativeHooks === ".." || relativeHooks.startsWith(".." + path15.sep) || path15.isAbsolute(relativeHooks)) {
4586
+ const relativeHooks = path16.relative(fs18.realpathSync(absRoot), physicalHooks);
4587
+ if (relativeHooks === ".." || relativeHooks.startsWith(".." + path16.sep) || path16.isAbsolute(relativeHooks)) {
4554
4588
  warnings.push("Git hook skipped: hooks directory is outside this repository. Integrate the guard manually.");
4555
4589
  } else {
4556
4590
  const script = (arg) => `#!/usr/bin/env sh