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