hivecode-mcp 0.11.5 → 0.12.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hivecode-mcp",
3
- "version": "0.11.5",
4
- "description": "MCP server that lets any AI coding agent join a Hivecode room \u2014 humans and multiple AI agents edit one live codebase with folder-scoped permissions and approval gates.",
3
+ "version": "0.12.0",
4
+ "description": "MCP server that lets any AI coding agent join a Hivecode room — humans and multiple AI agents edit one live codebase with folder-scoped permissions and approval gates.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/Ganesh-Kute/HiveCode",
@@ -23,11 +23,26 @@
23
23
  "yjs"
24
24
  ],
25
25
  "bin": {
26
- "hivecode-mcp": "hive-mcp.js"
26
+ "hivecode-mcp": "hive-mcp.js",
27
+ "hivecode-hook": "hive-hook-install.js"
27
28
  },
28
29
  "files": [
29
30
  "hive-mcp.js",
31
+ "hive-version.js",
32
+ "hive-pr-staleness.js",
30
33
  "hive-mailbox.js",
34
+ "hive-obligations.js",
35
+ "hive-receipts.js",
36
+ "hive-throttle.js",
37
+ "hive-instructions.js",
38
+ "hive-activity.js",
39
+ "hive-relay-health.js",
40
+ "hive-privacy.js",
41
+ "docs/SELF_HOSTING.md",
42
+ "reconcile.cjs",
43
+ "hive-busy.js",
44
+ "hive-stop-hook.js",
45
+ "hive-hook-install.js",
31
46
  "sync.js",
32
47
  "core.js",
33
48
  "limits.js",
@@ -43,6 +58,7 @@
43
58
  "lang-toml.js",
44
59
  "icr-merge.js",
45
60
  "hive-coord.js",
61
+ "hive-gate.js",
46
62
  "substrate.js",
47
63
  "HIVE_RULES.md",
48
64
  "docs/MCP.md",
@@ -58,14 +74,26 @@
58
74
  "relay": "node server.js",
59
75
  "sync:engine": "node sync-engine.mjs",
60
76
  "check:engine": "node sync-engine.mjs --check",
61
- "pretest": "node scripts/sync-engine.mjs --check && node scripts/check-files.mjs",
77
+ "pretest": "node scripts/sync-engine.mjs --check && node scripts/check-files.mjs && node scripts/check-deploy.mjs",
62
78
  "test": "node tests/test.js",
79
+ "test:features": "node tests/features-test.mjs",
80
+ "test:features:shape": "node tests/features-test.mjs --shape",
81
+ "test:learnings": "node tests/learnings-test.mjs",
82
+ "test:docs": "node tests/readme-promises-test.mjs",
83
+ "test:recreate": "node tests/blind-recreate-test.mjs",
84
+ "test:liveness": "node tests/agent-liveness-test.mjs && node tests/unreachable-recipient-test.mjs",
85
+ "build:ext": "node scripts/build-icr-cjs.cjs && node scripts/build-privacy-cjs.cjs",
86
+ "test:selfhost": "node tests/relay-pin-test.mjs && node tests/self-host-test.mjs && node tests/extension-privacy-parity-test.mjs",
87
+ "test:hardening": "node tests/genesis-claim-test.mjs && node tests/bare-root-write-test.mjs && node tests/version-truth-test.mjs && node tests/room-repo-drift-test.mjs && node tests/first-contact-refusal-test.mjs && node tests/open-file-cap-test.mjs && node tests/learnings-test.mjs && node tests/agent-liveness-test.mjs && node tests/unreachable-recipient-test.mjs && node tests/join-honesty-test.mjs && node tests/relay-diagnosis-test.mjs && node tests/blind-recreate-test.mjs && node tests/genesis-collision-test.mjs && node tests/relay-pin-test.mjs && node tests/self-host-test.mjs && node tests/extension-privacy-parity-test.mjs && node tests/readme-promises-test.mjs",
88
+ "test:extension": "node tests/extension-parity-test.mjs && node tests/extension-privacy-parity-test.mjs",
89
+ "test:ceiling": "node tests/refused-visible-test.mjs && node tests/ceiling-no-data-loss-test.mjs",
63
90
  "test:identity": "node tests/identity-audit-test.mjs",
64
91
  "test:billing": "node tests/billing-test.mjs",
65
92
  "test:license": "node tests/hive-license-test.js",
66
93
  "test:money": "node tests/payment-e2e-test.mjs && node tests/subscribe-test.mjs && node tests/lifecycle-test.mjs && node tests/billing-test.mjs && node tests/hive-license-test.js && node tests/recover-test.mjs",
67
94
  "check:web": "tsc --noEmit",
68
95
  "build:web": "esbuild web/src/main.ts --bundle --minify --format=iife --target=es2019 --outfile=public/app.js",
96
+ "build:crdt": "esbuild web/crdt.js --bundle --minify --format=esm --target=es2019 --outfile=public/hive-crdt.js",
69
97
  "test:substrate": "node substrate-test.js",
70
98
  "test:substrate:live": "node substrate-live-test.js",
71
99
  "test:substrate:enforce": "node substrate-enforce-test.js",
@@ -73,9 +101,10 @@
73
101
  "test:substrate:all": "node substrate-test.js && node substrate-live-test.js && node substrate-enforce-test.js && node substrate-authority-test.js",
74
102
  "test:recover": "node tests/recover-test.mjs",
75
103
  "test:mailbox": "node tests/mailbox-test.mjs && node tests/mcp-wait-test.mjs",
104
+ "test:stophook": "node tests/stop-hook-test.mjs",
76
105
  "test:limits": "node tests/limits-test.mjs",
77
106
  "test:rejoin": "node tests/rejoin-clobber-test.mjs",
78
- "test:prod": "node tests/test.js && node tests/firstrun-test.mjs && node tests/credential-split-test.mjs && node tests/limits-test.mjs && node tests/usage-meter-test.mjs && node tests/rejoin-clobber-test.mjs && node tests/durability-test.mjs && node tests/payment-e2e-test.mjs && node tests/subscribe-test.mjs && node tests/lifecycle-test.mjs && node tests/billing-test.mjs && node tests/hive-license-test.js && node tests/recover-test.mjs && node demo/wrap-test.mjs",
107
+ "test:prod": "node tests/test.js && node tests/firstrun-test.mjs && node tests/credential-split-test.mjs && node tests/limits-test.mjs && node tests/usage-meter-test.mjs && node tests/rejoin-clobber-test.mjs && node tests/stop-hook-test.mjs && node tests/coordination-test.mjs && node tests/control-panel-test.mjs && node tests/hive-map-test.mjs && node tests/hive-gate-test.mjs && node tests/durability-test.mjs && node tests/payment-e2e-test.mjs && node tests/subscribe-test.mjs && node tests/lifecycle-test.mjs && node tests/billing-test.mjs && node tests/hive-license-test.js && node tests/recover-test.mjs && node demo/wrap-test.mjs",
79
108
  "test:meter": "node tests/usage-meter-test.mjs",
80
109
  "test:subscribe": "node tests/subscribe-test.mjs",
81
110
  "test:cred": "node tests/credential-split-test.mjs",
@@ -89,7 +118,13 @@
89
118
  "demo:all": "node demo/record.mjs && node demo/record-panel.mjs",
90
119
  "check:files": "node scripts/check-files.mjs",
91
120
  "prepublishOnly": "node scripts/check-files.mjs",
92
- "demo:cast": "node demo/cast.mjs --dry-run"
121
+ "demo:cast": "node demo/cast.mjs --dry-run",
122
+ "check:deploy": "node scripts/check-deploy.mjs",
123
+ "test:coordination": "node tests/coordination-test.mjs",
124
+ "test:panel": "node tests/control-panel-test.mjs",
125
+ "test:map": "node tests/hive-map-test.mjs",
126
+ "test:gate": "node tests/hive-gate-test.mjs",
127
+ "bench:ab": "node bench/coordination-ab.mjs"
93
128
  },
