kith-coord 0.0.0-stage → 0.1.0

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/CODE_OF_CONDUCT.md +5 -0
  3. package/CONTRIBUTING.md +47 -0
  4. package/LICENSE +21 -0
  5. package/README.md +143 -2
  6. package/SECURITY.md +13 -0
  7. package/assets/brand/avatar.png +0 -0
  8. package/assets/brand/avatar.svg +1 -0
  9. package/assets/brand/lockup.png +0 -0
  10. package/assets/brand/lockup.svg +1 -0
  11. package/assets/brand/mark.svg +1 -0
  12. package/assets/brand/readme-header.png +0 -0
  13. package/assets/brand/readme-header.svg +1 -0
  14. package/assets/brand/social-preview.png +0 -0
  15. package/assets/brand/social-preview.svg +1 -0
  16. package/assets/brand/wordmark.png +0 -0
  17. package/assets/brand/wordmark.svg +1 -0
  18. package/dist/cli/index.d.ts +1 -0
  19. package/dist/cli/index.js +5022 -0
  20. package/dist/cli/index.js.map +1 -0
  21. package/dist/command-worker.d.ts +2 -0
  22. package/dist/command-worker.js +30 -0
  23. package/dist/command-worker.js.map +1 -0
  24. package/dist/index.d.ts +983 -0
  25. package/dist/index.js +3708 -0
  26. package/dist/index.js.map +1 -0
  27. package/docs/architecture.md +37 -0
  28. package/docs/client-validation.md +51 -0
  29. package/docs/integrations.md +75 -0
  30. package/docs/path-and-scope-rules.md +45 -0
  31. package/docs/protocol/tools.v1.json +425 -0
  32. package/docs/protocol.md +35 -0
  33. package/docs/reliability.md +78 -0
  34. package/docs/roadmap.md +42 -0
  35. package/docs/sessions-and-mutexes.md +97 -0
  36. package/docs/usage.md +101 -0
  37. package/docs/validation/2026-10-19-claude-client-report.json +67 -0
  38. package/docs/validation/2026-10-19-claude-hook-report.json +33 -0
  39. package/docs/validation/2026-10-19-claude-hook-unavailable-report.json +20 -0
  40. package/docs/validation/2026-10-19-claude-mutex-report.json +34 -0
  41. package/docs/validation/2026-10-19-codex-client-report.json +55 -0
  42. package/docs/validation/2026-10-19-codex-mutex-report.json +33 -0
  43. package/docs/validation/2026-10-19-path-hardening-claude.json +32 -0
  44. package/docs/validation/2026-10-19-path-hardening-codex.json +30 -0
  45. package/docs/validation/2026-10-19-recovery-claude.json +35 -0
  46. package/docs/validation/2026-10-19-recovery-codex.json +33 -0
  47. package/docs/validation/2026-10-19-sqlite-runtime-report.json +50 -0
  48. package/docs/validation/2026-10-20-adoption-claude.json +35 -0
  49. package/docs/validation/2026-10-20-adoption-codex-first-attempt.json +20 -0
  50. package/docs/validation/2026-10-20-adoption-codex.json +73 -0
  51. package/docs/validation/2026-10-20-adoption-hook-failure.json +18 -0
  52. package/docs/validation/2026-10-20-subtraction-claude.json +35 -0
  53. package/docs/validation/2026-10-20-subtraction-codex.json +33 -0
  54. package/package.json +83 -4
