kith-coord 0.1.5 → 0.1.7
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 +19 -0
- package/README.md +51 -24
- package/bin/kith-menubar +0 -0
- package/dist/THIRD_PARTY_NOTICES.txt +80 -0
- package/dist/cli/index.js +29486 -3894
- package/dist/cli/index.js.map +1 -1
- package/dist/command-worker.js +2 -0
- package/dist/command-worker.js.map +1 -1
- package/dist/index.d.ts +195 -3
- package/dist/index.js +600 -188
- package/dist/index.js.map +1 -1
- package/docs/client-validation.md +16 -2
- package/docs/integrations.md +14 -3
- package/docs/protocol/tools.v1.json +88 -1
- package/docs/protocol.md +45 -4
- package/docs/reliability.md +23 -1
- 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/index.js
CHANGED
|
@@ -1,40 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
async function waitForClaim(params) {
|
|
3
|
-
const { waitTimeoutMs, signal } = params;
|
|
4
|
-
if (!Number.isSafeInteger(waitTimeoutMs) || waitTimeoutMs < 0 || waitTimeoutMs > 6e5) {
|
|
5
|
-
throw new Error("waitTimeoutMs must be an integer between 0 and 600000");
|
|
6
|
-
}
|
|
7
|
-
const started = performance.now();
|
|
8
|
-
let previous;
|
|
9
|
-
for (; ; ) {
|
|
10
|
-
signal?.throwIfAborted();
|
|
11
|
-
const elapsed = Math.round(performance.now() - started);
|
|
12
|
-
if (previous && elapsed >= waitTimeoutMs) return { ...previous, waitedMs: elapsed, waitTimedOut: true };
|
|
13
|
-
params.heartbeat();
|
|
14
|
-
const result = params.attempt();
|
|
15
|
-
const waitedMs = Math.round(performance.now() - started);
|
|
16
|
-
if (result.ok || waitedMs >= waitTimeoutMs) return { ...result, waitedMs, waitTimedOut: !result.ok };
|
|
17
|
-
previous = result;
|
|
18
|
-
await new Promise((resolve, reject) => {
|
|
19
|
-
const abort = () => {
|
|
20
|
-
clearTimeout(timer);
|
|
21
|
-
reject(signal.reason);
|
|
22
|
-
};
|
|
23
|
-
const timer = setTimeout(() => {
|
|
24
|
-
signal?.removeEventListener("abort", abort);
|
|
25
|
-
resolve();
|
|
26
|
-
}, Math.min(100, waitTimeoutMs - waitedMs));
|
|
27
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
1
|
+
import { createRequire as __kithRequire } from 'node:module'; const require = __kithRequire(import.meta.url);
|
|
31
2
|
|
|
32
|
-
// src/
|
|
33
|
-
|
|
34
|
-
var KITH_PROTOCOL_VERSION = "1.3.0";
|
|
35
|
-
|
|
36
|
-
// src/core/workspace-observation.ts
|
|
37
|
-
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
3
|
+
// src/core/engine.ts
|
|
4
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
38
5
|
|
|
39
6
|
// src/core/repository.ts
|
|
40
7
|
import { execFileSync } from "child_process";
|
|
@@ -86,16 +53,16 @@ function discoverRepository(input) {
|
|
|
86
53
|
throw new Error("Cannot coordinate a filesystem root; select an explicit project with --repo.");
|
|
87
54
|
}
|
|
88
55
|
if (!fs.statSync(directory).isDirectory()) throw new Error(`Not a directory: ${input}`);
|
|
89
|
-
const
|
|
56
|
+
const git3 = (...args) => execFileSync("git", ["rev-parse", ...args], {
|
|
90
57
|
cwd: directory,
|
|
91
58
|
encoding: "utf8",
|
|
92
59
|
stdio: ["ignore", "pipe", "pipe"]
|
|
93
60
|
}).replace(/\r?\n$/, "");
|
|
94
61
|
try {
|
|
95
|
-
const checkoutRoot = fs.realpathSync(
|
|
62
|
+
const checkoutRoot = fs.realpathSync(git3("--show-toplevel"));
|
|
96
63
|
if (!isWithin(checkoutRoot, directory)) throw new Error("Git routing environment selects a different checkout");
|
|
97
|
-
const commonGitDir = fs.realpathSync(
|
|
98
|
-
const isLinkedWorktree = fs.realpathSync(
|
|
64
|
+
const commonGitDir = fs.realpathSync(git3("--path-format=absolute", "--git-common-dir"));
|
|
65
|
+
const isLinkedWorktree = fs.realpathSync(git3("--absolute-git-dir")) !== commonGitDir;
|
|
99
66
|
const coordDir = path.basename(commonGitDir) === ".git" ? path.join(path.dirname(commonGitDir), ".coord") : path.join(commonGitDir, "kith");
|
|
100
67
|
return { checkoutRoot, commonGitDir, coordDir, isLinkedWorktree };
|
|
101
68
|
} catch (error) {
|
|
@@ -135,7 +102,43 @@ function caseSensitivePaths(root) {
|
|
|
135
102
|
return false;
|
|
136
103
|
}
|
|
137
104
|
|
|
105
|
+
// src/core/claim-wait.ts
|
|
106
|
+
async function waitForClaim(params) {
|
|
107
|
+
const { waitTimeoutMs, signal } = params;
|
|
108
|
+
if (!Number.isSafeInteger(waitTimeoutMs) || waitTimeoutMs < 0 || waitTimeoutMs > 6e5) {
|
|
109
|
+
throw new Error("waitTimeoutMs must be an integer between 0 and 600000");
|
|
110
|
+
}
|
|
111
|
+
const started = performance.now();
|
|
112
|
+
let previous;
|
|
113
|
+
for (; ; ) {
|
|
114
|
+
signal?.throwIfAborted();
|
|
115
|
+
const elapsed = Math.round(performance.now() - started);
|
|
116
|
+
if (previous && elapsed >= waitTimeoutMs) return { ...previous, waitedMs: elapsed, waitTimedOut: true };
|
|
117
|
+
params.heartbeat();
|
|
118
|
+
const result = params.attempt();
|
|
119
|
+
const waitedMs = Math.round(performance.now() - started);
|
|
120
|
+
if (result.ok || waitedMs >= waitTimeoutMs) return { ...result, waitedMs, waitTimedOut: !result.ok };
|
|
121
|
+
previous = result;
|
|
122
|
+
await new Promise((resolve, reject) => {
|
|
123
|
+
const abort = () => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
reject(signal.reason);
|
|
126
|
+
};
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
signal?.removeEventListener("abort", abort);
|
|
129
|
+
resolve();
|
|
130
|
+
}, Math.min(100, waitTimeoutMs - waitedMs));
|
|
131
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/version.ts
|
|
137
|
+
var KITH_VERSION = "0.1.7";
|
|
138
|
+
var KITH_PROTOCOL_VERSION = "1.5.0";
|
|
139
|
+
|
|
138
140
|
// src/core/workspace-observation.ts
|
|
141
|
+
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
139
142
|
function git(cwd, ...args) {
|
|
140
143
|
return execFileSync2("git", ["-c", "core.fsmonitor=false", ...args], {
|
|
141
144
|
cwd,
|
|
@@ -264,8 +267,8 @@ function previewIntegration(repoRoot, agent) {
|
|
|
264
267
|
}
|
|
265
268
|
|
|
266
269
|
// src/core/engine.ts
|
|
267
|
-
import
|
|
268
|
-
import
|
|
270
|
+
import fs14 from "fs";
|
|
271
|
+
import path13 from "path";
|
|
269
272
|
|
|
270
273
|
// src/core/ast-analyzer.ts
|
|
271
274
|
import crypto from "crypto";
|
|
@@ -909,6 +912,7 @@ var LockEngine = class _LockEngine {
|
|
|
909
912
|
const first = segments.findIndex((s) => /[?*]/u.test(s));
|
|
910
913
|
if (first < 0) return false;
|
|
911
914
|
const prefix = segments.slice(0, first).join("/");
|
|
915
|
+
const recursivePrefix = Boolean(prefix) && segments.length === first + 1 && segments[first] === "**";
|
|
912
916
|
const roots = /* @__PURE__ */ new Set([this.coordDb.repoRoot, ...this.fencing.listAgents(true).map((a) => a.worktree_path).filter((p) => Boolean(p))]);
|
|
913
917
|
const pending = [...roots].map((root) => ({ root, relative: prefix }));
|
|
914
918
|
let budget = 1e4;
|
|
@@ -925,7 +929,17 @@ var LockEngine = class _LockEngine {
|
|
|
925
929
|
for (const entry of entries) {
|
|
926
930
|
if (--budget < 0) return true;
|
|
927
931
|
const file = relative ? relative + "/" + entry.name : entry.name;
|
|
928
|
-
if (entry.isSymbolicLink())
|
|
932
|
+
if (entry.isSymbolicLink()) {
|
|
933
|
+
if (!recursivePrefix) return true;
|
|
934
|
+
try {
|
|
935
|
+
const scopeRoot = physicalPath(path4.join(root, prefix));
|
|
936
|
+
const linkTarget = fs4.realpathSync(path4.join(dir, entry.name));
|
|
937
|
+
if (!isWithin(scopeRoot, linkTarget)) return true;
|
|
938
|
+
continue;
|
|
939
|
+
} catch {
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
929
943
|
const fold = (value) => value.normalize("NFC").toLowerCase().replace(/ς/g, "\u03C3");
|
|
930
944
|
if (!_LockEngine.patternsOverlap(fold(pattern), fold(file)) && !_LockEngine.patternsOverlap(fold(pattern), fold(file + "/**"))) continue;
|
|
931
945
|
if (entry.isDirectory()) pending.push({ root, relative: file });
|
|
@@ -1725,7 +1739,7 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
1725
1739
|
worker.once("error", reject);
|
|
1726
1740
|
}), stopped]);
|
|
1727
1741
|
if (params.signal?.aborted) failure ??= abortError();
|
|
1728
|
-
if (failure) throw failure;
|
|
1742
|
+
if (failure) throw Object.assign(failure, { stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString() });
|
|
1729
1743
|
if (!outcome) throw new Error("Command worker exited without a result");
|
|
1730
1744
|
if (outcome.error) throw new Error(outcome.error);
|
|
1731
1745
|
return { stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString(), exitCode: outcome.exitCode, waitedMs };
|
|
@@ -1941,7 +1955,7 @@ var CoordDatabase = class _CoordDatabase {
|
|
|
1941
1955
|
}
|
|
1942
1956
|
if (!tables.some((table) => table.name === "meta")) throw new Error("Missing coordination schema metadata. Preserve the database before repair.");
|
|
1943
1957
|
const row = db.prepare("SELECT value FROM meta WHERE key='schema_version'").get();
|
|
1944
|
-
if (row?.value !== "1" && row?.value !== "2") {
|
|
1958
|
+
if (row?.value !== "1" && row?.value !== "2" && row?.value !== "3") {
|
|
1945
1959
|
throw new Error(`Unsupported coordination schema version: ${String(row?.value)}. Preserve the database before upgrading or repairing it.`);
|
|
1946
1960
|
}
|
|
1947
1961
|
const version = Number(row.value);
|
|
@@ -1956,7 +1970,8 @@ var CoordDatabase = class _CoordDatabase {
|
|
|
1956
1970
|
"resources",
|
|
1957
1971
|
"quarantine",
|
|
1958
1972
|
"events",
|
|
1959
|
-
...version
|
|
1973
|
+
...version >= 2 ? ["agent_sessions", "mutex_waiters", "mutex_runs"] : [],
|
|
1974
|
+
...version >= 3 ? ["mcp_operations"] : []
|
|
1960
1975
|
];
|
|
1961
1976
|
if (required.some((name) => !tables.some((table) => table.name === name))) {
|
|
1962
1977
|
throw new Error("Incomplete coordination schema. Preserve the database before repair.");
|
|
@@ -1966,11 +1981,12 @@ var CoordDatabase = class _CoordDatabase {
|
|
|
1966
1981
|
tasks: "status,owner_agent_id,fencing_token,scope_globs_json,depends_on_json",
|
|
1967
1982
|
locks: "owner_agent_id,pattern,mode,fencing_token,expires_at",
|
|
1968
1983
|
resources: "type,name,owner_agent_id,fencing_token,expires_at",
|
|
1969
|
-
...version
|
|
1984
|
+
...version >= 2 ? {
|
|
1970
1985
|
agent_sessions: "agent_id,token_hash,epoch",
|
|
1971
1986
|
mutex_waiters: "ticket,mutex_name,request_id,owner_agent_id,session_epoch,waiter_pid,waiter_start,expires_at",
|
|
1972
1987
|
mutex_runs: "mutex_name,run_id,owner_agent_id,session_epoch,fencing_token,supervisor_pid,supervisor_start,worker_pid,worker_start"
|
|
1973
|
-
} : {}
|
|
1988
|
+
} : {},
|
|
1989
|
+
...version >= 3 ? { mcp_operations: "operation_key,request_hash,agent_id,session_epoch,capability_hash,owner_pid,owner_start,result_json,created_at" } : {}
|
|
1974
1990
|
};
|
|
1975
1991
|
for (const [table, fields] of Object.entries(columns)) {
|
|
1976
1992
|
try {
|
|
@@ -1985,7 +2001,8 @@ var CoordDatabase = class _CoordDatabase {
|
|
|
1985
2001
|
"SELECT fencing_token FROM tasks",
|
|
1986
2002
|
"SELECT fencing_token FROM resources",
|
|
1987
2003
|
"SELECT fencing_token FROM merge_queue",
|
|
1988
|
-
...version
|
|
2004
|
+
...version >= 2 ? ["SELECT epoch FROM agent_sessions", "SELECT session_epoch FROM mutex_waiters", "SELECT fencing_token FROM mutex_runs"] : [],
|
|
2005
|
+
...version >= 3 ? ["SELECT session_epoch FROM mcp_operations"] : []
|
|
1989
2006
|
];
|
|
1990
2007
|
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)) {
|
|
1991
2008
|
throw new Error("Invalid or regressed fencing counter. Preserve the database before repair.");
|
|
@@ -2163,6 +2180,14 @@ var CoordDatabase = class _CoordDatabase {
|
|
|
2163
2180
|
worker_pid INTEGER, worker_start TEXT
|
|
2164
2181
|
);
|
|
2165
2182
|
`);
|
|
2183
|
+
this.db.exec(`
|
|
2184
|
+
CREATE TABLE IF NOT EXISTS mcp_operations (
|
|
2185
|
+
operation_key TEXT PRIMARY KEY, request_hash TEXT NOT NULL,
|
|
2186
|
+
agent_id TEXT NOT NULL, session_epoch INTEGER NOT NULL, capability_hash TEXT NOT NULL,
|
|
2187
|
+
owner_pid INTEGER NOT NULL, owner_start TEXT NOT NULL,
|
|
2188
|
+
result_json TEXT, created_at INTEGER NOT NULL
|
|
2189
|
+
);
|
|
2190
|
+
`);
|
|
2166
2191
|
if (version < 2) {
|
|
2167
2192
|
this.db.exec(`
|
|
2168
2193
|
UPDATE tasks SET status='queued', owner_agent_id=NULL, fencing_token=NULL
|
|
@@ -2174,6 +2199,7 @@ var CoordDatabase = class _CoordDatabase {
|
|
|
2174
2199
|
UPDATE meta SET value='2' WHERE key='schema_version';
|
|
2175
2200
|
`);
|
|
2176
2201
|
}
|
|
2202
|
+
this.db.exec("UPDATE meta SET value='3' WHERE key='schema_version'");
|
|
2177
2203
|
});
|
|
2178
2204
|
}
|
|
2179
2205
|
/**
|
|
@@ -2706,10 +2732,21 @@ var IntentAndTaskEngine = class {
|
|
|
2706
2732
|
return this.coordDb.withTransaction(() => {
|
|
2707
2733
|
this.fencing.assertSession(agentId);
|
|
2708
2734
|
const allTasks = this.listTasks();
|
|
2735
|
+
const activeSiblingIntents = allTasks.filter(
|
|
2736
|
+
(t) => t.status === "active" && t.owner_agent_id !== agentId
|
|
2737
|
+
);
|
|
2709
2738
|
const taskById = new Map(allTasks.map((t) => [t.id, t]));
|
|
2710
2739
|
const queued = allTasks.filter((t) => t.status === "queued").sort((a, b) => b.priority - a.priority || a.created_at - b.created_at);
|
|
2711
2740
|
if (queued.length === 0) {
|
|
2712
|
-
return {
|
|
2741
|
+
return {
|
|
2742
|
+
ok: false,
|
|
2743
|
+
claimed: false,
|
|
2744
|
+
task: null,
|
|
2745
|
+
fencingToken: null,
|
|
2746
|
+
locks: [],
|
|
2747
|
+
activeSiblingIntents,
|
|
2748
|
+
reason: "no_queued_tasks"
|
|
2749
|
+
};
|
|
2713
2750
|
}
|
|
2714
2751
|
for (const candidate of queued) {
|
|
2715
2752
|
const depsSatisfied = candidate.depends_on.every(
|
|
@@ -2745,17 +2782,21 @@ var IntentAndTaskEngine = class {
|
|
|
2745
2782
|
fencingToken
|
|
2746
2783
|
});
|
|
2747
2784
|
return {
|
|
2785
|
+
ok: true,
|
|
2748
2786
|
claimed: true,
|
|
2749
2787
|
task: this.getTask(candidate.id),
|
|
2750
2788
|
fencingToken,
|
|
2751
|
-
locks
|
|
2789
|
+
locks,
|
|
2790
|
+
activeSiblingIntents
|
|
2752
2791
|
};
|
|
2753
2792
|
}
|
|
2754
2793
|
return {
|
|
2794
|
+
ok: false,
|
|
2755
2795
|
claimed: false,
|
|
2756
2796
|
task: null,
|
|
2757
2797
|
fencingToken: null,
|
|
2758
2798
|
locks: [],
|
|
2799
|
+
activeSiblingIntents,
|
|
2759
2800
|
reason: "all_queued_tasks_blocked_by_dependencies_or_locks"
|
|
2760
2801
|
};
|
|
2761
2802
|
});
|
|
@@ -3350,10 +3391,418 @@ var ShadowGitEngine = class {
|
|
|
3350
3391
|
}
|
|
3351
3392
|
};
|
|
3352
3393
|
|
|
3353
|
-
// src/core/
|
|
3394
|
+
// src/core/workspace-contracts.ts
|
|
3354
3395
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
3396
|
+
import fs12 from "fs";
|
|
3397
|
+
import path11 from "path";
|
|
3398
|
+
|
|
3399
|
+
// src/core/observation.ts
|
|
3355
3400
|
import fs11 from "fs";
|
|
3401
|
+
import os from "os";
|
|
3356
3402
|
import path10 from "path";
|
|
3403
|
+
import { createRequire as createRequire2 } from "module";
|
|
3404
|
+
var { DatabaseSync: DatabaseSync2 } = createRequire2(import.meta.url)("node:sqlite");
|
|
3405
|
+
function readCoordinationState(directory = process.cwd(), options = {}) {
|
|
3406
|
+
const layout = discoverRepository(directory), observedAt = Date.now();
|
|
3407
|
+
const empty = {
|
|
3408
|
+
protocolVersion: KITH_PROTOCOL_VERSION,
|
|
3409
|
+
observedAt,
|
|
3410
|
+
repository: { root: layout.checkoutRoot, commonGitDir: layout.commonGitDir, coordDir: layout.coordDir },
|
|
3411
|
+
runtime: { node: process.version },
|
|
3412
|
+
state: "missing",
|
|
3413
|
+
error: void 0,
|
|
3414
|
+
sessionEpochs: {},
|
|
3415
|
+
truncated: false,
|
|
3416
|
+
schemaVersion: 0,
|
|
3417
|
+
fencingEpoch: 0,
|
|
3418
|
+
agents: [],
|
|
3419
|
+
tasks: [],
|
|
3420
|
+
locks: [],
|
|
3421
|
+
conflicts: [],
|
|
3422
|
+
contracts: [],
|
|
3423
|
+
resources: [],
|
|
3424
|
+
mergeQueue: [],
|
|
3425
|
+
quarantined: [],
|
|
3426
|
+
mutexWaiters: [],
|
|
3427
|
+
commandGuards: [],
|
|
3428
|
+
events: [],
|
|
3429
|
+
workspaces: []
|
|
3430
|
+
};
|
|
3431
|
+
let scratch, db;
|
|
3432
|
+
try {
|
|
3433
|
+
const file = path10.join(layout.coordDir, "state.db");
|
|
3434
|
+
if (physicalPath(layout.coordDir) !== layout.coordDir || physicalPath(file) !== file) throw new Error("Coordination state must not be a symlink");
|
|
3435
|
+
for (let current = fs11.realpathSync.native(directory); isWithin(layout.checkoutRoot, current); current = path10.dirname(current)) {
|
|
3436
|
+
const local = path10.join(current, ".coord");
|
|
3437
|
+
if (local !== layout.coordDir && fs11.existsSync(path10.join(local, "state.db"))) throw new Error(`Legacy independent state at ${local}; preserve it before consolidation`);
|
|
3438
|
+
if (current === layout.checkoutRoot) break;
|
|
3439
|
+
}
|
|
3440
|
+
if (!fs11.existsSync(file)) {
|
|
3441
|
+
if (["setup.lock", "merge-train.lock"].some((name) => fs11.existsSync(path10.join(layout.coordDir, name))) || ["-wal", "-shm", "-journal"].some((s) => fs11.existsSync(file + s)) || ["worktrees", "quarantine", "snapshots", "shadow-index", "setup"].some((name) => {
|
|
3442
|
+
const p = path10.join(layout.coordDir, name);
|
|
3443
|
+
return fs11.existsSync(p) && (!fs11.lstatSync(p).isDirectory() || fs11.readdirSync(p).length > 0);
|
|
3444
|
+
})) throw new Error("Missing database with retained sidecars; preserve the recovery set");
|
|
3445
|
+
return empty;
|
|
3446
|
+
}
|
|
3447
|
+
const files = ["", "-wal", "-shm", "-journal"].map((s) => file + s);
|
|
3448
|
+
const identity = (f) => {
|
|
3449
|
+
if (!fs11.existsSync(f)) return null;
|
|
3450
|
+
if (physicalPath(f) !== f || !fs11.lstatSync(f).isFile()) throw new Error("Database or sidecar is redirected or not a regular file");
|
|
3451
|
+
const s = fs11.statSync(f, { bigint: true });
|
|
3452
|
+
return [s.dev, s.ino, s.size, s.mtimeNs, s.ctimeNs].join(":");
|
|
3453
|
+
};
|
|
3454
|
+
const before = files.map(identity);
|
|
3455
|
+
scratch = fs11.mkdtempSync(path10.join(os.tmpdir(), "kith-observe-"));
|
|
3456
|
+
for (let i = 0; i < files.length; i++) if (before[i]) {
|
|
3457
|
+
const target = path10.join(scratch, path10.basename(files[i]));
|
|
3458
|
+
fs11.copyFileSync(files[i], target);
|
|
3459
|
+
fs11.chmodSync(target, 384);
|
|
3460
|
+
}
|
|
3461
|
+
if (JSON.stringify(before) !== JSON.stringify(files.map(identity))) throw new Error("Coordination state changed during snapshot; stop writers or retry");
|
|
3462
|
+
db = new DatabaseSync2(path10.join(scratch, "state.db"), { readOnly: true });
|
|
3463
|
+
db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=1000; BEGIN");
|
|
3464
|
+
const schemaVersion = CoordDatabase.inspectSchema(db);
|
|
3465
|
+
if (schemaVersion < 2) throw new Error("Offline observation requires schema 2 or newer; preserve state and upgrade with clients stopped");
|
|
3466
|
+
const all = (sql) => db.prepare(sql).all();
|
|
3467
|
+
const agents = all("SELECT * FROM agents ORDER BY registered_at DESC" + (options.coordinationOnly ? " LIMIT 201" : ""));
|
|
3468
|
+
if (options.coordinationOnly) {
|
|
3469
|
+
const tasks = all("SELECT * FROM tasks WHERE status IN ('active','blocked','queued') ORDER BY priority DESC, updated_at DESC LIMIT 201").map(({ depends_on_json, scope_globs_json, ...r }) => ({ ...r, depends_on: JSON.parse(String(depends_on_json)), scope_globs: JSON.parse(String(scope_globs_json)) }));
|
|
3470
|
+
const locks = all("SELECT * FROM locks WHERE expires_at > " + observedAt + " ORDER BY acquired_at DESC LIMIT 501");
|
|
3471
|
+
const resources = all("SELECT * FROM resources ORDER BY acquired_at DESC LIMIT 201");
|
|
3472
|
+
const mutexWaiters = all("SELECT ticket,mutex_name,owner_agent_id,session_epoch,expires_at FROM mutex_waiters ORDER BY ticket LIMIT 201");
|
|
3473
|
+
const commandGuards = all("SELECT mutex_name,owner_agent_id,session_epoch,fencing_token FROM mutex_runs LIMIT 201");
|
|
3474
|
+
const mergeQueue = all("SELECT agent_id,branch,worktree_path,status,fencing_token,created_at,updated_at FROM merge_queue WHERE rowid IN (SELECT MAX(rowid) FROM merge_queue GROUP BY agent_id,branch) ORDER BY created_at DESC LIMIT 201");
|
|
3475
|
+
const sessionEpochs = Object.fromEntries(all("SELECT s.agent_id,s.epoch FROM agent_sessions s JOIN agents a ON s.agent_id=a.id ORDER BY a.registered_at DESC LIMIT 201").map((r) => [r.agent_id, r.epoch]));
|
|
3476
|
+
db.exec("COMMIT");
|
|
3477
|
+
return {
|
|
3478
|
+
...empty,
|
|
3479
|
+
state: "ready",
|
|
3480
|
+
schemaVersion,
|
|
3481
|
+
agents,
|
|
3482
|
+
sessionEpochs,
|
|
3483
|
+
tasks,
|
|
3484
|
+
locks,
|
|
3485
|
+
resources,
|
|
3486
|
+
mutexWaiters,
|
|
3487
|
+
commandGuards,
|
|
3488
|
+
mergeQueue,
|
|
3489
|
+
truncated: [agents, tasks, resources, mutexWaiters, commandGuards, mergeQueue].some((rows) => rows.length > 200) || locks.length > 500
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
if (options.ownersOnly) {
|
|
3493
|
+
db.exec("COMMIT");
|
|
3494
|
+
return { ...empty, state: "ready", schemaVersion, agents };
|
|
3495
|
+
}
|
|
3496
|
+
const state = {
|
|
3497
|
+
...empty,
|
|
3498
|
+
state: "ready",
|
|
3499
|
+
schemaVersion,
|
|
3500
|
+
fencingEpoch: Number(db.prepare("SELECT value FROM meta WHERE key='fencing_epoch'").get().value),
|
|
3501
|
+
agents,
|
|
3502
|
+
tasks: all("SELECT * FROM tasks ORDER BY priority DESC,created_at").map(({ depends_on_json, scope_globs_json, ...r }) => ({ ...r, depends_on: JSON.parse(String(depends_on_json)), scope_globs: JSON.parse(String(scope_globs_json)) })),
|
|
3503
|
+
locks: all("SELECT * FROM locks ORDER BY acquired_at"),
|
|
3504
|
+
conflicts: all("SELECT * FROM conflicts ORDER BY detected_at DESC").map(({ details_json, ...r }) => ({ ...r, details: JSON.parse(String(details_json)) })),
|
|
3505
|
+
contracts: all("SELECT * FROM contracts ORDER BY updated_at DESC").map((r) => ({ ...r, is_breaking: Boolean(r.is_breaking) })),
|
|
3506
|
+
resources: all("SELECT * FROM resources ORDER BY acquired_at"),
|
|
3507
|
+
mergeQueue: all("SELECT * FROM merge_queue ORDER BY created_at DESC"),
|
|
3508
|
+
quarantined: all("SELECT * FROM quarantine ORDER BY created_at DESC"),
|
|
3509
|
+
mutexWaiters: all("SELECT ticket,mutex_name,owner_agent_id,session_epoch,expires_at FROM mutex_waiters ORDER BY ticket"),
|
|
3510
|
+
commandGuards: all("SELECT mutex_name,owner_agent_id,worker_pid,supervisor_pid FROM mutex_runs ORDER BY mutex_name"),
|
|
3511
|
+
events: all("SELECT * FROM events ORDER BY id DESC LIMIT 100").map(({ payload_json, ...r }) => ({ ...r, payload: JSON.parse(String(payload_json)) })),
|
|
3512
|
+
workspaces: options.coordinationOnly ? [] : observeWorkspaces(layout.checkoutRoot, agents, observedAt)
|
|
3513
|
+
};
|
|
3514
|
+
db.exec("COMMIT");
|
|
3515
|
+
return state;
|
|
3516
|
+
} catch (error) {
|
|
3517
|
+
return { ...empty, state: "unavailable", error: error instanceof Error ? error.message : String(error) };
|
|
3518
|
+
} finally {
|
|
3519
|
+
db?.close();
|
|
3520
|
+
if (scratch) fs11.rmSync(scratch, { recursive: true, force: true });
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
function recoveryReport(directory = process.cwd()) {
|
|
3524
|
+
const state = readCoordinationState(directory), retainedRoot = path10.join(state.repository.coordDir, "worktrees");
|
|
3525
|
+
const retained = physicalPath(retainedRoot) === retainedRoot && fs11.existsSync(retainedRoot) && fs11.lstatSync(retainedRoot).isDirectory() && !fs11.lstatSync(retainedRoot).isSymbolicLink() ? fs11.readdirSync(retainedRoot, { withFileTypes: true }).map((entry) => ({
|
|
3526
|
+
path: path10.join(retainedRoot, entry.name),
|
|
3527
|
+
symlink: entry.isSymbolicLink(),
|
|
3528
|
+
agentIds: state.agents.filter((a) => a.worktree_path === path10.join(retainedRoot, entry.name)).map((a) => a.id)
|
|
3529
|
+
})) : [];
|
|
3530
|
+
return {
|
|
3531
|
+
...state,
|
|
3532
|
+
retainedWorktrees: retained,
|
|
3533
|
+
expiredClaims: state.locks.filter((lock) => Number(lock.expires_at) <= state.observedAt),
|
|
3534
|
+
unfinishedIntegrations: state.mergeQueue.filter((entry) => entry.status !== "merged"),
|
|
3535
|
+
setupLock: { path: path10.join(state.repository.coordDir, "setup.lock"), present: fs11.existsSync(path10.join(state.repository.coordDir, "setup.lock")) },
|
|
3536
|
+
integrationLock: { path: path10.join(state.repository.coordDir, "merge-train.lock"), present: fs11.existsSync(path10.join(state.repository.coordDir, "merge-train.lock")) }
|
|
3537
|
+
};
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
// src/core/workspace-contracts.ts
|
|
3541
|
+
function buildSwarmPrompts(repoRoot) {
|
|
3542
|
+
const layout = discoverRepository(repoRoot);
|
|
3543
|
+
if (!layout.commonGitDir) throw new Error("Select a Git checkout for task prompts, not a parent workspace.");
|
|
3544
|
+
const cleanRoot = layout.checkoutRoot;
|
|
3545
|
+
return {
|
|
3546
|
+
architect: `Use Kith on repository ${cleanRoot} to plan parallel multi-agent work:
|
|
3547
|
+
1. Call kith_start_session with a unique agentId (e.g. "architect-1") and repository "${cleanRoot}".
|
|
3548
|
+
2. Publish any shared interfaces, routes, or schemas upfront via kith_publish_contract.
|
|
3549
|
+
3. Enqueue parallelizable tasks with disjoint file scopes using kith_add_task (passing title, intentSummary, scopeGlobs, priority, and dependsOn for sequential steps).
|
|
3550
|
+
4. Summarize the queued task IDs and scopes so worker agents can claim them in parallel.`,
|
|
3551
|
+
worker: `Use Kith on repository ${cleanRoot} to claim and execute the next queued task:
|
|
3552
|
+
1. Call kith_claim_next_task with a unique agentId, repository "${cleanRoot}", and isolateWorktree: true.
|
|
3553
|
+
2. Edit only inside the returned worktree.worktreePath and within the task's locked scopeGlobs, respecting publishedContracts and linkedContracts.
|
|
3554
|
+
3. After each file edit, call kith_record_file_edit to check contract blast radius and live dirty-tree conflicts.
|
|
3555
|
+
4. Commit your changes in the worktree and call kith_complete_work with a read-only verificationCmd to verify and merge.`
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
function parseJsoncObject(raw) {
|
|
3559
|
+
let out = "";
|
|
3560
|
+
let inString = false;
|
|
3561
|
+
let stringQuote = '"';
|
|
3562
|
+
let i = 0;
|
|
3563
|
+
while (i < raw.length) {
|
|
3564
|
+
const ch = raw[i];
|
|
3565
|
+
const next = raw[i + 1];
|
|
3566
|
+
if (inString) {
|
|
3567
|
+
out += ch;
|
|
3568
|
+
if (ch === "\\" && i + 1 < raw.length) {
|
|
3569
|
+
out += raw[i + 1];
|
|
3570
|
+
i += 2;
|
|
3571
|
+
continue;
|
|
3572
|
+
}
|
|
3573
|
+
if (ch === stringQuote) {
|
|
3574
|
+
inString = false;
|
|
3575
|
+
}
|
|
3576
|
+
i++;
|
|
3577
|
+
continue;
|
|
3578
|
+
}
|
|
3579
|
+
if (ch === '"' || ch === "'") {
|
|
3580
|
+
inString = true;
|
|
3581
|
+
stringQuote = ch;
|
|
3582
|
+
out += ch;
|
|
3583
|
+
i++;
|
|
3584
|
+
continue;
|
|
3585
|
+
}
|
|
3586
|
+
if (ch === "/" && next === "/") {
|
|
3587
|
+
i += 2;
|
|
3588
|
+
while (i < raw.length && raw[i] !== "\n") i++;
|
|
3589
|
+
continue;
|
|
3590
|
+
}
|
|
3591
|
+
if (ch === "/" && next === "*") {
|
|
3592
|
+
i += 2;
|
|
3593
|
+
while (i + 1 < raw.length && !(raw[i] === "*" && raw[i + 1] === "/")) i++;
|
|
3594
|
+
i += 2;
|
|
3595
|
+
continue;
|
|
3596
|
+
}
|
|
3597
|
+
out += ch;
|
|
3598
|
+
i++;
|
|
3599
|
+
}
|
|
3600
|
+
const withoutTrailingCommas = out.replace(/,\s*([}\]])/g, "$1");
|
|
3601
|
+
try {
|
|
3602
|
+
const parsed = JSON.parse(withoutTrailingCommas);
|
|
3603
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
3604
|
+
} catch {
|
|
3605
|
+
return null;
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
var MAX_SOURCE_BYTES = 64 * 1024;
|
|
3609
|
+
var MAX_REPLY_BYTES = 16 * 1024;
|
|
3610
|
+
var MAX_LINKS = 8;
|
|
3611
|
+
var MAX_SYMBOLS = 64;
|
|
3612
|
+
function git2(root, ...args) {
|
|
3613
|
+
return execFileSync6("git", ["-c", "core.fsmonitor=false", ...args], {
|
|
3614
|
+
cwd: root,
|
|
3615
|
+
encoding: "utf8",
|
|
3616
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3617
|
+
timeout: 2e3,
|
|
3618
|
+
maxBuffer: MAX_SOURCE_BYTES,
|
|
3619
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }
|
|
3620
|
+
}).trim();
|
|
3621
|
+
}
|
|
3622
|
+
function readBounded(file) {
|
|
3623
|
+
const fd = fs12.openSync(file, "r");
|
|
3624
|
+
try {
|
|
3625
|
+
const stat = fs12.fstatSync(fd);
|
|
3626
|
+
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) throw new Error("Contract/config file exceeds inspection limit or is not a regular file");
|
|
3627
|
+
const bytes = Buffer.alloc(MAX_SOURCE_BYTES + 1);
|
|
3628
|
+
const size = fs12.readSync(fd, bytes, 0, bytes.length, 0);
|
|
3629
|
+
if (size > MAX_SOURCE_BYTES) throw new Error("Contract/config file exceeds inspection limit");
|
|
3630
|
+
return bytes.subarray(0, size).toString("utf8");
|
|
3631
|
+
} finally {
|
|
3632
|
+
fs12.closeSync(fd);
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
function resolveFile(base) {
|
|
3636
|
+
for (const name of [base, `${base}.ts`, `${base}.tsx`, `${base}.d.ts`, `${base}.js`, `${base}.jsx`, path11.join(base, "index.ts"), path11.join(base, "index.js")]) {
|
|
3637
|
+
if (fs12.existsSync(name) && fs12.statSync(name).isFile()) return physicalPath(name);
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
function inspectWorkspaceContracts(repoRoot, worktreeDir, imports) {
|
|
3641
|
+
const report = { linkedContracts: [], warnings: [], complete: true };
|
|
3642
|
+
const unavailable = (detail) => {
|
|
3643
|
+
report.complete = false;
|
|
3644
|
+
if (report.warnings.length < 8) report.warnings.push(`[CROSS-REPO UNAVAILABLE] ${detail.slice(0, 300)}`);
|
|
3645
|
+
};
|
|
3646
|
+
try {
|
|
3647
|
+
const layout = discoverRepository(worktreeDir ?? repoRoot);
|
|
3648
|
+
if (!layout.commonGitDir) return report;
|
|
3649
|
+
const activeRoot = layout.checkoutRoot;
|
|
3650
|
+
let config = null;
|
|
3651
|
+
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
3652
|
+
const file = path11.join(activeRoot, name);
|
|
3653
|
+
if (!fs12.existsSync(file)) continue;
|
|
3654
|
+
repositoryFile(activeRoot, file);
|
|
3655
|
+
config = parseJsoncObject(readBounded(file));
|
|
3656
|
+
if (!config) unavailable(`Cannot parse ${name}; aliases were not inspected.`);
|
|
3657
|
+
break;
|
|
3658
|
+
}
|
|
3659
|
+
if (!config) return report;
|
|
3660
|
+
const options = config.compilerOptions;
|
|
3661
|
+
if (config.extends || config.references) {
|
|
3662
|
+
unavailable("Inherited/referenced configuration is not inspected; use your normal compiler checks.");
|
|
3663
|
+
return report;
|
|
3664
|
+
}
|
|
3665
|
+
if (!options?.paths || typeof options.paths !== "object") return report;
|
|
3666
|
+
const caller = discoverRepository(repoRoot);
|
|
3667
|
+
if (caller.commonGitDir !== layout.commonGitDir) {
|
|
3668
|
+
unavailable("Worktree repository identity differs from the caller.");
|
|
3669
|
+
return report;
|
|
3670
|
+
}
|
|
3671
|
+
const bases = [.../* @__PURE__ */ new Set([activeRoot, caller.checkoutRoot])].map((root) => path11.resolve(root, typeof options.baseUrl === "string" ? options.baseUrl : "."));
|
|
3672
|
+
let symbolBudget = MAX_SYMBOLS;
|
|
3673
|
+
const aliases = Object.entries(options.paths).sort(([a], [b]) => b.length - a.length);
|
|
3674
|
+
if (aliases.length > 32) unavailable("Alias limit reached; inspection is partial.");
|
|
3675
|
+
const externalTargets = (targets) => Array.isArray(targets) && targets.slice(0, 8).some((target) => typeof target === "string" && !isWithin(caller.checkoutRoot, physicalPath(path11.resolve(bases[bases.length - 1], target.replace("*", "")))));
|
|
3676
|
+
const selected = imports ? [...new Set(imports)].slice(0, 32) : aliases.slice(0, 32).filter(([a]) => !a.includes("*")).map(([a]) => a);
|
|
3677
|
+
if (imports && imports.length > 32) unavailable("Import limit reached; inspection is partial.");
|
|
3678
|
+
if (!imports && aliases.some(([a, targets]) => a.includes("*") && externalTargets(targets))) unavailable("Wildcard aliases are checked for concrete imports after edits; not enumerated by status.");
|
|
3679
|
+
for (const specifier of selected) {
|
|
3680
|
+
const match = aliases.slice(0, 32).find(([alias2]) => {
|
|
3681
|
+
if (!alias2.includes("*")) return alias2 === specifier;
|
|
3682
|
+
const [prefix, suffix, extra] = alias2.split("*");
|
|
3683
|
+
return extra === void 0 && specifier.startsWith(prefix) && specifier.endsWith(suffix) && specifier.length >= prefix.length + suffix.length;
|
|
3684
|
+
});
|
|
3685
|
+
if (!match || !Array.isArray(match[1])) continue;
|
|
3686
|
+
const [alias, targets] = match;
|
|
3687
|
+
if (!externalTargets(targets)) continue;
|
|
3688
|
+
const star = alias.indexOf("*");
|
|
3689
|
+
const substitution = star < 0 ? "" : specifier.slice(star, specifier.length - (alias.length - star - 1));
|
|
3690
|
+
if (substitution.split(/[\\/]/).some((part) => part === "..")) {
|
|
3691
|
+
unavailable("Alias traversal was not inspected.");
|
|
3692
|
+
continue;
|
|
3693
|
+
}
|
|
3694
|
+
let externalFile;
|
|
3695
|
+
for (const target of targets.slice(0, 8)) {
|
|
3696
|
+
if (typeof target !== "string" || (target.match(/\*/g)?.length ?? 0) > 1) continue;
|
|
3697
|
+
for (const base of bases) {
|
|
3698
|
+
externalFile = resolveFile(path11.resolve(base, target.replace("*", substitution)));
|
|
3699
|
+
if (externalFile) break;
|
|
3700
|
+
}
|
|
3701
|
+
if (externalFile) break;
|
|
3702
|
+
}
|
|
3703
|
+
if (!externalFile) {
|
|
3704
|
+
unavailable(`Cannot resolve '${specifier}'; no compatibility verdict available.`);
|
|
3705
|
+
continue;
|
|
3706
|
+
}
|
|
3707
|
+
if (isWithin(activeRoot, externalFile)) continue;
|
|
3708
|
+
const sibling = discoverRepository(path11.dirname(externalFile));
|
|
3709
|
+
if (!sibling.commonGitDir || sibling.commonGitDir === layout.commonGitDir) {
|
|
3710
|
+
unavailable(`'${specifier}' does not select a separate Git repository; no external source was read.`);
|
|
3711
|
+
continue;
|
|
3712
|
+
}
|
|
3713
|
+
const siblingRoot = sibling.checkoutRoot;
|
|
3714
|
+
const relative = repositoryFile(siblingRoot, externalFile);
|
|
3715
|
+
git2(siblingRoot, "ls-files", "--error-unmatch", "--", relative);
|
|
3716
|
+
if (report.linkedContracts.length >= MAX_LINKS || symbolBudget <= 0) {
|
|
3717
|
+
unavailable("Contract/symbol limit reached; inspection is partial.");
|
|
3718
|
+
break;
|
|
3719
|
+
}
|
|
3720
|
+
const content = readBounded(externalFile);
|
|
3721
|
+
const extracted = AstAnalyzer.extractExports(content);
|
|
3722
|
+
const symbols = extracted.slice(0, symbolBudget).map((s) => ({
|
|
3723
|
+
symbolName: s.symbolName.slice(0, 128),
|
|
3724
|
+
kind: s.kind,
|
|
3725
|
+
signature: `${s.kind} ${s.symbolName}`.slice(0, 160),
|
|
3726
|
+
astHash: s.astHash
|
|
3727
|
+
}));
|
|
3728
|
+
symbolBudget -= symbols.length;
|
|
3729
|
+
const exportsComplete = extracted.length === symbols.length && !/export\s+(?:\*|\{)/.test(content);
|
|
3730
|
+
if (!exportsComplete) unavailable(`'${specifier}' exports are partial or re-exported; missing-export checks are unavailable.`);
|
|
3731
|
+
let dirtyInCheckout = null;
|
|
3732
|
+
try {
|
|
3733
|
+
dirtyInCheckout = Boolean(git2(siblingRoot, "status", "--porcelain=v1", "--", relative));
|
|
3734
|
+
} catch {
|
|
3735
|
+
unavailable(`'${specifier}' Git status is unavailable.`);
|
|
3736
|
+
}
|
|
3737
|
+
const state = readCoordinationState(siblingRoot, { coordinationOnly: true });
|
|
3738
|
+
const ownershipAvailable = state.state === "missing" || state.state === "ready" && state.schemaVersion === 3 && !state.truncated;
|
|
3739
|
+
if (!ownershipAvailable) unavailable(`'${specifier}' sibling ownership is unavailable or partial; inspect its stopped state before recovery.`);
|
|
3740
|
+
const agents = state.state === "ready" ? state.agents.filter((a) => a.status === "active" && a.lease_expires_at > state.observedAt && state.sessionEpochs[a.id] !== void 0) : [];
|
|
3741
|
+
const locked = state.locks.find((l) => agents.some((a) => a.id === l.owner_agent_id) && Number(l.expires_at) > state.observedAt && Number(l.fencing_token) >= state.sessionEpochs[String(l.owner_agent_id)] && LockEngine.patternsOverlap(relative, String(l.pattern)));
|
|
3742
|
+
const drifts = [];
|
|
3743
|
+
for (const agent of agents.slice(0, 8)) {
|
|
3744
|
+
if (!agent.worktree_path || physicalPath(agent.worktree_path) === siblingRoot) continue;
|
|
3745
|
+
try {
|
|
3746
|
+
const wt = discoverRepository(agent.worktree_path);
|
|
3747
|
+
if (wt.checkoutRoot !== physicalPath(agent.worktree_path) || wt.commonGitDir !== sibling.commonGitDir) throw new Error("Worktree identity changed");
|
|
3748
|
+
const file = path11.join(wt.checkoutRoot, relative);
|
|
3749
|
+
repositoryFile(wt.checkoutRoot, file);
|
|
3750
|
+
const current = AstAnalyzer.extractExports(readBounded(file));
|
|
3751
|
+
const original = new Map(extracted.map((s) => [s.symbolName, s.astHash]));
|
|
3752
|
+
const updated = new Map(current.map((s) => [s.symbolName, s.astHash]));
|
|
3753
|
+
const changed = [...updated].filter(([name, hash]) => original.get(name) !== hash).map(([name]) => name);
|
|
3754
|
+
const removed = [...original.keys()].filter((name) => !updated.has(name));
|
|
3755
|
+
if (changed.length || removed.length) drifts.push({ agentId: agent.id, worktreePath: wt.checkoutRoot, changedSymbols: changed.slice(0, 16), removedSymbols: removed.slice(0, 16) });
|
|
3756
|
+
if (changed.length > 16 || removed.length > 16) unavailable(`'${specifier}' drift detail is partial.`);
|
|
3757
|
+
} catch {
|
|
3758
|
+
unavailable(`'${specifier}' recorded worktree is unavailable; drift has not been verified.`);
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
if (agents.length > 8) unavailable(`'${specifier}' owner/worktree inspection limit reached.`);
|
|
3762
|
+
const link = { alias: specifier, sourceRepo: siblingRoot, sourceRepoName: path11.basename(siblingRoot), filePath: relative, absolutePath: externalFile, symbols, exportsComplete, ownershipAvailable, dirtyInCheckout, lockedByAgent: locked ? String(locked.owner_agent_id) : null, activeWorktreeDrifts: drifts };
|
|
3763
|
+
report.linkedContracts.push(link);
|
|
3764
|
+
if (Buffer.byteLength(JSON.stringify(report)) > MAX_REPLY_BYTES - 3e3) {
|
|
3765
|
+
report.linkedContracts.pop();
|
|
3766
|
+
unavailable("Reply byte limit reached; inspection is partial.");
|
|
3767
|
+
break;
|
|
3768
|
+
}
|
|
3769
|
+
if (locked) report.warnings.push(`[CROSS-REPO ACTIVE LOCK] '${specifier}' is claimed by '${locked.owner_agent_id}'.`.slice(0, 300));
|
|
3770
|
+
if (dirtyInCheckout) report.warnings.push(`[CROSS-REPO CONTRACT DRIFT] '${specifier}' has uncommitted changes.`.slice(0, 300));
|
|
3771
|
+
for (const drift of drifts) report.warnings.push(`[CROSS-REPO WORKTREE DRIFT] '${specifier}' has unmerged signature changes in '${drift.agentId}'.`.slice(0, 300));
|
|
3772
|
+
if (report.warnings.length > 8) {
|
|
3773
|
+
report.warnings = report.warnings.slice(0, 7);
|
|
3774
|
+
unavailable("Warning limit reached; inspection is partial.");
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
} catch {
|
|
3778
|
+
unavailable("Contract inspection could not finish; existing claims/work remain valid. Use normal project verification.");
|
|
3779
|
+
}
|
|
3780
|
+
return report;
|
|
3781
|
+
}
|
|
3782
|
+
function verifyCrossRepoImportsForFile(params) {
|
|
3783
|
+
if (Buffer.byteLength(params.content) > MAX_SOURCE_BYTES) return ["[CROSS-REPO UNAVAILABLE] Edited file exceeds inspection limit; use normal project verification."];
|
|
3784
|
+
const imports = AstAnalyzer.extractImports(params.worktreeDir ?? params.repoRoot, params.filePath, params.content);
|
|
3785
|
+
const report = inspectWorkspaceContracts(params.repoRoot, params.worktreeDir, imports.map((i) => i.rawSpecifier));
|
|
3786
|
+
const warnings = [...report.warnings];
|
|
3787
|
+
const namedImports = [...params.content.matchAll(/import\s+(?:type\s+)?(?:[\w$]+\s*,\s*)?\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g)];
|
|
3788
|
+
for (const imp of namedImports.slice(0, 32)) {
|
|
3789
|
+
const link = report.linkedContracts.find((l) => l.alias === imp[2]);
|
|
3790
|
+
if (!link?.exportsComplete) continue;
|
|
3791
|
+
const exported = new Set(link.symbols.map((s) => s.symbolName));
|
|
3792
|
+
for (const binding of imp[1].split(",")) {
|
|
3793
|
+
const symbol = binding.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0];
|
|
3794
|
+
if (!symbol || exported.has(symbol)) continue;
|
|
3795
|
+
if (warnings.length >= 8) break;
|
|
3796
|
+
warnings.push(`[CROSS-REPO CONTRACT MISMATCH] '${params.filePath.slice(0, 120)}' imports '${symbol.slice(0, 120)}' from '${imp[2].slice(0, 120)}', but that module has no such recorded export.`);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
return warnings;
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
// src/core/worktree-cow.ts
|
|
3803
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
3804
|
+
import fs13 from "fs";
|
|
3805
|
+
import path12 from "path";
|
|
3357
3806
|
var CoWWorktreeManager = class _CoWWorktreeManager {
|
|
3358
3807
|
constructor(coordDb, fencing) {
|
|
3359
3808
|
this.coordDb = coordDb;
|
|
@@ -3373,32 +3822,32 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
|
|
|
3373
3822
|
const safeId = params.agentId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
3374
3823
|
const branch = params.branch ?? `kith/${safeId}`;
|
|
3375
3824
|
const baseRef = params.baseRef ?? "HEAD";
|
|
3376
|
-
const worktreePath =
|
|
3825
|
+
const worktreePath = path12.join(this.coordDb.coordDir, "worktrees", safeId);
|
|
3377
3826
|
if (!this.fencing.getAgent(params.agentId)) throw new Error(`Unknown agent: ${params.agentId}`);
|
|
3378
3827
|
if (!safeId || safeId === "." || safeId === "..") throw new Error("Invalid agent ID");
|
|
3379
|
-
if (
|
|
3828
|
+
if (fs13.existsSync(worktreePath)) {
|
|
3380
3829
|
throw new Error(`Worktree already exists: ${worktreePath}. Commit or preserve its work before removing it.`);
|
|
3381
3830
|
}
|
|
3382
|
-
|
|
3383
|
-
|
|
3831
|
+
fs13.mkdirSync(path12.dirname(worktreePath), { recursive: true });
|
|
3832
|
+
execFileSync7("git", ["worktree", "add", "-b", branch, worktreePath, baseRef], {
|
|
3384
3833
|
cwd: this.coordDb.repoRoot,
|
|
3385
3834
|
stdio: "ignore"
|
|
3386
3835
|
});
|
|
3387
3836
|
const hydratedDirs = [];
|
|
3388
3837
|
let hydrationMethod = "none";
|
|
3389
3838
|
for (const dirName of _CoWWorktreeManager.HYDRATABLE_DIRS) {
|
|
3390
|
-
const srcDir =
|
|
3391
|
-
const destDir =
|
|
3392
|
-
if (!
|
|
3839
|
+
const srcDir = path12.join(this.coordDb.repoRoot, dirName);
|
|
3840
|
+
const destDir = path12.join(worktreePath, dirName);
|
|
3841
|
+
if (!fs13.existsSync(srcDir) || fs13.existsSync(destDir)) continue;
|
|
3393
3842
|
const method = this.cloneDirectoryCoW(srcDir, destDir);
|
|
3394
3843
|
hydrationMethod = method;
|
|
3395
3844
|
hydratedDirs.push(dirName);
|
|
3396
3845
|
}
|
|
3397
3846
|
for (const envName of _CoWWorktreeManager.ENV_FILES) {
|
|
3398
|
-
const srcEnv =
|
|
3399
|
-
const destEnv =
|
|
3400
|
-
if (
|
|
3401
|
-
|
|
3847
|
+
const srcEnv = path12.join(this.coordDb.repoRoot, envName);
|
|
3848
|
+
const destEnv = path12.join(worktreePath, envName);
|
|
3849
|
+
if (fs13.existsSync(srcEnv) && !fs13.existsSync(destEnv)) {
|
|
3850
|
+
fs13.copyFileSync(srcEnv, destEnv);
|
|
3402
3851
|
}
|
|
3403
3852
|
}
|
|
3404
3853
|
this.coordDb.withTransaction(() => {
|
|
@@ -3423,7 +3872,7 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
|
|
|
3423
3872
|
cloneDirectoryCoW(srcDir, destDir) {
|
|
3424
3873
|
if (process.platform === "darwin") {
|
|
3425
3874
|
try {
|
|
3426
|
-
|
|
3875
|
+
execFileSync7("cp", ["-cR", srcDir, destDir], {
|
|
3427
3876
|
stdio: "ignore",
|
|
3428
3877
|
timeout: 5e3
|
|
3429
3878
|
});
|
|
@@ -3432,7 +3881,7 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
|
|
|
3432
3881
|
}
|
|
3433
3882
|
} else if (process.platform === "linux") {
|
|
3434
3883
|
try {
|
|
3435
|
-
|
|
3884
|
+
execFileSync7("cp", ["--reflink=always", "-R", srcDir, destDir], {
|
|
3436
3885
|
stdio: "ignore",
|
|
3437
3886
|
timeout: 5e3
|
|
3438
3887
|
});
|
|
@@ -3440,27 +3889,27 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
|
|
|
3440
3889
|
} catch {
|
|
3441
3890
|
}
|
|
3442
3891
|
}
|
|
3443
|
-
|
|
3444
|
-
|
|
3892
|
+
fs13.rmSync(destDir, { recursive: true, force: true });
|
|
3893
|
+
fs13.cpSync(srcDir, destDir, { recursive: true, dereference: false });
|
|
3445
3894
|
return "copy";
|
|
3446
3895
|
}
|
|
3447
3896
|
removeWorktree(agentId) {
|
|
3448
3897
|
this.fencing.assertSession(agentId);
|
|
3449
3898
|
const safeId = agentId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
3450
|
-
const worktreePath =
|
|
3451
|
-
if (!
|
|
3899
|
+
const worktreePath = path12.join(this.coordDb.coordDir, "worktrees", safeId);
|
|
3900
|
+
if (!fs13.existsSync(worktreePath)) return false;
|
|
3452
3901
|
const registered = this.fencing.getAgent(agentId)?.worktree_path;
|
|
3453
3902
|
if (!registered || physicalPath(registered) !== physicalPath(worktreePath) || this.fencing.listAgents(true).some((agent) => agent.id !== agentId && agent.worktree_path && physicalPath(agent.worktree_path) === physicalPath(worktreePath))) {
|
|
3454
3903
|
throw new Error("Refusing to remove a worktree that is not exclusively registered to this agent.");
|
|
3455
3904
|
}
|
|
3456
|
-
const status =
|
|
3905
|
+
const status = execFileSync7("git", ["status", "--porcelain", "--untracked-files=normal", "--ignored=matching"], {
|
|
3457
3906
|
cwd: worktreePath,
|
|
3458
3907
|
encoding: "utf8"
|
|
3459
3908
|
});
|
|
3460
3909
|
if (status.trim()) {
|
|
3461
3910
|
throw new Error(`Refusing to remove dirty worktree: ${worktreePath}. Commit or preserve changed, untracked, and ignored files first.`);
|
|
3462
3911
|
}
|
|
3463
|
-
|
|
3912
|
+
execFileSync7("git", ["worktree", "remove", worktreePath], {
|
|
3464
3913
|
cwd: this.coordDb.repoRoot,
|
|
3465
3914
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3466
3915
|
});
|
|
@@ -3472,7 +3921,7 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
|
|
|
3472
3921
|
}
|
|
3473
3922
|
listWorktrees() {
|
|
3474
3923
|
try {
|
|
3475
|
-
const out =
|
|
3924
|
+
const out = execFileSync7("git", ["worktree", "list", "--porcelain"], {
|
|
3476
3925
|
cwd: this.coordDb.repoRoot,
|
|
3477
3926
|
encoding: "utf8"
|
|
3478
3927
|
});
|
|
@@ -3539,7 +3988,7 @@ var KithEngine = class {
|
|
|
3539
3988
|
const file = LockEngine.normalizeFile(targetRoot, params.filePath);
|
|
3540
3989
|
const access = this.locks.checkFileWriteAccess({ agentId: params.agentId, filePath: file });
|
|
3541
3990
|
if (!access.allowed) throw new Error(access.reason);
|
|
3542
|
-
const content = params.content ?? (
|
|
3991
|
+
const content = params.content ?? (fs14.existsSync(path13.join(targetRoot, file)) ? fs14.readFileSync(path13.join(targetRoot, file), "utf8") : "");
|
|
3543
3992
|
const contractReport = this.depGraph.recordFileModification(
|
|
3544
3993
|
params.agentId,
|
|
3545
3994
|
file,
|
|
@@ -3573,6 +4022,13 @@ var KithEngine = class {
|
|
|
3573
4022
|
`[LIVE DIRTY-TREE CONFLICT] Uncommitted changes in '${conf.file_path}' conflict with active agent '${peer}' (${conf.severity}: ${conf.details.reason}).`
|
|
3574
4023
|
);
|
|
3575
4024
|
}
|
|
4025
|
+
const crossRepoWarnings = verifyCrossRepoImportsForFile({
|
|
4026
|
+
repoRoot: this.coordDb.repoRoot,
|
|
4027
|
+
worktreeDir: targetRoot,
|
|
4028
|
+
filePath: file,
|
|
4029
|
+
content
|
|
4030
|
+
});
|
|
4031
|
+
warnings.push(...crossRepoWarnings);
|
|
3576
4032
|
return {
|
|
3577
4033
|
filePath: file,
|
|
3578
4034
|
contractReport,
|
|
@@ -3624,6 +4080,7 @@ ${lines.join("\n")}` : "";
|
|
|
3624
4080
|
}
|
|
3625
4081
|
getStatus() {
|
|
3626
4082
|
const agents = this.fencing.listAgents(true);
|
|
4083
|
+
const workspaceContracts = inspectWorkspaceContracts(this.coordDb.repoRoot);
|
|
3627
4084
|
return {
|
|
3628
4085
|
protocolVersion: KITH_PROTOCOL_VERSION,
|
|
3629
4086
|
repository: { root: this.coordDb.repoRoot, commonGitDir: this.coordDb.commonGitDir, coordDir: this.coordDb.coordDir },
|
|
@@ -3634,6 +4091,9 @@ ${lines.join("\n")}` : "";
|
|
|
3634
4091
|
tasks: this.intents.listTasks(),
|
|
3635
4092
|
conflicts: this.shadowGit.listConflicts(),
|
|
3636
4093
|
contracts: this.depGraph.listContracts(),
|
|
4094
|
+
linkedContracts: workspaceContracts.linkedContracts,
|
|
4095
|
+
crossRepoWarnings: workspaceContracts.warnings,
|
|
4096
|
+
crossRepoComplete: workspaceContracts.complete,
|
|
3637
4097
|
resources: this.broker.listResources(),
|
|
3638
4098
|
mergeQueue: this.mergeTrain.listQueue(),
|
|
3639
4099
|
quarantined: this.bashGuard.listQuarantined(),
|
|
@@ -3647,9 +4107,70 @@ ${lines.join("\n")}` : "";
|
|
|
3647
4107
|
return previewIntegration(this.coordDb.repoRoot, this.fencing.getAgent(agentId));
|
|
3648
4108
|
}
|
|
3649
4109
|
beginWork(params) {
|
|
3650
|
-
const intent = this.
|
|
3651
|
-
|
|
3652
|
-
|
|
4110
|
+
const { intent, worktree } = this.coordDb.withTransaction(() => {
|
|
4111
|
+
const intent2 = this.intents.declareIntent(params);
|
|
4112
|
+
const worktree2 = intent2.ok && params.isolateWorktree ? this.worktrees.spawnWorktree({ agentId: params.agentId }) : null;
|
|
4113
|
+
return { intent: intent2, worktree: worktree2 };
|
|
4114
|
+
});
|
|
4115
|
+
const workspaceContracts = inspectWorkspaceContracts(
|
|
4116
|
+
this.coordDb.repoRoot,
|
|
4117
|
+
worktree?.worktreePath ?? this.fencing.getAgent(params.agentId)?.worktree_path ?? void 0
|
|
4118
|
+
);
|
|
4119
|
+
return {
|
|
4120
|
+
...intent,
|
|
4121
|
+
worktree,
|
|
4122
|
+
linkedContracts: workspaceContracts.linkedContracts,
|
|
4123
|
+
crossRepoWarnings: workspaceContracts.warnings,
|
|
4124
|
+
crossRepoComplete: workspaceContracts.complete
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
4127
|
+
claimNextTask(params) {
|
|
4128
|
+
const { claimedRes, worktree } = this.coordDb.withTransaction(() => {
|
|
4129
|
+
const claimedRes2 = this.intents.claimNextTask(params.agentId);
|
|
4130
|
+
const worktree2 = claimedRes2.claimed && params.isolateWorktree ? this.claimWorktree(params.agentId) : null;
|
|
4131
|
+
return { claimedRes: claimedRes2, worktree: worktree2 };
|
|
4132
|
+
});
|
|
4133
|
+
const publishedContracts = this.depGraph.listContracts().slice(0, 30);
|
|
4134
|
+
const workspaceContracts = inspectWorkspaceContracts(
|
|
4135
|
+
this.coordDb.repoRoot,
|
|
4136
|
+
worktree?.worktreePath ?? this.fencing.getAgent(params.agentId)?.worktree_path ?? void 0
|
|
4137
|
+
);
|
|
4138
|
+
return {
|
|
4139
|
+
...claimedRes,
|
|
4140
|
+
worktree,
|
|
4141
|
+
publishedContracts,
|
|
4142
|
+
linkedContracts: workspaceContracts.linkedContracts,
|
|
4143
|
+
crossRepoWarnings: workspaceContracts.warnings,
|
|
4144
|
+
crossRepoComplete: workspaceContracts.complete
|
|
4145
|
+
};
|
|
4146
|
+
}
|
|
4147
|
+
claimWorktree(agentId) {
|
|
4148
|
+
this.fencing.assertSession(agentId);
|
|
4149
|
+
const agent = this.fencing.getAgent(agentId);
|
|
4150
|
+
if (!agent.worktree_path) return this.worktrees.spawnWorktree({ agentId });
|
|
4151
|
+
const root = physicalPath(agent.worktree_path);
|
|
4152
|
+
const owner = discoverRepository(this.coordDb.repoRoot);
|
|
4153
|
+
const layout = discoverRepository(root);
|
|
4154
|
+
const git3 = (...args) => execFileSync8("git", args, {
|
|
4155
|
+
cwd: root,
|
|
4156
|
+
encoding: "utf8",
|
|
4157
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4158
|
+
timeout: 5e3
|
|
4159
|
+
}).trim();
|
|
4160
|
+
if (!isWithin(physicalPath(path13.join(this.coordDb.coordDir, "worktrees")), root) || !layout.isLinkedWorktree || layout.checkoutRoot !== root || layout.commonGitDir !== owner.commonGitDir || !agent.branch || git3("symbolic-ref", "--short", "HEAD") !== agent.branch || !this.worktrees.listWorktrees().some((w) => physicalPath(w.path) === root && w.branch === agent.branch) || this.fencing.listAgents(true).some((a) => a.id !== agentId && a.worktree_path && physicalPath(a.worktree_path) === root)) {
|
|
4161
|
+
throw new Error("Saved worktree identity or ownership changed; inspect and preserve it before claiming another task.");
|
|
4162
|
+
}
|
|
4163
|
+
if (git3("status", "--porcelain", "--untracked-files=normal")) {
|
|
4164
|
+
throw new Error("Saved worktree has unfinished changes; preserve or finish them before claiming another task.");
|
|
4165
|
+
}
|
|
4166
|
+
const head = execFileSync8("git", ["rev-parse", "HEAD"], { cwd: this.coordDb.repoRoot, encoding: "utf8" }).trim();
|
|
4167
|
+
if (git3("rev-parse", "HEAD") !== head) {
|
|
4168
|
+
throw new Error("Saved worktree is ahead or behind the current checkout; integrate or replace it explicitly before claiming another task.");
|
|
4169
|
+
}
|
|
4170
|
+
return { agentId, worktreePath: root, branch: agent.branch, hydrationMethod: "none", hydratedDirs: [], durationMs: 0 };
|
|
4171
|
+
}
|
|
4172
|
+
async claimNextTaskWhenAvailable(params) {
|
|
4173
|
+
return this.waitForOwnedClaim(params, () => this.claimNextTask(params));
|
|
3653
4174
|
}
|
|
3654
4175
|
async beginWorkWhenAvailable(params) {
|
|
3655
4176
|
return this.waitForOwnedClaim(params, () => this.beginWork(params));
|
|
@@ -3692,118 +4213,6 @@ ${lines.join("\n")}` : "";
|
|
|
3692
4213
|
this.coordDb.close();
|
|
3693
4214
|
}
|
|
3694
4215
|
};
|
|
3695
|
-
|
|
3696
|
-
// src/core/observation.ts
|
|
3697
|
-
import fs13 from "fs";
|
|
3698
|
-
import os from "os";
|
|
3699
|
-
import path12 from "path";
|
|
3700
|
-
import { createRequire as createRequire2 } from "module";
|
|
3701
|
-
var { DatabaseSync: DatabaseSync2 } = createRequire2(import.meta.url)("node:sqlite");
|
|
3702
|
-
function readCoordinationState(directory = process.cwd(), options = {}) {
|
|
3703
|
-
const layout = discoverRepository(directory), observedAt = Date.now();
|
|
3704
|
-
const empty = {
|
|
3705
|
-
protocolVersion: KITH_PROTOCOL_VERSION,
|
|
3706
|
-
observedAt,
|
|
3707
|
-
repository: { root: layout.checkoutRoot, commonGitDir: layout.commonGitDir, coordDir: layout.coordDir },
|
|
3708
|
-
runtime: { node: process.version },
|
|
3709
|
-
state: "missing",
|
|
3710
|
-
error: void 0,
|
|
3711
|
-
fencingEpoch: 0,
|
|
3712
|
-
agents: [],
|
|
3713
|
-
tasks: [],
|
|
3714
|
-
locks: [],
|
|
3715
|
-
conflicts: [],
|
|
3716
|
-
contracts: [],
|
|
3717
|
-
resources: [],
|
|
3718
|
-
mergeQueue: [],
|
|
3719
|
-
quarantined: [],
|
|
3720
|
-
mutexWaiters: [],
|
|
3721
|
-
commandGuards: [],
|
|
3722
|
-
events: [],
|
|
3723
|
-
workspaces: []
|
|
3724
|
-
};
|
|
3725
|
-
let scratch, db;
|
|
3726
|
-
try {
|
|
3727
|
-
const file = path12.join(layout.coordDir, "state.db");
|
|
3728
|
-
if (physicalPath(layout.coordDir) !== layout.coordDir || physicalPath(file) !== file) throw new Error("Coordination state must not be a symlink");
|
|
3729
|
-
for (let current = fs13.realpathSync.native(directory); isWithin(layout.checkoutRoot, current); current = path12.dirname(current)) {
|
|
3730
|
-
const local = path12.join(current, ".coord");
|
|
3731
|
-
if (local !== layout.coordDir && fs13.existsSync(path12.join(local, "state.db"))) throw new Error(`Legacy independent state at ${local}; preserve it before consolidation`);
|
|
3732
|
-
if (current === layout.checkoutRoot) break;
|
|
3733
|
-
}
|
|
3734
|
-
if (!fs13.existsSync(file)) {
|
|
3735
|
-
if (["setup.lock", "merge-train.lock"].some((name) => fs13.existsSync(path12.join(layout.coordDir, name))) || ["-wal", "-shm", "-journal"].some((s) => fs13.existsSync(file + s)) || ["worktrees", "quarantine", "snapshots", "shadow-index", "setup"].some((name) => {
|
|
3736
|
-
const p = path12.join(layout.coordDir, name);
|
|
3737
|
-
return fs13.existsSync(p) && (!fs13.lstatSync(p).isDirectory() || fs13.readdirSync(p).length > 0);
|
|
3738
|
-
})) throw new Error("Missing database with retained sidecars; preserve the recovery set");
|
|
3739
|
-
return empty;
|
|
3740
|
-
}
|
|
3741
|
-
const files = ["", "-wal", "-shm", "-journal"].map((s) => file + s);
|
|
3742
|
-
const identity = (f) => {
|
|
3743
|
-
if (!fs13.existsSync(f)) return null;
|
|
3744
|
-
if (physicalPath(f) !== f || !fs13.lstatSync(f).isFile()) throw new Error("Database or sidecar is redirected or not a regular file");
|
|
3745
|
-
const s = fs13.statSync(f, { bigint: true });
|
|
3746
|
-
return [s.dev, s.ino, s.size, s.mtimeNs, s.ctimeNs].join(":");
|
|
3747
|
-
};
|
|
3748
|
-
const before = files.map(identity);
|
|
3749
|
-
scratch = fs13.mkdtempSync(path12.join(os.tmpdir(), "kith-observe-"));
|
|
3750
|
-
for (let i = 0; i < files.length; i++) if (before[i]) {
|
|
3751
|
-
const target = path12.join(scratch, path12.basename(files[i]));
|
|
3752
|
-
fs13.copyFileSync(files[i], target);
|
|
3753
|
-
fs13.chmodSync(target, 384);
|
|
3754
|
-
}
|
|
3755
|
-
if (JSON.stringify(before) !== JSON.stringify(files.map(identity))) throw new Error("Coordination state changed during snapshot; stop writers or retry");
|
|
3756
|
-
db = new DatabaseSync2(path12.join(scratch, "state.db"), { readOnly: true });
|
|
3757
|
-
db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=1000; BEGIN");
|
|
3758
|
-
if (CoordDatabase.inspectSchema(db) !== 2) throw new Error("Offline observation requires schema 2; preserve state and upgrade with clients stopped");
|
|
3759
|
-
const all = (sql) => db.prepare(sql).all();
|
|
3760
|
-
const agents = all("SELECT * FROM agents ORDER BY registered_at DESC");
|
|
3761
|
-
if (options.ownersOnly) {
|
|
3762
|
-
db.exec("COMMIT");
|
|
3763
|
-
return { ...empty, state: "ready", agents };
|
|
3764
|
-
}
|
|
3765
|
-
const state = {
|
|
3766
|
-
...empty,
|
|
3767
|
-
state: "ready",
|
|
3768
|
-
fencingEpoch: Number(db.prepare("SELECT value FROM meta WHERE key='fencing_epoch'").get().value),
|
|
3769
|
-
agents,
|
|
3770
|
-
tasks: all("SELECT * FROM tasks ORDER BY priority DESC,created_at").map(({ depends_on_json, scope_globs_json, ...r }) => ({ ...r, depends_on: JSON.parse(String(depends_on_json)), scope_globs: JSON.parse(String(scope_globs_json)) })),
|
|
3771
|
-
locks: all("SELECT * FROM locks ORDER BY acquired_at"),
|
|
3772
|
-
conflicts: all("SELECT * FROM conflicts ORDER BY detected_at DESC").map(({ details_json, ...r }) => ({ ...r, details: JSON.parse(String(details_json)) })),
|
|
3773
|
-
contracts: all("SELECT * FROM contracts ORDER BY updated_at DESC").map((r) => ({ ...r, is_breaking: Boolean(r.is_breaking) })),
|
|
3774
|
-
resources: all("SELECT * FROM resources ORDER BY acquired_at"),
|
|
3775
|
-
mergeQueue: all("SELECT * FROM merge_queue ORDER BY created_at DESC"),
|
|
3776
|
-
quarantined: all("SELECT * FROM quarantine ORDER BY created_at DESC"),
|
|
3777
|
-
mutexWaiters: all("SELECT ticket,mutex_name,owner_agent_id,session_epoch,expires_at FROM mutex_waiters ORDER BY ticket"),
|
|
3778
|
-
commandGuards: all("SELECT mutex_name,owner_agent_id,worker_pid,supervisor_pid FROM mutex_runs ORDER BY mutex_name"),
|
|
3779
|
-
events: all("SELECT * FROM events ORDER BY id DESC LIMIT 100").map(({ payload_json, ...r }) => ({ ...r, payload: JSON.parse(String(payload_json)) })),
|
|
3780
|
-
workspaces: observeWorkspaces(layout.checkoutRoot, agents, observedAt)
|
|
3781
|
-
};
|
|
3782
|
-
db.exec("COMMIT");
|
|
3783
|
-
return state;
|
|
3784
|
-
} catch (error) {
|
|
3785
|
-
return { ...empty, state: "unavailable", error: error instanceof Error ? error.message : String(error) };
|
|
3786
|
-
} finally {
|
|
3787
|
-
db?.close();
|
|
3788
|
-
if (scratch) fs13.rmSync(scratch, { recursive: true, force: true });
|
|
3789
|
-
}
|
|
3790
|
-
}
|
|
3791
|
-
function recoveryReport(directory = process.cwd()) {
|
|
3792
|
-
const state = readCoordinationState(directory), retainedRoot = path12.join(state.repository.coordDir, "worktrees");
|
|
3793
|
-
const retained = physicalPath(retainedRoot) === retainedRoot && fs13.existsSync(retainedRoot) && fs13.lstatSync(retainedRoot).isDirectory() && !fs13.lstatSync(retainedRoot).isSymbolicLink() ? fs13.readdirSync(retainedRoot, { withFileTypes: true }).map((entry) => ({
|
|
3794
|
-
path: path12.join(retainedRoot, entry.name),
|
|
3795
|
-
symlink: entry.isSymbolicLink(),
|
|
3796
|
-
agentIds: state.agents.filter((a) => a.worktree_path === path12.join(retainedRoot, entry.name)).map((a) => a.id)
|
|
3797
|
-
})) : [];
|
|
3798
|
-
return {
|
|
3799
|
-
...state,
|
|
3800
|
-
retainedWorktrees: retained,
|
|
3801
|
-
expiredClaims: state.locks.filter((lock) => Number(lock.expires_at) <= state.observedAt),
|
|
3802
|
-
unfinishedIntegrations: state.mergeQueue.filter((entry) => entry.status !== "merged"),
|
|
3803
|
-
setupLock: { path: path12.join(state.repository.coordDir, "setup.lock"), present: fs13.existsSync(path12.join(state.repository.coordDir, "setup.lock")) },
|
|
3804
|
-
integrationLock: { path: path12.join(state.repository.coordDir, "merge-train.lock"), present: fs13.existsSync(path12.join(state.repository.coordDir, "merge-train.lock")) }
|
|
3805
|
-
};
|
|
3806
|
-
}
|
|
3807
4216
|
export {
|
|
3808
4217
|
AstAnalyzer,
|
|
3809
4218
|
BashMerkleGuard,
|
|
@@ -3821,8 +4230,10 @@ export {
|
|
|
3821
4230
|
ShadowGitEngine,
|
|
3822
4231
|
SpeculativeMergeTrain,
|
|
3823
4232
|
StaleFencingTokenError,
|
|
4233
|
+
buildSwarmPrompts,
|
|
3824
4234
|
caseSensitivePaths,
|
|
3825
4235
|
discoverRepository,
|
|
4236
|
+
inspectWorkspaceContracts,
|
|
3826
4237
|
isWithin,
|
|
3827
4238
|
observeWorkspaces,
|
|
3828
4239
|
physicalPath,
|
|
@@ -3830,6 +4241,7 @@ export {
|
|
|
3830
4241
|
readCoordinationState,
|
|
3831
4242
|
recoveryReport,
|
|
3832
4243
|
repositoryFile,
|
|
4244
|
+
verifyCrossRepoImportsForFile,
|
|
3833
4245
|
worktreeChanges
|
|
3834
4246
|
};
|
|
3835
4247
|
//# sourceMappingURL=index.js.map
|