@kontextmind/kxm 0.6.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 (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,403 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import {
4
+ chmodSync,
5
+ closeSync,
6
+ existsSync,
7
+ fsyncSync,
8
+ lstatSync,
9
+ mkdirSync,
10
+ openSync,
11
+ readFileSync,
12
+ realpathSync,
13
+ renameSync,
14
+ rmSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { DatabaseSync } from "node:sqlite";
19
+ import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
20
+ import {
21
+ VnextConfigError,
22
+ VnextSchemaRegistry,
23
+ parseRestrictedYaml,
24
+ type JsonObject,
25
+ type VnextConfigIssue,
26
+ } from "./vnext-config.ts";
27
+
28
+ const MAX_BINDING_RECORD_BYTES = 256 * 1024;
29
+ const BINDING_LABEL = "Runtime-local repository bindings";
30
+
31
+ export interface VnextLocalBindingRecord {
32
+ schema: "kxm.local-repository-bindings.v1";
33
+ projectId: string;
34
+ projectRoot: string;
35
+ repositories: Readonly<Record<string, string>>;
36
+ }
37
+
38
+ export interface LoadedVnextLocalBindings extends VnextLocalBindingRecord {
39
+ file: string;
40
+ }
41
+
42
+ export interface VnextLocalBindingPathOptions {
43
+ stateRoot?: string;
44
+ env?: NodeJS.ProcessEnv;
45
+ homeDir?: string;
46
+ platform?: NodeJS.Platform;
47
+ }
48
+
49
+ export interface VnextLocalBindingStoreOptions extends VnextLocalBindingPathOptions {
50
+ schemasDir?: string;
51
+ }
52
+
53
+ export interface VnextLocalBindingWriteResult {
54
+ file: string;
55
+ written: boolean;
56
+ record: VnextLocalBindingRecord;
57
+ }
58
+
59
+ export interface VnextLocalBindingLock {
60
+ readonly projectRoot: string;
61
+ readonly file: string;
62
+ }
63
+
64
+ const activeLocks = new WeakSet<VnextLocalBindingLock>();
65
+
66
+ function bindingIssue(phase: VnextConfigIssue["phase"], code: string, message: string): VnextConfigIssue {
67
+ return { phase, code, file: BINDING_LABEL, message };
68
+ }
69
+
70
+ function bindingError(phase: VnextConfigIssue["phase"], code: string, message: string): never {
71
+ throw new VnextConfigError([bindingIssue(phase, code, message)]);
72
+ }
73
+
74
+ export function canonicalHostPath(path: string): string {
75
+ const absolute = resolve(path);
76
+ try {
77
+ return realpathSync.native(absolute);
78
+ } catch {
79
+ return absolute;
80
+ }
81
+ }
82
+
83
+ export function sameHostPath(left: string, right: string, platform = process.platform): boolean {
84
+ const first = canonicalHostPath(left);
85
+ const second = canonicalHostPath(right);
86
+ return platform === "win32"
87
+ ? first.toLocaleLowerCase("en-US") === second.toLocaleLowerCase("en-US")
88
+ : first === second;
89
+ }
90
+
91
+ /** Resolve the per-user state root without consulting any legacy KXM_* workspace override. */
92
+ export function vnextUserStateRoot(options: VnextLocalBindingPathOptions = {}): string {
93
+ const env = options.env ?? process.env;
94
+ const platform = options.platform ?? process.platform;
95
+ const home = options.homeDir ?? homedir();
96
+ const explicit = options.stateRoot ?? env.KXM_STATE_HOME?.trim();
97
+ if (explicit) {
98
+ if (!isAbsolute(explicit)) bindingError("path", "local_state_root_not_absolute", "KXM_STATE_HOME must be an absolute host-local path");
99
+ return resolve(explicit);
100
+ }
101
+ if (platform === "win32") {
102
+ const localAppData = env.LOCALAPPDATA?.trim();
103
+ const base = localAppData && isAbsolute(localAppData) ? localAppData : join(home, "AppData", "Local");
104
+ return resolve(base, "KXM");
105
+ }
106
+ if (platform === "darwin") return resolve(home, "Library", "Application Support", "KXM");
107
+ const xdgState = env.XDG_STATE_HOME?.trim();
108
+ const base = xdgState && isAbsolute(xdgState) ? xdgState : join(home, ".local", "state");
109
+ return resolve(base, "kxm");
110
+ }
111
+
112
+ function projectBindingKey(projectRoot: string, platform = process.platform): string {
113
+ const canonical = canonicalHostPath(projectRoot);
114
+ const keyInput = platform === "win32" ? canonical.toLocaleLowerCase("en-US") : canonical;
115
+ return createHash("sha256").update(keyInput, "utf8").digest("hex");
116
+ }
117
+
118
+ export function vnextLocalBindingFile(projectRoot: string, options: VnextLocalBindingPathOptions = {}): string {
119
+ const stateRoot = vnextUserStateRoot(options);
120
+ return join(stateRoot, "projects", projectBindingKey(projectRoot, options.platform), "repository-bindings.json");
121
+ }
122
+
123
+ function checkedDirectory(path: string, description: string): boolean {
124
+ if (!existsSync(path)) return false;
125
+ const stat = lstatSync(path);
126
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
127
+ bindingError("path", "local_binding_directory_invalid", `${description} must be a regular directory, not a link or file`);
128
+ }
129
+ return true;
130
+ }
131
+
132
+ function existingBindingDirectory(file: string, stateRoot: string): boolean {
133
+ const projects = join(stateRoot, "projects");
134
+ const project = dirname(file);
135
+ if (!checkedDirectory(stateRoot, "local state root")) return false;
136
+ if (!checkedDirectory(projects, "local projects directory")) return false;
137
+ return checkedDirectory(project, "local project binding directory");
138
+ }
139
+
140
+ function asRecord(value: JsonObject, file: string, projectRoot: string, schemasDir?: string): LoadedVnextLocalBindings {
141
+ const registry = new VnextSchemaRegistry(schemasDir);
142
+ const issues = registry.validateLocalBindings(value, BINDING_LABEL);
143
+ if (issues.length > 0) throw new VnextConfigError(issues);
144
+ const recordRoot = String(value.projectRoot);
145
+ if (!sameHostPath(recordRoot, projectRoot)) {
146
+ bindingError("semantic", "local_binding_project_root_mismatch", "binding record belongs to a different control worktree");
147
+ }
148
+ const repositories = Object.fromEntries(Object.entries(value.repositories as Record<string, string>)
149
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
150
+ return {
151
+ schema: "kxm.local-repository-bindings.v1",
152
+ projectId: String(value.projectId),
153
+ projectRoot: recordRoot,
154
+ repositories,
155
+ file,
156
+ };
157
+ }
158
+
159
+ /** Read one exact, bounded Runtime-local binding record. Missing state is not an error. */
160
+ export function readVnextLocalBindings(
161
+ projectRoot: string,
162
+ options: VnextLocalBindingStoreOptions = {},
163
+ ): LoadedVnextLocalBindings | undefined {
164
+ const root = resolve(projectRoot);
165
+ const stateRoot = vnextUserStateRoot(options);
166
+ const file = vnextLocalBindingFile(root, options);
167
+ if (!existingBindingDirectory(file, stateRoot) || !existsSync(file)) return undefined;
168
+ const stat = lstatSync(file);
169
+ if (stat.isSymbolicLink() || !stat.isFile()) {
170
+ bindingError("path", "local_binding_file_invalid", "binding record must be a regular file, not a link or directory");
171
+ }
172
+ if (stat.size > MAX_BINDING_RECORD_BYTES) {
173
+ bindingError("parse", "local_binding_file_too_large", `binding record exceeds ${MAX_BINDING_RECORD_BYTES} bytes`);
174
+ }
175
+ const parsed = parseRestrictedYaml(readFileSync(file), BINDING_LABEL);
176
+ return asRecord(parsed, file, root, options.schemasDir);
177
+ }
178
+
179
+ function normalizedRecord(
180
+ projectRoot: string,
181
+ projectId: string,
182
+ repositories: Readonly<Record<string, string>>,
183
+ schemasDir?: string,
184
+ ): VnextLocalBindingRecord {
185
+ const normalizedRepositories = Object.fromEntries(Object.entries(repositories)
186
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
187
+ .map(([repositoryId, path]) => {
188
+ if (!isAbsolute(path)) bindingError("path", "repository_binding_not_absolute", `host-local binding for ${repositoryId} must be absolute`);
189
+ return [repositoryId, canonicalHostPath(path)];
190
+ }));
191
+ const value: JsonObject = {
192
+ schema: "kxm.local-repository-bindings.v1",
193
+ projectId,
194
+ projectRoot: canonicalHostPath(projectRoot),
195
+ repositories: normalizedRepositories,
196
+ };
197
+ const issues = new VnextSchemaRegistry(schemasDir).validateLocalBindings(value, BINDING_LABEL);
198
+ if (issues.length > 0) throw new VnextConfigError(issues);
199
+ return value as unknown as VnextLocalBindingRecord;
200
+ }
201
+
202
+ function recordsEqual(left: VnextLocalBindingRecord, right: VnextLocalBindingRecord): boolean {
203
+ if (left.projectId !== right.projectId || !sameHostPath(left.projectRoot, right.projectRoot)) return false;
204
+ const leftEntries = Object.entries(left.repositories);
205
+ const rightEntries = Object.entries(right.repositories);
206
+ return leftEntries.length === rightEntries.length && leftEntries.every(([id, path], index) => {
207
+ const other = rightEntries[index];
208
+ return other?.[0] === id && sameHostPath(path, other[1]);
209
+ });
210
+ }
211
+
212
+ function syncDirectory(path: string): void {
213
+ let descriptor: number | undefined;
214
+ try {
215
+ descriptor = openSync(path, "r");
216
+ fsyncSync(descriptor);
217
+ } catch (error) {
218
+ const code = (error as NodeJS.ErrnoException).code;
219
+ if (process.platform !== "win32" && code !== "EINVAL" && code !== "ENOTSUP" && code !== "EISDIR") throw error;
220
+ } finally {
221
+ if (descriptor !== undefined) closeSync(descriptor);
222
+ }
223
+ }
224
+
225
+ function createDirectoryIfMissing(path: string): void {
226
+ if (existsSync(path)) return;
227
+ try {
228
+ mkdirSync(path, { mode: 0o700 });
229
+ syncDirectory(dirname(path));
230
+ } catch (error) {
231
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
232
+ }
233
+ }
234
+
235
+ function ensureDurableDirectory(path: string, description: string): void {
236
+ const missing: string[] = [];
237
+ let current = resolve(path);
238
+ while (!existsSync(current)) {
239
+ missing.push(current);
240
+ const parent = dirname(current);
241
+ if (parent === current) bindingError("path", "local_binding_directory_invalid", `${description} has no existing filesystem ancestor`);
242
+ current = parent;
243
+ }
244
+ checkedDirectory(current, `${description} ancestor`);
245
+ for (const directory of missing.reverse()) {
246
+ mkdirSync(directory, { mode: 0o700 });
247
+ checkedDirectory(directory, description);
248
+ syncDirectory(dirname(directory));
249
+ }
250
+ }
251
+
252
+ function ensureBindingDirectory(file: string, stateRoot: string): void {
253
+ ensureDurableDirectory(stateRoot, "local state root");
254
+ checkedDirectory(stateRoot, "local state root");
255
+ syncDirectory(stateRoot);
256
+ const projects = join(stateRoot, "projects");
257
+ createDirectoryIfMissing(projects);
258
+ checkedDirectory(projects, "local projects directory");
259
+ const project = dirname(file);
260
+ createDirectoryIfMissing(project);
261
+ checkedDirectory(project, "local project binding directory");
262
+ }
263
+
264
+ function assertNoLinkedDirectoryComponents(path: string, description: string): void {
265
+ const absolute = resolve(path);
266
+ const filesystemRoot = parse(absolute).root;
267
+ let current = filesystemRoot;
268
+ const remainder = relative(filesystemRoot, absolute);
269
+ for (const segment of remainder.split(sep).filter(Boolean)) {
270
+ current = join(current, segment);
271
+ const stat = lstatSync(current);
272
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
273
+ bindingError("path", "project_operation_lock_parent_invalid", `${description} contains a linked or non-directory component`);
274
+ }
275
+ }
276
+ }
277
+
278
+ function projectOperationLockFile(projectRoot: string): string {
279
+ const env = { ...process.env };
280
+ for (const key of Object.keys(env)) {
281
+ if (key.toLocaleUpperCase("en-US").startsWith("GIT_")) delete env[key];
282
+ }
283
+ const result = spawnSync("git", ["-C", projectRoot, "rev-parse", "--absolute-git-dir"], {
284
+ encoding: "utf8",
285
+ env,
286
+ timeout: 5000,
287
+ windowsHide: true,
288
+ });
289
+ if (result.status !== 0 || result.error || !result.stdout.trim()) {
290
+ bindingError("discovery", "project_operation_lock_unavailable", "cannot resolve the authoritative Git metadata path for project locking");
291
+ }
292
+ const gitDirectory = resolve(result.stdout.trim());
293
+ assertNoLinkedDirectoryComponents(gitDirectory, "authoritative Git metadata path");
294
+ const gitStat = lstatSync(gitDirectory);
295
+ if (gitStat.isSymbolicLink() || !gitStat.isDirectory()) {
296
+ bindingError("path", "project_operation_lock_parent_invalid", "authoritative Git metadata must resolve to a regular directory");
297
+ }
298
+ const lockDirectory = join(gitDirectory, "kxm");
299
+ if (existsSync(lockDirectory)) {
300
+ const stat = lstatSync(lockDirectory);
301
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
302
+ bindingError("path", "project_operation_lock_parent_invalid", "Git-local KXM lock directory must be regular, not a link or file");
303
+ }
304
+ } else {
305
+ mkdirSync(lockDirectory, { mode: 0o700 });
306
+ syncDirectory(gitDirectory);
307
+ }
308
+ return join(lockDirectory, "project-operation-lock.sqlite");
309
+ }
310
+
311
+ /** Serialize one project mutation with a process-death-released SQLite write lock. */
312
+ export function withVnextLocalBindingLock<T>(
313
+ projectRoot: string,
314
+ options: VnextLocalBindingStoreOptions,
315
+ callback: (lock: VnextLocalBindingLock) => T,
316
+ ): T {
317
+ const root = resolve(projectRoot);
318
+ const file = projectOperationLockFile(root);
319
+ if (existsSync(file)) {
320
+ const stat = lstatSync(file);
321
+ if (stat.isSymbolicLink() || !stat.isFile()) {
322
+ bindingError("path", "local_binding_lock_invalid", "project-operation lock must be a regular file, not a link or directory");
323
+ }
324
+ }
325
+ const database = new DatabaseSync(file);
326
+ try { chmodSync(file, 0o600); } catch { /* Windows and restrictive filesystems may ignore POSIX modes. */ }
327
+ try {
328
+ try {
329
+ database.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
330
+ } catch (error) {
331
+ if (/busy|locked/i.test(error instanceof Error ? error.message : String(error))) {
332
+ bindingError("semantic", "local_binding_lock_busy", "another project initialization or binding update is active");
333
+ }
334
+ throw error;
335
+ }
336
+ const lock: VnextLocalBindingLock = Object.freeze({ projectRoot: canonicalHostPath(root), file });
337
+ activeLocks.add(lock);
338
+ try {
339
+ return callback(lock);
340
+ } finally {
341
+ activeLocks.delete(lock);
342
+ database.exec("ROLLBACK");
343
+ }
344
+ } finally {
345
+ database.close();
346
+ }
347
+ }
348
+
349
+ export function planVnextLocalBindings(
350
+ projectRoot: string,
351
+ projectId: string,
352
+ repositories: Readonly<Record<string, string>>,
353
+ options: VnextLocalBindingStoreOptions = {},
354
+ ): VnextLocalBindingWriteResult {
355
+ const root = resolve(projectRoot);
356
+ const file = vnextLocalBindingFile(root, options);
357
+ const record = normalizedRecord(root, projectId, repositories, options.schemasDir);
358
+ const existing = readVnextLocalBindings(root, options);
359
+ if (existing?.projectId !== undefined && existing.projectId !== projectId) {
360
+ bindingError("semantic", "local_binding_project_id_mismatch", "binding record belongs to a different project identity");
361
+ }
362
+ return { file, written: !existing || !recordsEqual(existing, record), record };
363
+ }
364
+
365
+ /** Atomically persist validated host paths outside the Git-tracked project. */
366
+ export function writeVnextLocalBindings(
367
+ projectRoot: string,
368
+ projectId: string,
369
+ repositories: Readonly<Record<string, string>>,
370
+ options: VnextLocalBindingStoreOptions = {},
371
+ lock?: VnextLocalBindingLock,
372
+ ): VnextLocalBindingWriteResult {
373
+ const root = resolve(projectRoot);
374
+ if (!lock) {
375
+ return withVnextLocalBindingLock(root, options, (acquired) => writeVnextLocalBindings(root, projectId, repositories, options, acquired));
376
+ }
377
+ if (!activeLocks.has(lock) || !sameHostPath(lock.projectRoot, root)) {
378
+ bindingError("semantic", "local_binding_lock_invalid", "binding update does not hold the active project lock");
379
+ }
380
+ const planned = planVnextLocalBindings(root, projectId, repositories, options);
381
+ const { file, record } = planned;
382
+ if (!planned.written) return planned;
383
+ ensureBindingDirectory(file, vnextUserStateRoot(options));
384
+ const temporary = join(dirname(file), `.repository-bindings-${process.pid}-${randomUUID()}.tmp`);
385
+ let descriptor: number | undefined;
386
+ try {
387
+ descriptor = openSync(temporary, "wx", 0o600);
388
+ writeFileSync(descriptor, `${JSON.stringify(record, null, 2)}\n`, "utf8");
389
+ fsyncSync(descriptor);
390
+ closeSync(descriptor);
391
+ descriptor = undefined;
392
+ renameSync(temporary, file);
393
+ syncDirectory(dirname(file));
394
+ } finally {
395
+ if (descriptor !== undefined) closeSync(descriptor);
396
+ rmSync(temporary, { force: true });
397
+ }
398
+ const verified = readVnextLocalBindings(root, options);
399
+ if (!verified || !recordsEqual(verified, record)) {
400
+ bindingError("semantic", "local_binding_install_verification_failed", "installed binding record does not match the validated input");
401
+ }
402
+ return { file, written: true, record };
403
+ }