94
129
  "engines": {
95
130
  "node": ">=18"
@@ -99,6 +134,7 @@
99
134
  "@modelcontextprotocol/sdk": "^1.30.0",
100
135
  "acorn": "^8.17.0",
101
136
  "ignore": "^7.0.5",
137
+ "lib0": "^0.2.117",
102
138
  "ws": "^8.18.0",
103
139
  "y-websocket": "^2.0.4",
104
140
  "yjs": "^13.6.18",
package/reconcile.cjs ADDED
@@ -0,0 +1,477 @@
1
+ // reconcile.cjs — Single source of truth for the 3-way reconcile & merge engine.
2
+ //
3
+ // CommonJS on purpose: sync.js is ESM and the VS Code extension is CJS.
4
+ // A .cjs file is the one shape both can load synchronously without duplication.
5
+ //
6
+ // Acceptance criteria (HIV-8), re-checked 21 Sep 2026 -- FOUR OF FIVE MET:
7
+ // 1. MET one module exports reconcile() and its dependencies (11 exports).
8
+ // 2. MET core.js re-exports from here and sync.js imports core.js. One copy.
9
+ // 3. NOT MET extension/extension.js does NOT require this file. It requires
10
+ // ./icr.cjs (the ICR bundle) and keeps its OWN reconcile() at
11
+ // extension/extension.js:1166. The second copy is still there.
12
+ // 4. MET node tests/test.js -- 95 passed, 0 failed.
13
+ // 5. MET node tests/conflict-test.mjs -- ALL 4 PASS.
14
+ //
15
+ // Criterion 3 read "No second copy anywhere" while a second copy existed, in a file
16
+ // that ships in package.json files[]. A comment asserting an invariant it does not
17
+ // hold is worse than no comment: the next reader trusts it and stops looking. Corrected
18
+ // to what is true rather than deleted, because the gap is the useful part.
19
+ //
20
+ // WHAT ACTUALLY GUARDS THE DRIFT, since it is not this file:
21
+ // tests/extension-parity-test.mjs the ICR bundle vs the engine, 24/24
22
+ // tests/extension-reconcile-characterisation-test.mjs pins the extension's own copy
23
+ // Those two measure that the copies AGREE. They do not make it one copy, and closing
24
+ // criterion 3 for real means the extension loading this module.
25
+
26
+ const fs = require('fs')
27
+ const path = require('path')
28
+
29
+ // Minimal text diff: replace only the changed middle, preserving the common
30
+ // prefix/suffix. Applied to a Y.Text so edits stay small (good for CRDT churn).
31
+ function applyDiff(ytext, next) {
32
+ const cur = ytext.toString()
33
+ if (cur === next) return false
34
+ let p = 0
35
+ while (p < cur.length && p < next.length && cur[p] === next[p]) p++
36
+ let s = 0
37
+ while (s < cur.length - p && s < next.length - p && cur[cur.length - 1 - s] === next[next.length - 1 - s]) s++
38
+ if (cur.length - p - s > 0) ytext.delete(p, cur.length - p - s)
39
+ const ins = next.slice(p, next.length - s)
40
+ if (ins) ytext.insert(p, ins)
41
+ return true
42
+ }
43
+
44
+ // Detect REAL git-style conflict markers in a file. Line-anchored on purpose.
45
+ function hasConflictMarkers(text) {
46
+ return /^<<<<<<< /m.test(text || '') && /^>>>>>>> /m.test(text || '')
47
+ }
48
+
49
+ // Strip and de-nest conflict markers to prevent exponential 2^N code ballooning
50
+ function stripConflictMarkers(text, prefer = 'local') {
51
+ if (!text || !hasConflictMarkers(text)) return text || ''
52
+ const lines = text.split('\n')
53
+ const out = []
54
+ let inConflict = false
55
+ let inTheirs = false
56
+ const localBlock = []
57
+ const theirsBlock = []
58
+
59
+ for (let i = 0; i < lines.length; i++) {
60
+ const line = lines[i]
61
+ if (/^<<<<<<< /.test(line)) {
62
+ inConflict = true
63
+ inTheirs = false
64
+ localBlock.length = 0
65
+ theirsBlock.length = 0
66
+ continue
67
+ }
68
+ if (/^=======$/.test(line) && inConflict) {
69
+ inTheirs = true
70
+ continue
71
+ }
72
+ if (/^>>>>>>> /.test(line) && inConflict) {
73
+ inConflict = false
74
+ out.push(...(prefer === 'theirs' ? theirsBlock : localBlock))
75
+ continue
76
+ }
77
+ if (inConflict) {
78
+ if (inTheirs) theirsBlock.push(line)
79
+ else localBlock.push(line)
80
+ } else {
81
+ out.push(line)
82
+ }
83
+ }
84
+ return out.join('\n')
85
+ }
86
+
87
+ // Which base line-range an edit replaced, and the replacement lines.
88
+ function changedRange(base, other) {
89
+ let p = 0
90
+ while (p < base.length && p < other.length && base[p] === other[p]) p++
91
+ let s = 0
92
+ while (s < base.length - p && s < other.length - p && base[base.length - 1 - s] === other[other.length - 1 - s]) s++
93
+ return { startBase: p, endBase: base.length - s, newLines: other.slice(p, other.length - s) }
94
+ }
95
+
96
+ // Best-effort "what was defined here" across common languages — for the board.
97
+ function symbolsIn(text) {
98
+ const names = new Set()
99
+ const add = (re) => { let m; while ((m = re.exec(text)) && names.size < 8) names.add(m[1]) }
100
+ add(/\bfunction\s+(\w+)/g)
101
+ add(/\bclass\s+(\w+)/g)
102
+ add(/\bdef\s+(\w+)/g)
103
+ add(/\b(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=/g)
104
+ return [...names].slice(0, 8)
105
+ }
106
+
107
+ // Classify a change as a small PATCH vs wholesale REWRITE.
108
+ function summarizeChange(base, next) {
109
+ const b = (base || '').split('\n')
110
+ const n = (next || '').split('\n')
111
+ if (!base) return { isRewrite: false, changedLines: n.length, totalLines: n.length, symbols: symbolsIn(next || '') }
112
+ const r = changedRange(b, n)
113
+ const churn = Math.max(r.endBase - r.startBase, r.newLines.length)
114
+ const total = Math.max(b.length, n.length)
115
+ const isRewrite = total > 0 && churn >= 4 && churn / total >= 0.5
116
+ return { isRewrite, changedLines: churn, totalLines: total, symbols: symbolsIn(r.newLines.join('\n')) }
117
+ }
118
+
119
+ // 3-way merge for the sync layer (disk <-> shared doc)
120
+ function merge3(base, mine, theirs) {
121
+ if (mine === theirs) return { text: mine, conflict: false }
122
+ if (base === theirs) return { text: mine, conflict: false }
123
+ if (base === mine) return { text: theirs, conflict: false }
124
+
125
+ const cleanBase = hasConflictMarkers(base) ? stripConflictMarkers(base, 'local') : (base || '')
126
+ const cleanMine = hasConflictMarkers(mine) ? stripConflictMarkers(mine, 'local') : (mine || '')
127
+ const cleanTheirs = hasConflictMarkers(theirs) ? stripConflictMarkers(theirs, 'theirs') : (theirs || '')
128
+
129
+ if (cleanMine === cleanTheirs) return { text: cleanMine, conflict: false }
130
+ if (cleanBase === cleanTheirs) return { text: cleanMine, conflict: false }
131
+ if (cleanBase === cleanMine) return { text: cleanTheirs, conflict: false }
132
+
133
+ const b = cleanBase.split('\n')
134
+ const mr = changedRange(b, cleanMine.split('\n'))
135
+ const tr = changedRange(b, cleanTheirs.split('\n'))
136
+ if (mr.endBase <= tr.startBase || tr.endBase <= mr.startBase) {
137
+ const out = b.slice()
138
+ for (const e of [mr, tr].sort((x, y) => y.startBase - x.startBase)) {
139
+ out.splice(e.startBase, e.endBase - e.startBase, ...e.newLines)
140
+ }
141
+ return { text: out.join('\n'), conflict: false }
142
+ }
143
+ const start = Math.min(mr.startBase, tr.startBase)
144
+ const end = Math.max(mr.endBase, tr.endBase)
145
+ const mineBlock = [...b.slice(start, mr.startBase), ...mr.newLines, ...b.slice(mr.endBase, end)]
146
+ const theirsBlock = [...b.slice(start, tr.startBase), ...tr.newLines, ...b.slice(tr.endBase, end)]
147
+ const out = [
148
+ ...b.slice(0, start),
149
+ '<<<<<<< local (yours)',
150
+ ...mineBlock,
151
+ '=======',
152
+ ...theirsBlock,
153
+ '>>>>>>> incoming (theirs)',
154
+ ...b.slice(end),
155
+ ]
156
+ return { text: out.join('\n'), conflict: true }
157
+ }
158
+
159
+ function describeConflicts(cs) {
160
+ return (cs || []).map((c) =>
161
+ c.startsWith('ref:') ? `'${c.slice(4)}' was removed or renamed but is still used`
162
+ : c.startsWith('fn:') ? `both sides changed function ${c.slice(3)}`
163
+ : c.startsWith('class:') ? `both sides changed class ${c.slice(6)}`
164
+ : `both sides changed ${c.replace(/^\w+:/, '')}`).join('; ')
165
+ }
166
+
167
+ function icrMerge3(base, mine, theirs, relPath, icrlib) {
168
+ const lm = merge3(base, mine, theirs)
169
+ if (!icrlib || !icrlib.supports || !icrlib.supports(relPath)) return lm
170
+ let r
171
+ try { r = icrlib.structuralMerge(base, mine, theirs, { filename: relPath }) } catch (e) { return lm }
172
+ if (r.status === 'auto') {
173
+ const renamed = r.renames && r.renames.length
174
+ return { text: r.text, conflict: false, icr: renamed ? 'rename' : 'structural', icrWarning: renamed ? `auto-applied rename ${r.renames.join(', ')}` : undefined }
175
+ }
176
+ if (r.status === 'semantic-conflict') return Object.assign({}, lm, { icrWarning: describeConflicts(r.conflicts) })
177
+ return lm
178
+ }
179
+
180
+ // createReconciler: binds to a session's maps, state, and callbacks, returning
181
+ // a reconcile(relPath, origin = 'local') function.
182
+ function createReconciler(ctx) {
183
+ const {
184
+ root,
185
+ name = 'anon',
186
+ SKIP = new Set(['HIVE_BOARD.md', 'HIVE_CHAT.md', 'HIVE_TASKS.md', 'HIVE_RULES.md', 'HIVE_MEMBERS.md', 'HIVE_FOR_AGENTS.md', '.hive.json']),
187
+ isIgnored = () => false,
188
+ canOpen = () => true,
189
+ canWrite = () => true,
190
+ readText = (full) => {
191
+ try {
192
+ const buf = fs.readFileSync(full)
193
+ if (buf.length > 1_000_000 || buf.includes(0)) return null
194
+ return buf.toString('utf8')
195
+ } catch { return null }
196
+ },
197
+ writeToDisk,
198
+ openFile,
199
+ touchFile,
200
+ fileDocs,
201
+ manifest,
202
+ bases,
203
+ forkBases,
204
+ known,
205
+ mtimes,
206
+ conflicted = new Set(),
207
+ icrWarned = new Map(),
208
+ snappedBase = new Set(),
209
+ preexisting = new Set(),
210
+ priorShadow,
211
+ shadow,
212
+ say = () => {},
213
+ log = () => {},
214
+ markEditing = () => {},
215
+ noteCoEditing = () => {},
216
+ noteIfRewrite = () => {},
217
+ captureSnapshot = () => {},
218
+ lastLocalEdit,
219
+ // Fork-gate & provenance
220
+ FORK_GATE = false,
221
+ captureLocal,
222
+ forkRecord,
223
+ forkBlock,
224
+ detectFork,
225
+ renderConflict,
226
+ adoptResolved,
227
+ stampProvenance,
228
+ attestHead,
229
+ fusionReason,
230
+ supports,
231
+ languageFor,
232
+ icrlib
233
+ } = ctx
234
+
235
+ return function reconcile(relPath, origin = 'local') {
236
+ if (SKIP.has(relPath) || isIgnored(relPath)) return
237
+ if (!canOpen(relPath)) return
238
+ const full = path.join(root, relPath)
239
+ const exists = fs.existsSync(full)
240
+ const disk = exists ? readText(full) : null
241
+ if (exists && disk === null) return
242
+
243
+ if (origin === 'local' && !canWrite(relPath)) {
244
+ const fe2 = fileDocs.get(relPath)
245
+ if (fe2 && fe2.synced) {
246
+ const dt = fe2.text.toString()
247
+ if (dt && dt !== disk) writeToDisk(relPath, dt)
248
+ }
249
+ return
250
+ }
251
+
252
+ let fe = fileDocs.get(relPath)
253
+ if (!fe) { if (disk === null) return; openFile(relPath); return }
254
+ if (!fe.synced) return
255
+ if (touchFile) touchFile(relPath)
256
+
257
+ if (FORK_GATE && captureLocal) captureLocal(relPath)
258
+
259
+ if (FORK_GATE && fe.conflict && forkRecord) {
260
+ const rec = forkRecord(fe)
261
+ if (rec) {
262
+ const block = forkBlock(fe, rec)
263
+ if (origin === 'local') {
264
+ if (disk === block) return
265
+ const fork0 = forkBases.get(relPath)
266
+ const sawMarkers = fork0 === block || hasConflictMarkers(fork0 || '')
267
+ if (sawMarkers && disk !== null && !hasConflictMarkers(disk)) {
268
+ fe.doc.transact(() => {
269
+ applyDiff(fe.text, disk)
270
+ for (const v of rec.versions) fe.forkmark.set(v.hash, 1)
271
+ for (const key of [...fe.conflict.keys()]) fe.conflict.delete(key)
272
+ })
273
+ if (stampProvenance) stampProvenance(relPath, block || '', disk)
274
+ conflicted.delete(relPath)
275
+ known.add(relPath); bases.set(relPath, disk); forkBases.set(relPath, disk)
276
+ try { say(`✓ conflict in ${relPath} resolved by ${name}.`) } catch { }
277
+ return
278
+ }
279
+ if (!sawMarkers && disk !== null && disk !== fork0) {
280
+ if (detectFork) detectFork(relPath)
281
+ return
282
+ }
283
+ if (renderConflict) renderConflict(relPath)
284
+ return
285
+ }
286
+ if (renderConflict) renderConflict(relPath)
287
+ return
288
+ }
289
+ if (disk !== null && hasConflictMarkers(disk) && !hasConflictMarkers(fe.text.toString())) {
290
+ if (adoptResolved) { adoptResolved(relPath); return }
291
+ }
292
+ }
293
+
294
+ const docText = fe.text.toString()
295
+ const noHistory = !bases.has(relPath) && !(priorShadow && priorShadow.has(relPath))
296
+ if (docText === '' && (!manifest.has(relPath) || (disk && noHistory))) {
297
+ if (disk === null) return
298
+ fe.doc.transact(() => applyDiff(fe.text, disk))
299
+ if (stampProvenance) stampProvenance(relPath, '', disk)
300
+ manifest.set(relPath, 1)
301
+ known.add(relPath); bases.set(relPath, disk); forkBases.set(relPath, disk)
302
+ if (shadow) shadow.set(relPath, disk)
303
+ try { mtimes.set(relPath, fs.statSync(full).mtimeMs) } catch { }
304
+ captureSnapshot(relPath, disk, name, { force: true, kind: 'base', label: 'created' })
305
+ snappedBase.add(relPath)
306
+ return
307
+ }
308
+
309
+ const yt = fe.text
310
+ if (disk === null) {
311
+ writeToDisk(relPath, docText)
312
+ bases.set(relPath, docText); forkBases.set(relPath, docText)
313
+ return
314
+ }
315
+
316
+ if (disk === docText) {
317
+ if (origin === 'local' && !known.has(relPath) && disk) markEditing(relPath)
318
+ known.add(relPath); bases.set(relPath, disk)
319
+ if (shadow) shadow.set(relPath, disk)
320
+ if (!forkBases.has(relPath) || origin === 'local') forkBases.set(relPath, disk)
321
+ if (!snappedBase.has(relPath) && disk) {
322
+ captureSnapshot(relPath, disk, name, { force: true, kind: 'base', label: 'baseline' })
323
+ snappedBase.add(relPath)
324
+ }
325
+ return
326
+ }
327
+
328
+ // FIRST-CONTACT ADOPT
329
+ if (!bases.has(relPath) && docText !== '' && disk !== docText) {
330
+ if (!preexisting.has(relPath)) {
331
+ const nl = (s) => (s === '' || s.endsWith('\n') ? s : s + '\n')
332
+ const r0 = icrMerge3('', nl(disk), nl(docText), relPath, icrlib)
333
+ if (r0.text !== docText) fe.doc.transact(() => applyDiff(yt, r0.text))
334
+ if (r0.text !== disk) writeToDisk(relPath, r0.text)
335
+ if (r0.conflict) conflicted.add(relPath)
336
+ bases.set(relPath, r0.text); forkBases.set(relPath, r0.text)
337
+ if (shadow) shadow.set(relPath, r0.text)
338
+ if (!manifest.has(relPath)) manifest.set(relPath, 1)
339
+ try { say(`⚠ ${relPath} was created by two participants at once — both versions were merged${r0.conflict ? ', and CONFLICT markers need resolving' : ''}.`) } catch { }
340
+ return
341
+ }
342
+ const anc = priorShadow ? priorShadow.get(relPath) : undefined
343
+ if (anc !== undefined && disk !== null) {
344
+ if (disk === anc) {
345
+ writeToDisk(relPath, docText)
346
+ bases.set(relPath, docText); forkBases.set(relPath, docText)
347
+ if (!snappedBase.has(relPath)) {
348
+ captureSnapshot(relPath, docText, name, { force: true, kind: 'base', label: 'room copy adopted on rejoin (local was unchanged)' })
349
+ snappedBase.add(relPath)
350
+ }
351
+ return
352
+ }
353
+ captureSnapshot(relPath, disk, name, { force: true, kind: 'manual', label: 'local copy before rejoin merge' })
354
+ const rj = icrMerge3(anc, disk, docText, relPath, icrlib)
355
+ if (rj.text !== docText) fe.doc.transact(() => applyDiff(yt, rj.text))
356
+ if (rj.text !== disk) writeToDisk(relPath, rj.text)
357
+ if (rj.conflict) conflicted.add(relPath)
358
+ bases.set(relPath, rj.text); forkBases.set(relPath, rj.text)
359
+ if (shadow) shadow.set(relPath, rj.text)
360
+ if (!manifest.has(relPath)) manifest.set(relPath, 1)
361
+ try { say(`⚠ ${relPath} changed locally while ${name} was disconnected — the local edit and the room copy were merged${rj.conflict ? ', and CONFLICT markers need resolving' : ''}. Pre-merge local copy is in the file's history.`) } catch { }
362
+ return
363
+ }
364
+ captureSnapshot(relPath, disk, name, { force: true, kind: 'manual', label: 'local copy at join (room copy adopted)' })
365
+ writeToDisk(relPath, docText)
366
+ bases.set(relPath, docText); forkBases.set(relPath, docText)
367
+ if (!snappedBase.has(relPath)) {
368
+ captureSnapshot(relPath, docText, name, { force: true, kind: 'base', label: 'adopted from room at join' })
369
+ snappedBase.add(relPath)
370
+ }
371
+ try { say(`↓ ${relPath} already existed in this room — ${name} adopted the room's copy. The local version before joining is saved in the file's history.`) } catch { }
372
+ return
373
+ }
374
+
375
+ const base = bases.has(relPath) ? bases.get(relPath) : disk
376
+ const fork = forkBases.has(relPath) ? forkBases.get(relPath) : base
377
+ if (origin === 'local') { noteCoEditing(relPath); markEditing(relPath) }
378
+
379
+ let res, reAdded = false
380
+ if (origin === 'local' && hasConflictMarkers(docText) && !hasConflictMarkers(disk)) {
381
+ res = { text: disk, conflict: false }
382
+ } else if (origin === 'local') {
383
+ noteIfRewrite(relPath, fork, disk)
384
+ const sc = summarizeChange(fork, disk)
385
+ const isCleanRewrite = sc.isRewrite && supports && supports(relPath) && languageFor && languageFor(relPath).parses(disk)
386
+ const theirsNew = changedRange(fork.split('\n'), docText.split('\n')).newLines.filter((l) => l.length)
387
+ const mineLines = new Set(disk.split('\n'))
388
+ const integrated = theirsNew.every((l) => mineLines.has(l))
389
+ if (integrated || (isCleanRewrite && !hasConflictMarkers(docText))) {
390
+ res = { text: disk, conflict: false }
391
+ } else {
392
+ res = icrMerge3(fork, disk, docText, relPath, icrlib)
393
+ reAdded = !res.conflict && res.text !== disk
394
+ }
395
+ } else {
396
+ res = icrMerge3(base, disk, docText, relPath, icrlib)
397
+ }
398
+
399
+ if (res && !res.conflict && fusionReason) {
400
+ const why = fusionReason(res.text, disk, docText, (supports && supports(relPath)) ? languageFor(relPath) : null)
401
+ if (why) {
402
+ conflicted.add(relPath)
403
+ if (icrWarned.get(relPath) !== why) {
404
+ icrWarned.set(relPath, why)
405
+ log(`[${name}] !! REFUSED a fused merge of ${relPath}: ${why}`)
406
+ try {
407
+ say(`[!] @all ${relPath}: a merge was REFUSED because it fused rather than merged — ${why}. Nothing was written: the room's copy and ${name}'s local copy are both intact and unchanged. Someone must reconcile them deliberately; a plain file write will merge against the other version again, hive_edit replaces it outright.`)
408
+ } catch { }
409
+ }
410
+ return
411
+ }
412
+ }
413
+
414
+ if (origin === 'local' && res.text !== docText) {
415
+ const sc = summarizeChange(docText, res.text)
416
+ captureSnapshot(relPath, docText, name, { force: sc.isRewrite, churn: `${sc.changedLines}/${sc.totalLines} lines` })
417
+ if (typeof lastLocalEdit === 'function') lastLocalEdit(relPath)
418
+ }
419
+ fe.doc.transact(() => applyDiff(yt, res.text))
420
+ if (origin === 'local') {
421
+ if (FORK_GATE) { if (res.text !== docText && attestHead) attestHead(relPath, res.text, docText) }
422
+ else if (res.text !== docText && stampProvenance) stampProvenance(relPath, docText, res.text)
423
+ }
424
+ if (res.text !== disk) writeToDisk(relPath, res.text)
425
+ known.add(relPath); bases.set(relPath, res.text)
426
+ if (origin === 'local') forkBases.set(relPath, res.text)
427
+ else if (!forkBases.get(relPath)) forkBases.set(relPath, res.text)
428
+
429
+ const hasMarkers = res.conflict || hasConflictMarkers(res.text)
430
+ if (hasMarkers && !conflicted.has(relPath)) {
431
+ conflicted.add(relPath)
432
+ log(`[${name}] ⚠ merge conflict in ${relPath} — kept BOTH versions with <<<<<<< markers`)
433
+ try { say(`⚠ MERGE CONFLICT in ${relPath} — it has <<<<<<< markers. Whoever owns it: resolve before continuing.`) } catch { }
434
+ } else if (!hasMarkers && conflicted.has(relPath)) {
435
+ conflicted.delete(relPath)
436
+ try { say(`✓ conflict in ${relPath} resolved.`) } catch { }
437
+ } else if (reAdded) {
438
+ log(`[${name}] protected ${relPath}: your edit was based on an older version — re-added changes that arrived since (nobody's work lost)`)
439
+ try { say(`↺ ${relPath}: ${name}'s edit was based on an older copy — kept the changes that landed in between (nothing lost).`) } catch { }
440
+ } else if (!hasMarkers && res.icr === 'rename') {
441
+ log(`[${name}] ICR merged ${relPath} (detected a rename and updated the call sites)`)
442
+ } else if (!hasMarkers && res.icr === 'structural') {
443
+ log(`[${name}] ICR merged ${relPath} structurally (both edits kept, syntax guaranteed)`)
444
+ } else if (!hasMarkers) {
445
+ log(`[${name}] merged ${relPath} (both edits kept)`)
446
+ }
447
+
448
+ if (res.icrWarning && icrWarned.get(relPath) !== res.icrWarning) {
449
+ icrWarned.set(relPath, res.icrWarning)
450
+ log(`[${name}] ⚠ ICR flag in ${relPath} — ${res.icrWarning}`)
451
+ try { say(`⚠ ICR flag in ${relPath}: ${res.icrWarning}. It merged, but review — this is a break a plain merge can't see.`) } catch { }
452
+ } else if (!res.icrWarning && icrWarned.has(relPath)) {
453
+ icrWarned.delete(relPath)
454
+ }
455
+
456
+ if (detectFork) detectFork(relPath)
457
+ }
458
+ }
459
+
460
+ // reconcile direct call
461
+ function reconcile(ctx, relPath, origin = 'local') {
462
+ return createReconciler(ctx)(relPath, origin)
463
+ }
464
+
465
+ module.exports = {
466
+ reconcile,
467
+ createReconciler,
468
+ applyDiff,
469
+ hasConflictMarkers,
470
+ stripConflictMarkers,
471
+ changedRange,
472
+ symbolsIn,
473
+ summarizeChange,
474
+ merge3,
475
+ describeConflicts,
476
+ icrMerge3
477
+ }