kith-coord 0.1.3 → 0.1.5
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 +17 -0
- package/README.md +25 -3
- package/bin/kith-menubar +0 -0
- package/dist/cli/index.js +329 -50
- package/dist/cli/index.js.map +1 -1
- package/dist/command-worker.js +1 -1
- package/dist/command-worker.js.map +1 -1
- package/dist/index.d.ts +58 -16
- package/dist/index.js +101 -6
- package/dist/index.js.map +1 -1
- package/docs/client-validation.md +5 -0
- package/docs/integrations.md +5 -3
- package/docs/protocol/tools.v1.json +73 -1
- package/docs/protocol.md +8 -2
- package/docs/reliability.md +8 -1
- package/docs/roadmap.md +12 -33
- package/docs/usage.md +59 -0
- package/docs/validation/2026-10-22-product-claude.json +38 -0
- package/docs/validation/2026-10-22-product-codex.json +76 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -6,8 +6,8 @@ import path19 from "path";
|
|
|
6
6
|
import { spawn } from "child_process";
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
|
-
var KITH_VERSION = "0.1.
|
|
10
|
-
var KITH_PROTOCOL_VERSION = "1.
|
|
9
|
+
var KITH_VERSION = "0.1.5";
|
|
10
|
+
var KITH_PROTOCOL_VERSION = "1.3.0";
|
|
11
11
|
|
|
12
12
|
// src/cli/setup-ledger.ts
|
|
13
13
|
import fs2 from "fs";
|
|
@@ -243,6 +243,7 @@ function uninstallKith(directory = process.cwd(), dryRun = false) {
|
|
|
243
243
|
// src/cli/doctor.ts
|
|
244
244
|
import fs5 from "fs";
|
|
245
245
|
import path5 from "path";
|
|
246
|
+
import os2 from "os";
|
|
246
247
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
247
248
|
import { parse as parseToml } from "smol-toml";
|
|
248
249
|
|
|
@@ -792,7 +793,7 @@ function previewIntegration(repoRoot, agent) {
|
|
|
792
793
|
|
|
793
794
|
// src/core/observation.ts
|
|
794
795
|
var { DatabaseSync: DatabaseSync2 } = createRequire2(import.meta.url)("node:sqlite");
|
|
795
|
-
function readCoordinationState(directory = process.cwd()) {
|
|
796
|
+
function readCoordinationState(directory = process.cwd(), options = {}) {
|
|
796
797
|
const layout = discoverRepository(directory), observedAt = Date.now();
|
|
797
798
|
const empty = {
|
|
798
799
|
protocolVersion: KITH_PROTOCOL_VERSION,
|
|
@@ -851,6 +852,10 @@ function readCoordinationState(directory = process.cwd()) {
|
|
|
851
852
|
if (CoordDatabase.inspectSchema(db) !== 2) throw new Error("Offline observation requires schema 2; preserve state and upgrade with clients stopped");
|
|
852
853
|
const all = (sql) => db.prepare(sql).all();
|
|
853
854
|
const agents = all("SELECT * FROM agents ORDER BY registered_at DESC");
|
|
855
|
+
if (options.ownersOnly) {
|
|
856
|
+
db.exec("COMMIT");
|
|
857
|
+
return { ...empty, state: "ready", agents };
|
|
858
|
+
}
|
|
854
859
|
const state = {
|
|
855
860
|
...empty,
|
|
856
861
|
state: "ready",
|
|
@@ -895,7 +900,7 @@ function recoveryReport(directory = process.cwd()) {
|
|
|
895
900
|
}
|
|
896
901
|
|
|
897
902
|
// src/cli/doctor.ts
|
|
898
|
-
function doctorReport(directory = process.cwd()) {
|
|
903
|
+
function doctorReport(directory = process.cwd(), home = os2.homedir()) {
|
|
899
904
|
const state = readCoordinationState(directory), root = state.repository.root;
|
|
900
905
|
const checks = [];
|
|
901
906
|
const check = (name, ok, detail) => checks.push({ name, ok, detail });
|
|
@@ -920,24 +925,42 @@ function doctorReport(directory = process.cwd()) {
|
|
|
920
925
|
}
|
|
921
926
|
});
|
|
922
927
|
};
|
|
928
|
+
const desktop = process.platform === "win32" ? path5.join(home === os2.homedir() ? process.env.APPDATA ?? path5.join(home, "AppData/Roaming") : path5.join(home, "AppData/Roaming"), "Claude/claude_desktop_config.json") : process.platform === "darwin" ? path5.join(home, "Library/Application Support/Claude/claude_desktop_config.json") : path5.join(home, ".config/Claude/claude_desktop_config.json");
|
|
929
|
+
const files = [
|
|
930
|
+
["claude", path5.join(root, ".mcp.json"), false, root],
|
|
931
|
+
["cursor", path5.join(root, ".cursor/mcp.json"), false, root],
|
|
932
|
+
["codex", path5.join(root, ".codex/config.toml"), true, root],
|
|
933
|
+
["claude-global", path5.join(home, ".claude.json"), false, home],
|
|
934
|
+
["claude-desktop", desktop, false, path5.dirname(desktop)],
|
|
935
|
+
["codex-global", path5.join(home, ".codex/config.toml"), true, home],
|
|
936
|
+
["antigravity", path5.join(home, ".gemini/config/mcp_config.json"), false, home]
|
|
937
|
+
];
|
|
923
938
|
let configured = 0;
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
if (!fs5.existsSync(target)) continue;
|
|
939
|
+
const inspected = /* @__PURE__ */ new Set();
|
|
940
|
+
for (const [client, target, toml, base] of files) {
|
|
941
|
+
if (inspected.has(target) || !fs5.existsSync(target)) continue;
|
|
942
|
+
inspected.add(target);
|
|
927
943
|
try {
|
|
928
|
-
if (!isWithin(
|
|
944
|
+
if (!isWithin(physicalPath(base), physicalPath(target))) throw new Error("Configuration is redirected outside its setup root");
|
|
929
945
|
const text = fs5.readFileSync(target, "utf8");
|
|
930
|
-
const config =
|
|
931
|
-
const server =
|
|
946
|
+
const config = toml ? parseToml(text) : JSON.parse(text);
|
|
947
|
+
const server = toml ? config.mcp_servers?.kith : config.mcpServers?.kith;
|
|
932
948
|
if (!server) continue;
|
|
933
949
|
configured++;
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
950
|
+
if (typeof server.command !== "string" || !Array.isArray(server.args) || !server.args.every((a) => typeof a === "string") || !executable(server.command))
|
|
951
|
+
throw new Error("Kith launcher is missing or not executable; rerun setup from the installed version");
|
|
952
|
+
if (/^node(?:\.exe)?$/i.test(path5.basename(server.command)) && (!server.args[0] || !fs5.existsSync(server.args[0]) || !fs5.statSync(server.args[0]).isFile()))
|
|
953
|
+
throw new Error("Kith CLI entry is missing; rerun setup after moving or reinstalling Node/Kith");
|
|
954
|
+
const pin = server.args.indexOf("--repo");
|
|
955
|
+
if (pin >= 0 && (typeof server.args[pin + 1] !== "string" || !path5.isAbsolute(server.args[pin + 1]) || physicalPath(server.args[pin + 1]) !== root))
|
|
956
|
+
throw new Error("Kith launcher is pinned to a different checkout; use that checkout or review its setup");
|
|
957
|
+
check(`${client}-mcp`, true, "Launcher files are present; native connection, project trust and tool approval are not verified by doctor");
|
|
958
|
+
} catch (error) {
|
|
959
|
+
check(`${client}-mcp`, false, `${target}: ${error instanceof Error ? error.message : String(error)}; configuration preserved`);
|
|
937
960
|
}
|
|
938
961
|
}
|
|
939
|
-
check("mcp-configuration", configured > 0, "
|
|
940
|
-
check("coordination-state", state.state
|
|
962
|
+
check("mcp-configuration", configured > 0, "Use global kith setup or project kith init for your client; configuration alone does not establish a connection");
|
|
963
|
+
check("coordination-state", state.state !== "unavailable", state.state === "ready" ? "Schema and counters validated on a private snapshot" : state.error ?? "No project state yet; the first owned task initializes it. No state was created by doctor");
|
|
941
964
|
let nativeGuards = false;
|
|
942
965
|
for (const [file, event] of [[".claude/settings.json", "PreToolUse"], [".cursor/hooks.json", "preToolUse"]]) {
|
|
943
966
|
const target = path5.join(root, file);
|
|
@@ -1021,7 +1044,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
|
|
|
1021
1044
|
"-NoProfile",
|
|
1022
1045
|
"-NonInteractive",
|
|
1023
1046
|
"-Command",
|
|
1024
|
-
`(
|
|
1047
|
+
`([System.Diagnostics.Process]::GetProcessById(${pid})).StartTime.ToUniversalTime().Ticks.ToString([System.Globalization.CultureInfo]::InvariantCulture)`
|
|
1025
1048
|
],
|
|
1026
1049
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3, windowsHide: true }
|
|
1027
1050
|
).trim();
|
|
@@ -1201,6 +1224,37 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
|
|
|
1201
1224
|
}
|
|
1202
1225
|
};
|
|
1203
1226
|
|
|
1227
|
+
// src/core/claim-wait.ts
|
|
1228
|
+
async function waitForClaim(params) {
|
|
1229
|
+
const { waitTimeoutMs, signal } = params;
|
|
1230
|
+
if (!Number.isSafeInteger(waitTimeoutMs) || waitTimeoutMs < 0 || waitTimeoutMs > 6e5) {
|
|
1231
|
+
throw new Error("waitTimeoutMs must be an integer between 0 and 600000");
|
|
1232
|
+
}
|
|
1233
|
+
const started = performance.now();
|
|
1234
|
+
let previous;
|
|
1235
|
+
for (; ; ) {
|
|
1236
|
+
signal?.throwIfAborted();
|
|
1237
|
+
const elapsed = Math.round(performance.now() - started);
|
|
1238
|
+
if (previous && elapsed >= waitTimeoutMs) return { ...previous, waitedMs: elapsed, waitTimedOut: true };
|
|
1239
|
+
params.heartbeat();
|
|
1240
|
+
const result = params.attempt();
|
|
1241
|
+
const waitedMs = Math.round(performance.now() - started);
|
|
1242
|
+
if (result.ok || waitedMs >= waitTimeoutMs) return { ...result, waitedMs, waitTimedOut: !result.ok };
|
|
1243
|
+
previous = result;
|
|
1244
|
+
await new Promise((resolve, reject) => {
|
|
1245
|
+
const abort = () => {
|
|
1246
|
+
clearTimeout(timer);
|
|
1247
|
+
reject(signal.reason);
|
|
1248
|
+
};
|
|
1249
|
+
const timer = setTimeout(() => {
|
|
1250
|
+
signal?.removeEventListener("abort", abort);
|
|
1251
|
+
resolve();
|
|
1252
|
+
}, Math.min(100, waitTimeoutMs - waitedMs));
|
|
1253
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1204
1258
|
// src/core/engine.ts
|
|
1205
1259
|
import fs15 from "fs";
|
|
1206
1260
|
import path14 from "path";
|
|
@@ -2119,6 +2173,7 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
2119
2173
|
const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
|
|
2120
2174
|
const now = Date.now();
|
|
2121
2175
|
const expiresAt = now + leaseMs;
|
|
2176
|
+
if (!Number.isInteger(basePort) || basePort < 1 || basePort > 65535) throw new Error("basePort must be an integer between 1 and 65535");
|
|
2122
2177
|
const resourceKey = `${params.agentId}:${params.serviceName}`;
|
|
2123
2178
|
const existing = this.getResource("port", resourceKey);
|
|
2124
2179
|
if (existing && existing.expires_at > now) {
|
|
@@ -2253,8 +2308,44 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
2253
2308
|
return true;
|
|
2254
2309
|
});
|
|
2255
2310
|
}
|
|
2311
|
+
/** Launch on the allocated port, without interpreting arguments as shell code. */
|
|
2312
|
+
async executeWithPort(params) {
|
|
2313
|
+
if (!params.command.length || !params.command[0]) throw new Error("A command is required");
|
|
2314
|
+
if (!params.serviceName || params.serviceName.length > 128) throw new Error("serviceName must contain 1 to 128 characters");
|
|
2315
|
+
const mutexName = `dev-port:${params.agentId}:${params.serviceName}`;
|
|
2316
|
+
let allocation;
|
|
2317
|
+
const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
|
|
2318
|
+
const result = await this.executeGuarded({ ...params, mutexName }, {
|
|
2319
|
+
prepare: async () => {
|
|
2320
|
+
allocation = await this.acquirePort({ ...params, basePort: params.basePort ?? 3e3 });
|
|
2321
|
+
params.onPort?.(allocation.port);
|
|
2322
|
+
return {
|
|
2323
|
+
command: params.command.map((arg) => arg.replaceAll("{port}", String(allocation.port))),
|
|
2324
|
+
env: { PORT: String(allocation.port), KITH_PORT: String(allocation.port) }
|
|
2325
|
+
};
|
|
2326
|
+
},
|
|
2327
|
+
renew: () => {
|
|
2328
|
+
if (!allocation) return;
|
|
2329
|
+
this.coordDb.withTransaction(() => {
|
|
2330
|
+
this.fencing.assertSession(params.agentId);
|
|
2331
|
+
const resource = this.getResource("port", `${params.agentId}:${params.serviceName}`);
|
|
2332
|
+
if (!resource || resource.fencing_token !== allocation.fencingToken || resource.expires_at <= Date.now()) {
|
|
2333
|
+
throw new Error("Dev server port lease lost");
|
|
2334
|
+
}
|
|
2335
|
+
this.coordDb.db.prepare("UPDATE resources SET expires_at=? WHERE id=?").run(Date.now() + leaseMs, resource.id);
|
|
2336
|
+
});
|
|
2337
|
+
},
|
|
2338
|
+
release: () => {
|
|
2339
|
+
if (allocation) this.coordDb.db.prepare("DELETE FROM resources WHERE type='port' AND name=? AND fencing_token=?").run(`${params.agentId}:${params.serviceName}`, allocation.fencingToken);
|
|
2340
|
+
}
|
|
2341
|
+
});
|
|
2342
|
+
return { ...result, port: allocation.port };
|
|
2343
|
+
}
|
|
2256
2344
|
/** Renew leases while a detached, recorded process group runs; stop it before releasing. */
|
|
2257
|
-
|
|
2345
|
+
executeWithMutex(params) {
|
|
2346
|
+
return this.executeGuarded(params);
|
|
2347
|
+
}
|
|
2348
|
+
async executeGuarded(params, lifecycle) {
|
|
2258
2349
|
if (process.platform === "win32") throw new Error("Supervised mutex commands currently require macOS or Linux process groups");
|
|
2259
2350
|
if (!params.command.length) throw new Error("A command is required");
|
|
2260
2351
|
const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
|
|
@@ -2299,6 +2390,8 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
2299
2390
|
await delay(Math.min(50, Math.max(10, leaseMs / 4)));
|
|
2300
2391
|
}
|
|
2301
2392
|
const waitedMs = Date.now() - started;
|
|
2393
|
+
const prepared = await lifecycle?.prepare();
|
|
2394
|
+
if (params.signal?.aborted) throw abortError();
|
|
2302
2395
|
const candidates = ["./command-worker.js", "../command-worker.js", "../../dist/command-worker.js"].map((name) => fileURLToPath(new URL(name, import.meta.url)));
|
|
2303
2396
|
const script = candidates.find((name) => fs10.existsSync(name));
|
|
2304
2397
|
if (!script) throw new Error("Missing command worker. Build Kith before running supervised commands.");
|
|
@@ -2341,6 +2434,7 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
2341
2434
|
try {
|
|
2342
2435
|
this.fencing.heartbeat(params.agentId);
|
|
2343
2436
|
this.renewMutex({ ...params, fencingToken: run.fencing_token });
|
|
2437
|
+
lifecycle?.renew();
|
|
2344
2438
|
} catch (err) {
|
|
2345
2439
|
stop(err instanceof Error ? err : new Error(String(err)));
|
|
2346
2440
|
}
|
|
@@ -2368,8 +2462,9 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
2368
2462
|
run.worker_start = identity;
|
|
2369
2463
|
workerIdentity = identity;
|
|
2370
2464
|
this.renewMutex({ ...params, fencingToken: run.fencing_token });
|
|
2465
|
+
lifecycle?.renew();
|
|
2371
2466
|
});
|
|
2372
|
-
worker.send({ command: params.command, cwd: params.cwd ?? this.coordDb.repoRoot, mutexName: params.mutexName });
|
|
2467
|
+
worker.send({ command: prepared?.command ?? params.command, env: prepared?.env, cwd: params.cwd ?? this.coordDb.repoRoot, mutexName: params.mutexName });
|
|
2373
2468
|
} catch (err) {
|
|
2374
2469
|
stop(err instanceof Error ? err : new Error(String(err)));
|
|
2375
2470
|
}
|
|
@@ -2417,6 +2512,7 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
2417
2512
|
this.coordDb.db.prepare("DELETE FROM mutex_waiters WHERE mutex_name=? AND request_id=? AND session_epoch=?").run(params.mutexName, requestId, epoch);
|
|
2418
2513
|
if (run && !this.runIsAlive(run)) this.clearRun(run);
|
|
2419
2514
|
if (run && run.worker_pid === null) this.clearRun(run);
|
|
2515
|
+
if (!this.getRun(params.mutexName)) lifecycle?.release();
|
|
2420
2516
|
});
|
|
2421
2517
|
} finally {
|
|
2422
2518
|
this.activeExecutions--;
|
|
@@ -3749,6 +3845,7 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
|
|
|
3749
3845
|
|
|
3750
3846
|
// src/core/engine.ts
|
|
3751
3847
|
var KithEngine = class {
|
|
3848
|
+
activeClaimWaits = 0;
|
|
3752
3849
|
coordDb;
|
|
3753
3850
|
fencing;
|
|
3754
3851
|
locks;
|
|
@@ -3901,6 +3998,22 @@ ${lines.join("\n")}` : "";
|
|
|
3901
3998
|
const worktree = intent.ok && params.isolateWorktree ? this.worktrees.spawnWorktree({ agentId: params.agentId }) : null;
|
|
3902
3999
|
return { ...intent, worktree };
|
|
3903
4000
|
}
|
|
4001
|
+
async beginWorkWhenAvailable(params) {
|
|
4002
|
+
return this.waitForOwnedClaim(params, () => this.beginWork(params));
|
|
4003
|
+
}
|
|
4004
|
+
async acquireLocksWhenAvailable(params) {
|
|
4005
|
+
return this.waitForOwnedClaim(params, () => this.locks.acquireLocks(params));
|
|
4006
|
+
}
|
|
4007
|
+
async waitForOwnedClaim(params, attempt) {
|
|
4008
|
+
this.activeClaimWaits++;
|
|
4009
|
+
try {
|
|
4010
|
+
return await waitForClaim({ ...params, attempt, heartbeat: () => {
|
|
4011
|
+
this.fencing.heartbeat(params.agentId);
|
|
4012
|
+
} });
|
|
4013
|
+
} finally {
|
|
4014
|
+
this.activeClaimWaits--;
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
3904
4017
|
completeWork(params) {
|
|
3905
4018
|
this.fencing.assertSession(params.agentId);
|
|
3906
4019
|
const agent = this.fencing.getAgent(params.agentId);
|
|
@@ -3921,6 +4034,7 @@ ${lines.join("\n")}` : "";
|
|
|
3921
4034
|
return { ok: true, completedTasks, mergeResult };
|
|
3922
4035
|
}
|
|
3923
4036
|
close() {
|
|
4037
|
+
if (this.activeClaimWaits) throw new Error("Await or cancel active file claim waits before closing Kith");
|
|
3924
4038
|
if (this.broker.hasActiveCommands()) throw new Error("Await or cancel active mutex commands before closing Kith");
|
|
3925
4039
|
this.coordDb.close();
|
|
3926
4040
|
}
|
|
@@ -4040,17 +4154,30 @@ function buildKithMcpServer(repoRoot, pinned) {
|
|
|
4040
4154
|
title: z.string().describe("Short title of the task or intent"),
|
|
4041
4155
|
intentSummary: z.string().optional().describe("Detailed description of what will be built or modified"),
|
|
4042
4156
|
scopeGlobs: z.array(z.string()).describe('File paths or glob patterns this agent will edit (e.g. ["src/auth/**"])'),
|
|
4043
|
-
isolateWorktree: z.boolean().optional().describe("Recommended: true, giving each parallel agent a separate Git worktree")
|
|
4157
|
+
isolateWorktree: z.boolean().optional().describe("Recommended: true, giving each parallel agent a separate Git worktree"),
|
|
4158
|
+
waitTimeoutMs: z.number().int().min(0).max(6e5).optional().describe("Wait up to this many milliseconds for the whole scope; default fail-fast, not a FIFO file queue")
|
|
4044
4159
|
},
|
|
4045
4160
|
async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
|
|
4046
4161
|
const reg = register(args);
|
|
4047
|
-
const
|
|
4162
|
+
const options = {
|
|
4048
4163
|
agentId: args.agentId,
|
|
4049
4164
|
title: args.title,
|
|
4050
4165
|
intentSummary: args.intentSummary,
|
|
4051
4166
|
scopeGlobs: args.scopeGlobs,
|
|
4052
4167
|
isolateWorktree: args.isolateWorktree
|
|
4168
|
+
};
|
|
4169
|
+
const execution = args.waitTimeoutMs === void 0 ? void 0 : engine2.beginWorkWhenAvailable({
|
|
4170
|
+
...options,
|
|
4171
|
+
waitTimeoutMs: args.waitTimeoutMs,
|
|
4172
|
+
signal: AbortSignal.any([disconnected.signal, extra.signal])
|
|
4053
4173
|
});
|
|
4174
|
+
if (execution) commands.add(execution);
|
|
4175
|
+
let intentRes;
|
|
4176
|
+
try {
|
|
4177
|
+
intentRes = execution ? await execution : engine2.beginWork(options);
|
|
4178
|
+
} finally {
|
|
4179
|
+
if (execution) commands.delete(execution);
|
|
4180
|
+
}
|
|
4054
4181
|
const contracts = engine2.depGraph.listContracts().slice(0, 30);
|
|
4055
4182
|
return formatReply(args.agentId, {
|
|
4056
4183
|
ok: intentRes.ok,
|
|
@@ -4062,6 +4189,7 @@ function buildKithMcpServer(repoRoot, pinned) {
|
|
|
4062
4189
|
worktree: intentRes.worktree,
|
|
4063
4190
|
lockConflicts: intentRes.lockConflicts,
|
|
4064
4191
|
activeSiblingIntents: intentRes.activeSiblingIntents,
|
|
4192
|
+
..."waitedMs" in intentRes && "waitTimedOut" in intentRes ? { waitedMs: intentRes.waitedMs, waitTimedOut: intentRes.waitTimedOut } : {},
|
|
4065
4193
|
publishedContracts: contracts
|
|
4066
4194
|
});
|
|
4067
4195
|
}
|
|
@@ -4104,12 +4232,22 @@ function buildKithMcpServer(repoRoot, pinned) {
|
|
|
4104
4232
|
sessionToken: z.string().optional(),
|
|
4105
4233
|
patterns: z.array(z.string()),
|
|
4106
4234
|
mode: z.enum(["exclusive", "shared"]).optional(),
|
|
4107
|
-
reason: z.string().optional()
|
|
4235
|
+
reason: z.string().optional(),
|
|
4236
|
+
waitTimeoutMs: z.number().int().min(0).max(6e5).optional()
|
|
4108
4237
|
},
|
|
4109
4238
|
async (args, extra, { engine: engine2, register, authorize, formatReply }) => {
|
|
4110
4239
|
authorize(args);
|
|
4111
|
-
const
|
|
4112
|
-
|
|
4240
|
+
const execution = args.waitTimeoutMs === void 0 ? void 0 : engine2.acquireLocksWhenAvailable({
|
|
4241
|
+
...args,
|
|
4242
|
+
waitTimeoutMs: args.waitTimeoutMs,
|
|
4243
|
+
signal: AbortSignal.any([disconnected.signal, extra.signal])
|
|
4244
|
+
});
|
|
4245
|
+
if (execution) commands.add(execution);
|
|
4246
|
+
try {
|
|
4247
|
+
return formatReply(args.agentId, execution ? await execution : engine2.locks.acquireLocks(args));
|
|
4248
|
+
} finally {
|
|
4249
|
+
if (execution) commands.delete(execution);
|
|
4250
|
+
}
|
|
4113
4251
|
}
|
|
4114
4252
|
);
|
|
4115
4253
|
tool(
|
|
@@ -4206,6 +4344,45 @@ function buildKithMcpServer(repoRoot, pinned) {
|
|
|
4206
4344
|
}
|
|
4207
4345
|
}
|
|
4208
4346
|
);
|
|
4347
|
+
tool(
|
|
4348
|
+
"kith_run_dev_server",
|
|
4349
|
+
"Run a literal argv command with an allocated port in PORT/KITH_PORT and substituted {port} arguments. Renews the lease while running; cancellation/disconnect stops the process group before release. Uses coordinator permissions. Requires macOS/Linux; no implicit shell or readiness guarantee.",
|
|
4350
|
+
{
|
|
4351
|
+
agentId: z.string(),
|
|
4352
|
+
sessionToken: z.string().optional(),
|
|
4353
|
+
serviceName: z.string().min(1).max(128),
|
|
4354
|
+
command: z.array(z.string()).min(1),
|
|
4355
|
+
basePort: z.number().int().min(1).max(65535).optional(),
|
|
4356
|
+
cwd: z.string().optional(),
|
|
4357
|
+
leaseDurationMs: z.number().int().min(100).max(864e5).optional(),
|
|
4358
|
+
commandTimeoutMs: z.number().int().min(1).max(6e5).optional(),
|
|
4359
|
+
maxOutputBytes: z.number().int().min(1).max(1048576).optional()
|
|
4360
|
+
},
|
|
4361
|
+
async (args, extra, { engine: engine2, authorize, formatReply }) => {
|
|
4362
|
+
authorize(args);
|
|
4363
|
+
const root = fs16.realpathSync(engine2.fencing.getAgent(args.agentId)?.worktree_path ?? engine2.coordDb.repoRoot);
|
|
4364
|
+
const cwd = fs16.realpathSync(path15.resolve(root, args.cwd ?? "."));
|
|
4365
|
+
const relative = path15.relative(root, cwd);
|
|
4366
|
+
if (relative === ".." || relative.startsWith(".." + path15.sep) || path15.isAbsolute(relative)) {
|
|
4367
|
+
throw new Error("Command cwd must be inside the registered worktree or repository");
|
|
4368
|
+
}
|
|
4369
|
+
const execution = engine2.broker.executeWithPort({
|
|
4370
|
+
...args,
|
|
4371
|
+
cwd,
|
|
4372
|
+
basePort: args.basePort ?? 3e3,
|
|
4373
|
+
commandTimeoutMs: args.commandTimeoutMs ?? 6e4,
|
|
4374
|
+
maxOutputBytes: args.maxOutputBytes ?? 65536,
|
|
4375
|
+
signal: AbortSignal.any([disconnected.signal, extra.signal])
|
|
4376
|
+
});
|
|
4377
|
+
commands.add(execution);
|
|
4378
|
+
try {
|
|
4379
|
+
const result = await execution;
|
|
4380
|
+
return formatReply(args.agentId, { ok: result.exitCode === 0, ...result });
|
|
4381
|
+
} finally {
|
|
4382
|
+
commands.delete(execution);
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
);
|
|
4209
4386
|
tool(
|
|
4210
4387
|
"kith_get_status",
|
|
4211
4388
|
"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.",
|
|
@@ -4613,7 +4790,7 @@ Instruction protocol: ${KITH_PROTOCOL_VERSION}.
|
|
|
4613
4790
|
|
|
4614
4791
|
Use Kith MCP to claim work, isolate changes in worktrees, share local resources and verify integration. Keep one isolated worktree per parallel agent and keep the owning MCP connection open.
|
|
4615
4792
|
|
|
4616
|
-
1. Call \`kith_begin_work\` with a unique \`agentId\`, an explicit \`scopeGlobs\` array and \`isolateWorktree: true\`.
|
|
4793
|
+
1. Select the absolute Git repository path for this task. Include \`repository\` on calls from a global desktop connection; a pinned connection must stay in its configured checkout. Call \`kith_begin_work\` with a unique \`agentId\`, an explicit \`scopeGlobs\` array and \`isolateWorktree: true\`.
|
|
4617
4794
|
2. Edit only the returned \`worktree.worktreePath\`. Report changed files through \`kith_record_file_edit\` on that connection.
|
|
4618
4795
|
3. Run serialized commands with \`kith_run_with_mutex\`: pass a literal executable/argument array. Its cwd defaults to the registered worktree. Commands run with server permissions and require client approval.
|
|
4619
4796
|
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\`.
|
|
@@ -4628,7 +4805,7 @@ If Claude/Cursor edit hooks or CLI commands are also enabled, launch the client
|
|
|
4628
4805
|
}
|
|
4629
4806
|
content = content.includes("<!-- KITH:START -->") ? content.replace(/<!-- KITH:START -->[\s\S]*?<!-- KITH:END -->/, () => block) : content.trimEnd() + "\n\n" + block + "\n";
|
|
4630
4807
|
add("AGENTS.md", content);
|
|
4631
|
-
add(".agent/skills/kith/SKILL.md", '---\nname: kith\ndescription: Coordinate parallel coding work through Kith MCP and isolated worktrees.\n---\n\nPrefer the Kith MCP tools on one persistent connection. Begin with kith_begin_work using a unique agentId, explicit scopeGlobs and isolateWorktree:true. Edit the returned worktreePath, report files with kith_record_file_edit, run literal command arrays with kith_run_with_mutex, heartbeat before expiry and finish with kith_complete_work plus a project verificationCmd. Inspect kith_get_status. Keep capabilities private. If CLI or native hooks are enabled, first launch the client from a persistent shell with the same KITH_AGENT_ID/KITH_SESSION_TOKEN and --owner-pid "$$". Stateless shell calls cannot retain that setup. Locks require cooperating clients.\n');
|
|
4808
|
+
add(".agent/skills/kith/SKILL.md", '---\nname: kith\ndescription: Coordinate parallel coding work through Kith MCP and isolated worktrees.\n---\n\nPrefer the Kith MCP tools on one persistent connection. Begin with kith_begin_work using a unique agentId, explicit scopeGlobs and isolateWorktree:true. Edit the returned worktreePath, report files with kith_record_file_edit, run literal command arrays with kith_run_with_mutex, heartbeat before expiry and finish with kith_complete_work plus a project verificationCmd. Inspect kith_get_status. Keep capabilities private. If CLI or native hooks are enabled, first launch the client from a persistent shell with the same KITH_AGENT_ID/KITH_SESSION_TOKEN and --owner-pid "$$". Stateless shell calls cannot retain that setup. For global desktop connections, include the absolute Git repository in each call; never guess from the server startup directory. Configured tools do not establish ownership until begin-work succeeds. Locks require cooperating clients.\n');
|
|
4632
4809
|
}
|
|
4633
4810
|
if (selected.has("git")) {
|
|
4634
4811
|
try {
|
|
@@ -4752,6 +4929,8 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
|
|
|
4752
4929
|
activeCollisions = /* @__PURE__ */ new Map();
|
|
4753
4930
|
repositories = /* @__PURE__ */ new Map();
|
|
4754
4931
|
errors = [];
|
|
4932
|
+
primaryRoots = /* @__PURE__ */ new Map();
|
|
4933
|
+
worktrees = [];
|
|
4755
4934
|
static notifyMac(title, message) {
|
|
4756
4935
|
if (process.platform !== "darwin") return;
|
|
4757
4936
|
try {
|
|
@@ -4762,6 +4941,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
|
|
|
4762
4941
|
discoverGitRepos() {
|
|
4763
4942
|
this.errors = [];
|
|
4764
4943
|
this.repositories.clear();
|
|
4944
|
+
this.primaryRoots.clear();
|
|
4765
4945
|
const candidates = [];
|
|
4766
4946
|
const marker = (dir) => {
|
|
4767
4947
|
try {
|
|
@@ -4790,6 +4970,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
|
|
|
4790
4970
|
const layout = discoverRepository(candidate);
|
|
4791
4971
|
if (!layout.commonGitDir) throw new Error("Git repository unavailable");
|
|
4792
4972
|
this.repositories.set(layout.checkoutRoot, layout.commonGitDir);
|
|
4973
|
+
if (!layout.isLinkedWorktree) this.primaryRoots.set(layout.commonGitDir, layout.checkoutRoot);
|
|
4793
4974
|
if (inspected.has(layout.commonGitDir)) continue;
|
|
4794
4975
|
inspected.add(layout.commonGitDir);
|
|
4795
4976
|
const listing = execFileSync10("git", ["worktree", "list", "--porcelain", "-z"], { cwd: layout.checkoutRoot, encoding: "utf8", timeout: 4e3, maxBuffer: 4 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -4797,6 +4978,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
|
|
|
4797
4978
|
const linked = discoverRepository(record.slice(9));
|
|
4798
4979
|
if (linked.commonGitDir !== layout.commonGitDir) throw new Error("Worktree repository identity changed during discovery");
|
|
4799
4980
|
this.repositories.set(linked.checkoutRoot, layout.commonGitDir);
|
|
4981
|
+
if (!linked.isLinkedWorktree) this.primaryRoots.set(layout.commonGitDir, linked.checkoutRoot);
|
|
4800
4982
|
}
|
|
4801
4983
|
} catch (error) {
|
|
4802
4984
|
this.errors.push({ repository: candidate, message: `Cannot inspect repository: ${error instanceof Error ? error.message : String(error)}` });
|
|
@@ -4809,10 +4991,11 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
|
|
|
4809
4991
|
const repos = this.discoverGitRepos();
|
|
4810
4992
|
const now = Date.now();
|
|
4811
4993
|
const seen = /* @__PURE__ */ new Set();
|
|
4994
|
+
const dirtyCounts = /* @__PURE__ */ new Map();
|
|
4812
4995
|
const overlaps = /* @__PURE__ */ new Map();
|
|
4813
4996
|
for (const repo of repos) {
|
|
4814
4997
|
try {
|
|
4815
|
-
const { stdout } = await execFileAsync("git", ["-c", "core.fsmonitor=false", "status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: repo, timeout: 4e3, maxBuffer: 4 * 1024 * 1024 });
|
|
4998
|
+
const { stdout } = await execFileAsync("git", ["-c", "core.fsmonitor=false", "status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: repo, timeout: 4e3, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
|
|
4816
4999
|
const records = stdout.split("\0");
|
|
4817
5000
|
const dirty = /* @__PURE__ */ new Map();
|
|
4818
5001
|
for (let i = 0; i < records.length; i++) {
|
|
@@ -4822,20 +5005,22 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
|
|
|
4822
5005
|
dirty.set(record.slice(3), status);
|
|
4823
5006
|
if (/[RC]/.test(status)) dirty.set(records[++i], status);
|
|
4824
5007
|
}
|
|
5008
|
+
dirtyCounts.set(repo, [...dirty.keys()].filter((file) => file && file !== ".coord" && !file.startsWith(".coord/")).length);
|
|
4825
5009
|
for (const [file, status] of dirty) {
|
|
4826
5010
|
if (!file || file === ".coord" || file.startsWith(".coord/")) continue;
|
|
4827
5011
|
const key = JSON.stringify([repo, file]);
|
|
4828
5012
|
seen.add(key);
|
|
4829
|
-
let modified
|
|
5013
|
+
let modified, fingerprint = status;
|
|
4830
5014
|
try {
|
|
4831
5015
|
const stat = fs19.lstatSync(path17.join(repo, file));
|
|
4832
|
-
|
|
5016
|
+
if (Number.isFinite(stat.mtimeMs) && stat.mtimeMs <= now) modified = stat.mtimeMs;
|
|
4833
5017
|
fingerprint += `:${stat.mtimeMs}:${stat.size}`;
|
|
4834
5018
|
} catch (error) {
|
|
4835
5019
|
if (error.code !== "ENOENT") throw error;
|
|
4836
5020
|
}
|
|
4837
5021
|
const previous = this.recentEdits.get(key);
|
|
4838
|
-
if (
|
|
5022
|
+
if (modified === void 0) this.recentEdits.delete(key);
|
|
5023
|
+
else if (!previous || previous.fingerprint !== fingerprint) {
|
|
4839
5024
|
this.recentEdits.set(key, { repo, fingerprint, timestamp: modified });
|
|
4840
5025
|
this.options.onFileChange?.({ repoPath: repo, filePath: file, timestamp: modified });
|
|
4841
5026
|
}
|
|
@@ -4877,6 +5062,39 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
|
|
|
4877
5062
|
}
|
|
4878
5063
|
}
|
|
4879
5064
|
this.activeCollisions = this.errors.length ? new Map([...this.activeCollisions, ...current]) : current;
|
|
5065
|
+
const states = /* @__PURE__ */ new Map();
|
|
5066
|
+
this.worktrees = repos.map((repo) => {
|
|
5067
|
+
const common = this.repositories.get(repo);
|
|
5068
|
+
const primary = this.primaryRoots.get(common);
|
|
5069
|
+
const row = {
|
|
5070
|
+
path: repo,
|
|
5071
|
+
label: `${path17.basename(primary ?? path17.dirname(common))} / ${repo === primary ? "primary" : path17.basename(repo)}`,
|
|
5072
|
+
activity: "unknown",
|
|
5073
|
+
agentIds: [],
|
|
5074
|
+
detail: "Ownership unavailable; inspect kith status --offline before recovery.",
|
|
5075
|
+
dirtyFiles: dirtyCounts.get(repo) ?? null,
|
|
5076
|
+
sharedDirtyPaths: [...this.activeCollisions.values()].filter((alert) => alert.repos.includes(repo)).length
|
|
5077
|
+
};
|
|
5078
|
+
try {
|
|
5079
|
+
let state = states.get(common);
|
|
5080
|
+
if (!state) {
|
|
5081
|
+
state = readCoordinationState(primary ?? repo, { ownersOnly: true });
|
|
5082
|
+
states.set(common, state);
|
|
5083
|
+
}
|
|
5084
|
+
if (state.state === "unavailable") {
|
|
5085
|
+
row.detail += ` ${state.error}`;
|
|
5086
|
+
return row;
|
|
5087
|
+
}
|
|
5088
|
+
const owners = state.agents.filter((agent) => agent.worktree_path ? physicalPath(agent.worktree_path) === repo : repo === primary);
|
|
5089
|
+
const active = owners.filter((agent) => agent.status === "active" && Number.isFinite(agent.lease_expires_at) && agent.lease_expires_at > state.observedAt);
|
|
5090
|
+
row.agentIds = (active.length ? active : owners).map((agent) => agent.id);
|
|
5091
|
+
row.activity = active.length ? "active" : owners.length ? "retained" : "unregistered";
|
|
5092
|
+
row.detail = active.length ? "Current recorded Kith lease; does not identify file writers." : owners.length ? `Recorded ${[...new Set(owners.map((agent) => agent.status === "active" ? "expired lease" : agent.status))].join(", ")}; preserve work and inspect before cleanup.` : "No recorded Kith session; activity and ownership outside Kith are unknown.";
|
|
5093
|
+
} catch (error) {
|
|
5094
|
+
row.detail += ` ${error instanceof Error ? error.message : String(error)}`;
|
|
5095
|
+
}
|
|
5096
|
+
return row;
|
|
5097
|
+
});
|
|
4880
5098
|
if (this.errors.length) this.options.onScanError?.(this.errors);
|
|
4881
5099
|
return this.getActiveCollisions();
|
|
4882
5100
|
}
|
|
@@ -4900,7 +5118,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
|
|
|
4900
5118
|
this.watchTimer = null;
|
|
4901
5119
|
}
|
|
4902
5120
|
getScanState() {
|
|
4903
|
-
return { complete: this.errors.length === 0, errors: [...this.errors], repos: [...this.repositories.keys()] };
|
|
5121
|
+
return { complete: this.errors.length === 0, errors: [...this.errors], repos: [...this.repositories.keys()], ownershipComplete: this.worktrees.every((row) => row.activity !== "unknown"), worktrees: this.worktrees.map((row) => ({ ...row, agentIds: [...row.agentIds] })) };
|
|
4904
5122
|
}
|
|
4905
5123
|
getActiveCollisions() {
|
|
4906
5124
|
return [...this.activeCollisions.values()];
|
|
@@ -4914,7 +5132,7 @@ var PassiveConflictWatcher = class _PassiveConflictWatcher {
|
|
|
4914
5132
|
// src/cli/setup-auto.ts
|
|
4915
5133
|
import fs21 from "fs";
|
|
4916
5134
|
import path18 from "path";
|
|
4917
|
-
import
|
|
5135
|
+
import os3 from "os";
|
|
4918
5136
|
import { parse as parseToml3, stringify as stringifyToml2 } from "smol-toml";
|
|
4919
5137
|
|
|
4920
5138
|
// src/cli/runtime.ts
|
|
@@ -4943,7 +5161,7 @@ function ownedLauncher(value) {
|
|
|
4943
5161
|
return false;
|
|
4944
5162
|
}
|
|
4945
5163
|
}
|
|
4946
|
-
function autoConfigureAllClients(targetWorkspace = process.cwd(), home =
|
|
5164
|
+
function autoConfigureAllClients(targetWorkspace = process.cwd(), home = os3.homedir(), options = {}) {
|
|
4947
5165
|
const target = physicalPath(targetWorkspace);
|
|
4948
5166
|
const layout = target === path18.parse(target).root ? void 0 : discoverRepository(target);
|
|
4949
5167
|
const workspace = layout?.checkoutRoot ?? target;
|
|
@@ -5064,7 +5282,7 @@ function parseFlags(argv) {
|
|
|
5064
5282
|
} else {
|
|
5065
5283
|
const key = arg.slice(2);
|
|
5066
5284
|
const next = mainArgs[i + 1];
|
|
5067
|
-
if (!["worktree", "raw", "dry-run", "help", "version", "shell", "new", "mcp-only", "offline", "once", "json", "quiet", "no-notify"].includes(key) && next && !next.startsWith("--")) {
|
|
5285
|
+
if (!["worktree", "wait", "raw", "dry-run", "help", "version", "shell", "new", "mcp-only", "offline", "once", "json", "quiet", "no-notify"].includes(key) && next && !next.startsWith("--")) {
|
|
5068
5286
|
flags[key] = next;
|
|
5069
5287
|
i++;
|
|
5070
5288
|
} else {
|
|
@@ -5077,10 +5295,38 @@ function parseFlags(argv) {
|
|
|
5077
5295
|
}
|
|
5078
5296
|
return { positional, flags, passthroughCmd };
|
|
5079
5297
|
}
|
|
5298
|
+
async function interruptible(run) {
|
|
5299
|
+
const controller = new AbortController();
|
|
5300
|
+
let interrupted;
|
|
5301
|
+
const sigint = () => {
|
|
5302
|
+
interrupted = "SIGINT";
|
|
5303
|
+
controller.abort();
|
|
5304
|
+
};
|
|
5305
|
+
const sigterm = () => {
|
|
5306
|
+
interrupted = "SIGTERM";
|
|
5307
|
+
controller.abort();
|
|
5308
|
+
};
|
|
5309
|
+
process.on("SIGINT", sigint);
|
|
5310
|
+
process.on("SIGTERM", sigterm);
|
|
5311
|
+
try {
|
|
5312
|
+
return await run(controller.signal);
|
|
5313
|
+
} catch (err) {
|
|
5314
|
+
if (!interrupted) throw err;
|
|
5315
|
+
process.exitCode = interrupted === "SIGINT" ? 130 : 143;
|
|
5316
|
+
} finally {
|
|
5317
|
+
process.off("SIGINT", sigint);
|
|
5318
|
+
process.off("SIGTERM", sigterm);
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5080
5321
|
async function main() {
|
|
5081
5322
|
const { positional, flags, passthroughCmd } = parseFlags(process.argv.slice(2));
|
|
5082
5323
|
const cmd = positional[0] ?? "help";
|
|
5083
5324
|
const subCmd = positional[1];
|
|
5325
|
+
const claimWaitTimeout = () => {
|
|
5326
|
+
const value = flags["wait-ms"];
|
|
5327
|
+
if (value !== void 0 && typeof value !== "string") throw new Error("--wait-ms requires a millisecond value");
|
|
5328
|
+
return value === void 0 ? 6e4 : Number(value);
|
|
5329
|
+
};
|
|
5084
5330
|
const targetFromPositional = ["init", "setup", "watch", "menubar", "bar", "tray", "doctor", "recover"].includes(cmd) && positional[1] ? path19.resolve(positional[1]) : void 0;
|
|
5085
5331
|
const repoRoot = typeof flags.repo === "string" ? path19.resolve(flags.repo) : targetFromPositional ?? (cmd === "mcp" ? process.env.CLAUDE_PROJECT_DIR : void 0) ?? process.cwd();
|
|
5086
5332
|
if (flags.version || cmd === "version") {
|
|
@@ -5168,19 +5414,19 @@ async function main() {
|
|
|
5168
5414
|
notify: !isRaw && notify,
|
|
5169
5415
|
onScanError: (errors) => {
|
|
5170
5416
|
const message = errors.map((error) => `${error.repository}: ${error.message}`).join("\n");
|
|
5171
|
-
if (!flags.once && message !== lastScanError) console.error(`Scan unavailable:
|
|
5417
|
+
if (!flags.once && message !== lastScanError) console.error(`Scan unavailable; retained overlaps may be stale:
|
|
5172
5418
|
${message}`);
|
|
5173
5419
|
lastScanError = message;
|
|
5174
5420
|
},
|
|
5175
5421
|
onCollision: (alert) => {
|
|
5176
|
-
if (!isRaw) {
|
|
5422
|
+
if (!isRaw && !flags.once) {
|
|
5177
5423
|
console.log(`
|
|
5178
5424
|
\u26A0\uFE0F [WORKTREE OVERLAP] ${alert.message}`);
|
|
5179
5425
|
}
|
|
5180
5426
|
},
|
|
5181
5427
|
onFileChange: (e) => {
|
|
5182
|
-
if (!flags.quiet && !isRaw) {
|
|
5183
|
-
console.log(`\u{1F4DD} [
|
|
5428
|
+
if (!flags.quiet && !isRaw && !flags.once) {
|
|
5429
|
+
console.log(`\u{1F4DD} [FILE MTIME] ${path19.basename(e.repoPath)}/${e.filePath}`);
|
|
5184
5430
|
}
|
|
5185
5431
|
}
|
|
5186
5432
|
});
|
|
@@ -5198,6 +5444,12 @@ ${message}`);
|
|
|
5198
5444
|
\u{1F4E1} Kith Passive Radar: ${targetDir}`);
|
|
5199
5445
|
console.log(` Repositories (${repos.length}): ${repos.map((r) => path19.basename(r)).join(", ")}`);
|
|
5200
5446
|
console.log(` Recent Modified Files: ${edits.length}`);
|
|
5447
|
+
for (const row of state.worktrees) {
|
|
5448
|
+
const activity = { active: "Active Kith lease", retained: "Retained", unregistered: "No Kith session", unknown: "Ownership unavailable" }[row.activity];
|
|
5449
|
+
console.log(` ${row.label} \u2014 ${activity}; ${row.dirtyFiles ?? "unknown"} Git changes; ${row.sharedDirtyPaths} shared dirty paths`);
|
|
5450
|
+
console.log(` ${row.path}${row.agentIds.length ? ` (${row.agentIds.join(", ")})` : ""}
|
|
5451
|
+
${row.detail}`);
|
|
5452
|
+
}
|
|
5201
5453
|
if (!state.complete) {
|
|
5202
5454
|
console.error(`Scan unavailable:
|
|
5203
5455
|
${state.errors.map((error) => error.message).join("\n")}`);
|
|
@@ -5205,10 +5457,9 @@ ${state.errors.map((error) => error.message).join("\n")}`);
|
|
|
5205
5457
|
console.log("\nNo differing or unverified shared dirty paths observed. Identical copies are excluded; compatibility is not verified.\n");
|
|
5206
5458
|
} else {
|
|
5207
5459
|
console.log(`
|
|
5208
|
-
\u26A0\uFE0F
|
|
5460
|
+
\u26A0\uFE0F Review before integration: ${alerts.length} shared dirty paths:`);
|
|
5209
5461
|
for (const a of alerts) {
|
|
5210
5462
|
console.log(` \u26A0\uFE0F ${a.message}`);
|
|
5211
|
-
console.log(` Files: ${a.filePath}`);
|
|
5212
5463
|
}
|
|
5213
5464
|
console.log("");
|
|
5214
5465
|
}
|
|
@@ -5223,7 +5474,7 @@ ${state.errors.map((error) => error.message).join("\n")}`);
|
|
|
5223
5474
|
});
|
|
5224
5475
|
return;
|
|
5225
5476
|
}
|
|
5226
|
-
const commands = ["session", "status", "begin", "complete", "heartbeat", "task", "lock", "contract", "worktree", "port", "mutex", "hook"];
|
|
5477
|
+
const commands = ["session", "status", "begin", "complete", "heartbeat", "task", "lock", "contract", "worktree", "port", "dev", "mutex", "hook"];
|
|
5227
5478
|
if (!commands.includes(cmd)) throw new Error(`Unknown command '${cmd}'. Run kith help.`);
|
|
5228
5479
|
const subcommands = {
|
|
5229
5480
|
session: ["start", "end"],
|
|
@@ -5246,7 +5497,7 @@ ${state.errors.map((error) => error.message).join("\n")}`);
|
|
|
5246
5497
|
const register = () => engine.fencing.registerAgent({
|
|
5247
5498
|
id: agentId,
|
|
5248
5499
|
client: String(flags.client ?? "cli"),
|
|
5249
|
-
pid: typeof flags["owner-pid"] === "string" ? Number(flags["owner-pid"]) :
|
|
5500
|
+
pid: typeof flags["owner-pid"] === "string" ? Number(flags["owner-pid"]) : ["mutex", "dev"].includes(cmd) && !sessionToken ? process.pid : process.ppid,
|
|
5250
5501
|
sessionToken,
|
|
5251
5502
|
leaseDurationMs: typeof flags["lease-ms"] === "string" ? Number(flags["lease-ms"]) : void 0
|
|
5252
5503
|
});
|
|
@@ -5285,13 +5536,22 @@ export KITH_SESSION_TOKEN=${quote(session.sessionToken)}`);
|
|
|
5285
5536
|
const scopeRaw = typeof flags.scope === "string" ? flags.scope : "";
|
|
5286
5537
|
const scopeGlobs = scopeRaw ? scopeRaw.split(",").map((s) => s.trim()) : ["**"];
|
|
5287
5538
|
const session = register();
|
|
5288
|
-
const
|
|
5539
|
+
const options = {
|
|
5289
5540
|
agentId,
|
|
5290
5541
|
title,
|
|
5291
5542
|
intentSummary: typeof flags.summary === "string" ? flags.summary : title,
|
|
5292
5543
|
scopeGlobs,
|
|
5293
5544
|
isolateWorktree: Boolean(flags.worktree)
|
|
5294
|
-
}
|
|
5545
|
+
};
|
|
5546
|
+
const res = flags.wait || flags["wait-ms"] !== void 0 ? await interruptible((signal) => engine.beginWorkWhenAvailable({
|
|
5547
|
+
...options,
|
|
5548
|
+
signal,
|
|
5549
|
+
waitTimeoutMs: claimWaitTimeout()
|
|
5550
|
+
})) : engine.beginWork(options);
|
|
5551
|
+
if (!res) {
|
|
5552
|
+
if (!sessionToken) engine.fencing.abandonAgentSession(agentId);
|
|
5553
|
+
return;
|
|
5554
|
+
}
|
|
5295
5555
|
console.log(JSON.stringify({ ...res, sessionToken: session.sessionToken }, null, 2));
|
|
5296
5556
|
process.exitCode = res.ok ? 0 : 1;
|
|
5297
5557
|
return;
|
|
@@ -5337,7 +5597,16 @@ export KITH_SESSION_TOKEN=${quote(session.sessionToken)}`);
|
|
|
5337
5597
|
if (subCmd === "acquire") {
|
|
5338
5598
|
const patterns = String(flags.patterns ?? flags.scope ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
5339
5599
|
const session = register();
|
|
5340
|
-
const res =
|
|
5600
|
+
const res = flags.wait || flags["wait-ms"] !== void 0 ? await interruptible((signal) => engine.acquireLocksWhenAvailable({
|
|
5601
|
+
agentId,
|
|
5602
|
+
patterns,
|
|
5603
|
+
signal,
|
|
5604
|
+
waitTimeoutMs: claimWaitTimeout()
|
|
5605
|
+
})) : engine.locks.acquireLocks({ agentId, patterns });
|
|
5606
|
+
if (!res) {
|
|
5607
|
+
if (!sessionToken) engine.fencing.abandonAgentSession(agentId);
|
|
5608
|
+
return;
|
|
5609
|
+
}
|
|
5341
5610
|
console.log(JSON.stringify({ ...res, sessionToken: session.sessionToken }, null, 2));
|
|
5342
5611
|
process.exitCode = res.ok ? 0 : 1;
|
|
5343
5612
|
return;
|
|
@@ -5405,9 +5674,10 @@ export KITH_SESSION_TOKEN=${quote(session.sessionToken)}`);
|
|
|
5405
5674
|
}
|
|
5406
5675
|
return;
|
|
5407
5676
|
}
|
|
5408
|
-
if (cmd === "mutex") {
|
|
5677
|
+
if (cmd === "mutex" || cmd === "dev") {
|
|
5409
5678
|
const mutexName = String(flags.name ?? "default-mutex");
|
|
5410
5679
|
const commandToRun = passthroughCmd.length ? passthroughCmd : String(flags.cmd ?? "");
|
|
5680
|
+
if (cmd === "dev" && (!passthroughCmd.length || !passthroughCmd[0])) throw new Error("Provide a literal command after `--` for kith dev");
|
|
5411
5681
|
if (!commandToRun) {
|
|
5412
5682
|
console.error("Error: provide a command after `--` for `kith mutex`");
|
|
5413
5683
|
process.exitCode = 1;
|
|
@@ -5425,17 +5695,24 @@ export KITH_SESSION_TOKEN=${quote(session.sessionToken)}`);
|
|
|
5425
5695
|
process.on("SIGINT", sigint);
|
|
5426
5696
|
process.on("SIGTERM", sigterm);
|
|
5427
5697
|
try {
|
|
5428
|
-
const
|
|
5698
|
+
const shared = {
|
|
5429
5699
|
agentId,
|
|
5430
|
-
mutexName,
|
|
5431
|
-
command: commandToRun,
|
|
5432
5700
|
signal: controller.signal,
|
|
5433
5701
|
leaseDurationMs: typeof flags["lease-ms"] === "string" ? Number(flags["lease-ms"]) : void 0,
|
|
5434
5702
|
timeoutMs: typeof flags["wait-ms"] === "string" ? Number(flags["wait-ms"]) : void 0,
|
|
5435
5703
|
commandTimeoutMs: typeof flags["timeout-ms"] === "string" ? Number(flags["timeout-ms"]) : void 0,
|
|
5436
5704
|
onStdout: (chunk) => process.stdout.write(chunk),
|
|
5437
5705
|
onStderr: (chunk) => process.stderr.write(chunk)
|
|
5438
|
-
}
|
|
5706
|
+
};
|
|
5707
|
+
const res = cmd === "dev" ? await engine.broker.executeWithPort({
|
|
5708
|
+
...shared,
|
|
5709
|
+
command: passthroughCmd,
|
|
5710
|
+
serviceName: String(flags.service ?? "web"),
|
|
5711
|
+
basePort: flags.base === void 0 ? 3e3 : Number(flags.base),
|
|
5712
|
+
cwd: path19.resolve(engine.fencing.getAgent(agentId)?.worktree_path ?? engine.coordDb.repoRoot, typeof flags.cwd === "string" ? flags.cwd : "."),
|
|
5713
|
+
onPort: (port) => process.stderr.write(`Kith: ${String(flags.service ?? "web")} allocated port ${port} (PORT, KITH_PORT, {port})
|
|
5714
|
+
`)
|
|
5715
|
+
}) : await engine.broker.executeWithMutex({ ...shared, mutexName, command: commandToRun });
|
|
5439
5716
|
process.exitCode = res.exitCode;
|
|
5440
5717
|
} catch (err) {
|
|
5441
5718
|
if (interrupted) process.exitCode = interrupted === "SIGINT" ? 130 : 143;
|
|
@@ -5549,7 +5826,9 @@ Usage:
|
|
|
5549
5826
|
kith lock acquire|release|list Manage fencing-token file/glob locks
|
|
5550
5827
|
kith contract publish|list Manage Contract Blackboard
|
|
5551
5828
|
kith worktree spawn|remove|list|preview Manage isolated Git worktrees
|
|
5552
|
-
kith port --agent <id> --service <name>
|
|
5829
|
+
kith port --agent <id> --service <name> Reserve an available TCP port
|
|
5830
|
+
kith dev --agent <id> -- <cmd> Run with PORT and {port} argument substitution
|
|
5831
|
+
kith begin/lock acquire ... --wait --wait-ms 60000 Wait for a scope claim (bounded, cancellable)
|
|
5553
5832
|
kith mutex --agent <id> --name <m> -- <cmd> Serialize commands via mutex
|
|
5554
5833
|
kith hook pre|post|pre-commit Tool & git hook handlers
|
|
5555
5834
|
`);
|