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/CHANGELOG.md +18 -1
- package/README.md +6 -14
- package/dist/cli/index.js +105 -71
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +82 -60
- package/dist/index.js.map +1 -1
- package/docs/client-validation.md +21 -4
- package/docs/integrations.md +9 -4
- package/docs/path-and-scope-rules.md +1 -1
- package/docs/protocol/tools.v1.json +6 -2
- package/docs/protocol.md +6 -4
- package/docs/reliability.md +3 -1
- package/docs/roadmap.md +1 -1
- package/docs/usage.md +9 -1
- package/docs/validation/2026-10-20-cursor-cli.json +38 -0
- package/docs/validation/2026-10-20-cursor-hook-unavailable.json +18 -0
- package/docs/validation/2026-10-20-windows-node-22.13.0.json +17 -0
- package/docs/validation/2026-10-20-windows-node-24.json +17 -0
- package/package.json +4 -2
- package/server.json +52 -0
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.
|
|
5
|
-
var KITH_PROTOCOL_VERSION = "1.
|
|
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 || /[
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
1377
|
-
const baseTarget =
|
|
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
|
-
|
|
1400
|
+
path7.resolve(sourceDir, specifier),
|
|
1380
1401
|
`${baseTarget}.ts`,
|
|
1381
1402
|
`${baseTarget}.tsx`,
|
|
1382
1403
|
`${baseTarget}.js`,
|
|
1383
1404
|
`${baseTarget}.mjs`,
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
3082
|
-
const lockPath =
|
|
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 =
|
|
3134
|
-
const stageNodeModules =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
3605
|
-
const destDir =
|
|
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 =
|
|
3613
|
-
const destEnv =
|
|
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 =
|
|
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(
|
|
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
|
|
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.
|
|
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:
|
|
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(
|
|
4116
|
-
const relative =
|
|
4117
|
-
if (relative === ".." || relative.startsWith(".." +
|
|
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,
|
|
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("
|
|
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
|
|
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 =
|
|
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
|
|
4432
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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;
|
|
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 =
|
|
4584
|
+
const hook = path16.join(hooksDir, "pre-commit");
|
|
4551
4585
|
const physicalHooks = fs18.existsSync(hooksDir) ? fs18.realpathSync(hooksDir) : hooksDir;
|
|
4552
|
-
const relativeHooks =
|
|
4553
|
-
if (relativeHooks === ".." || relativeHooks.startsWith(".." +
|
|
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
|