package/dist/index.js ADDED
@@ -0,0 +1,3708 @@
1
+ // src/version.ts
2
+ var KITH_VERSION = "0.1.0";
3
+ var KITH_PROTOCOL_VERSION = "1.0.0";
4
+
5
+ // src/core/workspace-observation.ts
6
+ import { execFileSync as execFileSync2, spawnSync } from "child_process";
7
+
8
+ // src/core/repository.ts
9
+ import { execFileSync } from "child_process";
10
+ import fs from "fs";
11
+ import path from "path";
12
+ function physicalPath(input) {
13
+ const absolute = path.isAbsolute(input) ? input : process.cwd() + path.sep + input;
14
+ let current = path.parse(absolute).root;
15
+ for (const component of absolute.slice(current.length).split(path.sep)) {
16
+ if (!component || component === ".") continue;
17
+ if (component === "..") {
18
+ current = path.dirname(current);
19
+ continue;
20
+ }
21
+ const candidate = path.join(current, component);
22
+ try {
23
+ fs.lstatSync(candidate);
24
+ current = fs.realpathSync.native(candidate);
25
+ } catch (error) {
26
+ if (error.code !== "ENOENT") throw error;
27
+ try {
28
+ fs.lstatSync(candidate);
29
+ throw new Error(`Unresolved symlink: ${candidate}`);
30
+ } catch (statError) {
31
+ if (statError.code !== "ENOENT") throw statError;
32
+ }
33
+ current = candidate;
34
+ }
35
+ }
36
+ return current;
37
+ }
38
+ function isWithin(root, target) {
39
+ const relative = path.relative(root, target);
40
+ return relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
41
+ }
42
+ function repositoryFile(root, file) {
43
+ if (!file || /[\\\x00-\x1f\x7f]/u.test(file)) throw new Error(`Invalid file path: ${file}`);
44
+ const canonicalRoot = physicalPath(root);
45
+ const target = physicalPath(path.isAbsolute(file) ? file : canonicalRoot + path.sep + file);
46
+ if (!isWithin(canonicalRoot, target) || target === canonicalRoot) {
47
+ throw new Error(`File is outside repository: ${file}`);
48
+ }
49
+ return path.relative(canonicalRoot, target).split(path.sep).join("/");
50
+ }
51
+ function discoverRepository(input) {
52
+ const directory = fs.realpathSync.native(input);
53
+ if (!fs.statSync(directory).isDirectory()) throw new Error(`Not a directory: ${input}`);
54
+ const git2 = (...args) => execFileSync("git", ["rev-parse", ...args], {
55
+ cwd: directory,
56
+ encoding: "utf8",
57
+ stdio: ["ignore", "pipe", "pipe"]
58
+ }).replace(/\r?\n$/, "");
59
+ try {
60
+ const checkoutRoot = fs.realpathSync(git2("--show-toplevel"));
61
+ if (!isWithin(checkoutRoot, directory)) throw new Error("Git routing environment selects a different checkout");
62
+ const commonGitDir = fs.realpathSync(git2("--path-format=absolute", "--git-common-dir"));
63
+ const isLinkedWorktree = fs.realpathSync(git2("--absolute-git-dir")) !== commonGitDir;
64
+ const coordDir = path.basename(commonGitDir) === ".git" ? path.join(path.dirname(commonGitDir), ".coord") : path.join(commonGitDir, "kith");
65
+ return { checkoutRoot, commonGitDir, coordDir, isLinkedWorktree };
66
+ } catch (error) {
67
+ const markerExists = (file) => {
68
+ try {
69
+ fs.lstatSync(file);
70
+ return true;
71
+ } catch (failure) {
72
+ if (failure.code === "ENOENT") return false;
73
+ throw failure;
74
+ }
75
+ };
76
+ for (let current = directory; ; current = path.dirname(current)) {
77
+ if (markerExists(path.join(current, ".git")) || fs.existsSync(path.join(current, "HEAD")) && fs.existsSync(path.join(current, "objects"))) {
78
+ throw new Error(`Cannot discover Git repository at ${directory}: ${error instanceof Error ? error.message : String(error)}`);
79
+ }
80
+ if (path.dirname(current) === current) break;
81
+ }
82
+ if (process.env.GIT_DIR || process.env.GIT_WORK_TREE || process.env.GIT_COMMON_DIR) throw error;
83
+ return { checkoutRoot: directory, commonGitDir: null, coordDir: path.join(directory, ".coord"), isLinkedWorktree: false };
84
+ }
85
+ }
86
+ function caseSensitivePaths(root) {
87
+ for (let current = physicalPath(root); path.dirname(current) !== current; current = path.dirname(current)) {
88
+ const name = path.basename(current);
89
+ const variant = name.replace(/[a-zA-Z]/, (c) => c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase());
90
+ if (variant === name) continue;
91
+ try {
92
+ const original = fs.statSync(current);
93
+ const alias = fs.statSync(path.join(path.dirname(current), variant));
94
+ return original.dev !== alias.dev || original.ino !== alias.ino;
95
+ } catch (error) {
96
+ if (error.code === "ENOENT") return true;
97
+ return false;
98
+ }
99
+ }
100
+ return false;
101
+ }
102
+
103
+ // src/core/workspace-observation.ts
104
+ function git(cwd, ...args) {
105
+ return execFileSync2("git", ["-c", "core.fsmonitor=false", ...args], {
106
+ cwd,
107
+ encoding: "utf8",
108
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
109
+ stdio: ["ignore", "pipe", "pipe"],
110
+ timeout: 5e3,
111
+ maxBuffer: 4 * 1024 * 1024
112
+ });
113
+ }
114
+ function worktreeChanges(cwd) {
115
+ const records = git(cwd, "status", "--porcelain=v1", "-z", "--untracked-files=all").split("\0");
116
+ const changes = [];
117
+ for (let i = 0; i < records.length; i++) {
118
+ const record = records[i];
119
+ if (!record) continue;
120
+ const change = { status: record.slice(0, 2), path: record.slice(3) };
121
+ if (/[RC]/.test(change.status)) change.originalPath = records[++i];
122
+ changes.push(change);
123
+ }
124
+ return changes;
125
+ }
126
+ function observeWorkspaces(repoRoot, agents, now = Date.now()) {
127
+ let base = null;
128
+ try {
129
+ base = git(repoRoot, "rev-parse", "--verify", "HEAD").trim();
130
+ } catch {
131
+ }
132
+ const workspaces = agents.filter((a) => a.worktree_path || a.status === "active").map((a) => {
133
+ const result = {
134
+ agentId: a.id,
135
+ path: a.worktree_path ?? repoRoot,
136
+ recordedBranch: a.branch,
137
+ status: a.status,
138
+ leaseExpired: a.lease_expires_at <= now,
139
+ lastHeartbeatAt: a.last_heartbeat_at,
140
+ available: false,
141
+ branch: null,
142
+ head: null,
143
+ comparisonBase: base,
144
+ dirtyFiles: [],
145
+ lastCommitAt: null,
146
+ hoursSinceLastCommit: null,
147
+ ahead: null,
148
+ behind: null,
149
+ overlaps: []
150
+ };
151
+ try {
152
+ if (discoverRepository(result.path).commonGitDir !== discoverRepository(repoRoot).commonGitDir) throw new Error("Workspace belongs to another repository");
153
+ result.dirtyFiles = worktreeChanges(result.path);
154
+ result.head = git(result.path, "rev-parse", "--verify", "HEAD").trim();
155
+ result.branch = git(result.path, "branch", "--show-current").trim() || null;
156
+ result.lastCommitAt = Number(git(result.path, "show", "-s", "--format=%ct", "HEAD").trim()) * 1e3;
157
+ result.hoursSinceLastCommit = Math.max(0, now - result.lastCommitAt) / 36e5;
158
+ if (base) {
159
+ const [behind, ahead] = git(result.path, "rev-list", "--left-right", "--count", `${base}...${result.head}`).trim().split(/\s+/).map(Number);
160
+ result.ahead = ahead;
161
+ result.behind = behind;
162
+ }
163
+ result.available = true;
164
+ } catch (error) {
165
+ result.error = error instanceof Error ? error.message : String(error);
166
+ }
167
+ return result;
168
+ });
169
+ for (const workspace of workspaces) {
170
+ if (!workspace.available) continue;
171
+ const files = new Set(workspace.dirtyFiles.flatMap((c) => [c.path, ...c.originalPath ? [c.originalPath] : []]));
172
+ for (const file of files) {
173
+ const peers = workspaces.filter((peer) => peer.agentId !== workspace.agentId && peer.status === "active" && peer.available && peer.dirtyFiles.some((c) => c.path === file || c.originalPath === file)).map((peer) => peer.agentId);
174
+ if (peers.length) workspace.overlaps.push({ path: file, agentIds: peers });
175
+ }
176
+ }
177
+ return workspaces;
178
+ }
179
+ function previewIntegration(repoRoot, agent) {
180
+ if (!agent.worktree_path || !agent.branch) throw new Error("Integration preview requires a registered worktree and branch");
181
+ const source = discoverRepository(agent.worktree_path), target = discoverRepository(repoRoot);
182
+ if (source.checkoutRoot !== physicalPath(agent.worktree_path) || source.commonGitDir !== target.commonGitDir || git(agent.worktree_path, "branch", "--show-current").trim() !== agent.branch.replace(/^refs\/heads\//, "")) {
183
+ throw new Error("Registered source checkout repository or branch does not match");
184
+ }
185
+ const sourceHead = git(agent.worktree_path, "rev-parse", "--verify", "HEAD").trim();
186
+ const targetHead = git(repoRoot, "rev-parse", "--verify", "HEAD").trim();
187
+ const dirtyFiles = worktreeChanges(agent.worktree_path);
188
+ const targetDirtyFiles = worktreeChanges(repoRoot).filter((change) => !(change.status === "??" && target.coordDir === target.checkoutRoot + "/.coord" && change.path.startsWith(".coord/")));
189
+ const sourceBranch = git(agent.worktree_path, "branch", "--show-current").trim(), targetBranch = git(repoRoot, "branch", "--show-current").trim();
190
+ if (!targetBranch) throw new Error("Integration target must be on a branch");
191
+ if (targetDirtyFiles.length) return { ok: false, clean: null, sourceHead, targetHead, sourceBranch, targetBranch, conflicts: [], dirtyFiles, targetDirtyFiles, reason: "Integration target has local changes. Preserve or commit them before preview." };
192
+ if (dirtyFiles.length) return {
193
+ ok: false,
194
+ clean: null,
195
+ sourceHead,
196
+ targetHead,
197
+ sourceBranch,
198
+ targetBranch,
199
+ conflicts: [],
200
+ dirtyFiles,
201
+ targetDirtyFiles,
202
+ reason: "Commit or preserve source changes before preview. Uncommitted edits are not included in a committed-tree preview."
203
+ };
204
+ const drivers = spawnSync("git", ["config", "--get-regexp", "^merge\\..*\\.driver$"], {
205
+ cwd: repoRoot,
206
+ encoding: "utf8",
207
+ timeout: 5e3,
208
+ maxBuffer: 4 * 1024 * 1024
209
+ });
210
+ if (drivers.error || drivers.status !== 0 && drivers.status !== 1) throw new Error("Cannot inspect merge-driver configuration safely");
211
+ if (drivers.status === 0) throw new Error("Integration preview is unavailable with configured custom merge drivers; they can execute commands");
212
+ const probe = spawnSync("git", ["merge-tree", "--write-tree", "-z", targetHead, sourceHead], {
213
+ cwd: repoRoot,
214
+ encoding: "utf8",
215
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
216
+ timeout: 1e4,
217
+ maxBuffer: 4 * 1024 * 1024
218
+ });
219
+ if (probe.error || probe.status !== 0 && probe.status !== 1) throw new Error(`Git integration preview failed: ${probe.error?.message ?? probe.stderr}`);
220
+ const stages = probe.stdout.split("\0").slice(1);
221
+ const boundary = stages.indexOf("");
222
+ const conflicts = [...new Set(stages.slice(0, boundary < 0 ? stages.length : boundary).flatMap((record) => {
223
+ const match = /^\d{6} [0-9a-f]+ [123]\t([\s\S]+)$/.exec(record);
224
+ return match ? [match[1]] : [];
225
+ }))];
226
+ if (probe.status === 1 && !conflicts.length) throw new Error("Git returned a conflict without identifiable paths; preview is unavailable");
227
+ if (git(repoRoot, "rev-parse", "HEAD").trim() !== targetHead || git(agent.worktree_path, "rev-parse", "HEAD").trim() !== sourceHead || git(repoRoot, "branch", "--show-current").trim() !== targetBranch || git(agent.worktree_path, "branch", "--show-current").trim() !== sourceBranch || worktreeChanges(repoRoot).some((change) => !(change.status === "??" && target.coordDir === target.checkoutRoot + "/.coord" && change.path.startsWith(".coord/"))) || worktreeChanges(agent.worktree_path).length) throw new Error("Source or target changed during preview; retry");
228
+ return { ok: true, clean: probe.status === 0, sourceHead, targetHead, sourceBranch, targetBranch, conflicts, dirtyFiles, targetDirtyFiles, verified: false };
229
+ }
230
+
231
+ // src/core/engine.ts
232
+ import fs12 from "fs";
233
+ import path10 from "path";
234
+
235
+ // src/core/ast-analyzer.ts
236
+ import crypto from "crypto";
237
+ import fs2 from "fs";
238
+ import path2 from "path";
239
+ var AstAnalyzer = class _AstAnalyzer {
240
+ /**
241
+ * Analyzes a source file's content to extract exported contracts (functions, types,
242
+ * interfaces, schemas, routes) and imported module dependencies.
243
+ */
244
+ static analyzeSource(repoRoot, filePath, sourceContent) {
245
+ const relPath = repositoryFile(repoRoot, filePath);
246
+ const absPath = path2.join(repoRoot, relPath);
247
+ const content = sourceContent ?? (fs2.existsSync(absPath) ? fs2.readFileSync(absPath, "utf8") : "");
248
+ const fileHash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
249
+ const exports = _AstAnalyzer.extractExports(content);
250
+ const imports = _AstAnalyzer.extractImports(repoRoot, relPath, content);
251
+ return {
252
+ filePath: relPath,
253
+ fileHash,
254
+ exports,
255
+ imports
256
+ };
257
+ }
258
+ static extractExports(content) {
259
+ const results = /* @__PURE__ */ new Map();
260
+ const lines = content.split(/\r?\n/);
261
+ const getPrecedingDoc = (lineIndex) => {
262
+ const comments = [];
263
+ let i = lineIndex - 1;
264
+ while (i >= 0) {
265
+ const trimmed = lines[i].trim();
266
+ if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed.startsWith("*/") || trimmed.startsWith("#")) {
267
+ comments.unshift(
268
+ trimmed.replace(/^(\/\/+|\/\*+|\*+\/|#|\*)\s?/, "").trim()
269
+ );
270
+ i--;
271
+ } else if (trimmed === "") {
272
+ break;
273
+ } else {
274
+ break;
275
+ }
276
+ }
277
+ return comments.filter(Boolean).join(" ").slice(0, 240);
278
+ };
279
+ for (let i = 0; i < lines.length; i++) {
280
+ const line = lines[i];
281
+ const trimmed = line.trim();
282
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*")) continue;
283
+ const fnMatch = trimmed.match(
284
+ /^export\s+(?:default\s+)?(?:async\s+)?function\s+([A-Za-z0-9_$]+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)\s*(?::\s*([^{;]+))?/
285
+ );
286
+ if (fnMatch) {
287
+ const [, name, rawParams, rawReturn] = fnMatch;
288
+ const cleanReturn = (rawReturn ?? "unknown").trim();
289
+ const sig = `function ${name}(${_AstAnalyzer.normalizeWhitespace(rawParams)}): ${cleanReturn}`;
290
+ const kind = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/.test(name) ? "route" : "function";
291
+ results.set(name, {
292
+ symbolName: name,
293
+ kind,
294
+ signature: sig,
295
+ astHash: _AstAnalyzer.hashSignature(sig),
296
+ paramsFingerprint: _AstAnalyzer.computeParamsFingerprint(rawParams, cleanReturn),
297
+ docComment: getPrecedingDoc(i)
298
+ });
299
+ continue;
300
+ }
301
+ const arrowMatch = trimmed.match(
302
+ /^export\s+const\s+([A-Za-z0-9_$]+)\s*(?::\s*[^=]+)?=\s*(?:async\s+)?(?:\(([^)]*)\)|([A-Za-z0-9_$]+))\s*(?::\s*([^=]+?))?\s*=>/
303
+ );
304
+ if (arrowMatch) {
305
+ const [, name, tupleParams, singleParam, rawReturn] = arrowMatch;
306
+ const rawParams = tupleParams ?? singleParam ?? "";
307
+ const cleanReturn = (rawReturn ?? "unknown").trim();
308
+ const sig = `function ${name}(${_AstAnalyzer.normalizeWhitespace(rawParams)}): ${cleanReturn}`;
309
+ results.set(name, {
310
+ symbolName: name,
311
+ kind: "function",
312
+ signature: sig,
313
+ astHash: _AstAnalyzer.hashSignature(sig),
314
+ paramsFingerprint: _AstAnalyzer.computeParamsFingerprint(rawParams, cleanReturn),
315
+ docComment: getPrecedingDoc(i)
316
+ });
317
+ continue;
318
+ }
319
+ const ifaceMatch = trimmed.match(
320
+ /^export\s+interface\s+([A-Za-z0-9_$]+)(?:\s*<[^>]*>)?(?:\s+extends\s+[^{]+)?\s*\{/
321
+ );
322
+ if (ifaceMatch) {
323
+ const name = ifaceMatch[1];
324
+ const bodyFields = _AstAnalyzer.extractBracedBlockSummary(lines, i);
325
+ const sig = `interface ${name} { ${bodyFields} }`;
326
+ results.set(name, {
327
+ symbolName: name,
328
+ kind: "interface",
329
+ signature: sig,
330
+ astHash: _AstAnalyzer.hashSignature(sig),
331
+ paramsFingerprint: `interface:${_AstAnalyzer.extractFieldTypesFingerprint(bodyFields)}`,
332
+ docComment: getPrecedingDoc(i)
333
+ });
334
+ continue;
335
+ }
336
+ const typeMatch = trimmed.match(/^export\s+type\s+([A-Za-z0-9_$]+)(?:\s*<[^>]*>)?\s*=\s*(.+)$/);
337
+ if (typeMatch) {
338
+ const [, name, rhs] = typeMatch;
339
+ const fullRhs = rhs.includes("{") && !rhs.includes("}") ? `{ ${_AstAnalyzer.extractBracedBlockSummary(lines, i)} }` : rhs.replace(/;$/, "").trim();
340
+ const sig = `type ${name} = ${_AstAnalyzer.normalizeWhitespace(fullRhs)}`;
341
+ results.set(name, {
342
+ symbolName: name,
343
+ kind: "type",
344
+ signature: sig,
345
+ astHash: _AstAnalyzer.hashSignature(sig),
346
+ paramsFingerprint: `type:${_AstAnalyzer.normalizeWhitespace(fullRhs)}`,
347
+ docComment: getPrecedingDoc(i)
348
+ });
349
+ continue;
350
+ }
351
+ const classMatch = trimmed.match(
352
+ /^export\s+(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z0-9_$]+)/
353
+ );
354
+ if (classMatch) {
355
+ const name = classMatch[1];
356
+ const sig = `class ${name}`;
357
+ results.set(name, {
358
+ symbolName: name,
359
+ kind: "class",
360
+ signature: sig,
361
+ astHash: _AstAnalyzer.hashSignature(sig),
362
+ paramsFingerprint: `class:${name}`,
363
+ docComment: getPrecedingDoc(i)
364
+ });
365
+ continue;
366
+ }
367
+ const constMatch = trimmed.match(
368
+ /^export\s+const\s+([A-Za-z0-9_$]+)\s*(?::\s*([^=]+))?\s*=\s*(.+)$/
369
+ );
370
+ if (constMatch) {
371
+ const [, name, explicitType, rhs] = constMatch;
372
+ const isSchema = /Schema$|z\.|pgTable|sqliteTable|mysqlTable/.test(name + rhs);
373
+ const kind = isSchema ? "schema" : "const";
374
+ const sig = `const ${name}: ${(explicitType ?? rhs.slice(0, 60)).replace(/;$/, "").trim()}`;
375
+ results.set(name, {
376
+ symbolName: name,
377
+ kind,
378
+ signature: sig,
379
+ astHash: _AstAnalyzer.hashSignature(sig),
380
+ paramsFingerprint: `${kind}:${name}`,
381
+ docComment: getPrecedingDoc(i)
382
+ });
383
+ continue;
384
+ }
385
+ const pyFnMatch = line.match(
386
+ /^(?:async\s+)?def\s+([a-zA-Z0-9_]+)\s*\(([^)]*)\)\s*(?:->\s*([^:]+))?:/
387
+ );
388
+ if (pyFnMatch && !pyFnMatch[1].startsWith("_")) {
389
+ const [, name, rawParams, rawRet] = pyFnMatch;
390
+ const ret = (rawRet ?? "Any").trim();
391
+ const sig = `def ${name}(${_AstAnalyzer.normalizeWhitespace(rawParams)}) -> ${ret}`;
392
+ results.set(name, {
393
+ symbolName: name,
394
+ kind: "function",
395
+ signature: sig,
396
+ astHash: _AstAnalyzer.hashSignature(sig),
397
+ paramsFingerprint: _AstAnalyzer.computeParamsFingerprint(rawParams, ret),
398
+ docComment: getPrecedingDoc(i)
399
+ });
400
+ }
401
+ }
402
+ return Array.from(results.values());
403
+ }
404
+ static extractImports(repoRoot, sourceRelPath, content) {
405
+ const imports = [];
406
+ const importRegex = /(?:import\s+(?:type\s+)?(?:(\{[^}]*\})|([A-Za-z0-9_$*]+))?\s*(?:,\s*(\{[^}]*\}))?\s*from\s+['"]([^'"]+)['"]|export\s+(?:\*|\{[^}]*\})\s+from\s+['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\))/g;
407
+ let match;
408
+ while ((match = importRegex.exec(content)) !== null) {
409
+ const namedBlock = match[1] ?? match[3] ?? "";
410
+ const defaultIdent = match[2] ?? "";
411
+ const specifier = match[4] ?? match[5] ?? match[6] ?? "";
412
+ if (!specifier) continue;
413
+ const symbols = [];
414
+ if (defaultIdent) symbols.push(defaultIdent.trim());
415
+ if (namedBlock) {
416
+ const inner = namedBlock.replace(/[{}]/g, "");
417
+ for (const part of inner.split(",")) {
418
+ const clean = part.trim().split(/\s+as\s+/)[0].replace(/^type\s+/, "").trim();
419
+ if (clean) symbols.push(clean);
420
+ }
421
+ }
422
+ const resolvedPath = _AstAnalyzer.resolveRelativeImport(
423
+ repoRoot,
424
+ sourceRelPath,
425
+ specifier
426
+ );
427
+ imports.push({
428
+ rawSpecifier: specifier,
429
+ resolvedPath,
430
+ importedSymbols: symbols
431
+ });
432
+ }
433
+ return imports;
434
+ }
435
+ static resolveRelativeImport(repoRoot, sourceRelPath, specifier) {
436
+ if (!specifier.startsWith(".")) {
437
+ return null;
438
+ }
439
+ const sourceDir = path2.dirname(path2.join(repoRoot, sourceRelPath));
440
+ const baseTarget = path2.resolve(sourceDir, specifier.replace(/\.(js|jsx|mjs|cjs)$/, ""));
441
+ const candidates = [
442
+ path2.resolve(sourceDir, specifier),
443
+ `${baseTarget}.ts`,
444
+ `${baseTarget}.tsx`,
445
+ `${baseTarget}.js`,
446
+ `${baseTarget}.mjs`,
447
+ path2.join(baseTarget, "index.ts"),
448
+ path2.join(baseTarget, "index.tsx"),
449
+ path2.join(baseTarget, "index.js")
450
+ ];
451
+ for (const candidate of candidates) {
452
+ if (fs2.existsSync(candidate) && fs2.statSync(candidate).isFile()) {
453
+ try {
454
+ return repositoryFile(repoRoot, candidate);
455
+ } catch {
456
+ return null;
457
+ }
458
+ }
459
+ }
460
+ const inferred = `${baseTarget}.ts`;
461
+ try {
462
+ return repositoryFile(repoRoot, inferred);
463
+ } catch {
464
+ return null;
465
+ }
466
+ }
467
+ static extractBracedBlockSummary(lines, startIndex) {
468
+ const fields = [];
469
+ let depth = 0;
470
+ for (let i = startIndex; i < Math.min(lines.length, startIndex + 35); i++) {
471
+ const line = lines[i];
472
+ for (const ch of line) {
473
+ if (ch === "{") depth++;
474
+ else if (ch === "}") depth--;
475
+ }
476
+ if (i > startIndex && depth >= 1) {
477
+ const clean = line.trim().replace(/;$/, "").replace(/,$/, "");
478
+ if (clean && !clean.startsWith("//") && clean !== "}") {
479
+ fields.push(clean);
480
+ }
481
+ }
482
+ if (depth <= 0 && i >= startIndex && line.includes("}")) {
483
+ break;
484
+ }
485
+ }
486
+ return fields.join("; ");
487
+ }
488
+ static extractFieldTypesFingerprint(bodyFields) {
489
+ return bodyFields.split(";").map((f) => {
490
+ const parts = f.split(":");
491
+ return parts.length > 1 ? parts[1].trim().toLowerCase() : f.trim().toLowerCase();
492
+ }).filter(Boolean).sort().join(",");
493
+ }
494
+ static computeParamsFingerprint(rawParams, rawReturn) {
495
+ const paramTypes = rawParams.split(",").map((p) => p.trim()).filter(Boolean).map((p) => {
496
+ const colonIdx = p.indexOf(":");
497
+ if (colonIdx === -1) return "any";
498
+ return p.slice(colonIdx + 1).replace(/=.*$/, "").trim().toLowerCase();
499
+ });
500
+ const retNorm = rawReturn.replace(/^Promise<(.+)>$/, "$1").trim().toLowerCase();
501
+ return `(${paramTypes.join(",")})->${retNorm}`;
502
+ }
503
+ static normalizeWhitespace(s) {
504
+ return s.replace(/\s+/g, " ").trim();
505
+ }
506
+ static hashSignature(sig) {
507
+ return crypto.createHash("sha256").update(sig).digest("hex").slice(0, 16);
508
+ }
509
+ };
510
+
511
+ // src/core/bash-guard.ts
512
+ import crypto4 from "crypto";
513
+ import fs5 from "fs";
514
+ import path4 from "path";
515
+
516
+ // src/core/locks.ts
517
+ import crypto3 from "crypto";
518
+ import fs4 from "fs";
519
+ import path3 from "path";
520
+
521
+ // src/core/glob.ts
522
+ function scopeSegments(pattern, literal = false) {
523
+ if (!pattern || pattern.length > 512 || (literal ? /[\x00-\x1f\x7f\\]/u : /[\x00-\x1f\x7f\\[\]{}()!]/u).test(pattern)) {
524
+ throw new Error(`Unsupported scope syntax: ${pattern}`);
525
+ }
526
+ const segments = pattern.split("/");
527
+ if (segments.length > 64 || segments.some((s) => !s || s === "." || s === ".." || !literal && s.includes("**") && s !== "**")) {
528
+ throw new Error(`Invalid scope segments: ${pattern}`);
529
+ }
530
+ return segments;
531
+ }
532
+ function segmentsOverlap(a, b, literalA, literalB) {
533
+ const STAR = /* @__PURE__ */ Symbol("star"), ANY = /* @__PURE__ */ Symbol("any");
534
+ const tokens = (text, literal) => Array.from(text).map((c) => !literal && c === "*" ? STAR : !literal && c === "?" ? ANY : c);
535
+ const left = tokens(a, literalA), right = tokens(b, literalB);
536
+ const queue = [[0, 0, 0]];
537
+ const seen = /* @__PURE__ */ new Set();
538
+ for (let q = 0; q < queue.length; q++) {
539
+ const [i, j, kind] = queue[q];
540
+ const key = `${i},${j},${kind}`;
541
+ if (seen.has(key)) continue;
542
+ seen.add(key);
543
+ if (i === left.length && j === right.length && kind === 3) return true;
544
+ const x = left[i], y = right[j];
545
+ if (x === STAR) queue.push([i + 1, j, kind]);
546
+ if (y === STAR) queue.push([i, j + 1, kind]);
547
+ if (x === void 0 || y === void 0) continue;
548
+ const anyX = x === STAR || x === ANY, anyY = y === STAR || y === ANY;
549
+ if (!anyX && !anyY && x !== y) continue;
550
+ const literal = !anyX ? x : !anyY ? y : void 0;
551
+ const nextI = x === STAR ? i : i + 1, nextJ = y === STAR ? j : j + 1;
552
+ if (literal === void 0 || literal === ".") {
553
+ queue.push([nextI, nextJ, kind === 0 ? 1 : kind === 1 ? 2 : 3]);
554
+ }
555
+ if (literal === void 0 || literal !== ".") queue.push([nextI, nextJ, 3]);
556
+ }
557
+ return false;
558
+ }
559
+ function scopesOverlap(a, b, literalA = false, literalB = false) {
560
+ const left = scopeSegments(a, literalA), right = scopeSegments(b, literalB);
561
+ const queue = [[0, 0, false]];
562
+ const seen = /* @__PURE__ */ new Set();
563
+ const memo = /* @__PURE__ */ new Map();
564
+ for (let q = 0; q < queue.length; q++) {
565
+ const [i, j, consumed] = queue[q];
566
+ const key = `${i},${j},${consumed}`;
567
+ if (seen.has(key)) continue;
568
+ seen.add(key);
569
+ if (i === left.length && j === right.length && consumed) return true;
570
+ const x = left[i], y = right[j];
571
+ const starA = !literalA && x === "**", starB = !literalB && y === "**";
572
+ if (starA) queue.push([i + 1, j, consumed]);
573
+ if (starB) queue.push([i, j + 1, consumed]);
574
+ if (x === void 0 || y === void 0) continue;
575
+ const pair = JSON.stringify([x, y]);
576
+ let overlap = memo.get(pair);
577
+ if (overlap === void 0) {
578
+ overlap = segmentsOverlap(starA ? "*" : x, starB ? "*" : y, starA ? false : literalA, starB ? false : literalB);
579
+ memo.set(pair, overlap);
580
+ }
581
+ if (overlap) queue.push([starA ? i : i + 1, starB ? j : j + 1, true]);
582
+ }
583
+ return false;
584
+ }
585
+
586
+ // src/core/fencing.ts
587
+ import { execFileSync as execFileSync3 } from "child_process";
588
+ import crypto2 from "crypto";
589
+ import fs3 from "fs";
590
+ var SessionOwnershipError = class extends Error {
591
+ constructor(agentId) {
592
+ super(`No valid active session capability for '${agentId}'. Start a session, or resume it with its session token.`);
593
+ this.name = "SessionOwnershipError";
594
+ }
595
+ };
596
+ var StaleFencingTokenError = class extends Error {
597
+ constructor(resourceId, providedToken, expectedToken) {
598
+ super(`Fencing token mismatch for '${resourceId}': provided ${providedToken}, expected ${expectedToken}.`);
599
+ this.resourceId = resourceId;
600
+ this.providedToken = providedToken;
601
+ this.expectedToken = expectedToken;
602
+ this.name = "StaleFencingTokenError";
603
+ }
604
+ resourceId;
605
+ providedToken;
606
+ expectedToken;
607
+ };
608
+ var FencingAndAgentManager = class _FencingAndAgentManager {
609
+ constructor(coordDb) {
610
+ this.coordDb = coordDb;
611
+ }
612
+ coordDb;
613
+ static DEFAULT_LEASE_MS = 5 * 60 * 1e3;
614
+ capabilities = /* @__PURE__ */ new Map();
615
+ static validateLease(duration) {
616
+ if (!Number.isSafeInteger(duration) || duration < 100 || duration > 864e5) {
617
+ throw new Error("Lease duration must be an integer between 100 and 86400000 milliseconds");
618
+ }
619
+ return duration;
620
+ }
621
+ /** A real kernel process identity; tests must not supply liveness sentinels. */
622
+ static getProcessStartTime(pid) {
623
+ if (!Number.isSafeInteger(pid) || pid <= 0) return null;
624
+ try {
625
+ if (process.platform === "linux") {
626
+ const stat = fs3.readFileSync(`/proc/${pid}/stat`, "utf8");
627
+ return `linux:${stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]}`;
628
+ }
629
+ const out = execFileSync3("ps", ["-o", "lstart=", "-p", String(pid)], {
630
+ encoding: "utf8",
631
+ stdio: ["ignore", "pipe", "ignore"],
632
+ timeout: 1500,
633
+ env: { ...process.env, LC_ALL: "C" }
634
+ }).trim();
635
+ return out || null;
636
+ } catch {
637
+ return null;
638
+ }
639
+ }
640
+ static isProcessAlive(pid, expectedStartTime) {
641
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
642
+ try {
643
+ process.kill(pid, 0);
644
+ } catch (err) {
645
+ if (err.code === "ESRCH") return false;
646
+ }
647
+ const current = this.getProcessStartTime(pid);
648
+ return current === null || current === expectedStartTime;
649
+ }
650
+ registerAgent(params) {
651
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/.test(params.id)) throw new Error("Invalid agent ID");
652
+ const leaseMs = _FencingAndAgentManager.validateLease(params.leaseDurationMs ?? _FencingAndAgentManager.DEFAULT_LEASE_MS);
653
+ this.reapDeadAgents();
654
+ return this.coordDb.withTransaction(() => {
655
+ const existing = this.getAgent(params.id);
656
+ const supplied = params.sessionToken ?? this.capabilities.get(params.id);
657
+ if (existing?.status === "active") {
658
+ const epoch2 = this.assertSession(params.id, supplied);
659
+ this.capabilities.set(params.id, supplied);
660
+ this.heartbeat(params.id, leaseMs);
661
+ return { agent: this.getAgent(params.id), fencingToken: epoch2, sessionToken: supplied, sessionEpoch: epoch2 };
662
+ }
663
+ if (params.sessionToken !== void 0) throw new SessionOwnershipError(params.id);
664
+ const pid = params.pid ?? process.pid;
665
+ const start = _FencingAndAgentManager.getProcessStartTime(pid);
666
+ if (!start) throw new Error(`Cannot determine start time of live session owner PID ${pid}`);
667
+ const now = Date.now();
668
+ const epoch = this.coordDb.nextFencingToken();
669
+ const token = crypto2.randomBytes(32).toString("base64url");
670
+ const latest = this.coordDb.db.prepare("SELECT COALESCE(MAX(id), 0) AS id FROM events").get().id;
671
+ this.coordDb.db.prepare(`
672
+ INSERT INTO agents (id, name, client, pid, pid_start_time, pgid, worktree_path, branch,
673
+ shadow_tree_sha, status, last_seen_event_id, registered_at, last_heartbeat_at, lease_expires_at)
674
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'active', ?, ?, ?, ?)
675
+ ON CONFLICT(id) DO UPDATE SET name=excluded.name, client=excluded.client, pid=excluded.pid,
676
+ pid_start_time=excluded.pid_start_time, pgid=excluded.pgid, status='active',
677
+ worktree_path=COALESCE(agents.worktree_path, excluded.worktree_path),
678
+ shadow_tree_sha=NULL, last_seen_event_id=excluded.last_seen_event_id,
679
+ registered_at=excluded.registered_at, last_heartbeat_at=excluded.last_heartbeat_at,
680
+ lease_expires_at=excluded.lease_expires_at
681
+ `).run(
682
+ params.id,
683
+ params.name ?? existing?.name ?? params.id,
684
+ params.client ?? "unknown",
685
+ pid,
686
+ start,
687
+ params.pgid ?? pid,
688
+ params.worktreePath ?? (this.coordDb.isLinkedWorktree ? this.coordDb.repoRoot : null),
689
+ params.branch ?? null,
690
+ latest,
691
+ now,
692
+ now,
693
+ now + leaseMs
694
+ );
695
+ this.coordDb.db.prepare("INSERT OR REPLACE INTO agent_sessions(agent_id, token_hash, epoch) VALUES (?, ?, ?)").run(params.id, this.hash(token), epoch);
696
+ this.capabilities.set(params.id, token);
697
+ this.coordDb.logEvent(params.id, "agent_registered", { client: params.client ?? "unknown", pid, sessionEpoch: epoch });
698
+ return { agent: this.getAgent(params.id), fencingToken: epoch, sessionToken: token, sessionEpoch: epoch };
699
+ });
700
+ }
701
+ resumeSession(agentId, sessionToken) {
702
+ const epoch = this.assertSession(agentId, sessionToken);
703
+ this.capabilities.set(agentId, sessionToken);
704
+ return epoch;
705
+ }
706
+ assertSession(agentId, sessionToken = this.capabilities.get(agentId)) {
707
+ const agent = this.getAgent(agentId);
708
+ const session = this.coordDb.db.prepare("SELECT token_hash, epoch FROM agent_sessions WHERE agent_id = ?").get(agentId);
709
+ if (!agent || agent.status !== "active" || agent.lease_expires_at <= Date.now() || !session || !sessionToken || this.hash(sessionToken) !== session.token_hash || !_FencingAndAgentManager.isProcessAlive(agent.pid, agent.pid_start_time)) {
710
+ throw new SessionOwnershipError(agentId);
711
+ }
712
+ return session.epoch;
713
+ }
714
+ sessionEnvironment(agentId) {
715
+ this.assertSession(agentId);
716
+ return { ...process.env, KITH_AGENT_ID: agentId, KITH_SESSION_TOKEN: this.capabilities.get(agentId) };
717
+ }
718
+ heartbeat(agentId, leaseDurationMs = _FencingAndAgentManager.DEFAULT_LEASE_MS) {
719
+ const lease = _FencingAndAgentManager.validateLease(leaseDurationMs);
720
+ return this.coordDb.withTransaction(() => {
721
+ this.assertSession(agentId);
722
+ const now = Date.now();
723
+ const expires = now + lease;
724
+ this.coordDb.db.prepare("UPDATE agents SET last_heartbeat_at=?, lease_expires_at=? WHERE id=?").run(now, expires, agentId);
725
+ this.coordDb.db.prepare("UPDATE locks SET expires_at=MAX(expires_at, ?) WHERE owner_agent_id=? AND expires_at>?").run(expires, agentId, now);
726
+ this.coordDb.db.prepare("UPDATE resources SET expires_at=MAX(expires_at, ?) WHERE type='port' AND owner_agent_id=? AND expires_at>?").run(expires, agentId, now);
727
+ return this.getAgent(agentId);
728
+ });
729
+ }
730
+ getAgent(agentId) {
731
+ return this.coordDb.db.prepare("SELECT * FROM agents WHERE id=?").get(agentId) ?? null;
732
+ }
733
+ listAgents(onlyActive = false) {
734
+ this.reapDeadAgents();
735
+ return this.coordDb.db.prepare(onlyActive ? "SELECT * FROM agents WHERE status='active' ORDER BY registered_at" : "SELECT * FROM agents ORDER BY registered_at DESC").all();
736
+ }
737
+ reapDeadAgents() {
738
+ const agents = this.coordDb.db.prepare("SELECT * FROM agents WHERE status='active'").all();
739
+ const liveness = /* @__PURE__ */ new Map();
740
+ const candidates = agents.filter((a) => {
741
+ const key = `${a.pid}:${a.pid_start_time}`;
742
+ if (!liveness.has(key)) liveness.set(key, _FencingAndAgentManager.isProcessAlive(a.pid, a.pid_start_time));
743
+ return a.lease_expires_at <= Date.now() || !liveness.get(key);
744
+ });
745
+ if (!candidates.length) return [];
746
+ return this.coordDb.withTransaction(() => {
747
+ const reaped = [];
748
+ for (const previous of candidates) {
749
+ const current = this.getAgent(previous.id);
750
+ if (!current || current.status !== "active" || current.registered_at !== previous.registered_at || current.pid !== previous.pid || current.pid_start_time !== previous.pid_start_time) continue;
751
+ if (current.lease_expires_at > Date.now() && _FencingAndAgentManager.isProcessAlive(current.pid, current.pid_start_time)) continue;
752
+ this.reclaim(current.id, "dead", "process_died_or_lease_expired");
753
+ reaped.push(current.id);
754
+ }
755
+ return reaped;
756
+ });
757
+ }
758
+ completeAgentSession(agentId) {
759
+ this.coordDb.withTransaction(() => {
760
+ this.assertSession(agentId);
761
+ this.reclaim(agentId, "completed", "session_completed");
762
+ });
763
+ this.capabilities.delete(agentId);
764
+ }
765
+ abandonAgentSession(agentId) {
766
+ this.coordDb.withTransaction(() => {
767
+ this.assertSession(agentId);
768
+ this.reclaim(agentId, "dead", "client_disconnected");
769
+ });
770
+ this.capabilities.delete(agentId);
771
+ }
772
+ reclaim(agentId, status, reason) {
773
+ const epoch = this.coordDb.nextFencingToken();
774
+ const now = Date.now();
775
+ this.coordDb.db.prepare("UPDATE agents SET status=?, last_heartbeat_at=? WHERE id=?").run(status, now, agentId);
776
+ this.coordDb.db.prepare("DELETE FROM locks WHERE owner_agent_id=?").run(agentId);
777
+ this.coordDb.db.prepare("DELETE FROM resources WHERE owner_agent_id=?").run(agentId);
778
+ this.coordDb.db.prepare("DELETE FROM mutex_waiters WHERE owner_agent_id=?").run(agentId);
779
+ this.coordDb.db.prepare("DELETE FROM conflicts WHERE agent_a=? OR agent_b=?").run(agentId, agentId);
780
+ if (status === "dead") {
781
+ this.coordDb.db.prepare("UPDATE tasks SET status='queued', owner_agent_id=NULL, fencing_token=NULL, updated_at=? WHERE owner_agent_id=? AND status='active'").run(now, agentId);
782
+ } else {
783
+ this.coordDb.db.prepare("UPDATE tasks SET status='done', updated_at=? WHERE owner_agent_id=? AND status='active'").run(now, agentId);
784
+ }
785
+ this.coordDb.logEvent(agentId, `agent_${status}`, { reason, newFencingEpoch: epoch });
786
+ }
787
+ verifyFencingToken(resourceId, provided, expected) {
788
+ if (provided !== expected) throw new StaleFencingTokenError(resourceId, provided, expected);
789
+ }
790
+ hash(token) {
791
+ return crypto2.createHash("sha256").update(token).digest("hex");
792
+ }
793
+ };
794
+
795
+ // src/core/locks.ts
796
+ var LockEngine = class _LockEngine {
797
+ constructor(coordDb, fencing) {
798
+ this.coordDb = coordDb;
799
+ this.fencing = fencing;
800
+ }
801
+ coordDb;
802
+ fencing;
803
+ pathCase = /* @__PURE__ */ new Map();
804
+ static normalizePattern(repoRoot, rawPattern) {
805
+ if (rawPattern.startsWith("file:")) return _LockEngine.literalScope(_LockEngine.normalizeFile(repoRoot, rawPattern.slice(5)));
806
+ if (rawPattern.startsWith("symbol:")) {
807
+ const hash = rawPattern.indexOf("#");
808
+ const name = rawPattern.slice(hash + 1);
809
+ if (hash < 0 || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) throw new Error("Symbol scope must include #symbolName");
810
+ const file = _LockEngine.normalizeFile(repoRoot, rawPattern.slice(7, hash));
811
+ return `symbol:${file}#${name}`;
812
+ }
813
+ if (!rawPattern || /[\\\x00-\x1f\x7f]/u.test(rawPattern)) throw new Error(`Invalid repository scope: ${rawPattern}`);
814
+ const root = physicalPath(repoRoot);
815
+ const parts = rawPattern.split("/");
816
+ const magic = parts.findIndex((p) => /[?*]/u.test(p));
817
+ const prefix = magic < 0 ? rawPattern : parts.slice(0, magic).join("/") || ".";
818
+ const absolute = physicalPath(path3.isAbsolute(prefix) ? prefix : root + "/" + prefix);
819
+ if (!isWithin(root, absolute)) throw new Error(`Scope is outside repository: ${rawPattern}`);
820
+ const relative = path3.relative(root, absolute).split(path3.sep).join("/");
821
+ const suffix = magic < 0 ? "" : parts.slice(magic).join("/");
822
+ const normalized = [relative, suffix].filter(Boolean).join("/");
823
+ scopeSegments(normalized);
824
+ return normalized;
825
+ }
826
+ static normalizeFile(repoRoot, file) {
827
+ const normalized = repositoryFile(repoRoot, file);
828
+ scopeSegments(normalized, true);
829
+ return normalized;
830
+ }
831
+ static literalScope(file) {
832
+ try {
833
+ scopeSegments(file);
834
+ if (!/[?*]/u.test(file) && !file.startsWith("symbol:") && !file.startsWith("file:")) return file;
835
+ } catch {
836
+ }
837
+ return "file:" + file;
838
+ }
839
+ rootForAgent(agentId) {
840
+ return this.fencing.getAgent(agentId)?.worktree_path ?? this.coordDb.repoRoot;
841
+ }
842
+ // A wildcard can traverse an alias that maps to a different scope. If exact
843
+ // lexical comparison was disjoint, retain exclusion when such aliases exist.
844
+ wildcardHasAliases(pattern) {
845
+ if (pattern.startsWith("symbol:") || pattern.startsWith("file:")) return false;
846
+ const segments = scopeSegments(pattern);
847
+ const first = segments.findIndex((s) => /[?*]/u.test(s));
848
+ if (first < 0) return false;
849
+ const prefix = segments.slice(0, first).join("/");
850
+ const roots = /* @__PURE__ */ new Set([this.coordDb.repoRoot, ...this.fencing.listAgents(true).map((a) => a.worktree_path).filter((p) => Boolean(p))]);
851
+ const pending = [...roots].map((root) => ({ root, relative: prefix }));
852
+ let budget = 1e4;
853
+ while (pending.length) {
854
+ const { root, relative } = pending.pop();
855
+ const dir = path3.join(root, relative);
856
+ let entries;
857
+ try {
858
+ entries = fs4.readdirSync(dir, { withFileTypes: true });
859
+ } catch (error) {
860
+ if (error.code === "ENOENT") continue;
861
+ return true;
862
+ }
863
+ for (const entry of entries) {
864
+ if (--budget < 0) return true;
865
+ const file = relative ? relative + "/" + entry.name : entry.name;
866
+ if (entry.isSymbolicLink()) return true;
867
+ const fold = (value) => value.normalize("NFC").toLowerCase().replace(/ς/g, "\u03C3");
868
+ if (!_LockEngine.patternsOverlap(fold(pattern), fold(file)) && !_LockEngine.patternsOverlap(fold(pattern), fold(file + "/**"))) continue;
869
+ if (entry.isDirectory()) pending.push({ root, relative: file });
870
+ }
871
+ }
872
+ return false;
873
+ }
874
+ /** Exact overlap for the documented scope grammar; legacy unsupported locks block conservatively. */
875
+ static patternsOverlap(rawA, rawB) {
876
+ const decode = (value) => {
877
+ if (value.startsWith("symbol:")) {
878
+ const match = /^symbol:(.+)#([A-Za-z_$][A-Za-z0-9_$]*)$/.exec(value);
879
+ if (!match) throw new Error("Invalid symbol scope");
880
+ scopeSegments(match[1], true);
881
+ return { file: match[1], symbol: match[2], literal: true };
882
+ }
883
+ const literal = value.startsWith("file:");
884
+ const file = literal ? value.slice(5) : value;
885
+ scopeSegments(file, literal);
886
+ return { file, symbol: null, literal };
887
+ };
888
+ try {
889
+ const a = decode(rawA), b = decode(rawB);
890
+ if (a.symbol && b.symbol) return a.file === b.file && a.symbol === b.symbol;
891
+ return scopesOverlap(a.file, b.file, a.literal, b.literal);
892
+ } catch {
893
+ return true;
894
+ }
895
+ }
896
+ /**
897
+ * Atomically acquires locks on one or more file/glob/symbol patterns.
898
+ * All-or-nothing: if any pattern conflicts with another live agent, no locks are created.
899
+ */
900
+ acquireLocks(params) {
901
+ this.fencing.reapDeadAgents();
902
+ const mode = params.mode ?? "exclusive";
903
+ const lockType = params.lockType ?? "glob";
904
+ const now = Date.now();
905
+ const expiresAt = now + (params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
906
+ const normalizedPatterns = Array.from(
907
+ new Set(params.patterns.map((p) => _LockEngine.normalizePattern(this.rootForAgent(params.agentId), lockType === "file" && !p.startsWith("file:") ? "file:" + p : p)))
908
+ );
909
+ return this.coordDb.withTransaction(() => {
910
+ this.fencing.assertSession(params.agentId);
911
+ const existingLocks = this.coordDb.db.prepare(`SELECT * FROM locks WHERE expires_at > ?`).all(now);
912
+ const conflicts = [];
913
+ for (const reqPattern of normalizedPatterns) {
914
+ for (const existing of existingLocks) {
915
+ if (existing.owner_agent_id === params.agentId) {
916
+ continue;
917
+ }
918
+ const bothShared = mode === "shared" && existing.mode === "shared";
919
+ if (bothShared) {
920
+ continue;
921
+ }
922
+ if (this.overlapsExisting(reqPattern, existing.pattern)) {
923
+ const owner = this.fencing.getAgent(existing.owner_agent_id);
924
+ const activeTask = this.coordDb.db.prepare(
925
+ `SELECT intent_summary FROM tasks WHERE owner_agent_id = ? AND status = 'active' LIMIT 1`
926
+ ).get(existing.owner_agent_id);
927
+ conflicts.push({
928
+ existingLock: existing,
929
+ requestedPattern: reqPattern,
930
+ ownerAgentName: owner?.name ?? existing.owner_agent_id,
931
+ ownerIntent: activeTask?.intent_summary ?? existing.reason ?? null
932
+ });
933
+ }
934
+ }
935
+ }
936
+ if (conflicts.length > 0) {
937
+ this.coordDb.logEvent(params.agentId, "lock_conflict", {
938
+ requestedPatterns: normalizedPatterns,
939
+ conflicts: conflicts.map((c) => ({
940
+ pattern: c.existingLock.pattern,
941
+ owner: c.existingLock.owner_agent_id
942
+ }))
943
+ });
944
+ return {
945
+ ok: false,
946
+ fencingToken: this.coordDb.currentFencingEpoch(),
947
+ locks: [],
948
+ conflicts
949
+ };
950
+ }
951
+ const token = this.coordDb.nextFencingToken();
952
+ const acquired = [];
953
+ for (const pattern of normalizedPatterns) {
954
+ this.coordDb.db.prepare(`DELETE FROM locks WHERE owner_agent_id = ? AND pattern = ?`).run(params.agentId, pattern);
955
+ const id = `lock_${crypto3.randomBytes(6).toString("hex")}`;
956
+ const inferredType = pattern.startsWith("file:") ? "file" : pattern.startsWith("symbol:") ? "symbol" : pattern.includes("*") || pattern.includes("?") || pattern.includes("{") ? "glob" : lockType === "glob" ? "file" : lockType;
957
+ this.coordDb.db.prepare(`
958
+ INSERT INTO locks (
959
+ id, pattern, lock_type, mode, owner_agent_id,
960
+ task_id, fencing_token, reason, acquired_at, expires_at
961
+ )
962
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
963
+ `).run(
964
+ id,
965
+ pattern,
966
+ inferredType,
967
+ mode,
968
+ params.agentId,
969
+ params.taskId ?? null,
970
+ token,
971
+ params.reason ?? "",
972
+ now,
973
+ expiresAt
974
+ );
975
+ acquired.push({
976
+ id,
977
+ pattern,
978
+ lock_type: inferredType,
979
+ mode,
980
+ owner_agent_id: params.agentId,
981
+ task_id: params.taskId ?? null,
982
+ fencing_token: token,
983
+ reason: params.reason ?? "",
984
+ acquired_at: now,
985
+ expires_at: expiresAt
986
+ });
987
+ }
988
+ this.coordDb.logEvent(params.agentId, "locks_acquired", {
989
+ patterns: normalizedPatterns,
990
+ fencingToken: token
991
+ });
992
+ return {
993
+ ok: true,
994
+ fencingToken: token,
995
+ locks: acquired,
996
+ conflicts: []
997
+ };
998
+ });
999
+ }
1000
+ /**
1001
+ * Checks whether `agentId` is allowed to edit `filePath`.
1002
+ * Also verifies the fencing token if provided.
1003
+ */
1004
+ checkFileWriteAccess(params) {
1005
+ this.fencing.reapDeadAgents();
1006
+ this.fencing.assertSession(params.agentId);
1007
+ const normPath = _LockEngine.normalizeFile(this.rootForAgent(params.agentId), params.filePath);
1008
+ const requested = _LockEngine.literalScope(normPath);
1009
+ const now = Date.now();
1010
+ const activeLocks = this.coordDb.db.prepare(`SELECT * FROM locks WHERE expires_at > ?`).all(now);
1011
+ for (const lock of activeLocks) {
1012
+ if (this.overlapsExisting(requested, lock.pattern)) {
1013
+ if (lock.owner_agent_id !== params.agentId) {
1014
+ const activeTask = this.coordDb.db.prepare(
1015
+ `SELECT intent_summary FROM tasks WHERE owner_agent_id = ? AND status = 'active' LIMIT 1`
1016
+ ).get(lock.owner_agent_id);
1017
+ return {
1018
+ allowed: false,
1019
+ reason: `File '${normPath}' is locked by agent '${lock.owner_agent_id}' (pattern: '${lock.pattern}', token: ${lock.fencing_token}). Intent: ${activeTask?.intent_summary ?? (lock.reason || "active work")}`,
1020
+ conflictingLock: lock,
1021
+ ownerIntent: activeTask?.intent_summary ?? lock.reason ?? null
1022
+ };
1023
+ } else if (params.fencingToken !== void 0) {
1024
+ this.fencing.verifyFencingToken(normPath, params.fencingToken, lock.fencing_token);
1025
+ }
1026
+ }
1027
+ }
1028
+ return { allowed: true };
1029
+ }
1030
+ releaseLocks(params) {
1031
+ return this.coordDb.withTransaction(() => {
1032
+ this.fencing.assertSession(params.agentId);
1033
+ const myLocks = this.coordDb.db.prepare(`SELECT * FROM locks WHERE owner_agent_id = ?`).all(params.agentId);
1034
+ if (params.fencingToken !== void 0) {
1035
+ for (const lock of myLocks) {
1036
+ if (!params.patterns || params.patterns.some(
1037
+ (p) => _LockEngine.normalizePattern(this.rootForAgent(params.agentId), p) === lock.pattern
1038
+ )) {
1039
+ this.fencing.verifyFencingToken(lock.pattern, params.fencingToken, lock.fencing_token);
1040
+ }
1041
+ }
1042
+ }
1043
+ if (!params.patterns || params.patterns.length === 0) {
1044
+ const res = this.coordDb.db.prepare(`DELETE FROM locks WHERE owner_agent_id = ?`).run(params.agentId);
1045
+ return Number(res.changes);
1046
+ }
1047
+ let count = 0;
1048
+ for (const rawPattern of params.patterns) {
1049
+ const norm = _LockEngine.normalizePattern(this.rootForAgent(params.agentId), rawPattern);
1050
+ const res = this.coordDb.db.prepare(`DELETE FROM locks WHERE owner_agent_id = ? AND pattern = ?`).run(params.agentId, norm);
1051
+ count += Number(res.changes);
1052
+ }
1053
+ return count;
1054
+ });
1055
+ }
1056
+ overlapsExisting(requested, existing) {
1057
+ try {
1058
+ const normalized = _LockEngine.normalizePattern(this.coordDb.repoRoot, existing);
1059
+ const roots = [this.coordDb.repoRoot, ...this.fencing.listAgents(true).map((a) => a.worktree_path).filter((p) => Boolean(p))];
1060
+ const insensitive = roots.some((root) => {
1061
+ if (!this.pathCase.has(root)) this.pathCase.set(root, caseSensitivePaths(root));
1062
+ return !this.pathCase.get(root);
1063
+ });
1064
+ const fold = (scope) => {
1065
+ const hash = scope.startsWith("symbol:") ? scope.lastIndexOf("#") : -1;
1066
+ const file = hash < 0 ? scope : scope.slice(0, hash);
1067
+ return file.normalize("NFC").toLowerCase().replace(/ς/g, "\u03C3") + (hash < 0 ? "" : scope.slice(hash));
1068
+ };
1069
+ const overlap = (a, b) => _LockEngine.patternsOverlap(a, b) || insensitive && _LockEngine.patternsOverlap(fold(a), fold(b));
1070
+ return overlap(requested, existing) || overlap(requested, normalized) || this.wildcardHasAliases(requested) || this.wildcardHasAliases(normalized);
1071
+ } catch {
1072
+ return true;
1073
+ }
1074
+ }
1075
+ listLocks() {
1076
+ this.fencing.reapDeadAgents();
1077
+ return this.coordDb.db.prepare(`SELECT * FROM locks WHERE expires_at > ? ORDER BY acquired_at ASC`).all(Date.now());
1078
+ }
1079
+ };
1080
+
1081
+ // src/core/bash-guard.ts
1082
+ var GIT_PREFIX = String.raw`\bgit(?:\s+(?:-C|-c|--git-dir|--work-tree)(?:=|\s+)(?:"[^"]*"|'[^']*'|[^\s;&|]+)|\s+--(?:no-pager|literal-pathspecs|no-optional-locks))*\s+`;
1083
+ var DESTRUCTIVE_SHARED_TREE = new RegExp(GIT_PREFIX + String.raw`(?:reset|clean|checkout|restore|switch|stash|merge|rebase|pull|cherry-pick|revert|am)\b`);
1084
+ var DESTRUCTIVE_SHARED_REFS = new RegExp(GIT_PREFIX + String.raw`(?:update-ref|symbolic-ref|worktree\s+(?:remove|prune)|branch\s+(?:-[dDmMfF]|--(?:delete|move|force)))\b`);
1085
+ var BashMerkleGuard = class {
1086
+ constructor(coordDb, fencing, lockEngine, depGraph, shadowGit) {
1087
+ this.coordDb = coordDb;
1088
+ this.fencing = fencing;
1089
+ this.lockEngine = lockEngine;
1090
+ this.depGraph = depGraph;
1091
+ this.shadowGit = shadowGit;
1092
+ }
1093
+ coordDb;
1094
+ fencing;
1095
+ lockEngine;
1096
+ depGraph;
1097
+ shadowGit;
1098
+ /**
1099
+ * Checks whether a shell command is safe to execute in the workspace.
1100
+ */
1101
+ checkCommandSafety(params) {
1102
+ this.fencing.assertSession(params.agentId);
1103
+ const root = physicalPath(this.fencing.getAgent(params.agentId)?.worktree_path ?? this.coordDb.repoRoot);
1104
+ const peers = this.fencing.listAgents(true).filter((a) => a.id !== params.agentId);
1105
+ const sharesCheckout = peers.some((a) => physicalPath(a.worktree_path ?? this.coordDb.repoRoot) === root);
1106
+ if (peers.length && DESTRUCTIVE_SHARED_REFS.test(params.command) || sharesCheckout && DESTRUCTIVE_SHARED_TREE.test(params.command)) {
1107
+ return { safe: false, reason: "Git operation can replace shared work or refs while peer agents are active. Coordinate the operation before retrying." };
1108
+ }
1109
+ return { safe: true };
1110
+ }
1111
+ getSnapshotFilePath(agentId) {
1112
+ const epoch = this.fencing.assertSession(agentId);
1113
+ const safeId = crypto4.createHash("sha256").update(`${agentId}:${epoch}`).digest("hex");
1114
+ return path4.join(this.coordDb.coordDir, "snapshots", `${safeId}.json`);
1115
+ }
1116
+ /**
1117
+ * Captures a Merkle snapshot of workspace files before a Bash tool execution.
1118
+ * For any file locked by another active agent, stores a full lossless backup so
1119
+ * unauthorized mutations by bash scripts (`sed`, `python`, etc.) can be reverted atomically.
1120
+ */
1121
+ capturePreBashSnapshot(agentId, rootDir = this.coordDb.repoRoot) {
1122
+ const activeLocks = this.lockEngine.listLocks().filter((l) => l.owner_agent_id !== agentId);
1123
+ const entries = {};
1124
+ const files = this.walkWorkspaceFiles(rootDir);
1125
+ for (const absPath of files) {
1126
+ const relPath = path4.relative(rootDir, absPath).replace(/\\/g, "/");
1127
+ const stat = fs5.statSync(absPath);
1128
+ if (stat.size > 5 * 1024 * 1024) continue;
1129
+ const buf = fs5.readFileSync(absPath);
1130
+ const sha256 = crypto4.createHash("sha256").update(buf).digest("hex");
1131
+ const lockedByOther = activeLocks.some(
1132
+ (l) => LockEngine.patternsOverlap(relPath, l.pattern)
1133
+ );
1134
+ entries[relPath] = {
1135
+ relPath,
1136
+ mtimeMs: stat.mtimeMs,
1137
+ size: stat.size,
1138
+ sha256,
1139
+ lockedByOther,
1140
+ backupContentBase64: lockedByOther ? buf.toString("base64") : void 0
1141
+ };
1142
+ }
1143
+ const snapPath = this.getSnapshotFilePath(agentId);
1144
+ fs5.writeFileSync(
1145
+ snapPath,
1146
+ JSON.stringify({ timestamp: Date.now(), rootDir, entries }),
1147
+ { encoding: "utf8", mode: 384 }
1148
+ );
1149
+ return snapPath;
1150
+ }
1151
+ /**
1152
+ * Compares workspace state after a Bash command completes against the pre-Bash Merkle snapshot.
1153
+ * Saves recovery copies of edits to files locked by sibling agents. Restoration is opt-in,
1154
+ * and indexes authorized modifications into the AST Contract Blackboard & Dirty-Tree Conflict Radar.
1155
+ */
1156
+ verifyPostBashAndQuarantine(params) {
1157
+ const snapPath = this.getSnapshotFilePath(params.agentId);
1158
+ if (!fs5.existsSync(snapPath)) {
1159
+ return { allowed: true, authorizedModifiedFiles: [], quarantinedViolations: [] };
1160
+ }
1161
+ const rawSnap = JSON.parse(fs5.readFileSync(snapPath, "utf8"));
1162
+ const rootDir = params.rootDir ?? rawSnap.rootDir ?? this.coordDb.repoRoot;
1163
+ const beforeEntries = rawSnap.entries;
1164
+ const currentFiles = this.walkWorkspaceFiles(rootDir);
1165
+ const seenCurrentRelPaths = /* @__PURE__ */ new Set();
1166
+ const authorizedModifiedFiles = [];
1167
+ const quarantinedViolations = [];
1168
+ for (const absPath of currentFiles) {
1169
+ const relPath = path4.relative(rootDir, absPath).replace(/\\/g, "/");
1170
+ seenCurrentRelPaths.add(relPath);
1171
+ const stat = fs5.statSync(absPath);
1172
+ if (stat.size > 5 * 1024 * 1024) continue;
1173
+ const prev = beforeEntries[relPath];
1174
+ if (prev && prev.mtimeMs === stat.mtimeMs && prev.size === stat.size) {
1175
+ continue;
1176
+ }
1177
+ const buf = fs5.readFileSync(absPath);
1178
+ const sha256 = crypto4.createHash("sha256").update(buf).digest("hex");
1179
+ if (prev && prev.sha256 === sha256) {
1180
+ continue;
1181
+ }
1182
+ const access = this.lockEngine.checkFileWriteAccess({
1183
+ agentId: params.agentId,
1184
+ filePath: relPath
1185
+ });
1186
+ if (!access.allowed) {
1187
+ const violation = this.quarantineAndRevertFile({
1188
+ agentId: params.agentId,
1189
+ command: params.command,
1190
+ rootDir,
1191
+ relPath,
1192
+ unauthorizedBuf: buf,
1193
+ restoreLockedFiles: params.restoreLockedFiles ?? false,
1194
+ prevEntry: prev ?? null,
1195
+ reason: access.reason ?? "Locked by another agent"
1196
+ });
1197
+ quarantinedViolations.push(violation);
1198
+ } else {
1199
+ authorizedModifiedFiles.push(relPath);
1200
+ }
1201
+ }
1202
+ for (const [relPath, prev] of Object.entries(beforeEntries)) {
1203
+ if (!seenCurrentRelPaths.has(relPath)) {
1204
+ const access = this.lockEngine.checkFileWriteAccess({
1205
+ agentId: params.agentId,
1206
+ filePath: relPath
1207
+ });
1208
+ if (!access.allowed) {
1209
+ const violation = this.quarantineAndRevertFile({
1210
+ agentId: params.agentId,
1211
+ command: params.command,
1212
+ rootDir,
1213
+ relPath,
1214
+ unauthorizedBuf: null,
1215
+ restoreLockedFiles: params.restoreLockedFiles ?? false,
1216
+ prevEntry: prev,
1217
+ reason: access.reason ?? "Deleted file locked by another agent"
1218
+ });
1219
+ quarantinedViolations.push(violation);
1220
+ } else {
1221
+ authorizedModifiedFiles.push(relPath);
1222
+ }
1223
+ }
1224
+ }
1225
+ try {
1226
+ fs5.unlinkSync(snapPath);
1227
+ } catch {
1228
+ }
1229
+ if (authorizedModifiedFiles.length > 0) {
1230
+ for (const relFile of authorizedModifiedFiles) {
1231
+ const absFile = path4.join(rootDir, relFile);
1232
+ if (fs5.existsSync(absFile) && /\.(ts|tsx|js|jsx|mjs|py)$/.test(relFile)) {
1233
+ this.depGraph.recordFileModification(params.agentId, relFile, fs5.readFileSync(absFile, "utf8"));
1234
+ }
1235
+ }
1236
+ this.shadowGit.probeAllActiveConflicts(
1237
+ params.agentId,
1238
+ authorizedModifiedFiles.map((f) => ({ filePath: f })),
1239
+ rootDir
1240
+ );
1241
+ }
1242
+ return {
1243
+ allowed: quarantinedViolations.length === 0,
1244
+ authorizedModifiedFiles,
1245
+ quarantinedViolations
1246
+ };
1247
+ }
1248
+ quarantineAndRevertFile(params) {
1249
+ const id = `quar_${crypto4.randomBytes(6).toString("hex")}`;
1250
+ const now = Date.now();
1251
+ const safeName = params.relPath.replace(/[^a-zA-Z0-9._-]/g, "_");
1252
+ const patchPath = path4.join(
1253
+ this.coordDb.coordDir,
1254
+ "quarantine",
1255
+ `${id}_${safeName}.quarantine`
1256
+ );
1257
+ if (params.unauthorizedBuf) {
1258
+ fs5.writeFileSync(patchPath, params.unauthorizedBuf, { mode: 384 });
1259
+ } else {
1260
+ fs5.writeFileSync(patchPath, `[DELETED BY COMMAND: ${params.command}]
1261
+ `, { encoding: "utf8", mode: 384 });
1262
+ }
1263
+ if (params.prevEntry?.backupContentBase64 !== void 0) {
1264
+ fs5.writeFileSync(patchPath + ".before", Buffer.from(params.prevEntry.backupContentBase64, "base64"), { mode: 384 });
1265
+ }
1266
+ const absTarget = path4.join(params.rootDir, params.relPath);
1267
+ if (params.restoreLockedFiles && params.prevEntry === null) {
1268
+ if (fs5.existsSync(absTarget)) {
1269
+ fs5.unlinkSync(absTarget);
1270
+ }
1271
+ } else if (params.restoreLockedFiles && params.prevEntry?.backupContentBase64 !== void 0) {
1272
+ fs5.mkdirSync(path4.dirname(absTarget), { recursive: true });
1273
+ fs5.writeFileSync(
1274
+ absTarget,
1275
+ Buffer.from(params.prevEntry.backupContentBase64, "base64")
1276
+ );
1277
+ }
1278
+ const record = {
1279
+ id,
1280
+ agent_id: params.agentId,
1281
+ command: params.command,
1282
+ file_path: params.relPath,
1283
+ patch_path: patchPath,
1284
+ reason: params.reason,
1285
+ created_at: now
1286
+ };
1287
+ this.coordDb.withTransaction(() => {
1288
+ this.coordDb.db.prepare(`
1289
+ INSERT INTO quarantine (id, agent_id, command, file_path, patch_path, reason, created_at)
1290
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1291
+ `).run(
1292
+ record.id,
1293
+ record.agent_id,
1294
+ record.command,
1295
+ record.file_path,
1296
+ record.patch_path,
1297
+ record.reason,
1298
+ record.created_at
1299
+ );
1300
+ this.coordDb.logEvent(params.agentId, "bash_mutation_quarantined", {
1301
+ filePath: params.relPath,
1302
+ command: params.command,
1303
+ patchPath,
1304
+ reason: params.reason
1305
+ });
1306
+ });
1307
+ return record;
1308
+ }
1309
+ walkWorkspaceFiles(dir, acc = []) {
1310
+ if (!fs5.existsSync(dir)) return acc;
1311
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
1312
+ for (const entry of entries) {
1313
+ if (entry.name === ".git" || entry.name === ".coord" || entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage" || entry.name === ".next" || entry.name === "target") {
1314
+ continue;
1315
+ }
1316
+ const fullPath = path4.join(dir, entry.name);
1317
+ if (entry.isDirectory()) {
1318
+ this.walkWorkspaceFiles(fullPath, acc);
1319
+ } else if (entry.isFile()) {
1320
+ acc.push(fullPath);
1321
+ }
1322
+ }
1323
+ return acc;
1324
+ }
1325
+ listQuarantined() {
1326
+ return this.coordDb.db.prepare(`SELECT * FROM quarantine ORDER BY created_at DESC`).all();
1327
+ }
1328
+ };
1329
+
1330
+ // src/core/broker.ts
1331
+ import { fork } from "child_process";
1332
+ import fs6 from "fs";
1333
+ import { fileURLToPath } from "url";
1334
+ import crypto5 from "crypto";
1335
+ import net from "net";
1336
+ var ResourceBroker = class _ResourceBroker {
1337
+ constructor(coordDb, fencing) {
1338
+ this.coordDb = coordDb;
1339
+ this.fencing = fencing;
1340
+ }
1341
+ coordDb;
1342
+ fencing;
1343
+ activeExecutions = 0;
1344
+ hasActiveCommands() {
1345
+ return this.activeExecutions > 0;
1346
+ }
1347
+ /**
1348
+ * Acquires a collision-free port lease for `(agentId, serviceName)`.
1349
+ * Verifies both SQLite resource table and OS TCP socket availability.
1350
+ */
1351
+ async acquirePort(params) {
1352
+ this.fencing.reapDeadAgents();
1353
+ this.fencing.assertSession(params.agentId);
1354
+ const basePort = params.basePort ?? 3100;
1355
+ const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
1356
+ const now = Date.now();
1357
+ const expiresAt = now + leaseMs;
1358
+ const resourceKey = `${params.agentId}:${params.serviceName}`;
1359
+ const existing = this.getResource("port", resourceKey);
1360
+ if (existing && existing.expires_at > now) {
1361
+ return this.coordDb.withTransaction(() => {
1362
+ this.fencing.assertSession(params.agentId);
1363
+ this.coordDb.db.prepare(`UPDATE resources SET expires_at = ? WHERE id = ?`).run(expiresAt, existing.id);
1364
+ return {
1365
+ port: Number(existing.value),
1366
+ fencingToken: existing.fencing_token,
1367
+ resource: this.getResource("port", resourceKey)
1368
+ };
1369
+ });
1370
+ }
1371
+ if (!Number.isInteger(basePort) || basePort < 1 || basePort > 65535) {
1372
+ throw new Error("basePort must be an integer between 1 and 65535");
1373
+ }
1374
+ const limit = Math.min(basePort + 500, 65536);
1375
+ for (let candidatePort = basePort; candidatePort < limit; candidatePort++) {
1376
+ if (!await _ResourceBroker.isTcpPortFree(candidatePort)) continue;
1377
+ const reservation = this.coordDb.withTransaction(() => {
1378
+ this.fencing.assertSession(params.agentId);
1379
+ const now2 = Date.now();
1380
+ const sameService = this.getResource("port", resourceKey);
1381
+ if (sameService && sameService.expires_at > now2) {
1382
+ return { port: Number(sameService.value), fencingToken: sameService.fencing_token, resource: sameService };
1383
+ }
1384
+ const taken = this.coordDb.db.prepare(
1385
+ `SELECT id FROM resources WHERE type = 'port' AND value = ? AND expires_at > ?`
1386
+ ).get(String(candidatePort), now2);
1387
+ if (taken) return null;
1388
+ const token = this.coordDb.nextFencingToken();
1389
+ this.coordDb.db.prepare(`
1390
+ INSERT OR REPLACE INTO resources (
1391
+ id, type, name, value, owner_agent_id, fencing_token, queue_json, acquired_at, expires_at
1392
+ ) VALUES (?, 'port', ?, ?, ?, ?, '[]', ?, ?)
1393
+ `).run(
1394
+ `res_${crypto5.randomBytes(6).toString("hex")}`,
1395
+ resourceKey,
1396
+ String(candidatePort),
1397
+ params.agentId,
1398
+ token,
1399
+ now2,
1400
+ now2 + leaseMs
1401
+ );
1402
+ this.coordDb.logEvent(params.agentId, "port_leased", {
1403
+ serviceName: params.serviceName,
1404
+ port: candidatePort,
1405
+ fencingToken: token
1406
+ });
1407
+ return { port: candidatePort, fencingToken: token, resource: this.getResource("port", resourceKey) };
1408
+ });
1409
+ if (reservation) return reservation;
1410
+ }
1411
+ throw new Error(`No free port available in range ${basePort}-${limit - 1}`);
1412
+ }
1413
+ static isTcpPortFree(port) {
1414
+ return new Promise((resolve) => {
1415
+ const server = net.createServer();
1416
+ server.unref();
1417
+ server.on("error", () => resolve(false));
1418
+ server.listen(port, "127.0.0.1", () => {
1419
+ server.close(() => resolve(true));
1420
+ });
1421
+ });
1422
+ }
1423
+ /** FIFO order survives releases; queued requests belong to a specific session epoch. */
1424
+ tryAcquireMutex(params) {
1425
+ this.fencing.reapDeadAgents();
1426
+ const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
1427
+ return this.coordDb.withTransaction(() => {
1428
+ const epoch = this.fencing.assertSession(params.agentId);
1429
+ const now = Date.now();
1430
+ const requestId = params.requestId ?? `session:${epoch}`;
1431
+ this.pruneWaiters(params.mutexName);
1432
+ const run = this.getRun(params.mutexName);
1433
+ const guarded = run && this.runIsAlive(run);
1434
+ if (run && !guarded) this.clearRun(run);
1435
+ const existing = this.getResource("mutex", params.mutexName);
1436
+ if (!guarded && existing && existing.expires_at > now && existing.owner_agent_id === params.agentId) {
1437
+ this.coordDb.db.prepare("UPDATE resources SET expires_at=? WHERE id=?").run(now + leaseMs, existing.id);
1438
+ return { acquired: true, fencingToken: existing.fencing_token, holderAgentId: params.agentId, queuePosition: 0 };
1439
+ }
1440
+ const start = FencingAndAgentManager.getProcessStartTime(process.pid);
1441
+ if (!start) throw new Error("Cannot inspect mutex waiter process. Sandboxed MCP clients can use kith_run_with_mutex instead.");
1442
+ this.coordDb.db.prepare(`INSERT INTO mutex_waiters
1443
+ (mutex_name, request_id, owner_agent_id, session_epoch, waiter_pid, waiter_start, expires_at)
1444
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1445
+ ON CONFLICT(mutex_name, request_id) DO UPDATE SET expires_at=excluded.expires_at
1446
+ `).run(params.mutexName, requestId, params.agentId, epoch, process.pid, start, now + leaseMs);
1447
+ const waiters = this.waiters(params.mutexName);
1448
+ const position = waiters.findIndex((w) => w.request_id === requestId) + 1;
1449
+ if (guarded || existing && existing.expires_at > now || position !== 1) {
1450
+ return {
1451
+ acquired: false,
1452
+ fencingToken: run?.fencing_token ?? existing?.fencing_token ?? 0,
1453
+ holderAgentId: guarded ? run.owner_agent_id : existing?.owner_agent_id ?? waiters[0].owner_agent_id,
1454
+ queuePosition: position
1455
+ };
1456
+ }
1457
+ const token = this.coordDb.nextFencingToken();
1458
+ this.coordDb.db.prepare(`INSERT OR REPLACE INTO resources
1459
+ (id, type, name, value, owner_agent_id, fencing_token, queue_json, acquired_at, expires_at)
1460
+ VALUES (?, 'mutex', ?, 'locked', ?, ?, '[]', ?, ?)
1461
+ `).run(existing?.id ?? `res_${crypto5.randomBytes(6).toString("hex")}`, params.mutexName, params.agentId, token, now, now + leaseMs);
1462
+ this.coordDb.db.prepare("DELETE FROM mutex_waiters WHERE mutex_name=? AND request_id=?").run(params.mutexName, requestId);
1463
+ this.coordDb.logEvent(params.agentId, "mutex_acquired", { mutexName: params.mutexName, fencingToken: token });
1464
+ return { acquired: true, fencingToken: token, holderAgentId: params.agentId, queuePosition: 0 };
1465
+ });
1466
+ }
1467
+ renewMutex(params) {
1468
+ const lease = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
1469
+ this.coordDb.withTransaction(() => {
1470
+ this.fencing.assertSession(params.agentId);
1471
+ const resource = this.getResource("mutex", params.mutexName);
1472
+ if (!resource || resource.owner_agent_id !== params.agentId || resource.expires_at <= Date.now()) {
1473
+ throw new Error(`Mutex lease lost: ${params.mutexName}`);
1474
+ }
1475
+ this.fencing.verifyFencingToken(params.mutexName, params.fencingToken, resource.fencing_token);
1476
+ this.coordDb.db.prepare("UPDATE resources SET expires_at=? WHERE id=?").run(Date.now() + lease, resource.id);
1477
+ });
1478
+ }
1479
+ releaseMutex(params) {
1480
+ return this.coordDb.withTransaction(() => {
1481
+ this.fencing.assertSession(params.agentId);
1482
+ const resource = this.getResource("mutex", params.mutexName);
1483
+ if (!resource || resource.owner_agent_id !== params.agentId) return false;
1484
+ this.fencing.verifyFencingToken(params.mutexName, params.fencingToken, resource.fencing_token);
1485
+ const run = this.getRun(params.mutexName);
1486
+ if (run && this.runIsAlive(run)) throw new Error("Cannot release a mutex while its supervised process group is alive");
1487
+ this.coordDb.db.prepare("DELETE FROM resources WHERE id=?").run(resource.id);
1488
+ this.coordDb.logEvent(params.agentId, "mutex_released", { mutexName: params.mutexName });
1489
+ return true;
1490
+ });
1491
+ }
1492
+ /** Renew leases while a detached, recorded process group runs; stop it before releasing. */
1493
+ async executeWithMutex(params) {
1494
+ if (process.platform === "win32") throw new Error("Supervised mutex commands currently require macOS or Linux process groups");
1495
+ if (!params.command.length) throw new Error("A command is required");
1496
+ const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
1497
+ const waitTimeout = params.timeoutMs ?? 6e4;
1498
+ const grace = params.killGraceMs ?? 1e3;
1499
+ if (!Number.isFinite(waitTimeout) || waitTimeout < 0 || !Number.isFinite(grace) || grace < 0 || params.commandTimeoutMs !== void 0 && (!Number.isFinite(params.commandTimeoutMs) || params.commandTimeoutMs <= 0)) {
1500
+ throw new Error("Timeouts must be finite nonnegative milliseconds (command timeout must be positive)");
1501
+ }
1502
+ if (params.maxOutputBytes !== void 0 && (!Number.isSafeInteger(params.maxOutputBytes) || params.maxOutputBytes <= 0)) throw new Error("maxOutputBytes must be a positive integer");
1503
+ const epoch = this.fencing.assertSession(params.agentId);
1504
+ const requestId = crypto5.randomUUID();
1505
+ const started = Date.now();
1506
+ let run = null;
1507
+ const abortError = () => new DOMException("Mutex command cancelled", "AbortError");
1508
+ this.activeExecutions++;
1509
+ try {
1510
+ while (!run) {
1511
+ if (params.signal?.aborted) throw abortError();
1512
+ run = this.coordDb.withTransaction(() => {
1513
+ const attempt = this.tryAcquireMutex({ ...params, requestId });
1514
+ if (!attempt.acquired) return null;
1515
+ const record = {
1516
+ mutex_name: params.mutexName,
1517
+ run_id: requestId,
1518
+ owner_agent_id: params.agentId,
1519
+ session_epoch: epoch,
1520
+ fencing_token: attempt.fencingToken,
1521
+ supervisor_pid: process.pid,
1522
+ supervisor_start: FencingAndAgentManager.getProcessStartTime(process.pid),
1523
+ worker_pid: null,
1524
+ worker_start: null
1525
+ };
1526
+ this.coordDb.db.prepare(`INSERT INTO mutex_runs
1527
+ (mutex_name, run_id, owner_agent_id, session_epoch, fencing_token, supervisor_pid, supervisor_start)
1528
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1529
+ `).run(record.mutex_name, record.run_id, record.owner_agent_id, epoch, record.fencing_token, process.pid, record.supervisor_start);
1530
+ return record;
1531
+ });
1532
+ if (run) break;
1533
+ if (Date.now() - started >= waitTimeout) throw new Error(`Timed out waiting for mutex '${params.mutexName}'`);
1534
+ this.fencing.heartbeat(params.agentId);
1535
+ await delay(Math.min(50, Math.max(10, leaseMs / 4)));
1536
+ }
1537
+ const waitedMs = Date.now() - started;
1538
+ const candidates = ["./command-worker.js", "../command-worker.js", "../../dist/command-worker.js"].map((name) => fileURLToPath(new URL(name, import.meta.url)));
1539
+ const script = candidates.find((name) => fs6.existsSync(name));
1540
+ if (!script) throw new Error("Missing command worker. Build Kith before running supervised commands.");
1541
+ const worker = fork(script, [], { detached: true, stdio: ["ignore", "pipe", "pipe", "ipc"], execArgv: [] });
1542
+ const workerPid = worker.pid;
1543
+ if (!workerPid) {
1544
+ worker.on("error", () => {
1545
+ });
1546
+ throw new Error("Could not start command supervisor");
1547
+ }
1548
+ let workerIdentity = FencingAndAgentManager.getProcessStartTime(workerPid);
1549
+ let failure;
1550
+ let outcome;
1551
+ const stdout = [];
1552
+ const stderr = [];
1553
+ let outputBytes = 0;
1554
+ let killTimer;
1555
+ let stopTimer;
1556
+ let stopDeadline;
1557
+ let disposed = false;
1558
+ let finishStopped;
1559
+ const stopped = new Promise((resolve) => {
1560
+ finishStopped = resolve;
1561
+ });
1562
+ const stop = (err) => {
1563
+ if (disposed) return;
1564
+ failure ??= err;
1565
+ const termError = signalGroup(workerPid, "SIGTERM", workerIdentity);
1566
+ failure ??= termError;
1567
+ killTimer ??= setTimeout(() => {
1568
+ const killError = signalGroup(workerPid, "SIGKILL", workerIdentity);
1569
+ failure ??= killError;
1570
+ }, grace);
1571
+ stopDeadline ??= Date.now() + grace + 5e3;
1572
+ stopTimer ??= setTimeout(finishStopped, Math.max(0, stopDeadline - Date.now()));
1573
+ };
1574
+ const onAbort = () => stop(abortError());
1575
+ params.signal?.addEventListener("abort", onAbort, { once: true });
1576
+ const interval = setInterval(() => {
1577
+ try {
1578
+ this.fencing.heartbeat(params.agentId);
1579
+ this.renewMutex({ ...params, fencingToken: run.fencing_token });
1580
+ } catch (err) {
1581
+ stop(err instanceof Error ? err : new Error(String(err)));
1582
+ }
1583
+ }, Math.max(10, Math.floor(leaseMs / 3)));
1584
+ const commandTimer = params.commandTimeoutMs === void 0 ? void 0 : setTimeout(() => stop(new Error("Mutex command execution timed out")), params.commandTimeoutMs);
1585
+ const capture = (destination, stream) => (chunk) => {
1586
+ stream?.(chunk);
1587
+ outputBytes += chunk.length;
1588
+ if (outputBytes > (params.maxOutputBytes ?? 16 * 1024 * 1024)) stop(new Error("Mutex command output limit exceeded"));
1589
+ else destination.push(chunk);
1590
+ };
1591
+ worker.stdout.on("data", capture(stdout, params.onStdout));
1592
+ worker.stderr.on("data", capture(stderr, params.onStderr));
1593
+ worker.on("message", (message) => {
1594
+ if (disposed) return;
1595
+ if (message.ready) {
1596
+ try {
1597
+ if (params.signal?.aborted) throw abortError();
1598
+ this.coordDb.withTransaction(() => {
1599
+ this.fencing.assertSession(params.agentId);
1600
+ const identity = FencingAndAgentManager.getProcessStartTime(workerPid);
1601
+ if (!identity) throw new Error("Cannot inspect command worker");
1602
+ this.coordDb.db.prepare("UPDATE mutex_runs SET worker_pid=?, worker_start=? WHERE run_id=?").run(workerPid, identity, requestId);
1603
+ run.worker_pid = workerPid;
1604
+ run.worker_start = identity;
1605
+ workerIdentity = identity;
1606
+ this.renewMutex({ ...params, fencingToken: run.fencing_token });
1607
+ });
1608
+ worker.send({ command: params.command, cwd: params.cwd ?? this.coordDb.repoRoot, mutexName: params.mutexName });
1609
+ } catch (err) {
1610
+ stop(err instanceof Error ? err : new Error(String(err)));
1611
+ }
1612
+ } else {
1613
+ outcome = { exitCode: message.exitCode ?? 1, error: message.error };
1614
+ }
1615
+ });
1616
+ worker.once("exit", () => {
1617
+ if (!disposed && groupIsAlive(workerPid, workerIdentity)) stop();
1618
+ });
1619
+ try {
1620
+ await Promise.race([new Promise((resolve, reject) => {
1621
+ worker.once("close", () => resolve());
1622
+ worker.once("error", reject);
1623
+ }), stopped]);
1624
+ if (params.signal?.aborted) failure ??= abortError();
1625
+ if (failure) throw failure;
1626
+ if (!outcome) throw new Error("Command worker exited without a result");
1627
+ if (outcome.error) throw new Error(outcome.error);
1628
+ return { stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString(), exitCode: outcome.exitCode, waitedMs };
1629
+ } finally {
1630
+ clearInterval(interval);
1631
+ if (commandTimer) clearTimeout(commandTimer);
1632
+ params.signal?.removeEventListener("abort", onAbort);
1633
+ if (groupIsAlive(workerPid, workerIdentity)) {
1634
+ const killError = signalGroup(workerPid, "SIGKILL", workerIdentity);
1635
+ failure ??= killError;
1636
+ const deadline = stopDeadline ?? Date.now() + 5e3;
1637
+ while (groupIsAlive(workerPid, workerIdentity) && Date.now() < deadline) await delay(20);
1638
+ }
1639
+ disposed = true;
1640
+ if (killTimer) clearTimeout(killTimer);
1641
+ if (stopTimer) clearTimeout(stopTimer);
1642
+ if (groupIsAlive(workerPid, workerIdentity)) {
1643
+ worker.stdout?.destroy();
1644
+ worker.stderr?.destroy();
1645
+ if (worker.connected) worker.disconnect();
1646
+ worker.unref();
1647
+ throw new Error("Command group has not stopped; mutex guard retained for safety");
1648
+ }
1649
+ }
1650
+ } finally {
1651
+ try {
1652
+ this.coordDb.withTransaction(() => {
1653
+ this.coordDb.db.prepare("DELETE FROM mutex_waiters WHERE mutex_name=? AND request_id=? AND session_epoch=?").run(params.mutexName, requestId, epoch);
1654
+ if (run && !this.runIsAlive(run)) this.clearRun(run);
1655
+ if (run && run.worker_pid === null) this.clearRun(run);
1656
+ });
1657
+ } finally {
1658
+ this.activeExecutions--;
1659
+ }
1660
+ }
1661
+ }
1662
+ getRun(name) {
1663
+ return this.coordDb.db.prepare("SELECT * FROM mutex_runs WHERE mutex_name=?").get(name) ?? null;
1664
+ }
1665
+ runIsAlive(run) {
1666
+ return run.worker_pid !== null ? groupIsAlive(run.worker_pid, run.worker_start) : FencingAndAgentManager.isProcessAlive(run.supervisor_pid, run.supervisor_start);
1667
+ }
1668
+ clearRun(run) {
1669
+ this.coordDb.db.prepare("DELETE FROM mutex_runs WHERE mutex_name=? AND run_id=?").run(run.mutex_name, run.run_id);
1670
+ this.coordDb.db.prepare("DELETE FROM resources WHERE type='mutex' AND name=? AND fencing_token=?").run(run.mutex_name, run.fencing_token);
1671
+ }
1672
+ waiters(name) {
1673
+ return this.coordDb.db.prepare("SELECT * FROM mutex_waiters WHERE mutex_name=? ORDER BY ticket").all(name);
1674
+ }
1675
+ pruneWaiters(name) {
1676
+ for (const waiter of this.waiters(name)) {
1677
+ const agent = this.fencing.getAgent(waiter.owner_agent_id);
1678
+ const session = this.coordDb.db.prepare("SELECT epoch FROM agent_sessions WHERE agent_id=?").get(waiter.owner_agent_id);
1679
+ if (!agent || agent.status !== "active" || agent.lease_expires_at <= Date.now() || session?.epoch !== waiter.session_epoch || waiter.expires_at <= Date.now() || !FencingAndAgentManager.isProcessAlive(waiter.waiter_pid, waiter.waiter_start)) {
1680
+ this.coordDb.db.prepare("DELETE FROM mutex_waiters WHERE ticket=?").run(waiter.ticket);
1681
+ }
1682
+ }
1683
+ }
1684
+ getResource(type, name) {
1685
+ const row = this.coordDb.db.prepare(`SELECT * FROM resources WHERE type = ? AND name = ?`).get(type, name);
1686
+ if (!row) return null;
1687
+ return this.mapRow(row);
1688
+ }
1689
+ listResources() {
1690
+ this.fencing.reapDeadAgents();
1691
+ const rows = this.coordDb.db.prepare(`SELECT * FROM resources WHERE expires_at > ? ORDER BY acquired_at ASC`).all(Date.now());
1692
+ return rows.map((r) => this.mapRow(r));
1693
+ }
1694
+ mapRow(row) {
1695
+ return {
1696
+ id: String(row.id),
1697
+ type: row.type,
1698
+ name: String(row.name),
1699
+ value: String(row.value),
1700
+ owner_agent_id: String(row.owner_agent_id),
1701
+ fencing_token: Number(row.fencing_token),
1702
+ queue: row.type === "mutex" ? this.waiters(String(row.name)).map((w) => w.owner_agent_id) : [],
1703
+ acquired_at: Number(row.acquired_at),
1704
+ expires_at: Number(row.expires_at)
1705
+ };
1706
+ }
1707
+ };
1708
+ function groupIsAlive(pgid, expectedStart) {
1709
+ if (expectedStart) {
1710
+ const currentStart = FencingAndAgentManager.getProcessStartTime(pgid);
1711
+ if (currentStart && currentStart !== expectedStart) return false;
1712
+ }
1713
+ try {
1714
+ process.kill(-pgid, 0);
1715
+ } catch (err) {
1716
+ return err.code !== "ESRCH";
1717
+ }
1718
+ if (process.platform === "linux") {
1719
+ try {
1720
+ for (const pid of fs6.readdirSync("/proc").filter((name) => /^\d+$/.test(name))) {
1721
+ let stat;
1722
+ try {
1723
+ stat = fs6.readFileSync(`/proc/${pid}/stat`, "utf8");
1724
+ } catch (err) {
1725
+ if (err.code === "ENOENT" || err.code === "ESRCH") continue;
1726
+ return true;
1727
+ }
1728
+ const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
1729
+ if (Number(fields[2]) === pgid && fields[0] !== "Z" && fields[0] !== "X") return true;
1730
+ }
1731
+ return false;
1732
+ } catch {
1733
+ return true;
1734
+ }
1735
+ }
1736
+ return true;
1737
+ }
1738
+ function signalGroup(pgid, signal, expectedStart) {
1739
+ if (!groupIsAlive(pgid, expectedStart)) return;
1740
+ try {
1741
+ process.kill(-pgid, signal);
1742
+ } catch (err) {
1743
+ if (err.code !== "ESRCH") return err instanceof Error ? err : new Error(String(err));
1744
+ }
1745
+ }
1746
+ function delay(ms) {
1747
+ return new Promise((resolve) => setTimeout(resolve, ms));
1748
+ }
1749
+
1750
+ // src/core/db.ts
1751
+ import { createRequire } from "module";
1752
+ import fs7 from "fs";
1753
+ import path5 from "path";
1754
+ var require2 = createRequire(import.meta.url);
1755
+ var { DatabaseSync } = require2("node:sqlite");
1756
+ var CoordDatabase = class _CoordDatabase {
1757
+ db;
1758
+ repoRoot;
1759
+ commonGitDir;
1760
+ isLinkedWorktree;
1761
+ coordDir;
1762
+ dbPath;
1763
+ inTransaction = false;
1764
+ constructor(repoRoot, customDbPath) {
1765
+ const layout = discoverRepository(repoRoot);
1766
+ this.repoRoot = layout.checkoutRoot;
1767
+ this.commonGitDir = layout.commonGitDir;
1768
+ this.isLinkedWorktree = layout.isLinkedWorktree;
1769
+ for (let current = fs7.realpathSync.native(repoRoot); isWithin(this.repoRoot, current); current = path5.dirname(current)) {
1770
+ const local = path5.join(current, ".coord");
1771
+ if (local !== layout.coordDir && fs7.existsSync(path5.join(local, "state.db"))) {
1772
+ throw new Error(`Legacy checkout-local coordination state found at ${local}. Stop clients and preserve that directory before consolidating shared state.`);
1773
+ }
1774
+ if (current === this.repoRoot) break;
1775
+ }
1776
+ this.coordDir = layout.coordDir;
1777
+ if (physicalPath(this.coordDir) !== this.coordDir) throw new Error("Coordination directory must not be a symlink");
1778
+ this.dbPath = customDbPath ?? path5.join(this.coordDir, "state.db");
1779
+ if (!customDbPath && physicalPath(this.dbPath) !== this.dbPath) throw new Error("Coordination database must not be a symlink");
1780
+ const recoveryFiles = !customDbPath && (fs7.existsSync(path5.join(this.coordDir, "merge-train.lock")) || fs7.existsSync(path5.join(this.coordDir, "setup.lock")) || ["shadow-index", "quarantine", "snapshots", "worktrees", "setup"].some((name) => {
1781
+ const dir = path5.join(this.coordDir, name);
1782
+ return fs7.existsSync(dir) && (!fs7.lstatSync(dir).isDirectory() || fs7.readdirSync(dir).length > 0);
1783
+ }));
1784
+ const sidecars = ["-wal", "-shm", "-journal"].some((suffix) => fs7.existsSync(this.dbPath + suffix));
1785
+ if (!fs7.existsSync(this.dbPath) && (recoveryFiles || sidecars)) {
1786
+ throw new Error("Missing coordination database with existing recovery state. Stop clients and preserve the database, sidecars and worktrees before repair.");
1787
+ }
1788
+ if (fs7.existsSync(this.dbPath)) {
1789
+ const probe = new DatabaseSync(this.dbPath, { readOnly: true });
1790
+ try {
1791
+ probe.exec("PRAGMA busy_timeout=10000; BEGIN");
1792
+ _CoordDatabase.schemaVersion(probe, !recoveryFiles);
1793
+ const check = probe.prepare("PRAGMA quick_check(1)").get();
1794
+ if (check.quick_check !== "ok") throw new Error("Damaged coordination database. Preserve all state before repair.");
1795
+ } finally {
1796
+ probe.close();
1797
+ }
1798
+ }
1799
+ fs7.mkdirSync(this.coordDir, { recursive: true, mode: 448 });
1800
+ for (const name of ["shadow-index", "quarantine", "snapshots", "worktrees"]) {
1801
+ const target = path5.join(this.coordDir, name);
1802
+ if (!isWithin(this.coordDir, physicalPath(target)) || physicalPath(target) !== target) {
1803
+ throw new Error(`Coordination subdirectory must not be a symlink: ${name}`);
1804
+ }
1805
+ fs7.mkdirSync(target, { recursive: true });
1806
+ }
1807
+ this.db = new DatabaseSync(this.dbPath);
1808
+ try {
1809
+ this.configurePragmas();
1810
+ this.migrate(!recoveryFiles);
1811
+ } catch (err) {
1812
+ this.db.close();
1813
+ throw err;
1814
+ }
1815
+ }
1816
+ /** Validate an existing snapshot without migration or ownership changes. */
1817
+ static inspectSchema(db) {
1818
+ const version = this.schemaVersion(db, false);
1819
+ const check = db.prepare("PRAGMA quick_check(1)").get();
1820
+ if (check.quick_check !== "ok") throw new Error("Damaged coordination database. Preserve all state before repair.");
1821
+ return version;
1822
+ }
1823
+ configurePragmas() {
1824
+ this.db.exec(`
1825
+ PRAGMA busy_timeout = 10000;
1826
+ PRAGMA journal_mode = WAL;
1827
+ PRAGMA synchronous = NORMAL;
1828
+ PRAGMA foreign_keys = ON;
1829
+ PRAGMA temp_store = MEMORY;
1830
+ `);
1831
+ }
1832
+ static schemaVersion(db, allowEmpty) {
1833
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT GLOB 'sqlite_*'").all();
1834
+ if (!tables.length) {
1835
+ if (!allowEmpty) throw new Error("Empty coordination database with existing recovery files. Preserve all state before repair.");
1836
+ return 1;
1837
+ }
1838
+ if (!tables.some((table) => table.name === "meta")) throw new Error("Missing coordination schema metadata. Preserve the database before repair.");
1839
+ const row = db.prepare("SELECT value FROM meta WHERE key='schema_version'").get();
1840
+ if (row?.value !== "1" && row?.value !== "2") {
1841
+ throw new Error(`Unsupported coordination schema version: ${String(row?.value)}. Preserve the database before upgrading or repairing it.`);
1842
+ }
1843
+ const version = Number(row.value);
1844
+ const required = [
1845
+ "agents",
1846
+ "tasks",
1847
+ "locks",
1848
+ "file_deps",
1849
+ "contracts",
1850
+ "conflicts",
1851
+ "merge_queue",
1852
+ "resources",
1853
+ "quarantine",
1854
+ "events",
1855
+ ...version === 2 ? ["agent_sessions", "mutex_waiters", "mutex_runs"] : []
1856
+ ];
1857
+ if (required.some((name) => !tables.some((table) => table.name === name))) {
1858
+ throw new Error("Incomplete coordination schema. Preserve the database before repair.");
1859
+ }
1860
+ const columns = {
1861
+ agents: "id,pid,pid_start_time,status,registered_at,lease_expires_at,worktree_path,branch",
1862
+ tasks: "status,owner_agent_id,fencing_token,scope_globs_json,depends_on_json",
1863
+ locks: "owner_agent_id,pattern,mode,fencing_token,expires_at",
1864
+ resources: "type,name,owner_agent_id,fencing_token,expires_at",
1865
+ ...version === 2 ? {
1866
+ agent_sessions: "agent_id,token_hash,epoch",
1867
+ mutex_waiters: "ticket,mutex_name,request_id,owner_agent_id,session_epoch,waiter_pid,waiter_start,expires_at",
1868
+ mutex_runs: "mutex_name,run_id,owner_agent_id,session_epoch,fencing_token,supervisor_pid,supervisor_start,worker_pid,worker_start"
1869
+ } : {}
1870
+ };
1871
+ for (const [table, fields] of Object.entries(columns)) {
1872
+ try {
1873
+ db.prepare(`SELECT ${fields} FROM ${table} LIMIT 0`);
1874
+ } catch {
1875
+ throw new Error(`Incomplete coordination schema (${table}). Preserve the database before repair.`);
1876
+ }
1877
+ }
1878
+ const epoch = _CoordDatabase.fencingEpoch(db);
1879
+ const tokens = [
1880
+ "SELECT fencing_token AS token FROM locks",
1881
+ "SELECT fencing_token FROM tasks",
1882
+ "SELECT fencing_token FROM resources",
1883
+ "SELECT fencing_token FROM merge_queue",
1884
+ ...version === 2 ? ["SELECT epoch FROM agent_sessions", "SELECT session_epoch FROM mutex_waiters", "SELECT fencing_token FROM mutex_runs"] : []
1885
+ ];
1886
+ 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)) {
1887
+ throw new Error("Invalid or regressed fencing counter. Preserve the database before repair.");
1888
+ }
1889
+ return version;
1890
+ }
1891
+ static fencingEpoch(db) {
1892
+ const row = db.prepare("SELECT value FROM meta WHERE key='fencing_epoch'").get();
1893
+ const epoch = Number(row?.value);
1894
+ if (!row || typeof row.value !== "string" || !/^(0|[1-9][0-9]*)$/.test(row.value) || !Number.isSafeInteger(epoch)) {
1895
+ throw new Error("Missing or invalid fencing counter. Preserve the database before repair.");
1896
+ }
1897
+ return epoch;
1898
+ }
1899
+ migrate(allowEmpty) {
1900
+ this.withTransaction(() => {
1901
+ const version = _CoordDatabase.schemaVersion(this.db, allowEmpty);
1902
+ this.db.exec(`
1903
+ CREATE TABLE IF NOT EXISTS meta (
1904
+ key TEXT PRIMARY KEY,
1905
+ value TEXT NOT NULL
1906
+ );
1907
+
1908
+ INSERT OR IGNORE INTO meta (key, value) VALUES ('fencing_epoch', '1000');
1909
+ INSERT OR IGNORE INTO meta (key, value) VALUES ('schema_version', '1');
1910
+
1911
+ CREATE TABLE IF NOT EXISTS agents (
1912
+ id TEXT PRIMARY KEY,
1913
+ name TEXT NOT NULL,
1914
+ client TEXT NOT NULL,
1915
+ pid INTEGER NOT NULL,
1916
+ pid_start_time TEXT NOT NULL,
1917
+ pgid INTEGER NOT NULL,
1918
+ worktree_path TEXT,
1919
+ branch TEXT,
1920
+ shadow_tree_sha TEXT,
1921
+ status TEXT NOT NULL DEFAULT 'active',
1922
+ last_seen_event_id INTEGER NOT NULL DEFAULT 0,
1923
+ registered_at INTEGER NOT NULL,
1924
+ last_heartbeat_at INTEGER NOT NULL,
1925
+ lease_expires_at INTEGER NOT NULL
1926
+ );
1927
+
1928
+ CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
1929
+
1930
+ CREATE TABLE IF NOT EXISTS tasks (
1931
+ id TEXT PRIMARY KEY,
1932
+ title TEXT NOT NULL,
1933
+ intent_summary TEXT NOT NULL,
1934
+ status TEXT NOT NULL DEFAULT 'queued',
1935
+ priority INTEGER NOT NULL DEFAULT 0,
1936
+ owner_agent_id TEXT,
1937
+ fencing_token INTEGER,
1938
+ depends_on_json TEXT NOT NULL DEFAULT '[]',
1939
+ scope_globs_json TEXT NOT NULL DEFAULT '[]',
1940
+ created_at INTEGER NOT NULL,
1941
+ updated_at INTEGER NOT NULL
1942
+ );
1943
+
1944
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status, priority DESC);
1945
+ CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner_agent_id);
1946
+
1947
+ CREATE TABLE IF NOT EXISTS locks (
1948
+ id TEXT PRIMARY KEY,
1949
+ pattern TEXT NOT NULL,
1950
+ lock_type TEXT NOT NULL DEFAULT 'glob',
1951
+ mode TEXT NOT NULL DEFAULT 'exclusive',
1952
+ owner_agent_id TEXT NOT NULL,
1953
+ task_id TEXT,
1954
+ fencing_token INTEGER NOT NULL,
1955
+ reason TEXT NOT NULL DEFAULT '',
1956
+ acquired_at INTEGER NOT NULL,
1957
+ expires_at INTEGER NOT NULL
1958
+ );
1959
+
1960
+ CREATE INDEX IF NOT EXISTS idx_locks_owner ON locks(owner_agent_id);
1961
+ CREATE INDEX IF NOT EXISTS idx_locks_pattern ON locks(pattern);
1962
+
1963
+ CREATE TABLE IF NOT EXISTS file_deps (
1964
+ source_file TEXT NOT NULL,
1965
+ target_file TEXT NOT NULL,
1966
+ imported_symbols_json TEXT NOT NULL DEFAULT '[]',
1967
+ file_hash TEXT NOT NULL,
1968
+ updated_at INTEGER NOT NULL,
1969
+ PRIMARY KEY (source_file, target_file)
1970
+ );
1971
+
1972
+ CREATE INDEX IF NOT EXISTS idx_file_deps_target ON file_deps(target_file);
1973
+
1974
+ CREATE TABLE IF NOT EXISTS contracts (
1975
+ id TEXT PRIMARY KEY,
1976
+ file_path TEXT NOT NULL,
1977
+ symbol_name TEXT NOT NULL,
1978
+ kind TEXT NOT NULL,
1979
+ signature TEXT NOT NULL,
1980
+ ast_hash TEXT NOT NULL,
1981
+ params_fingerprint TEXT NOT NULL,
1982
+ doc_comment TEXT NOT NULL DEFAULT '',
1983
+ owner_agent_id TEXT,
1984
+ version INTEGER NOT NULL DEFAULT 1,
1985
+ is_breaking INTEGER NOT NULL DEFAULT 0,
1986
+ updated_at INTEGER NOT NULL,
1987
+ UNIQUE(file_path, symbol_name)
1988
+ );
1989
+
1990
+ CREATE TABLE IF NOT EXISTS conflicts (
1991
+ id TEXT PRIMARY KEY,
1992
+ agent_a TEXT NOT NULL,
1993
+ agent_b TEXT NOT NULL,
1994
+ file_path TEXT NOT NULL,
1995
+ severity TEXT NOT NULL,
1996
+ details_json TEXT NOT NULL,
1997
+ detected_at INTEGER NOT NULL,
1998
+ UNIQUE(agent_a, agent_b, file_path)
1999
+ );
2000
+
2001
+ CREATE TABLE IF NOT EXISTS merge_queue (
2002
+ id TEXT PRIMARY KEY,
2003
+ agent_id TEXT NOT NULL,
2004
+ branch TEXT NOT NULL,
2005
+ worktree_path TEXT,
2006
+ status TEXT NOT NULL DEFAULT 'queued',
2007
+ fencing_token INTEGER NOT NULL,
2008
+ verification_cmd TEXT,
2009
+ diagnostics_output TEXT,
2010
+ created_at INTEGER NOT NULL,
2011
+ updated_at INTEGER NOT NULL
2012
+ );
2013
+
2014
+ CREATE TABLE IF NOT EXISTS resources (
2015
+ id TEXT PRIMARY KEY,
2016
+ type TEXT NOT NULL,
2017
+ name TEXT NOT NULL,
2018
+ value TEXT NOT NULL,
2019
+ owner_agent_id TEXT NOT NULL,
2020
+ fencing_token INTEGER NOT NULL,
2021
+ queue_json TEXT NOT NULL DEFAULT '[]',
2022
+ acquired_at INTEGER NOT NULL,
2023
+ expires_at INTEGER NOT NULL,
2024
+ UNIQUE(type, name)
2025
+ );
2026
+
2027
+ CREATE TABLE IF NOT EXISTS quarantine (
2028
+ id TEXT PRIMARY KEY,
2029
+ agent_id TEXT NOT NULL,
2030
+ command TEXT NOT NULL,
2031
+ file_path TEXT NOT NULL,
2032
+ patch_path TEXT NOT NULL,
2033
+ reason TEXT NOT NULL,
2034
+ created_at INTEGER NOT NULL
2035
+ );
2036
+
2037
+ CREATE TABLE IF NOT EXISTS events (
2038
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2039
+ timestamp INTEGER NOT NULL,
2040
+ agent_id TEXT,
2041
+ event_type TEXT NOT NULL,
2042
+ payload_json TEXT NOT NULL
2043
+ );
2044
+ `);
2045
+ this.db.exec(`
2046
+ CREATE TABLE IF NOT EXISTS agent_sessions (
2047
+ agent_id TEXT PRIMARY KEY, token_hash TEXT NOT NULL, epoch INTEGER NOT NULL
2048
+ );
2049
+ CREATE TABLE IF NOT EXISTS mutex_waiters (
2050
+ ticket INTEGER PRIMARY KEY AUTOINCREMENT, mutex_name TEXT NOT NULL,
2051
+ request_id TEXT NOT NULL, owner_agent_id TEXT NOT NULL, session_epoch INTEGER NOT NULL,
2052
+ waiter_pid INTEGER NOT NULL, waiter_start TEXT NOT NULL, expires_at INTEGER NOT NULL,
2053
+ UNIQUE(mutex_name, request_id)
2054
+ );
2055
+ CREATE TABLE IF NOT EXISTS mutex_runs (
2056
+ mutex_name TEXT PRIMARY KEY, run_id TEXT NOT NULL UNIQUE, owner_agent_id TEXT NOT NULL,
2057
+ session_epoch INTEGER NOT NULL, fencing_token INTEGER NOT NULL,
2058
+ supervisor_pid INTEGER NOT NULL, supervisor_start TEXT NOT NULL,
2059
+ worker_pid INTEGER, worker_start TEXT
2060
+ );
2061
+ `);
2062
+ if (version < 2) {
2063
+ this.db.exec(`
2064
+ UPDATE tasks SET status='queued', owner_agent_id=NULL, fencing_token=NULL
2065
+ WHERE status='active' AND owner_agent_id IN (SELECT id FROM agents);
2066
+ DELETE FROM locks;
2067
+ DELETE FROM resources;
2068
+ DELETE FROM conflicts;
2069
+ UPDATE agents SET status='dead' WHERE status='active';
2070
+ UPDATE meta SET value='2' WHERE key='schema_version';
2071
+ `);
2072
+ }
2073
+ });
2074
+ }
2075
+ /**
2076
+ * Executes a synchronous callback inside a `BEGIN IMMEDIATE` transaction
2077
+ * with automatic jittered backoff retries if another process temporarily holds the WAL write lock.
2078
+ */
2079
+ withTransaction(fn, maxRetries = 12) {
2080
+ if (this.inTransaction) {
2081
+ return fn();
2082
+ }
2083
+ let attempt = 0;
2084
+ while (true) {
2085
+ try {
2086
+ this.db.exec("BEGIN IMMEDIATE");
2087
+ this.inTransaction = true;
2088
+ try {
2089
+ const result = fn();
2090
+ this.db.exec("COMMIT");
2091
+ this.inTransaction = false;
2092
+ return result;
2093
+ } catch (innerErr) {
2094
+ try {
2095
+ this.db.exec("ROLLBACK");
2096
+ } catch {
2097
+ }
2098
+ this.inTransaction = false;
2099
+ throw innerErr;
2100
+ }
2101
+ } catch (err) {
2102
+ this.inTransaction = false;
2103
+ const msg = err instanceof Error ? err.message : String(err);
2104
+ if (attempt < maxRetries && (msg.includes("SQLITE_BUSY") || msg.includes("database is locked"))) {
2105
+ attempt++;
2106
+ const sleepMs = Math.min(250, 5 * Math.pow(1.5, attempt) + Math.random() * 10);
2107
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.round(sleepMs));
2108
+ continue;
2109
+ }
2110
+ throw err;
2111
+ }
2112
+ }
2113
+ }
2114
+ /**
2115
+ * Atomically increments and returns the next monotonic fencing token.
2116
+ */
2117
+ nextFencingToken() {
2118
+ return this.withTransaction(() => {
2119
+ const next = this.currentFencingEpoch() + 1;
2120
+ if (!Number.isSafeInteger(next)) throw new Error("Fencing counter exhausted; preserve state before recovery.");
2121
+ this.db.prepare("UPDATE meta SET value=? WHERE key='fencing_epoch'").run(String(next));
2122
+ return next;
2123
+ });
2124
+ }
2125
+ currentFencingEpoch() {
2126
+ return _CoordDatabase.fencingEpoch(this.db);
2127
+ }
2128
+ logEvent(agentId, eventType, payload = {}) {
2129
+ const res = this.db.prepare(`
2130
+ INSERT INTO events (timestamp, agent_id, event_type, payload_json)
2131
+ VALUES (?, ?, ?, ?)
2132
+ `).run(Date.now(), agentId, eventType, JSON.stringify(payload));
2133
+ return Number(res.lastInsertRowid);
2134
+ }
2135
+ getEventsSince(sinceId, limit = 100) {
2136
+ const rows = this.db.prepare(`
2137
+ SELECT id, timestamp, agent_id, event_type, payload_json
2138
+ FROM events
2139
+ WHERE id > ?
2140
+ ORDER BY id ASC
2141
+ LIMIT ?
2142
+ `).all(sinceId, limit);
2143
+ return rows.map((r) => ({
2144
+ id: r.id,
2145
+ timestamp: r.timestamp,
2146
+ agent_id: r.agent_id,
2147
+ event_type: r.event_type,
2148
+ payload: JSON.parse(r.payload_json)
2149
+ }));
2150
+ }
2151
+ close() {
2152
+ try {
2153
+ this.db.close();
2154
+ } catch {
2155
+ }
2156
+ }
2157
+ };
2158
+
2159
+ // src/core/dep-graph.ts
2160
+ import crypto6 from "crypto";
2161
+ import fs8 from "fs";
2162
+ import path6 from "path";
2163
+ var DependencyAndContractEngine = class {
2164
+ constructor(coordDb, lockEngine, fencing) {
2165
+ this.coordDb = coordDb;
2166
+ this.lockEngine = lockEngine;
2167
+ this.fencing = fencing;
2168
+ }
2169
+ coordDb;
2170
+ lockEngine;
2171
+ fencing;
2172
+ /**
2173
+ * Scans a repository directory to index initial file dependencies and exported contracts.
2174
+ */
2175
+ indexRepositoryFiles(dirPath = this.coordDb.repoRoot) {
2176
+ const files = this.walkSourceFiles(dirPath);
2177
+ let indexedCount = 0;
2178
+ for (const file of files) {
2179
+ this.recordFileModification(null, file);
2180
+ indexedCount++;
2181
+ }
2182
+ return indexedCount;
2183
+ }
2184
+ walkSourceFiles(dir, acc = []) {
2185
+ if (!fs8.existsSync(dir) || dir !== this.coordDb.repoRoot && fs8.existsSync(path6.join(dir, ".git"))) return acc;
2186
+ const entries = fs8.readdirSync(dir, { withFileTypes: true });
2187
+ for (const entry of entries) {
2188
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "target" || entry.name === "coverage") {
2189
+ continue;
2190
+ }
2191
+ const fullPath = path6.join(dir, entry.name);
2192
+ if (entry.isDirectory()) {
2193
+ this.walkSourceFiles(fullPath, acc);
2194
+ } else if (entry.isFile() && /\.(ts|tsx|js|jsx|mjs|py)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
2195
+ acc.push(fullPath);
2196
+ }
2197
+ }
2198
+ return acc;
2199
+ }
2200
+ /**
2201
+ * Automatically parses a modified file's imports and exports, updates the dependency graph
2202
+ * and Contract Blackboard, and calculates the transitive blast radius
2203
+ * if any exported signature changed or was removed.
2204
+ */
2205
+ recordFileModification(agentId, rawFilePath, sourceContent) {
2206
+ const analysis = AstAnalyzer.analyzeSource(
2207
+ this.coordDb.repoRoot,
2208
+ rawFilePath,
2209
+ sourceContent
2210
+ );
2211
+ const relPath = analysis.filePath;
2212
+ const now = Date.now();
2213
+ return this.coordDb.withTransaction(() => {
2214
+ if (agentId) this.fencing.assertSession(agentId);
2215
+ this.coordDb.db.prepare(`DELETE FROM file_deps WHERE source_file = ?`).run(relPath);
2216
+ for (const imp of analysis.imports) {
2217
+ if (!imp.resolvedPath) continue;
2218
+ this.coordDb.db.prepare(`
2219
+ INSERT OR REPLACE INTO file_deps (
2220
+ source_file, target_file, imported_symbols_json, file_hash, updated_at
2221
+ )
2222
+ VALUES (?, ?, ?, ?, ?)
2223
+ `).run(
2224
+ relPath,
2225
+ imp.resolvedPath,
2226
+ JSON.stringify(imp.importedSymbols),
2227
+ analysis.fileHash,
2228
+ now
2229
+ );
2230
+ }
2231
+ const existingRows = this.coordDb.db.prepare(`SELECT * FROM contracts WHERE file_path = ?`).all(relPath);
2232
+ const existingBySymbol = new Map(
2233
+ existingRows.map((r) => [String(r.symbol_name), this.mapRowToContract(r)])
2234
+ );
2235
+ const newSymbolNames = new Set(analysis.exports.map((e) => e.symbolName));
2236
+ const upsertedContracts = [];
2237
+ const removedSymbols = [];
2238
+ const changedSymbols = [];
2239
+ for (const [oldName, oldContract] of existingBySymbol.entries()) {
2240
+ if (!newSymbolNames.has(oldName)) {
2241
+ removedSymbols.push(oldName);
2242
+ changedSymbols.push({
2243
+ symbolName: oldName,
2244
+ previousSignature: oldContract.signature,
2245
+ newSignature: null
2246
+ });
2247
+ this.coordDb.db.prepare(`DELETE FROM contracts WHERE id = ?`).run(oldContract.id);
2248
+ }
2249
+ }
2250
+ for (const exp of analysis.exports) {
2251
+ const prev = existingBySymbol.get(exp.symbolName);
2252
+ const isBreaking = prev !== void 0 && prev.ast_hash !== exp.astHash;
2253
+ if (isBreaking && prev) {
2254
+ changedSymbols.push({
2255
+ symbolName: exp.symbolName,
2256
+ previousSignature: prev.signature,
2257
+ newSignature: exp.signature
2258
+ });
2259
+ }
2260
+ const contract = this.upsertContractInternal({
2261
+ filePath: relPath,
2262
+ symbol: exp,
2263
+ agentId,
2264
+ prev,
2265
+ isBreaking,
2266
+ now
2267
+ });
2268
+ upsertedContracts.push(contract);
2269
+ }
2270
+ const transitiveDependents = this.getTransitiveDependents(relPath, 4);
2271
+ const breakingChanges = [];
2272
+ if (changedSymbols.length > 0 && transitiveDependents.length > 0) {
2273
+ const activeLocks = this.lockEngine.listLocks();
2274
+ for (const change of changedSymbols) {
2275
+ for (const dep of transitiveDependents) {
2276
+ if (dep.hopDistance === 1 && dep.importedSymbols.length > 0) {
2277
+ if (!dep.importedSymbols.includes("*") && !dep.importedSymbols.includes(change.symbolName)) {
2278
+ continue;
2279
+ }
2280
+ }
2281
+ const affectedLock = activeLocks.find(
2282
+ (l) => l.owner_agent_id !== agentId && LockEngine.patternsOverlap(dep.filePath, l.pattern)
2283
+ );
2284
+ const alert = {
2285
+ modifiedFile: relPath,
2286
+ symbolName: change.symbolName,
2287
+ previousSignature: change.previousSignature,
2288
+ newSignature: change.newSignature,
2289
+ affectedDownstreamFile: dep.filePath,
2290
+ hopDistance: dep.hopDistance,
2291
+ affectedAgentId: affectedLock?.owner_agent_id ?? null
2292
+ };
2293
+ breakingChanges.push(alert);
2294
+ if (agentId && affectedLock) {
2295
+ const conflictId = `conf_${crypto6.randomBytes(6).toString("hex")}`;
2296
+ this.coordDb.db.prepare(`
2297
+ INSERT OR REPLACE INTO conflicts (
2298
+ id, agent_a, agent_b, file_path, severity, details_json, detected_at
2299
+ )
2300
+ VALUES (?, ?, ?, ?, 'transitive_contract_break', ?, ?)
2301
+ `).run(
2302
+ conflictId,
2303
+ agentId,
2304
+ affectedLock.owner_agent_id,
2305
+ dep.filePath,
2306
+ JSON.stringify(alert),
2307
+ now
2308
+ );
2309
+ }
2310
+ }
2311
+ }
2312
+ this.coordDb.logEvent(agentId, "contract_breaking_change", {
2313
+ filePath: relPath,
2314
+ breakingChanges
2315
+ });
2316
+ } else if (upsertedContracts.length > 0 && agentId) {
2317
+ this.coordDb.logEvent(agentId, "contracts_updated", {
2318
+ filePath: relPath,
2319
+ symbols: upsertedContracts.map((c) => ({
2320
+ name: c.symbol_name,
2321
+ signature: c.signature,
2322
+ version: c.version
2323
+ }))
2324
+ });
2325
+ }
2326
+ return {
2327
+ filePath: relPath,
2328
+ upsertedContracts,
2329
+ removedSymbols,
2330
+ breakingChanges,
2331
+ transitiveDependents: transitiveDependents.map((d) => ({
2332
+ filePath: d.filePath,
2333
+ hopDistance: d.hopDistance
2334
+ }))
2335
+ };
2336
+ });
2337
+ }
2338
+ /**
2339
+ * Explicitly publishes a contract to the shared blackboard (e.g. REST route, env var, or type).
2340
+ */
2341
+ publishExplicitContract(params) {
2342
+ const relPath = LockEngine.normalizeFile(this.coordDb.repoRoot, params.filePath);
2343
+ const now = Date.now();
2344
+ const astHash = crypto6.createHash("sha256").update(params.signature).digest("hex").slice(0, 16);
2345
+ return this.coordDb.withTransaction(() => {
2346
+ this.fencing.assertSession(params.agentId);
2347
+ const prevRow = this.coordDb.db.prepare(`SELECT * FROM contracts WHERE file_path = ? AND symbol_name = ?`).get(relPath, params.symbolName);
2348
+ const prev = prevRow ? this.mapRowToContract(prevRow) : void 0;
2349
+ const isBreaking = prev !== void 0 && prev.ast_hash !== astHash;
2350
+ const contract = this.upsertContractInternal({
2351
+ filePath: relPath,
2352
+ symbol: {
2353
+ symbolName: params.symbolName,
2354
+ kind: params.kind,
2355
+ signature: params.signature,
2356
+ astHash,
2357
+ paramsFingerprint: `${params.kind}:${params.symbolName}`,
2358
+ docComment: params.docComment ?? ""
2359
+ },
2360
+ agentId: params.agentId,
2361
+ prev,
2362
+ isBreaking,
2363
+ now
2364
+ });
2365
+ this.coordDb.logEvent(params.agentId, "contract_published", {
2366
+ filePath: relPath,
2367
+ symbolName: params.symbolName,
2368
+ signature: params.signature,
2369
+ isBreaking
2370
+ });
2371
+ return contract;
2372
+ });
2373
+ }
2374
+ upsertContractInternal(params) {
2375
+ const id = params.prev?.id ?? `ctr_${crypto6.randomBytes(6).toString("hex")}`;
2376
+ const version = params.prev ? params.prev.ast_hash !== params.symbol.astHash ? params.prev.version + 1 : params.prev.version : 1;
2377
+ this.coordDb.db.prepare(`
2378
+ INSERT OR REPLACE INTO contracts (
2379
+ id, file_path, symbol_name, kind, signature,
2380
+ ast_hash, params_fingerprint, doc_comment,
2381
+ owner_agent_id, version, is_breaking, updated_at
2382
+ )
2383
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2384
+ `).run(
2385
+ id,
2386
+ params.filePath,
2387
+ params.symbol.symbolName,
2388
+ params.symbol.kind,
2389
+ params.symbol.signature,
2390
+ params.symbol.astHash,
2391
+ params.symbol.paramsFingerprint,
2392
+ params.symbol.docComment,
2393
+ params.agentId ?? params.prev?.owner_agent_id ?? null,
2394
+ version,
2395
+ params.isBreaking ? 1 : 0,
2396
+ params.now
2397
+ );
2398
+ return {
2399
+ id,
2400
+ file_path: params.filePath,
2401
+ symbol_name: params.symbol.symbolName,
2402
+ kind: params.symbol.kind,
2403
+ signature: params.symbol.signature,
2404
+ ast_hash: params.symbol.astHash,
2405
+ params_fingerprint: params.symbol.paramsFingerprint,
2406
+ doc_comment: params.symbol.docComment,
2407
+ owner_agent_id: params.agentId ?? params.prev?.owner_agent_id ?? null,
2408
+ version,
2409
+ is_breaking: params.isBreaking,
2410
+ updated_at: params.now
2411
+ };
2412
+ }
2413
+ /**
2414
+ * Computes all files that directly or transitively import `targetFile` up to `maxHops` away.
2415
+ */
2416
+ getTransitiveDependents(targetFile, maxHops = 4) {
2417
+ const normTarget = LockEngine.normalizeFile(this.coordDb.repoRoot, targetFile);
2418
+ const visited = /* @__PURE__ */ new Map();
2419
+ let frontier = [normTarget];
2420
+ for (let hop = 1; hop <= maxHops; hop++) {
2421
+ const nextFrontier = [];
2422
+ for (const curr of frontier) {
2423
+ const rows = this.coordDb.db.prepare(`SELECT source_file, imported_symbols_json FROM file_deps WHERE target_file = ?`).all(curr);
2424
+ for (const r of rows) {
2425
+ if (r.source_file === normTarget || visited.has(r.source_file)) continue;
2426
+ visited.set(r.source_file, {
2427
+ hopDistance: hop,
2428
+ importedSymbols: JSON.parse(r.imported_symbols_json)
2429
+ });
2430
+ nextFrontier.push(r.source_file);
2431
+ }
2432
+ }
2433
+ if (nextFrontier.length === 0) break;
2434
+ frontier = nextFrontier;
2435
+ }
2436
+ return Array.from(visited.entries()).map(([filePath, meta]) => ({
2437
+ filePath,
2438
+ hopDistance: meta.hopDistance,
2439
+ importedSymbols: meta.importedSymbols
2440
+ }));
2441
+ }
2442
+ listContracts(filePathFilter) {
2443
+ const rows = filePathFilter ? this.coordDb.db.prepare(`SELECT * FROM contracts WHERE file_path = ? ORDER BY updated_at DESC`).all(LockEngine.normalizeFile(this.coordDb.repoRoot, filePathFilter)) : this.coordDb.db.prepare(`SELECT * FROM contracts ORDER BY updated_at DESC`).all();
2444
+ return rows.map((r) => this.mapRowToContract(r));
2445
+ }
2446
+ mapRowToContract(row) {
2447
+ return {
2448
+ id: String(row.id),
2449
+ file_path: String(row.file_path),
2450
+ symbol_name: String(row.symbol_name),
2451
+ kind: row.kind,
2452
+ signature: String(row.signature),
2453
+ ast_hash: String(row.ast_hash),
2454
+ params_fingerprint: String(row.params_fingerprint),
2455
+ doc_comment: String(row.doc_comment ?? ""),
2456
+ owner_agent_id: row.owner_agent_id ?? null,
2457
+ version: Number(row.version),
2458
+ is_breaking: Boolean(row.is_breaking),
2459
+ updated_at: Number(row.updated_at)
2460
+ };
2461
+ }
2462
+ };
2463
+
2464
+ // src/core/intents.ts
2465
+ import crypto7 from "crypto";
2466
+ var IntentAndTaskEngine = class {
2467
+ constructor(coordDb, fencing, lockEngine) {
2468
+ this.coordDb = coordDb;
2469
+ this.fencing = fencing;
2470
+ this.lockEngine = lockEngine;
2471
+ }
2472
+ coordDb;
2473
+ fencing;
2474
+ lockEngine;
2475
+ /**
2476
+ * Peer-to-Peer Mode: Declares an agent's intent, checks scope-lock collisions
2477
+ * and reserves the scope if available.
2478
+ */
2479
+ declareIntent(params) {
2480
+ this.fencing.reapDeadAgents();
2481
+ const summary = params.intentSummary ?? params.title;
2482
+ return this.coordDb.withTransaction(() => {
2483
+ this.fencing.assertSession(params.agentId);
2484
+ const activeTasks = this.listTasks({ status: "active" }).filter(
2485
+ (t) => t.owner_agent_id !== params.agentId
2486
+ );
2487
+ const taskId = `task_${crypto7.randomBytes(6).toString("hex")}`;
2488
+ const lockRes = this.lockEngine.acquireLocks({
2489
+ agentId: params.agentId,
2490
+ patterns: params.scopeGlobs,
2491
+ taskId,
2492
+ reason: params.title
2493
+ });
2494
+ if (!lockRes.ok) {
2495
+ return {
2496
+ ok: false,
2497
+ task: null,
2498
+ fencingToken: lockRes.fencingToken,
2499
+ locks: [],
2500
+ lockConflicts: lockRes.conflicts,
2501
+ activeSiblingIntents: activeTasks
2502
+ };
2503
+ }
2504
+ const now = Date.now();
2505
+ const normGlobs = params.scopeGlobs.map(
2506
+ (g) => LockEngine.normalizePattern(this.fencing.getAgent(params.agentId)?.worktree_path ?? this.coordDb.repoRoot, g)
2507
+ );
2508
+ this.coordDb.db.prepare(`
2509
+ INSERT INTO tasks (
2510
+ id, title, intent_summary, status, priority,
2511
+ owner_agent_id, fencing_token, depends_on_json, scope_globs_json,
2512
+ created_at, updated_at
2513
+ )
2514
+ VALUES (?, ?, ?, 'active', 50, ?, ?, '[]', ?, ?, ?)
2515
+ `).run(
2516
+ taskId,
2517
+ params.title,
2518
+ summary,
2519
+ params.agentId,
2520
+ lockRes.fencingToken,
2521
+ JSON.stringify(normGlobs),
2522
+ now,
2523
+ now
2524
+ );
2525
+ this.coordDb.logEvent(params.agentId, "intent_declared", {
2526
+ taskId,
2527
+ title: params.title,
2528
+ scopeGlobs: normGlobs,
2529
+ fencingToken: lockRes.fencingToken
2530
+ });
2531
+ return {
2532
+ ok: true,
2533
+ task: this.getTask(taskId),
2534
+ fencingToken: lockRes.fencingToken,
2535
+ locks: lockRes.locks,
2536
+ lockConflicts: [],
2537
+ activeSiblingIntents: activeTasks
2538
+ };
2539
+ });
2540
+ }
2541
+ /**
2542
+ * Orchestrator Mode: Adds a task to the DAG queue with optional dependencies and priority.
2543
+ */
2544
+ addTask(params) {
2545
+ const id = params.id ?? `task_${crypto7.randomBytes(6).toString("hex")}`;
2546
+ const dependsOn = params.dependsOn ?? [];
2547
+ const scopeGlobs = (params.scopeGlobs ?? []).map(
2548
+ (g) => LockEngine.normalizePattern(this.coordDb.repoRoot, g)
2549
+ );
2550
+ return this.coordDb.withTransaction(() => {
2551
+ for (const depId of dependsOn) {
2552
+ if (depId === id) {
2553
+ throw new Error(`Task '${id}' cannot depend on itself.`);
2554
+ }
2555
+ const dep = this.getTask(depId);
2556
+ if (!dep) {
2557
+ throw new Error(`Dependency task '${depId}' does not exist.`);
2558
+ }
2559
+ if (this.hasPathInDag(depId, id)) {
2560
+ throw new Error(`Circular dependency detected between '${id}' and '${depId}'.`);
2561
+ }
2562
+ }
2563
+ const now = Date.now();
2564
+ this.coordDb.db.prepare(`
2565
+ INSERT INTO tasks (
2566
+ id, title, intent_summary, status, priority,
2567
+ owner_agent_id, fencing_token, depends_on_json, scope_globs_json,
2568
+ created_at, updated_at
2569
+ )
2570
+ VALUES (?, ?, ?, 'queued', ?, NULL, NULL, ?, ?, ?, ?)
2571
+ `).run(
2572
+ id,
2573
+ params.title,
2574
+ params.intentSummary ?? params.title,
2575
+ params.priority ?? 10,
2576
+ JSON.stringify(dependsOn),
2577
+ JSON.stringify(scopeGlobs),
2578
+ now,
2579
+ now
2580
+ );
2581
+ this.coordDb.logEvent(null, "task_queued", { id, title: params.title, dependsOn });
2582
+ return this.getTask(id);
2583
+ });
2584
+ }
2585
+ hasPathInDag(startId, targetId, visited = /* @__PURE__ */ new Set()) {
2586
+ if (startId === targetId) return true;
2587
+ if (visited.has(startId)) return false;
2588
+ visited.add(startId);
2589
+ const task = this.getTask(startId);
2590
+ if (!task) return false;
2591
+ for (const dep of task.depends_on) {
2592
+ if (this.hasPathInDag(dep, targetId, visited)) return true;
2593
+ }
2594
+ return false;
2595
+ }
2596
+ /**
2597
+ * Orchestrator Mode: Atomically claims the highest-priority unblocked task whose scope locks are available.
2598
+ */
2599
+ claimNextTask(agentId) {
2600
+ this.fencing.reapDeadAgents();
2601
+ return this.coordDb.withTransaction(() => {
2602
+ this.fencing.assertSession(agentId);
2603
+ const allTasks = this.listTasks();
2604
+ const taskById = new Map(allTasks.map((t) => [t.id, t]));
2605
+ const queued = allTasks.filter((t) => t.status === "queued").sort((a, b) => b.priority - a.priority || a.created_at - b.created_at);
2606
+ if (queued.length === 0) {
2607
+ return { claimed: false, task: null, fencingToken: null, locks: [], reason: "no_queued_tasks" };
2608
+ }
2609
+ for (const candidate of queued) {
2610
+ const depsSatisfied = candidate.depends_on.every(
2611
+ (depId) => taskById.get(depId)?.status === "done"
2612
+ );
2613
+ if (!depsSatisfied) continue;
2614
+ let fencingToken = this.coordDb.currentFencingEpoch();
2615
+ let locks = [];
2616
+ if (candidate.scope_globs.length > 0) {
2617
+ const lockRes = this.lockEngine.acquireLocks({
2618
+ agentId,
2619
+ patterns: candidate.scope_globs,
2620
+ taskId: candidate.id,
2621
+ reason: candidate.title
2622
+ });
2623
+ if (!lockRes.ok) {
2624
+ continue;
2625
+ }
2626
+ fencingToken = lockRes.fencingToken;
2627
+ locks = lockRes.locks;
2628
+ } else {
2629
+ fencingToken = this.coordDb.nextFencingToken();
2630
+ }
2631
+ const now = Date.now();
2632
+ this.coordDb.db.prepare(`
2633
+ UPDATE tasks
2634
+ SET status = 'active', owner_agent_id = ?, fencing_token = ?, updated_at = ?
2635
+ WHERE id = ?
2636
+ `).run(agentId, fencingToken, now, candidate.id);
2637
+ this.coordDb.logEvent(agentId, "task_claimed", {
2638
+ taskId: candidate.id,
2639
+ title: candidate.title,
2640
+ fencingToken
2641
+ });
2642
+ return {
2643
+ claimed: true,
2644
+ task: this.getTask(candidate.id),
2645
+ fencingToken,
2646
+ locks
2647
+ };
2648
+ }
2649
+ return {
2650
+ claimed: false,
2651
+ task: null,
2652
+ fencingToken: null,
2653
+ locks: [],
2654
+ reason: "all_queued_tasks_blocked_by_dependencies_or_locks"
2655
+ };
2656
+ });
2657
+ }
2658
+ completeTask(params) {
2659
+ return this.coordDb.withTransaction(() => {
2660
+ this.fencing.assertSession(params.agentId);
2661
+ const rows = params.taskId ? [this.getTask(params.taskId)].filter((t) => t !== null) : this.listTasks({ status: "active" }).filter((t) => t.owner_agent_id === params.agentId);
2662
+ const now = Date.now();
2663
+ const completed = [];
2664
+ for (const task of rows) {
2665
+ if (task.status !== "active" || task.owner_agent_id !== params.agentId) {
2666
+ throw new Error(
2667
+ `Task '${task.id}' is owned by '${task.owner_agent_id}', not '${params.agentId}'.`
2668
+ );
2669
+ }
2670
+ if (params.fencingToken !== void 0 && task.fencing_token !== null) {
2671
+ this.fencing.verifyFencingToken(task.id, params.fencingToken, task.fencing_token);
2672
+ }
2673
+ this.coordDb.db.prepare(`UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?`).run(now, task.id);
2674
+ if (params.releaseLocks !== false) {
2675
+ this.coordDb.db.prepare(`DELETE FROM locks WHERE owner_agent_id = ? AND task_id = ?`).run(params.agentId, task.id);
2676
+ }
2677
+ this.coordDb.logEvent(params.agentId, "task_completed", { taskId: task.id });
2678
+ completed.push(this.getTask(task.id));
2679
+ }
2680
+ return completed;
2681
+ });
2682
+ }
2683
+ getTask(taskId) {
2684
+ const row = this.coordDb.db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(taskId);
2685
+ if (!row) return null;
2686
+ return {
2687
+ id: row.id,
2688
+ title: row.title,
2689
+ intent_summary: row.intent_summary,
2690
+ status: row.status,
2691
+ priority: row.priority,
2692
+ owner_agent_id: row.owner_agent_id,
2693
+ fencing_token: row.fencing_token,
2694
+ depends_on: JSON.parse(row.depends_on_json),
2695
+ scope_globs: JSON.parse(row.scope_globs_json),
2696
+ created_at: row.created_at,
2697
+ updated_at: row.updated_at
2698
+ };
2699
+ }
2700
+ listTasks(filter) {
2701
+ this.fencing.reapDeadAgents();
2702
+ const rows = filter?.status ? this.coordDb.db.prepare(`SELECT * FROM tasks WHERE status = ? ORDER BY priority DESC, created_at ASC`).all(filter.status) : this.coordDb.db.prepare(`SELECT * FROM tasks ORDER BY priority DESC, created_at ASC`).all();
2703
+ return rows.map((row) => ({
2704
+ id: String(row.id),
2705
+ title: String(row.title),
2706
+ intent_summary: String(row.intent_summary),
2707
+ status: row.status,
2708
+ priority: Number(row.priority),
2709
+ owner_agent_id: row.owner_agent_id ?? null,
2710
+ fencing_token: row.fencing_token ?? null,
2711
+ depends_on: JSON.parse(String(row.depends_on_json)),
2712
+ scope_globs: JSON.parse(String(row.scope_globs_json)),
2713
+ created_at: Number(row.created_at),
2714
+ updated_at: Number(row.updated_at)
2715
+ }));
2716
+ }
2717
+ };
2718
+
2719
+ // src/core/merge-train.ts
2720
+ import { execFileSync as execFileSync4, execSync } from "child_process";
2721
+ import crypto8 from "crypto";
2722
+ import fs9 from "fs";
2723
+ import path7 from "path";
2724
+ var SpeculativeMergeTrain = class {
2725
+ constructor(coordDb, fencing) {
2726
+ this.coordDb = coordDb;
2727
+ this.fencing = fencing;
2728
+ }
2729
+ coordDb;
2730
+ fencing;
2731
+ /**
2732
+ * Enqueues and executes a Bors-style speculative integration test in an isolated staging
2733
+ * worktree before advancing the main repository branch.
2734
+ * Refuses Git conflicts and runs `verificationCmd` (e.g. `tsc --noEmit` or `npm test`) before fast-forwarding `main`.
2735
+ */
2736
+ enqueueAndMerge(params) {
2737
+ this.fencing.assertSession(params.agentId);
2738
+ const sessionEnv = this.fencing.sessionEnvironment(params.agentId);
2739
+ const now = Date.now();
2740
+ const id = `mrg_${crypto8.randomBytes(6).toString("hex")}`;
2741
+ const token = this.coordDb.nextFencingToken();
2742
+ this.coordDb.withTransaction(() => {
2743
+ this.fencing.assertSession(params.agentId);
2744
+ this.coordDb.db.prepare(`
2745
+ INSERT INTO merge_queue (
2746
+ id, agent_id, branch, worktree_path, status,
2747
+ fencing_token, verification_cmd, diagnostics_output,
2748
+ created_at, updated_at
2749
+ )
2750
+ VALUES (?, ?, ?, ?, 'testing', ?, ?, NULL, ?, ?)
2751
+ `).run(
2752
+ id,
2753
+ params.agentId,
2754
+ params.branch,
2755
+ params.worktreePath ?? null,
2756
+ token,
2757
+ params.verificationCmd ?? null,
2758
+ now,
2759
+ now
2760
+ );
2761
+ });
2762
+ const stageDir = path7.join(this.coordDb.coordDir, "worktrees", `_merge_train_${id}`);
2763
+ const lockPath = path7.join(this.coordDb.coordDir, "merge-train.lock");
2764
+ let lockFd;
2765
+ try {
2766
+ try {
2767
+ lockFd = fs9.openSync(lockPath, "wx", 384);
2768
+ fs9.writeFileSync(lockFd, JSON.stringify({ pid: process.pid, entryId: id }));
2769
+ } catch {
2770
+ throw new Error("Merge train is busy. If its owner crashed, inspect .coord/merge-train.lock before removing it.");
2771
+ }
2772
+ const baseHead = this.gitHead(this.coordDb.repoRoot);
2773
+ const targetBranch = this.gitBranch(this.coordDb.repoRoot);
2774
+ if (!targetBranch) throw new Error("Merge target must be on a branch, not a detached HEAD.");
2775
+ this.requireCleanTarget();
2776
+ if (params.worktreePath) {
2777
+ const registered = this.fencing.getAgent(params.agentId)?.worktree_path;
2778
+ if (!registered || physicalPath(registered) !== physicalPath(params.worktreePath)) {
2779
+ throw new Error("Source checkout must be the agent's registered worktree.");
2780
+ }
2781
+ const source = discoverRepository(params.worktreePath);
2782
+ if (source.commonGitDir !== this.coordDb.commonGitDir || source.checkoutRoot !== physicalPath(params.worktreePath) || this.gitBranch(source.checkoutRoot) !== params.branch.replace(/^refs\/heads\//, "")) {
2783
+ throw new Error("Source checkout repository or branch does not match the requested integration.");
2784
+ }
2785
+ }
2786
+ if (params.worktreePath) {
2787
+ const statusOut = execFileSync4("git", ["status", "--porcelain"], {
2788
+ cwd: params.worktreePath,
2789
+ encoding: "utf8"
2790
+ }).trim();
2791
+ if (statusOut.length > 0) {
2792
+ execFileSync4("git", ["add", "-A"], { cwd: params.worktreePath, env: sessionEnv, stdio: "ignore" });
2793
+ execFileSync4(
2794
+ "git",
2795
+ [
2796
+ "commit",
2797
+ "-m",
2798
+ params.commitMessage ?? `feat(${params.agentId}): automated worktree checkpoint`
2799
+ ],
2800
+ { cwd: params.worktreePath, env: sessionEnv, stdio: "ignore" }
2801
+ );
2802
+ }
2803
+ }
2804
+ const sourceSha = execFileSync4("git", ["rev-parse", "--verify", "--end-of-options", `${params.branch}^{commit}`], {
2805
+ cwd: this.coordDb.repoRoot,
2806
+ encoding: "utf8",
2807
+ stdio: ["ignore", "pipe", "pipe"]
2808
+ }).trim();
2809
+ this.cleanupStageWorktree(stageDir);
2810
+ execFileSync4("git", ["worktree", "add", "--detach", stageDir, baseHead], {
2811
+ cwd: this.coordDb.repoRoot,
2812
+ stdio: "ignore"
2813
+ });
2814
+ const rootNodeModules = path7.join(this.coordDb.repoRoot, "node_modules");
2815
+ const stageNodeModules = path7.join(stageDir, "node_modules");
2816
+ const hydratedNodeModules = fs9.existsSync(rootNodeModules) && !fs9.existsSync(stageNodeModules);
2817
+ if (hydratedNodeModules) {
2818
+ fs9.symlinkSync(rootNodeModules, stageNodeModules, "dir");
2819
+ }
2820
+ try {
2821
+ execFileSync4(
2822
+ "git",
2823
+ ["merge", "--no-ff", "-m", `Merge branch '${params.branch}' (${params.agentId})`, sourceSha],
2824
+ {
2825
+ cwd: stageDir,
2826
+ env: sessionEnv,
2827
+ encoding: "utf8",
2828
+ stdio: ["ignore", "pipe", "pipe"]
2829
+ }
2830
+ );
2831
+ } catch {
2832
+ const conflictedFiles = execFileSync4("git", ["diff", "--name-only", "--diff-filter=U", "-z"], {
2833
+ cwd: stageDir,
2834
+ encoding: "utf8"
2835
+ }).split("\0").filter(Boolean);
2836
+ throw new Error(conflictedFiles.length ? `Merge conflicts in ${conflictedFiles.map((file) => JSON.stringify(file)).join(", ")}. Resolve conflicts in '${params.branch}' before merging.` : `git merge failed on branch '${params.branch}'.`);
2837
+ }
2838
+ const verifiedSha = this.gitHead(stageDir);
2839
+ let diagnostics = "Speculative merge clean.";
2840
+ if (params.verificationCmd) {
2841
+ try {
2842
+ const out = execSync(params.verificationCmd, {
2843
+ cwd: stageDir,
2844
+ env: sessionEnv,
2845
+ encoding: "utf8",
2846
+ stdio: ["ignore", "pipe", "pipe"],
2847
+ timeout: 12e4
2848
+ });
2849
+ diagnostics = `Verification passed (${params.verificationCmd}):
2850
+ ${out.trim()}`.trim();
2851
+ } catch (verifyErr) {
2852
+ const stdout = verifyErr?.stdout ?? "";
2853
+ const stderr = verifyErr?.stderr ?? "";
2854
+ const combined = `${stdout}
2855
+ ${stderr}`.trim();
2856
+ throw new Error(
2857
+ `Speculative verification failed ('${params.verificationCmd}'):
2858
+ ${combined}`
2859
+ );
2860
+ }
2861
+ }
2862
+ const stageChanges = execFileSync4("git", ["status", "--porcelain", "-z", "--untracked-files=normal"], {
2863
+ cwd: stageDir,
2864
+ encoding: "utf8"
2865
+ }).split("\0").filter(Boolean);
2866
+ const unchangedHydration = hydratedNodeModules && fs9.lstatSync(stageNodeModules, { throwIfNoEntry: false })?.isSymbolicLink() && fs9.readlinkSync(stageNodeModules) === rootNodeModules;
2867
+ if (this.gitHead(stageDir) !== verifiedSha || stageChanges.some((change) => !(unchangedHydration && change === "?? node_modules"))) {
2868
+ throw new Error("Verification changed the staging checkout. Apply fixes in the source worktree and retry with a read-only check.");
2869
+ }
2870
+ if (this.gitHead(this.coordDb.repoRoot) !== baseHead || this.gitBranch(this.coordDb.repoRoot) !== targetBranch) {
2871
+ throw new Error("Merge target changed during verification. Retry against the new target.");
2872
+ }
2873
+ this.fencing.assertSession(params.agentId);
2874
+ this.requireCleanTarget();
2875
+ execFileSync4("git", ["merge", "--ff-only", verifiedSha], {
2876
+ cwd: this.coordDb.repoRoot,
2877
+ stdio: "ignore"
2878
+ });
2879
+ this.cleanupStageWorktree(stageDir);
2880
+ const updatedAt = Date.now();
2881
+ this.coordDb.withTransaction(() => {
2882
+ this.coordDb.db.prepare(`
2883
+ UPDATE merge_queue
2884
+ SET status = 'merged', diagnostics_output = ?, updated_at = ?
2885
+ WHERE id = ?
2886
+ `).run(diagnostics, updatedAt, id);
2887
+ this.coordDb.logEvent(params.agentId, "merge_train_succeeded", {
2888
+ id,
2889
+ branch: params.branch,
2890
+ verifiedSha
2891
+ });
2892
+ });
2893
+ return {
2894
+ merged: true,
2895
+ entry: this.getEntry(id),
2896
+ mergedCommitSha: verifiedSha,
2897
+ diagnostics
2898
+ };
2899
+ } catch (err) {
2900
+ this.cleanupStageWorktree(stageDir);
2901
+ const diagnostics = err instanceof Error ? err.message : String(err);
2902
+ const updatedAt = Date.now();
2903
+ this.coordDb.withTransaction(() => {
2904
+ this.coordDb.db.prepare(`
2905
+ UPDATE merge_queue
2906
+ SET status = 'failed', diagnostics_output = ?, updated_at = ?
2907
+ WHERE id = ?
2908
+ `).run(diagnostics, updatedAt, id);
2909
+ this.coordDb.logEvent(params.agentId, "merge_train_failed", {
2910
+ id,
2911
+ branch: params.branch,
2912
+ diagnostics
2913
+ });
2914
+ });
2915
+ return {
2916
+ merged: false,
2917
+ entry: this.getEntry(id),
2918
+ mergedCommitSha: null,
2919
+ diagnostics
2920
+ };
2921
+ } finally {
2922
+ if (lockFd !== void 0) {
2923
+ fs9.closeSync(lockFd);
2924
+ fs9.unlinkSync(lockPath);
2925
+ }
2926
+ }
2927
+ }
2928
+ gitHead(cwd) {
2929
+ return execFileSync4("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
2930
+ }
2931
+ gitBranch(cwd) {
2932
+ return execFileSync4("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], {
2933
+ cwd,
2934
+ encoding: "utf8",
2935
+ stdio: ["ignore", "pipe", "ignore"]
2936
+ }).trim();
2937
+ }
2938
+ requireCleanTarget() {
2939
+ const status = execFileSync4("git", ["status", "--porcelain", "--untracked-files=normal"], {
2940
+ cwd: this.coordDb.repoRoot,
2941
+ encoding: "utf8"
2942
+ });
2943
+ if (status.split(/\r?\n/).some((line) => line && line !== "?? .coord/")) {
2944
+ throw new Error("Merge target has local changes. Commit or preserve them before integrating.");
2945
+ }
2946
+ }
2947
+ cleanupStageWorktree(stageDir) {
2948
+ if (!fs9.existsSync(stageDir)) return;
2949
+ try {
2950
+ execFileSync4("git", ["worktree", "remove", "--force", stageDir], {
2951
+ cwd: this.coordDb.repoRoot,
2952
+ stdio: "ignore"
2953
+ });
2954
+ } catch {
2955
+ if (fs9.existsSync(stageDir)) {
2956
+ fs9.rmSync(stageDir, { recursive: true, force: true });
2957
+ }
2958
+ try {
2959
+ execFileSync4("git", ["worktree", "prune"], {
2960
+ cwd: this.coordDb.repoRoot,
2961
+ stdio: "ignore"
2962
+ });
2963
+ } catch {
2964
+ }
2965
+ }
2966
+ }
2967
+ getEntry(id) {
2968
+ const row = this.coordDb.db.prepare(`SELECT * FROM merge_queue WHERE id = ?`).get(id);
2969
+ return row ?? null;
2970
+ }
2971
+ listQueue() {
2972
+ return this.coordDb.db.prepare(`SELECT * FROM merge_queue ORDER BY created_at DESC`).all();
2973
+ }
2974
+ };
2975
+
2976
+ // src/core/shadow-git.ts
2977
+ import { execFileSync as execFileSync5 } from "child_process";
2978
+ import crypto9 from "crypto";
2979
+ import fs10 from "fs";
2980
+ import path8 from "path";
2981
+ var ShadowGitEngine = class {
2982
+ constructor(coordDb, fencing) {
2983
+ this.coordDb = coordDb;
2984
+ this.fencing = fencing;
2985
+ }
2986
+ coordDb;
2987
+ fencing;
2988
+ isGitRepository(dir = this.coordDb.repoRoot) {
2989
+ try {
2990
+ execFileSync5("git", ["rev-parse", "--is-inside-work-tree"], {
2991
+ cwd: dir,
2992
+ stdio: "ignore"
2993
+ });
2994
+ return true;
2995
+ } catch {
2996
+ return false;
2997
+ }
2998
+ }
2999
+ getHeadCommit(dir = this.coordDb.repoRoot) {
3000
+ try {
3001
+ return execFileSync5("git", ["rev-parse", "HEAD"], {
3002
+ cwd: dir,
3003
+ encoding: "utf8",
3004
+ stdio: ["ignore", "pipe", "ignore"]
3005
+ }).trim();
3006
+ } catch {
3007
+ return null;
3008
+ }
3009
+ }
3010
+ getShadowIndexPath(agentId) {
3011
+ const safeId = agentId.replace(/[^a-zA-Z0-9_-]/g, "_");
3012
+ const epoch = this.fencing.assertSession(agentId);
3013
+ return path8.join(this.coordDb.coordDir, "shadow-index", `${safeId}-${epoch}.index`);
3014
+ }
3015
+ /**
3016
+ * Updates an agent's ephemeral shadow Git index (`.coord/shadow-index/<agent-id>.index`)
3017
+ * without touching the repository's real `.git/index`, and creates a synthetic shadow
3018
+ * commit object in `.git/objects` so `git merge-tree --write-tree` can run 3-way merges
3019
+ * across uncommitted files in <15ms.
3020
+ */
3021
+ updateAgentShadowTree(params) {
3022
+ this.fencing.assertSession(params.agentId);
3023
+ const targetDir = params.worktreeDir ?? this.coordDb.repoRoot;
3024
+ if (!this.isGitRepository(targetDir)) return null;
3025
+ const headCommit = this.getHeadCommit(targetDir);
3026
+ if (!headCommit) return null;
3027
+ const shadowIndex = this.getShadowIndexPath(params.agentId);
3028
+ const env = { ...process.env, GIT_INDEX_FILE: shadowIndex };
3029
+ if (!fs10.existsSync(shadowIndex)) {
3030
+ execFileSync5("git", ["read-tree", headCommit], {
3031
+ cwd: targetDir,
3032
+ env,
3033
+ stdio: "ignore"
3034
+ });
3035
+ }
3036
+ if (params.worktreeDir && (!params.modifiedFiles || params.modifiedFiles.length === 0)) {
3037
+ execFileSync5("git", ["read-tree", headCommit], {
3038
+ cwd: targetDir,
3039
+ env,
3040
+ stdio: "ignore"
3041
+ });
3042
+ execFileSync5("git", ["add", "-A"], {
3043
+ cwd: targetDir,
3044
+ env,
3045
+ stdio: "ignore"
3046
+ });
3047
+ } else if (params.modifiedFiles && params.modifiedFiles.length > 0) {
3048
+ for (const mod of params.modifiedFiles) {
3049
+ const relPath = LockEngine.normalizeFile(targetDir, mod.filePath);
3050
+ const absPath = path8.join(targetDir, relPath);
3051
+ const content = mod.content ?? (fs10.existsSync(absPath) ? fs10.readFileSync(absPath, "utf8") : null);
3052
+ if (content === null) {
3053
+ {
3054
+ execFileSync5("git", ["--literal-pathspecs", "update-index", "--force-remove", "--", relPath], {
3055
+ cwd: targetDir,
3056
+ env,
3057
+ stdio: "ignore"
3058
+ });
3059
+ }
3060
+ } else {
3061
+ const tracked = execFileSync5("git", ["--literal-pathspecs", "ls-files", "--stage", "-z", "--", relPath], {
3062
+ cwd: targetDir,
3063
+ env,
3064
+ encoding: "utf8"
3065
+ }).split("\0").find(Boolean);
3066
+ const trackedMode = tracked?.split(" ")[0];
3067
+ const stat = fs10.existsSync(absPath) ? fs10.lstatSync(absPath) : null;
3068
+ if (trackedMode && !["100644", "100755"].includes(trackedMode) || stat?.isSymbolicLink()) {
3069
+ throw new Error(`Cannot report symlinks or special entries as source content: ${relPath}`);
3070
+ }
3071
+ const mode = trackedMode ?? (stat && stat.mode & 73 ? "100755" : "100644");
3072
+ const blobSha = execFileSync5("git", ["hash-object", "-w", "--stdin"], {
3073
+ cwd: targetDir,
3074
+ input: content,
3075
+ encoding: "utf8"
3076
+ }).trim();
3077
+ execFileSync5(
3078
+ "git",
3079
+ ["update-index", "--add", "--cacheinfo", `${mode},${blobSha},${relPath}`],
3080
+ {
3081
+ cwd: targetDir,
3082
+ env,
3083
+ stdio: "ignore"
3084
+ }
3085
+ );
3086
+ }
3087
+ }
3088
+ }
3089
+ const treeSha = execFileSync5("git", ["write-tree"], {
3090
+ cwd: targetDir,
3091
+ env,
3092
+ encoding: "utf8"
3093
+ }).trim();
3094
+ const shadowCommitSha = execFileSync5(
3095
+ "git",
3096
+ ["commit-tree", treeSha, "-p", headCommit, "-m", `kith-shadow:${params.agentId}`],
3097
+ {
3098
+ cwd: targetDir,
3099
+ encoding: "utf8"
3100
+ }
3101
+ ).trim();
3102
+ this.coordDb.withTransaction(() => {
3103
+ this.fencing.assertSession(params.agentId);
3104
+ this.coordDb.db.prepare(`UPDATE agents SET shadow_tree_sha = ? WHERE id = ?`).run(shadowCommitSha, params.agentId);
3105
+ });
3106
+ return shadowCommitSha;
3107
+ }
3108
+ /**
3109
+ * Runs `git merge-tree --write-tree` across `agentId`'s dirty shadow commit and every
3110
+ * other active agent's dirty shadow commit (or branch).
3111
+ */
3112
+ probeAllActiveConflicts(agentId, modifiedFiles, worktreeDir) {
3113
+ const myShadowCommit = this.updateAgentShadowTree({
3114
+ agentId,
3115
+ modifiedFiles,
3116
+ worktreeDir
3117
+ });
3118
+ if (!myShadowCommit) {
3119
+ return { clean: true, mergedTreeSha: null, conflicts: [] };
3120
+ }
3121
+ const activeAgents = this.fencing.listAgents(true).filter((a) => a.id !== agentId && a.shadow_tree_sha);
3122
+ const allConflicts = [];
3123
+ let lastMergedTreeSha = null;
3124
+ this.coordDb.withTransaction(() => {
3125
+ this.fencing.assertSession(agentId);
3126
+ this.coordDb.db.prepare(`DELETE FROM conflicts WHERE (agent_a = ? OR agent_b = ?) AND severity IN ('text_conflict', 'ast_conflict', 'auto_mergeable')`).run(agentId, agentId);
3127
+ });
3128
+ for (const peer of activeAgents) {
3129
+ if (!peer.shadow_tree_sha) continue;
3130
+ const res = this.probePairConflict(
3131
+ agentId,
3132
+ myShadowCommit,
3133
+ peer.id,
3134
+ peer.shadow_tree_sha
3135
+ );
3136
+ if (res.mergedTreeSha) {
3137
+ lastMergedTreeSha = res.mergedTreeSha;
3138
+ }
3139
+ allConflicts.push(...res.conflicts);
3140
+ }
3141
+ return {
3142
+ clean: allConflicts.length === 0,
3143
+ mergedTreeSha: lastMergedTreeSha,
3144
+ conflicts: allConflicts
3145
+ };
3146
+ }
3147
+ probePairConflict(agentA, commitA, agentB, commitB) {
3148
+ this.fencing.assertSession(agentA);
3149
+ try {
3150
+ const stdout = execFileSync5(
3151
+ "git",
3152
+ ["merge-tree", "--write-tree", "-z", commitA, commitB],
3153
+ {
3154
+ cwd: this.coordDb.repoRoot,
3155
+ encoding: "utf8",
3156
+ stdio: ["ignore", "pipe", "ignore"]
3157
+ }
3158
+ ).trim();
3159
+ const mergedTreeSha = stdout.split("\0")[0]?.trim() ?? null;
3160
+ return {
3161
+ clean: true,
3162
+ mergedTreeSha,
3163
+ conflicts: []
3164
+ };
3165
+ } catch (err) {
3166
+ const stdout = typeof err?.stdout === "string" ? err.stdout ?? "" : "";
3167
+ const stageBlobsByFile = /* @__PURE__ */ new Map();
3168
+ for (const line of stdout.split("\0")) {
3169
+ const m = line.match(/^(\d{6}) ([0-9a-f]+) ([123])\t([\s\S]+)$/);
3170
+ if (m) {
3171
+ const [, , blobSha, stage, filePath] = m;
3172
+ const entry = stageBlobsByFile.get(filePath) ?? {};
3173
+ if (stage === "1") entry.stage1 = blobSha;
3174
+ if (stage === "2") entry.stage2 = blobSha;
3175
+ if (stage === "3") entry.stage3 = blobSha;
3176
+ stageBlobsByFile.set(filePath, entry);
3177
+ }
3178
+ }
3179
+ if (stageBlobsByFile.size === 0) {
3180
+ throw new Error(`git merge-tree failed without a usable conflict result: ${err instanceof Error ? err.message : String(err)}`);
3181
+ }
3182
+ const conflicts = [];
3183
+ const now = Date.now();
3184
+ for (const [filePath, stages] of stageBlobsByFile.entries()) {
3185
+ const severity = "text_conflict";
3186
+ const record = {
3187
+ id: `conf_${crypto9.randomBytes(6).toString("hex")}`,
3188
+ agent_a: agentA,
3189
+ agent_b: agentB,
3190
+ file_path: filePath,
3191
+ severity,
3192
+ details: {
3193
+ reason: "Git merge conflict requires explicit resolution",
3194
+ baseBlob: stages.stage1,
3195
+ blobA: stages.stage2,
3196
+ blobB: stages.stage3
3197
+ },
3198
+ detected_at: now
3199
+ };
3200
+ this.coordDb.withTransaction(() => {
3201
+ this.fencing.assertSession(agentA);
3202
+ this.coordDb.db.prepare(`
3203
+ INSERT OR REPLACE INTO conflicts (
3204
+ id, agent_a, agent_b, file_path, severity, details_json, detected_at
3205
+ )
3206
+ VALUES (?, ?, ?, ?, ?, ?, ?)
3207
+ `).run(
3208
+ record.id,
3209
+ agentA,
3210
+ agentB,
3211
+ filePath,
3212
+ severity,
3213
+ JSON.stringify(record.details),
3214
+ now
3215
+ );
3216
+ });
3217
+ conflicts.push(record);
3218
+ }
3219
+ this.fencing.assertSession(agentA);
3220
+ if (conflicts.length) {
3221
+ this.coordDb.logEvent(agentA, "dirty_merge_conflict", {
3222
+ peerAgent: agentB,
3223
+ files: conflicts.map((c) => c.file_path)
3224
+ });
3225
+ }
3226
+ return {
3227
+ clean: false,
3228
+ mergedTreeSha: null,
3229
+ conflicts
3230
+ };
3231
+ }
3232
+ }
3233
+ listConflicts() {
3234
+ this.fencing.reapDeadAgents();
3235
+ const rows = this.coordDb.db.prepare(`SELECT * FROM conflicts ORDER BY detected_at DESC`).all();
3236
+ return rows.map((r) => ({
3237
+ id: String(r.id),
3238
+ agent_a: String(r.agent_a),
3239
+ agent_b: String(r.agent_b),
3240
+ file_path: String(r.file_path),
3241
+ severity: r.severity,
3242
+ details: JSON.parse(String(r.details_json)),
3243
+ detected_at: Number(r.detected_at)
3244
+ }));
3245
+ }
3246
+ };
3247
+
3248
+ // src/core/worktree-cow.ts
3249
+ import { execFileSync as execFileSync6 } from "child_process";
3250
+ import fs11 from "fs";
3251
+ import path9 from "path";
3252
+ var CoWWorktreeManager = class _CoWWorktreeManager {
3253
+ constructor(coordDb, fencing) {
3254
+ this.coordDb = coordDb;
3255
+ this.fencing = fencing;
3256
+ }
3257
+ coordDb;
3258
+ fencing;
3259
+ static HYDRATABLE_DIRS = ["node_modules", ".venv", "venv", ".turbo"];
3260
+ static ENV_FILES = [".env", ".env.local", ".env.development", ".env.test"];
3261
+ /**
3262
+ * Spawns an isolated Git worktree for `agentId` and hydrates `node_modules` / `.env`
3263
+ * using APFS Copy-on-Write (`cp -cR`) on macOS or reflink on Linux (with independent-copy fallback).
3264
+ */
3265
+ spawnWorktree(params) {
3266
+ this.fencing.assertSession(params.agentId);
3267
+ const start = Date.now();
3268
+ const safeId = params.agentId.replace(/[^a-zA-Z0-9_-]/g, "-");
3269
+ const branch = params.branch ?? `kith/${safeId}`;
3270
+ const baseRef = params.baseRef ?? "HEAD";
3271
+ const worktreePath = path9.join(this.coordDb.coordDir, "worktrees", safeId);
3272
+ if (!this.fencing.getAgent(params.agentId)) throw new Error(`Unknown agent: ${params.agentId}`);
3273
+ if (!safeId || safeId === "." || safeId === "..") throw new Error("Invalid agent ID");
3274
+ if (fs11.existsSync(worktreePath)) {
3275
+ throw new Error(`Worktree already exists: ${worktreePath}. Commit or preserve its work before removing it.`);
3276
+ }
3277
+ fs11.mkdirSync(path9.dirname(worktreePath), { recursive: true });
3278
+ execFileSync6("git", ["worktree", "add", "-b", branch, worktreePath, baseRef], {
3279
+ cwd: this.coordDb.repoRoot,
3280
+ stdio: "ignore"
3281
+ });
3282
+ const hydratedDirs = [];
3283
+ let hydrationMethod = "none";
3284
+ for (const dirName of _CoWWorktreeManager.HYDRATABLE_DIRS) {
3285
+ const srcDir = path9.join(this.coordDb.repoRoot, dirName);
3286
+ const destDir = path9.join(worktreePath, dirName);
3287
+ if (!fs11.existsSync(srcDir) || fs11.existsSync(destDir)) continue;
3288
+ const method = this.cloneDirectoryCoW(srcDir, destDir);
3289
+ hydrationMethod = method;
3290
+ hydratedDirs.push(dirName);
3291
+ }
3292
+ for (const envName of _CoWWorktreeManager.ENV_FILES) {
3293
+ const srcEnv = path9.join(this.coordDb.repoRoot, envName);
3294
+ const destEnv = path9.join(worktreePath, envName);
3295
+ if (fs11.existsSync(srcEnv) && !fs11.existsSync(destEnv)) {
3296
+ fs11.copyFileSync(srcEnv, destEnv);
3297
+ }
3298
+ }
3299
+ this.coordDb.withTransaction(() => {
3300
+ this.fencing.assertSession(params.agentId);
3301
+ this.coordDb.db.prepare(`UPDATE agents SET worktree_path = ?, branch = ? WHERE id = ?`).run(worktreePath, branch, params.agentId);
3302
+ this.coordDb.logEvent(params.agentId, "worktree_spawned", {
3303
+ worktreePath,
3304
+ branch,
3305
+ hydrationMethod,
3306
+ hydratedDirs
3307
+ });
3308
+ });
3309
+ return {
3310
+ agentId: params.agentId,
3311
+ worktreePath,
3312
+ branch,
3313
+ hydrationMethod,
3314
+ hydratedDirs,
3315
+ durationMs: Date.now() - start
3316
+ };
3317
+ }
3318
+ cloneDirectoryCoW(srcDir, destDir) {
3319
+ if (process.platform === "darwin") {
3320
+ try {
3321
+ execFileSync6("cp", ["-cR", srcDir, destDir], {
3322
+ stdio: "ignore",
3323
+ timeout: 5e3
3324
+ });
3325
+ return "apfs-cow";
3326
+ } catch {
3327
+ }
3328
+ } else if (process.platform === "linux") {
3329
+ try {
3330
+ execFileSync6("cp", ["--reflink=always", "-R", srcDir, destDir], {
3331
+ stdio: "ignore",
3332
+ timeout: 5e3
3333
+ });
3334
+ return "reflink-cow";
3335
+ } catch {
3336
+ }
3337
+ }
3338
+ fs11.rmSync(destDir, { recursive: true, force: true });
3339
+ fs11.cpSync(srcDir, destDir, { recursive: true, dereference: false });
3340
+ return "copy";
3341
+ }
3342
+ removeWorktree(agentId) {
3343
+ this.fencing.assertSession(agentId);
3344
+ const safeId = agentId.replace(/[^a-zA-Z0-9_-]/g, "-");
3345
+ const worktreePath = path9.join(this.coordDb.coordDir, "worktrees", safeId);
3346
+ if (!fs11.existsSync(worktreePath)) return false;
3347
+ const registered = this.fencing.getAgent(agentId)?.worktree_path;
3348
+ if (!registered || physicalPath(registered) !== physicalPath(worktreePath) || this.fencing.listAgents(true).some((agent) => agent.id !== agentId && agent.worktree_path && physicalPath(agent.worktree_path) === physicalPath(worktreePath))) {
3349
+ throw new Error("Refusing to remove a worktree that is not exclusively registered to this agent.");
3350
+ }
3351
+ const status = execFileSync6("git", ["status", "--porcelain", "--untracked-files=normal", "--ignored=matching"], {
3352
+ cwd: worktreePath,
3353
+ encoding: "utf8"
3354
+ });
3355
+ if (status.trim()) {
3356
+ throw new Error(`Refusing to remove dirty worktree: ${worktreePath}. Commit or preserve changed, untracked, and ignored files first.`);
3357
+ }
3358
+ execFileSync6("git", ["worktree", "remove", worktreePath], {
3359
+ cwd: this.coordDb.repoRoot,
3360
+ stdio: ["ignore", "pipe", "pipe"]
3361
+ });
3362
+ this.coordDb.withTransaction(() => {
3363
+ this.fencing.assertSession(agentId);
3364
+ this.coordDb.db.prepare(`UPDATE agents SET worktree_path = NULL, branch = NULL WHERE id = ?`).run(agentId);
3365
+ });
3366
+ return true;
3367
+ }
3368
+ listWorktrees() {
3369
+ try {
3370
+ const out = execFileSync6("git", ["worktree", "list", "--porcelain"], {
3371
+ cwd: this.coordDb.repoRoot,
3372
+ encoding: "utf8"
3373
+ });
3374
+ const entries = [];
3375
+ let current = null;
3376
+ for (const line of out.split(/\r?\n/)) {
3377
+ if (line.startsWith("worktree ")) {
3378
+ if (current) entries.push(current);
3379
+ current = { path: line.slice("worktree ".length).trim(), head: "", branch: null };
3380
+ } else if (line.startsWith("HEAD ") && current) {
3381
+ current.head = line.slice("HEAD ".length).trim();
3382
+ } else if (line.startsWith("branch ") && current) {
3383
+ current.branch = line.slice("branch ".length).replace(/^refs\/heads\//, "").trim();
3384
+ }
3385
+ }
3386
+ if (current) entries.push(current);
3387
+ return entries;
3388
+ } catch {
3389
+ return [];
3390
+ }
3391
+ }
3392
+ };
3393
+
3394
+ // src/core/engine.ts
3395
+ var KithEngine = class {
3396
+ coordDb;
3397
+ fencing;
3398
+ locks;
3399
+ intents;
3400
+ depGraph;
3401
+ shadowGit;
3402
+ worktrees;
3403
+ mergeTrain;
3404
+ bashGuard;
3405
+ broker;
3406
+ constructor(repoRoot = process.cwd(), customDbPath) {
3407
+ this.coordDb = new CoordDatabase(repoRoot, customDbPath);
3408
+ this.fencing = new FencingAndAgentManager(this.coordDb);
3409
+ this.locks = new LockEngine(this.coordDb, this.fencing);
3410
+ this.intents = new IntentAndTaskEngine(this.coordDb, this.fencing, this.locks);
3411
+ this.depGraph = new DependencyAndContractEngine(this.coordDb, this.locks, this.fencing);
3412
+ this.shadowGit = new ShadowGitEngine(this.coordDb, this.fencing);
3413
+ this.worktrees = new CoWWorktreeManager(this.coordDb, this.fencing);
3414
+ this.mergeTrain = new SpeculativeMergeTrain(this.coordDb, this.fencing);
3415
+ this.bashGuard = new BashMerkleGuard(
3416
+ this.coordDb,
3417
+ this.fencing,
3418
+ this.locks,
3419
+ this.depGraph,
3420
+ this.shadowGit
3421
+ );
3422
+ this.broker = new ResourceBroker(this.coordDb, this.fencing);
3423
+ }
3424
+ /**
3425
+ * Executes the complete PostToolUse pipeline whenever an agent writes or edits a file:
3426
+ * 1. Extracts AST exports/imports and updates the Contract Blackboard & Import Graph
3427
+ * 2. Computes transitive breaking-change blast radius across downstream dependent files
3428
+ * 3. Updates the agent's shadow Git index (`GIT_INDEX_FILE`) and runs `git merge-tree` against sibling dirty trees
3429
+ */
3430
+ processPostFileEdit(params) {
3431
+ this.fencing.heartbeat(params.agentId);
3432
+ const targetRoot = params.worktreeDir ?? this.fencing.getAgent(params.agentId)?.worktree_path ?? this.coordDb.repoRoot;
3433
+ const file = LockEngine.normalizeFile(targetRoot, params.filePath);
3434
+ const access = this.locks.checkFileWriteAccess({ agentId: params.agentId, filePath: file });
3435
+ if (!access.allowed) throw new Error(access.reason);
3436
+ const content = params.content ?? (fs12.existsSync(path10.join(targetRoot, file)) ? fs12.readFileSync(path10.join(targetRoot, file), "utf8") : "");
3437
+ const contractReport = this.depGraph.recordFileModification(
3438
+ params.agentId,
3439
+ file,
3440
+ content
3441
+ );
3442
+ const mergeProbe = this.shadowGit.probeAllActiveConflicts(
3443
+ params.agentId,
3444
+ [{ filePath: file, content }],
3445
+ targetRoot
3446
+ );
3447
+ const warnings = [];
3448
+ const existingSymbols = this.coordDb.db.prepare(`
3449
+ SELECT peer.symbol_name, peer.kind, peer.file_path
3450
+ FROM contracts AS edited JOIN contracts AS peer
3451
+ ON peer.symbol_name = edited.symbol_name COLLATE BINARY AND peer.kind = edited.kind
3452
+ WHERE edited.file_path = ? AND peer.file_path != edited.file_path
3453
+ ORDER BY peer.symbol_name, peer.file_path
3454
+ `).all(file);
3455
+ for (const symbol of existingSymbols) {
3456
+ warnings.push(`[EXISTING SYMBOL] '${symbol.symbol_name}' (${symbol.kind}) is also recorded in '${symbol.file_path}'. Consider reuse if it serves the same purpose.`);
3457
+ }
3458
+ for (const brk of contractReport.breakingChanges) {
3459
+ const targetWho = brk.affectedAgentId ? ` (actively locked by agent '${brk.affectedAgentId}')` : "";
3460
+ warnings.push(
3461
+ `[BLAST RADIUS ALERT] Breaking signature change on '${brk.symbolName}' in '${brk.modifiedFile}' impacts downstream consumer '${brk.affectedDownstreamFile}' (${brk.hopDistance} hop(s) away)${targetWho}.`
3462
+ );
3463
+ }
3464
+ for (const conf of mergeProbe.conflicts) {
3465
+ const peer = conf.agent_a === params.agentId ? conf.agent_b : conf.agent_a;
3466
+ warnings.push(
3467
+ `[LIVE DIRTY-TREE CONFLICT] Uncommitted changes in '${conf.file_path}' conflict with active agent '${peer}' (${conf.severity}: ${conf.details.reason}).`
3468
+ );
3469
+ }
3470
+ return {
3471
+ filePath: file,
3472
+ contractReport,
3473
+ dirtyMergeConflicts: mergeProbe.conflicts,
3474
+ warnings
3475
+ };
3476
+ }
3477
+ /**
3478
+ * Consumes unread coordination events and active conflicts for `agentId` since its last tool call
3479
+ * and formats a concise context-delta footer to append to MCP/CLI responses.
3480
+ */
3481
+ consumeAgentDelta(agentId) {
3482
+ this.fencing.assertSession(agentId);
3483
+ const agent = this.fencing.getAgent(agentId);
3484
+ const sinceId = agent?.last_seen_event_id ?? 0;
3485
+ const page = this.coordDb.getEventsSince(sinceId, 50);
3486
+ const allNewEvents = page.filter((e) => e.agent_id !== agentId);
3487
+ const deliveredId = page.at(-1)?.id ?? sinceId;
3488
+ if (agent && deliveredId > sinceId) {
3489
+ this.coordDb.withTransaction(() => {
3490
+ this.fencing.assertSession(agentId);
3491
+ this.coordDb.db.prepare(`UPDATE agents SET last_seen_event_id = ? WHERE id = ?`).run(deliveredId, agentId);
3492
+ });
3493
+ }
3494
+ const activeConflicts = this.shadowGit.listConflicts().filter(
3495
+ (c) => c.agent_a === agentId || c.agent_b === agentId
3496
+ );
3497
+ const lines = [];
3498
+ for (const ev of allNewEvents) {
3499
+ if (ev.event_type === "contracts_updated" || ev.event_type === "contract_published" || ev.event_type === "contract_breaking_change" || ev.event_type === "intent_declared") {
3500
+ lines.push(`\u2022 [${ev.event_type}] by ${ev.agent_id ?? "system"}: ${JSON.stringify(ev.payload)}`);
3501
+ }
3502
+ }
3503
+ for (const c of activeConflicts) {
3504
+ const peer = c.agent_a === agentId ? c.agent_b : c.agent_a;
3505
+ lines.push(
3506
+ `\u2022 [ACTIVE CONFLICT] ${c.file_path} with agent '${peer}' (${c.severity}: ${c.details.reason})`
3507
+ );
3508
+ }
3509
+ const formattedFooter = lines.length > 0 ? `
3510
+
3511
+ --- KITH LIVE SIBLING DELTA ---
3512
+ ${lines.join("\n")}` : "";
3513
+ return {
3514
+ events: allNewEvents,
3515
+ activeConflicts,
3516
+ formattedFooter
3517
+ };
3518
+ }
3519
+ getStatus() {
3520
+ const agents = this.fencing.listAgents(true);
3521
+ return {
3522
+ protocolVersion: KITH_PROTOCOL_VERSION,
3523
+ repository: { root: this.coordDb.repoRoot, commonGitDir: this.coordDb.commonGitDir, coordDir: this.coordDb.coordDir },
3524
+ runtime: { node: process.version },
3525
+ fencingEpoch: this.coordDb.currentFencingEpoch(),
3526
+ agents,
3527
+ locks: this.locks.listLocks(),
3528
+ tasks: this.intents.listTasks(),
3529
+ conflicts: this.shadowGit.listConflicts(),
3530
+ contracts: this.depGraph.listContracts(),
3531
+ resources: this.broker.listResources(),
3532
+ mergeQueue: this.mergeTrain.listQueue(),
3533
+ quarantined: this.bashGuard.listQuarantined(),
3534
+ mutexWaiters: this.coordDb.db.prepare("SELECT ticket,mutex_name,owner_agent_id,session_epoch,expires_at FROM mutex_waiters ORDER BY ticket").all(),
3535
+ commandGuards: this.coordDb.db.prepare("SELECT mutex_name,owner_agent_id,worker_pid,supervisor_pid FROM mutex_runs ORDER BY mutex_name").all(),
3536
+ workspaces: observeWorkspaces(this.coordDb.repoRoot, agents)
3537
+ };
3538
+ }
3539
+ previewIntegration(agentId) {
3540
+ this.fencing.assertSession(agentId);
3541
+ return previewIntegration(this.coordDb.repoRoot, this.fencing.getAgent(agentId));
3542
+ }
3543
+ beginWork(params) {
3544
+ const intent = this.intents.declareIntent(params);
3545
+ const worktree = intent.ok && params.isolateWorktree ? this.worktrees.spawnWorktree({ agentId: params.agentId }) : null;
3546
+ return { ...intent, worktree };
3547
+ }
3548
+ completeWork(params) {
3549
+ this.fencing.assertSession(params.agentId);
3550
+ const agent = this.fencing.getAgent(params.agentId);
3551
+ const branch = params.mergeBranch ?? agent?.branch;
3552
+ const mergeResult = branch ? this.mergeTrain.enqueueAndMerge({
3553
+ agentId: params.agentId,
3554
+ branch,
3555
+ worktreePath: agent?.worktree_path,
3556
+ verificationCmd: params.verificationCmd
3557
+ }) : null;
3558
+ if (mergeResult && !mergeResult.merged) return {
3559
+ ok: false,
3560
+ mergeResult,
3561
+ error: "Speculative merge train failed. Fix the reported issues before completing work."
3562
+ };
3563
+ const completedTasks = this.intents.completeTask(params);
3564
+ this.fencing.completeAgentSession(params.agentId);
3565
+ return { ok: true, completedTasks, mergeResult };
3566
+ }
3567
+ close() {
3568
+ if (this.broker.hasActiveCommands()) throw new Error("Await or cancel active mutex commands before closing Kith");
3569
+ this.coordDb.close();
3570
+ }
3571
+ };
3572
+
3573
+ // src/core/observation.ts
3574
+ import fs13 from "fs";
3575
+ import os from "os";
3576
+ import path11 from "path";
3577
+ import { createRequire as createRequire2 } from "module";
3578
+ var { DatabaseSync: DatabaseSync2 } = createRequire2(import.meta.url)("node:sqlite");
3579
+ function readCoordinationState(directory = process.cwd()) {
3580
+ const layout = discoverRepository(directory), observedAt = Date.now();
3581
+ const empty = {
3582
+ protocolVersion: KITH_PROTOCOL_VERSION,
3583
+ observedAt,
3584
+ repository: { root: layout.checkoutRoot, commonGitDir: layout.commonGitDir, coordDir: layout.coordDir },
3585
+ runtime: { node: process.version },
3586
+ state: "missing",
3587
+ error: void 0,
3588
+ fencingEpoch: 0,
3589
+ agents: [],
3590
+ tasks: [],
3591
+ locks: [],
3592
+ conflicts: [],
3593
+ contracts: [],
3594
+ resources: [],
3595
+ mergeQueue: [],
3596
+ quarantined: [],
3597
+ mutexWaiters: [],
3598
+ commandGuards: [],
3599
+ events: [],
3600
+ workspaces: []
3601
+ };
3602
+ let scratch, db;
3603
+ try {
3604
+ const file = path11.join(layout.coordDir, "state.db");
3605
+ if (physicalPath(layout.coordDir) !== layout.coordDir || physicalPath(file) !== file) throw new Error("Coordination state must not be a symlink");
3606
+ for (let current = fs13.realpathSync.native(directory); isWithin(layout.checkoutRoot, current); current = path11.dirname(current)) {
3607
+ const local = path11.join(current, ".coord");
3608
+ if (local !== layout.coordDir && fs13.existsSync(path11.join(local, "state.db"))) throw new Error(`Legacy independent state at ${local}; preserve it before consolidation`);
3609
+ if (current === layout.checkoutRoot) break;
3610
+ }
3611
+ if (!fs13.existsSync(file)) {
3612
+ if (["setup.lock", "merge-train.lock"].some((name) => fs13.existsSync(path11.join(layout.coordDir, name))) || ["-wal", "-shm", "-journal"].some((s) => fs13.existsSync(file + s)) || ["worktrees", "quarantine", "snapshots", "shadow-index", "setup"].some((name) => {
3613
+ const p = path11.join(layout.coordDir, name);
3614
+ return fs13.existsSync(p) && (!fs13.lstatSync(p).isDirectory() || fs13.readdirSync(p).length > 0);
3615
+ })) throw new Error("Missing database with retained sidecars; preserve the recovery set");
3616
+ return empty;
3617
+ }
3618
+ const files = ["", "-wal", "-shm", "-journal"].map((s) => file + s);
3619
+ const identity = (f) => {
3620
+ if (!fs13.existsSync(f)) return null;
3621
+ if (physicalPath(f) !== f || !fs13.lstatSync(f).isFile()) throw new Error("Database or sidecar is redirected or not a regular file");
3622
+ const s = fs13.statSync(f, { bigint: true });
3623
+ return [s.dev, s.ino, s.size, s.mtimeNs, s.ctimeNs].join(":");
3624
+ };
3625
+ const before = files.map(identity);
3626
+ scratch = fs13.mkdtempSync(path11.join(os.tmpdir(), "kith-observe-"));
3627
+ for (let i = 0; i < files.length; i++) if (before[i]) {
3628
+ const target = path11.join(scratch, path11.basename(files[i]));
3629
+ fs13.copyFileSync(files[i], target);
3630
+ fs13.chmodSync(target, 384);
3631
+ }
3632
+ if (JSON.stringify(before) !== JSON.stringify(files.map(identity))) throw new Error("Coordination state changed during snapshot; stop writers or retry");
3633
+ db = new DatabaseSync2(path11.join(scratch, "state.db"), { readOnly: true });
3634
+ db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=1000; BEGIN");
3635
+ if (CoordDatabase.inspectSchema(db) !== 2) throw new Error("Offline observation requires schema 2; preserve state and upgrade with clients stopped");
3636
+ const all = (sql) => db.prepare(sql).all();
3637
+ const agents = all("SELECT * FROM agents ORDER BY registered_at DESC");
3638
+ const state = {
3639
+ ...empty,
3640
+ state: "ready",
3641
+ fencingEpoch: Number(db.prepare("SELECT value FROM meta WHERE key='fencing_epoch'").get().value),
3642
+ agents,
3643
+ 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)) })),
3644
+ locks: all("SELECT * FROM locks ORDER BY acquired_at"),
3645
+ conflicts: all("SELECT * FROM conflicts ORDER BY detected_at DESC").map(({ details_json, ...r }) => ({ ...r, details: JSON.parse(String(details_json)) })),
3646
+ contracts: all("SELECT * FROM contracts ORDER BY updated_at DESC").map((r) => ({ ...r, is_breaking: Boolean(r.is_breaking) })),
3647
+ resources: all("SELECT * FROM resources ORDER BY acquired_at"),
3648
+ mergeQueue: all("SELECT * FROM merge_queue ORDER BY created_at DESC"),
3649
+ quarantined: all("SELECT * FROM quarantine ORDER BY created_at DESC"),
3650
+ mutexWaiters: all("SELECT ticket,mutex_name,owner_agent_id,session_epoch,expires_at FROM mutex_waiters ORDER BY ticket"),
3651
+ commandGuards: all("SELECT mutex_name,owner_agent_id,worker_pid,supervisor_pid FROM mutex_runs ORDER BY mutex_name"),
3652
+ events: all("SELECT * FROM events ORDER BY id DESC LIMIT 100").map(({ payload_json, ...r }) => ({ ...r, payload: JSON.parse(String(payload_json)) })),
3653
+ workspaces: observeWorkspaces(layout.checkoutRoot, agents, observedAt)
3654
+ };
3655
+ db.exec("COMMIT");
3656
+ return state;
3657
+ } catch (error) {
3658
+ return { ...empty, state: "unavailable", error: error instanceof Error ? error.message : String(error) };
3659
+ } finally {
3660
+ db?.close();
3661
+ if (scratch) fs13.rmSync(scratch, { recursive: true, force: true });
3662
+ }
3663
+ }
3664
+ function recoveryReport(directory = process.cwd()) {
3665
+ const state = readCoordinationState(directory), retainedRoot = path11.join(state.repository.coordDir, "worktrees");
3666
+ const retained = physicalPath(retainedRoot) === retainedRoot && fs13.existsSync(retainedRoot) && fs13.lstatSync(retainedRoot).isDirectory() && !fs13.lstatSync(retainedRoot).isSymbolicLink() ? fs13.readdirSync(retainedRoot, { withFileTypes: true }).map((entry) => ({
3667
+ path: path11.join(retainedRoot, entry.name),
3668
+ symlink: entry.isSymbolicLink(),
3669
+ agentIds: state.agents.filter((a) => a.worktree_path === path11.join(retainedRoot, entry.name)).map((a) => a.id)
3670
+ })) : [];
3671
+ return {
3672
+ ...state,
3673
+ retainedWorktrees: retained,
3674
+ expiredClaims: state.locks.filter((lock) => Number(lock.expires_at) <= state.observedAt),
3675
+ unfinishedIntegrations: state.mergeQueue.filter((entry) => entry.status !== "merged"),
3676
+ setupLock: { path: path11.join(state.repository.coordDir, "setup.lock"), present: fs13.existsSync(path11.join(state.repository.coordDir, "setup.lock")) },
3677
+ integrationLock: { path: path11.join(state.repository.coordDir, "merge-train.lock"), present: fs13.existsSync(path11.join(state.repository.coordDir, "merge-train.lock")) }
3678
+ };
3679
+ }
3680
+ export {
3681
+ AstAnalyzer,
3682
+ BashMerkleGuard,
3683
+ CoWWorktreeManager,
3684
+ CoordDatabase,
3685
+ DependencyAndContractEngine,
3686
+ FencingAndAgentManager,
3687
+ IntentAndTaskEngine,
3688
+ KITH_PROTOCOL_VERSION,
3689
+ KITH_VERSION,
3690
+ KithEngine,
3691
+ LockEngine,
3692
+ ResourceBroker,
3693
+ SessionOwnershipError,
3694
+ ShadowGitEngine,
3695
+ SpeculativeMergeTrain,
3696
+ StaleFencingTokenError,
3697
+ caseSensitivePaths,
3698
+ discoverRepository,
3699
+ isWithin,
3700
+ observeWorkspaces,
3701
+ physicalPath,
3702
+ previewIntegration,
3703
+ readCoordinationState,
3704
+ recoveryReport,
3705
+ repositoryFile,
3706
+ worktreeChanges
3707
+ };
3708
+ //# sourceMappingURL=index.js.map