kith-coord 0.1.0 → 0.1.1
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 +9 -1
- package/README.md +3 -3
- package/dist/cli/index.js +91 -64
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +80 -59
- package/dist/index.js.map +1 -1
- package/docs/client-validation.md +15 -4
- package/docs/integrations.md +9 -4
- package/docs/path-and-scope-rules.md +1 -1
- package/docs/reliability.md +3 -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 +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
- Validate the real Cursor CLI model/editor/MCP workflow and missing-guard refusal, with redacted reusable proof.
|
|
6
|
+
- Identify native Windows session owners through process creation time; accept native and Git-style concrete paths while preserving link containment.
|
|
7
|
+
- Generate Windows MCP launchers with the current Node executable and built CLI, avoiding npm `.cmd` spawning. Refuse unsupported native shell-hook setup before writes.
|
|
8
|
+
- Exercise native Windows worktrees, conflict refusal, ports, verification/retry, crash recovery and conservative setup/uninstall on hosted runners against clean installed packages (registry installs on release tags). Supervised mutex commands remain refused on native Windows.
|
|
9
|
+
- Flush setup backup files on Windows without unsupported directory fsync; directory-entry durability across power loss is not promised there.
|
|
10
|
+
|
|
3
11
|
## 0.1.0 — 2026-10-20
|
|
4
12
|
|
|
5
13
|
- Use the npm identity `kith-coord` with CLI `kith`. Protocol version 1.0.0 and storage schema 2 are independent. Earlier source manifests used 1.0.0 without a registry release.
|
|
@@ -9,7 +17,7 @@
|
|
|
9
17
|
- Save private per-checkout setup backups before configuration writes. Uninstall restores only unchanged tracked writes, retaining edited configuration, state, worktrees, recovery copies and ignore protection.
|
|
10
18
|
- Export the 13 canonical MCP input schemas from actual stdio discovery and check snapshot drift in CI; document protocol ownership and integration semantics. No database tables or runtime dependencies added.
|
|
11
19
|
|
|
12
|
-
##
|
|
20
|
+
## Pre-release development history
|
|
13
21
|
|
|
14
22
|
### Coordination positioning
|
|
15
23
|
|
package/README.md
CHANGED
|
@@ -35,12 +35,12 @@ The [reliability contract](docs/reliability.md) defines ownership, verification,
|
|
|
35
35
|
|
|
36
36
|
## Quick start
|
|
37
37
|
|
|
38
|
-
Requires **Git 2.38+** and **Node.js 22.13+ on 22.x or Node.js 24+**.
|
|
38
|
+
Requires **Git 2.38+** and **Node.js 22.13+ on 22.x or Node.js 24+**. Full workflows target macOS and Linux. Version 0.1.1 also validates native Windows MCP claims, worktrees, ports, integration and recovery; supervised mutex commands and shell hooks require macOS/Linux or WSL. See the [support evidence](docs/client-validation.md#native-windows).
|
|
39
39
|
|
|
40
40
|
Install the release, then configure your project:
|
|
41
41
|
|
|
42
42
|
```sh
|
|
43
|
-
npm install --global kith-coord@0.1.
|
|
43
|
+
npm install --global kith-coord@0.1.1
|
|
44
44
|
cd /path/to/your/project
|
|
45
45
|
kith init
|
|
46
46
|
```
|
|
@@ -96,7 +96,7 @@ Default setup installs MCP configuration for Claude, Codex and Cursor plus agent
|
|
|
96
96
|
| --- | --- |
|
|
97
97
|
| Claude Code | MCP; optional edit hooks need shared session setup; recorded native CLI runs |
|
|
98
98
|
| Codex | Project MCP configuration; recorded native CLI runs with configuration overrides |
|
|
99
|
-
| Cursor |
|
|
99
|
+
| Cursor | Native CLI workflow and fail-closed edit hooks validated; GUI discovery/trust unvalidated |
|
|
100
100
|
| Other MCP clients | stdio server via `kith mcp`; client-specific validation required |
|
|
101
101
|
|
|
102
102
|
Native checks are version-specific. GUI discovery and persisted project trust need separate checks. See [integration setup](docs/integrations.md), [recorded validation](docs/client-validation.md), and [session ownership](docs/sessions-and-mutexes.md) before mixing hooks, CLI commands, and MCP clients.
|
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/version.ts
|
|
4
|
-
var KITH_VERSION = "0.1.
|
|
4
|
+
var KITH_VERSION = "0.1.1";
|
|
5
5
|
var KITH_PROTOCOL_VERSION = "1.0.0";
|
|
6
6
|
|
|
7
7
|
// src/cli/setup-ledger.ts
|
|
@@ -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")) {
|
|
@@ -3039,7 +3060,7 @@ var IntentAndTaskEngine = class {
|
|
|
3039
3060
|
import { execFileSync as execFileSync5, execSync } from "child_process";
|
|
3040
3061
|
import crypto9 from "crypto";
|
|
3041
3062
|
import fs12 from "fs";
|
|
3042
|
-
import
|
|
3063
|
+
import path11 from "path";
|
|
3043
3064
|
var SpeculativeMergeTrain = class {
|
|
3044
3065
|
constructor(coordDb, fencing) {
|
|
3045
3066
|
this.coordDb = coordDb;
|
|
@@ -3078,8 +3099,8 @@ var SpeculativeMergeTrain = class {
|
|
|
3078
3099
|
now
|
|
3079
3100
|
);
|
|
3080
3101
|
});
|
|
3081
|
-
const stageDir =
|
|
3082
|
-
const lockPath =
|
|
3102
|
+
const stageDir = path11.join(this.coordDb.coordDir, "worktrees", `_merge_train_${id}`);
|
|
3103
|
+
const lockPath = path11.join(this.coordDb.coordDir, "merge-train.lock");
|
|
3083
3104
|
let lockFd;
|
|
3084
3105
|
try {
|
|
3085
3106
|
try {
|
|
@@ -3130,8 +3151,8 @@ var SpeculativeMergeTrain = class {
|
|
|
3130
3151
|
cwd: this.coordDb.repoRoot,
|
|
3131
3152
|
stdio: "ignore"
|
|
3132
3153
|
});
|
|
3133
|
-
const rootNodeModules =
|
|
3134
|
-
const stageNodeModules =
|
|
3154
|
+
const rootNodeModules = path11.join(this.coordDb.repoRoot, "node_modules");
|
|
3155
|
+
const stageNodeModules = path11.join(stageDir, "node_modules");
|
|
3135
3156
|
const hydratedNodeModules = fs12.existsSync(rootNodeModules) && !fs12.existsSync(stageNodeModules);
|
|
3136
3157
|
if (hydratedNodeModules) {
|
|
3137
3158
|
fs12.symlinkSync(rootNodeModules, stageNodeModules, "dir");
|
|
@@ -3296,7 +3317,7 @@ ${combined}`
|
|
|
3296
3317
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
3297
3318
|
import crypto10 from "crypto";
|
|
3298
3319
|
import fs13 from "fs";
|
|
3299
|
-
import
|
|
3320
|
+
import path12 from "path";
|
|
3300
3321
|
var ShadowGitEngine = class {
|
|
3301
3322
|
constructor(coordDb, fencing) {
|
|
3302
3323
|
this.coordDb = coordDb;
|
|
@@ -3329,7 +3350,7 @@ var ShadowGitEngine = class {
|
|
|
3329
3350
|
getShadowIndexPath(agentId) {
|
|
3330
3351
|
const safeId = agentId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
3331
3352
|
const epoch = this.fencing.assertSession(agentId);
|
|
3332
|
-
return
|
|
3353
|
+
return path12.join(this.coordDb.coordDir, "shadow-index", `${safeId}-${epoch}.index`);
|
|
3333
3354
|
}
|
|
3334
3355
|
/**
|
|
3335
3356
|
* Updates an agent's ephemeral shadow Git index (`.coord/shadow-index/<agent-id>.index`)
|
|
@@ -3366,7 +3387,7 @@ var ShadowGitEngine = class {
|
|
|
3366
3387
|
} else if (params.modifiedFiles && params.modifiedFiles.length > 0) {
|
|
3367
3388
|
for (const mod of params.modifiedFiles) {
|
|
3368
3389
|
const relPath = LockEngine.normalizeFile(targetDir, mod.filePath);
|
|
3369
|
-
const absPath =
|
|
3390
|
+
const absPath = path12.join(targetDir, relPath);
|
|
3370
3391
|
const content = mod.content ?? (fs13.existsSync(absPath) ? fs13.readFileSync(absPath, "utf8") : null);
|
|
3371
3392
|
if (content === null) {
|
|
3372
3393
|
{
|
|
@@ -3567,7 +3588,7 @@ var ShadowGitEngine = class {
|
|
|
3567
3588
|
// src/core/worktree-cow.ts
|
|
3568
3589
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
3569
3590
|
import fs14 from "fs";
|
|
3570
|
-
import
|
|
3591
|
+
import path13 from "path";
|
|
3571
3592
|
var CoWWorktreeManager = class _CoWWorktreeManager {
|
|
3572
3593
|
constructor(coordDb, fencing) {
|
|
3573
3594
|
this.coordDb = coordDb;
|
|
@@ -3587,13 +3608,13 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
|
|
|
3587
3608
|
const safeId = params.agentId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
3588
3609
|
const branch = params.branch ?? `kith/${safeId}`;
|
|
3589
3610
|
const baseRef = params.baseRef ?? "HEAD";
|
|
3590
|
-
const worktreePath =
|
|
3611
|
+
const worktreePath = path13.join(this.coordDb.coordDir, "worktrees", safeId);
|
|
3591
3612
|
if (!this.fencing.getAgent(params.agentId)) throw new Error(`Unknown agent: ${params.agentId}`);
|
|
3592
3613
|
if (!safeId || safeId === "." || safeId === "..") throw new Error("Invalid agent ID");
|
|
3593
3614
|
if (fs14.existsSync(worktreePath)) {
|
|
3594
3615
|
throw new Error(`Worktree already exists: ${worktreePath}. Commit or preserve its work before removing it.`);
|
|
3595
3616
|
}
|
|
3596
|
-
fs14.mkdirSync(
|
|
3617
|
+
fs14.mkdirSync(path13.dirname(worktreePath), { recursive: true });
|
|
3597
3618
|
execFileSync7("git", ["worktree", "add", "-b", branch, worktreePath, baseRef], {
|
|
3598
3619
|
cwd: this.coordDb.repoRoot,
|
|
3599
3620
|
stdio: "ignore"
|
|
@@ -3601,16 +3622,16 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
|
|
|
3601
3622
|
const hydratedDirs = [];
|
|
3602
3623
|
let hydrationMethod = "none";
|
|
3603
3624
|
for (const dirName of _CoWWorktreeManager.HYDRATABLE_DIRS) {
|
|
3604
|
-
const srcDir =
|
|
3605
|
-
const destDir =
|
|
3625
|
+
const srcDir = path13.join(this.coordDb.repoRoot, dirName);
|
|
3626
|
+
const destDir = path13.join(worktreePath, dirName);
|
|
3606
3627
|
if (!fs14.existsSync(srcDir) || fs14.existsSync(destDir)) continue;
|
|
3607
3628
|
const method = this.cloneDirectoryCoW(srcDir, destDir);
|
|
3608
3629
|
hydrationMethod = method;
|
|
3609
3630
|
hydratedDirs.push(dirName);
|
|
3610
3631
|
}
|
|
3611
3632
|
for (const envName of _CoWWorktreeManager.ENV_FILES) {
|
|
3612
|
-
const srcEnv =
|
|
3613
|
-
const destEnv =
|
|
3633
|
+
const srcEnv = path13.join(this.coordDb.repoRoot, envName);
|
|
3634
|
+
const destEnv = path13.join(worktreePath, envName);
|
|
3614
3635
|
if (fs14.existsSync(srcEnv) && !fs14.existsSync(destEnv)) {
|
|
3615
3636
|
fs14.copyFileSync(srcEnv, destEnv);
|
|
3616
3637
|
}
|
|
@@ -3661,7 +3682,7 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
|
|
|
3661
3682
|
removeWorktree(agentId) {
|
|
3662
3683
|
this.fencing.assertSession(agentId);
|
|
3663
3684
|
const safeId = agentId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
3664
|
-
const worktreePath =
|
|
3685
|
+
const worktreePath = path13.join(this.coordDb.coordDir, "worktrees", safeId);
|
|
3665
3686
|
if (!fs14.existsSync(worktreePath)) return false;
|
|
3666
3687
|
const registered = this.fencing.getAgent(agentId)?.worktree_path;
|
|
3667
3688
|
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 +3773,7 @@ var KithEngine = class {
|
|
|
3752
3773
|
const file = LockEngine.normalizeFile(targetRoot, params.filePath);
|
|
3753
3774
|
const access = this.locks.checkFileWriteAccess({ agentId: params.agentId, filePath: file });
|
|
3754
3775
|
if (!access.allowed) throw new Error(access.reason);
|
|
3755
|
-
const content = params.content ?? (fs15.existsSync(
|
|
3776
|
+
const content = params.content ?? (fs15.existsSync(path14.join(targetRoot, file)) ? fs15.readFileSync(path14.join(targetRoot, file), "utf8") : "");
|
|
3756
3777
|
const contractReport = this.depGraph.recordFileModification(
|
|
3757
3778
|
params.agentId,
|
|
3758
3779
|
file,
|
|
@@ -3894,7 +3915,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3894
3915
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3895
3916
|
import { z } from "zod";
|
|
3896
3917
|
import fs16 from "fs";
|
|
3897
|
-
import
|
|
3918
|
+
import path15 from "path";
|
|
3898
3919
|
function createKithMcpServer(repoRoot = process.cwd()) {
|
|
3899
3920
|
const engine = new KithEngine(repoRoot);
|
|
3900
3921
|
const server = new McpServer({
|
|
@@ -4112,9 +4133,9 @@ function createKithMcpServer(repoRoot = process.cwd()) {
|
|
|
4112
4133
|
async (args, extra) => {
|
|
4113
4134
|
authorize(args);
|
|
4114
4135
|
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(".." +
|
|
4136
|
+
const cwd = fs16.realpathSync(path15.resolve(root, args.cwd ?? "."));
|
|
4137
|
+
const relative = path15.relative(root, cwd);
|
|
4138
|
+
if (relative === ".." || relative.startsWith(".." + path15.sep) || path15.isAbsolute(relative)) {
|
|
4118
4139
|
throw new Error("Command cwd must be inside the registered worktree or repository");
|
|
4119
4140
|
}
|
|
4120
4141
|
const execution = engine.broker.executeWithMutex({
|
|
@@ -4384,7 +4405,8 @@ var HookHandler = class {
|
|
|
4384
4405
|
// src/cli/init.ts
|
|
4385
4406
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
4386
4407
|
import fs18 from "fs";
|
|
4387
|
-
import
|
|
4408
|
+
import path16 from "path";
|
|
4409
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4388
4410
|
import { parse as parseToml2, stringify as stringifyToml } from "smol-toml";
|
|
4389
4411
|
var INTEGRATIONS = ["claude", "codex", "cursor", "agents", "git", "shims"];
|
|
4390
4412
|
function object(value, label) {
|
|
@@ -4416,11 +4438,14 @@ function initializeKithRepository(repoRoot = process.cwd(), cliBinaryCommand = "
|
|
|
4416
4438
|
for (const integration of selected) {
|
|
4417
4439
|
if (!INTEGRATIONS.includes(integration)) throw new Error(`Unknown integration: ${integration}`);
|
|
4418
4440
|
}
|
|
4441
|
+
if (process.platform === "win32" && !mcpOnly && ["claude", "cursor", "git", "shims"].some((name) => selected.has(name))) {
|
|
4442
|
+
throw new Error("Native Windows setup supports MCP-only configuration; use --mcp-only or WSL for shell hooks and shims.");
|
|
4443
|
+
}
|
|
4419
4444
|
if (!fs18.statSync(absRoot).isDirectory()) throw new Error(`Not a directory: ${absRoot}`);
|
|
4420
4445
|
const plan = /* @__PURE__ */ new Map();
|
|
4421
4446
|
const warnings = [];
|
|
4422
4447
|
const add = (name, content, mode2) => {
|
|
4423
|
-
const target =
|
|
4448
|
+
const target = path16.isAbsolute(name) ? name : path16.join(absRoot, name);
|
|
4424
4449
|
if (!isWithin(absRoot, physicalPath(target))) throw new Error(`Configuration path leaves repository: ${name}`);
|
|
4425
4450
|
if (!fs18.existsSync(target) || fs18.readFileSync(target, "utf8") !== content) {
|
|
4426
4451
|
plan.set(name, { content, mode: mode2 });
|
|
@@ -4428,14 +4453,16 @@ function initializeKithRepository(repoRoot = process.cwd(), cliBinaryCommand = "
|
|
|
4428
4453
|
};
|
|
4429
4454
|
const json = (name, value) => add(name, JSON.stringify(value, null, 2) + "\n");
|
|
4430
4455
|
const hookCommand = `${cliBinaryCommand} --repo ${shellQuote(absRoot)} hook`;
|
|
4431
|
-
const
|
|
4432
|
-
|
|
4456
|
+
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;
|
|
4457
|
+
if (process.platform === "win32" && !windowsCli) throw new Error("Build Kith before generating native Windows MCP configuration");
|
|
4458
|
+
const server = windowsCli ? { command: process.execPath, args: [windowsCli, "mcp", "--repo", absRoot] } : { command: "kith", args: ["mcp", "--repo", absRoot] };
|
|
4459
|
+
const ignorePath = path16.join(absRoot, ".gitignore");
|
|
4433
4460
|
const ignore = fs18.existsSync(ignorePath) ? fs18.readFileSync(ignorePath, "utf8") : "";
|
|
4434
4461
|
if (!ignore.split(/\r?\n/).some((line) => [".coord/", "/.coord/"].includes(line.trim()))) {
|
|
4435
4462
|
add(".gitignore", ignore + (ignore && !ignore.endsWith("\n") ? "\n" : "") + "\n# Kith local state and worktrees\n.coord/\n");
|
|
4436
4463
|
}
|
|
4437
4464
|
if (selected.has("claude")) {
|
|
4438
|
-
const settings = readConfig(
|
|
4465
|
+
const settings = readConfig(path16.join(absRoot, ".claude/settings.json"));
|
|
4439
4466
|
const hooks = object(settings.hooks ?? {}, "Claude hooks");
|
|
4440
4467
|
if (mcpOnly && ["PreToolUse", "PostToolUse"].some((event) => Array.isArray(hooks[event]) && hooks[event].length)) {
|
|
4441
4468
|
warnings.push("Existing Claude tool hooks preserved. Review whether they require inherited KITH credentials; --mcp-only does not disable guards.");
|
|
@@ -4462,12 +4489,12 @@ function initializeKithRepository(repoRoot = process.cwd(), cliBinaryCommand = "
|
|
|
4462
4489
|
settings.hooks = hooks;
|
|
4463
4490
|
json(".claude/settings.json", settings);
|
|
4464
4491
|
}
|
|
4465
|
-
const mcp = readConfig(
|
|
4492
|
+
const mcp = readConfig(path16.join(absRoot, ".mcp.json"));
|
|
4466
4493
|
mcp.mcpServers = { ...object(mcp.mcpServers ?? {}, "Claude MCP servers"), kith: server };
|
|
4467
4494
|
json(".mcp.json", mcp);
|
|
4468
4495
|
}
|
|
4469
4496
|
if (selected.has("codex")) {
|
|
4470
|
-
const file =
|
|
4497
|
+
const file = path16.join(absRoot, ".codex/config.toml");
|
|
4471
4498
|
const original = fs18.existsSync(file) ? fs18.readFileSync(file, "utf8") : "";
|
|
4472
4499
|
let config;
|
|
4473
4500
|
try {
|
|
@@ -4492,10 +4519,10 @@ function initializeKithRepository(repoRoot = process.cwd(), cliBinaryCommand = "
|
|
|
4492
4519
|
}
|
|
4493
4520
|
}
|
|
4494
4521
|
if (selected.has("cursor")) {
|
|
4495
|
-
const mcp = readConfig(
|
|
4522
|
+
const mcp = readConfig(path16.join(absRoot, ".cursor/mcp.json"));
|
|
4496
4523
|
mcp.mcpServers = { ...object(mcp.mcpServers ?? {}, "Cursor MCP servers"), kith: server };
|
|
4497
4524
|
json(".cursor/mcp.json", mcp);
|
|
4498
|
-
const existingCursorHooks = readConfig(
|
|
4525
|
+
const existingCursorHooks = readConfig(path16.join(absRoot, ".cursor/hooks.json"));
|
|
4499
4526
|
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
4527
|
if (!mcpOnly) {
|
|
4501
4528
|
const config = existingCursorHooks;
|
|
@@ -4514,7 +4541,7 @@ function initializeKithRepository(repoRoot = process.cwd(), cliBinaryCommand = "
|
|
|
4514
4541
|
}
|
|
4515
4542
|
}
|
|
4516
4543
|
if (selected.has("agents")) {
|
|
4517
|
-
const file =
|
|
4544
|
+
const file = path16.join(absRoot, "AGENTS.md");
|
|
4518
4545
|
let content = fs18.existsSync(file) ? fs18.readFileSync(file, "utf8") : "# Repository Agent Instructions\n";
|
|
4519
4546
|
const block = `<!-- KITH:START -->
|
|
4520
4547
|
## Parallel Agent Coordination (kith)
|
|
@@ -4547,10 +4574,10 @@ If Claude/Cursor edit hooks or CLI commands are also enabled, launch the client
|
|
|
4547
4574
|
encoding: "utf8",
|
|
4548
4575
|
stdio: ["ignore", "pipe", "ignore"]
|
|
4549
4576
|
}).trim();
|
|
4550
|
-
const hook =
|
|
4577
|
+
const hook = path16.join(hooksDir, "pre-commit");
|
|
4551
4578
|
const physicalHooks = fs18.existsSync(hooksDir) ? fs18.realpathSync(hooksDir) : hooksDir;
|
|
4552
|
-
const relativeHooks =
|
|
4553
|
-
if (relativeHooks === ".." || relativeHooks.startsWith(".." +
|
|
4579
|
+
const relativeHooks = path16.relative(fs18.realpathSync(absRoot), physicalHooks);
|
|
4580
|
+
if (relativeHooks === ".." || relativeHooks.startsWith(".." + path16.sep) || path16.isAbsolute(relativeHooks)) {
|
|
4554
4581
|
warnings.push("Git hook skipped: hooks directory is outside this repository. Integrate the guard manually.");
|
|
4555
4582
|
} else {
|
|
4556
4583
|
const script = (arg) => `#!/usr/bin/env sh
|