hivecode-mcp 0.14.3 → 0.14.5
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/HIVE_RULES.md +41 -98
- package/core.js +2 -0
- package/docs/TROUBLESHOOTING.md +3 -0
- package/hive-await.js +57 -21
- package/hive-gate.js +116 -2
- package/hive-instructions.js +110 -61
- package/hive-mailbox.js +74 -7
- package/hive-mcp.js +200 -42
- package/hive-notify.js +21 -5
- package/hive-receipts.js +56 -5
- package/hive-room-mode.js +13 -0
- package/hive-room-watcher.js +30 -12
- package/hive-seen.js +135 -0
- package/hive-slack.js +27 -2
- package/hive-stop-hook.js +33 -18
- package/hive-transport.js +39 -4
- package/icr-merge.js +8 -1
- package/icr.js +3 -1
- package/package.json +3 -2
- package/reconcile.cjs +123 -21
- package/sync.js +684 -203
- package/docs/HIVE_RULES.md +0 -41
package/icr.js
CHANGED
|
@@ -644,7 +644,9 @@ function spliceUnits(baseText, baseUnits, order, textByKey, opts = {}) {
|
|
|
644
644
|
// New unit: insert it exactly where the merge order dictated
|
|
645
645
|
const val = textByKey.get(k)
|
|
646
646
|
if (val != null) {
|
|
647
|
-
|
|
647
|
+
// A unit that already ends with its newline (Python, Ruby, TOML, YAML) gets no second one: that
|
|
648
|
+
// put a blank line no author wrote after every edited top-level line (#19).
|
|
649
|
+
out += (out.endsWith('\n') ? '' : '\n') + val + (val.endsWith('\n') ? '' : '\n')
|
|
648
650
|
}
|
|
649
651
|
}
|
|
650
652
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hivecode-mcp",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.5",
|
|
4
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",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"hive-room-mode.js",
|
|
43
43
|
"hive-room-watcher.js",
|
|
44
44
|
"hive-mailbox.js",
|
|
45
|
+
"hive-seen.js",
|
|
45
46
|
"hive-obligations.js",
|
|
46
47
|
"hive-receipts.js",
|
|
47
48
|
"hive-throttle.js",
|
|
@@ -138,7 +139,7 @@
|
|
|
138
139
|
"test:substrate:authority": "node substrate-authority-test.js",
|
|
139
140
|
"test:substrate:all": "node substrate-test.js && node substrate-live-test.js && node substrate-enforce-test.js && node substrate-authority-test.js",
|
|
140
141
|
"test:recover": "node tests/recover-test.mjs",
|
|
141
|
-
"test:mailbox": "node tests/mailbox-test.mjs && node tests/mcp-wait-test.mjs && node tests/mailbox-noise-test.mjs",
|
|
142
|
+
"test:mailbox": "node tests/mailbox-test.mjs && node tests/mcp-wait-test.mjs && node tests/mailbox-noise-test.mjs && node tests/mail-identity-test.mjs",
|
|
142
143
|
"test:stophook": "node tests/stop-hook-test.mjs",
|
|
143
144
|
"test:limits": "node tests/limits-test.mjs && node tests/rate-hold-test.mjs && node tests/persist-batch-test.mjs",
|
|
144
145
|
"test:merge": "node tests/diff3-test.mjs && node tests/merge-balloon-test.mjs && node tests/icr-merge-test.js && node tests/icr-gap-comments-test.mjs && node tests/icr-comment-fuzz-test.mjs",
|
package/reconcile.cjs
CHANGED
|
@@ -47,39 +47,38 @@ function hasConflictMarkers(text) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// Strip and de-nest conflict markers to prevent exponential 2^N code ballooning
|
|
50
|
+
// Strip conflict markers, keeping the 'local' side (before the first =======), the 'theirs' side (after
|
|
51
|
+
// it), or 'all': every version in the block, in order, each distinct one once. A separator may carry a
|
|
52
|
+
// label ("======= Bob"): the 3-version blocks of a same-line race do.
|
|
50
53
|
function stripConflictMarkers(text, prefer = 'local') {
|
|
51
54
|
if (!text || !hasConflictMarkers(text)) return text || ''
|
|
52
55
|
const lines = text.split('\n')
|
|
53
56
|
const out = []
|
|
54
57
|
let inConflict = false
|
|
55
|
-
let
|
|
56
|
-
const localBlock = []
|
|
57
|
-
const theirsBlock = []
|
|
58
|
+
let sections = []
|
|
58
59
|
|
|
59
60
|
for (let i = 0; i < lines.length; i++) {
|
|
60
61
|
const line = lines[i]
|
|
61
62
|
if (/^<<<<<<< /.test(line)) {
|
|
62
63
|
inConflict = true
|
|
63
|
-
|
|
64
|
-
localBlock.length = 0
|
|
65
|
-
theirsBlock.length = 0
|
|
64
|
+
sections = [[]]
|
|
66
65
|
continue
|
|
67
66
|
}
|
|
68
|
-
if (
|
|
69
|
-
|
|
67
|
+
if (/^=======( |$)/.test(line) && inConflict) {
|
|
68
|
+
sections.push([])
|
|
70
69
|
continue
|
|
71
70
|
}
|
|
72
71
|
if (/^>>>>>>> /.test(line) && inConflict) {
|
|
73
72
|
inConflict = false
|
|
74
|
-
|
|
73
|
+
if (prefer === 'all') {
|
|
74
|
+
const seen = new Set()
|
|
75
|
+
for (const sec of sections) { const k = sec.join('\n'); if (seen.has(k)) continue; seen.add(k); out.push(...sec) }
|
|
76
|
+
} else if (prefer === 'theirs') out.push(...sections.slice(1).flat())
|
|
77
|
+
else out.push(...sections[0])
|
|
75
78
|
continue
|
|
76
79
|
}
|
|
77
|
-
if (inConflict)
|
|
78
|
-
|
|
79
|
-
else localBlock.push(line)
|
|
80
|
-
} else {
|
|
81
|
-
out.push(line)
|
|
82
|
-
}
|
|
80
|
+
if (inConflict) sections[sections.length - 1].push(line)
|
|
81
|
+
else out.push(line)
|
|
83
82
|
}
|
|
84
83
|
return out.join('\n')
|
|
85
84
|
}
|
|
@@ -193,22 +192,38 @@ function slideSpan(b, [bs, be, os, oe], src) {
|
|
|
193
192
|
// changes conflict, as in git: two edits with no unchanged line between them are one decision). A
|
|
194
193
|
// region only one side changed takes that side; both sides making the same change takes it once;
|
|
195
194
|
// anything else is a conflict of THAT region only. Null when either diff is past the work bound.
|
|
196
|
-
|
|
195
|
+
//
|
|
196
|
+
// SIDE BY SIDE (sideBySide, the sync merge only; #17, 10 Nov 2026). Two edits that only TOUCH
|
|
197
|
+
// are applied side by side when both replace lines one for one (the same number of lines out as
|
|
198
|
+
// in). It is P0-F's footprint rule, and what the character CRDT does with the same two edits made at
|
|
199
|
+
// the same moment, so a save that lands a moment late no longer gets markers the on-time one did not
|
|
200
|
+
// (bigproject W1: README.md, five agents on adjacent lines). Overlap, an insertion or deletion at the
|
|
201
|
+
// edge, and a change of line count still conflict. git-clash and plain merge3 keep git's rule.
|
|
202
|
+
function diff3Lines(b, mine, theirs, sideBySide = false) {
|
|
197
203
|
const hm = lineHunks(b, mine), ht = lineHunks(b, theirs)
|
|
198
204
|
if (!hm || !ht) return null
|
|
199
205
|
const sm = hm.map((h) => slideSpan(b, h, mine)), st = ht.map((h) => slideSpan(b, h, theirs))
|
|
206
|
+
const oneForOne = (h) => h[1] - h[0] === h[3] - h[2] // lineHunks has no empty hunk, so this is a real replacement
|
|
207
|
+
// Does a hunk starting at `s` join a region reaching `reach`? Overlap always; touching, unless both
|
|
208
|
+
// the hunk and every hunk ending at `reach` are one-for-one replacements (side by side).
|
|
209
|
+
const joins = (s, h, reach, edgeRep) => s < reach || (s === reach && !(sideBySide && edgeRep && oneForOne(h)))
|
|
200
210
|
const out = []
|
|
201
211
|
let conflict = false, pos = 0, i = 0, j = 0
|
|
202
212
|
while (i < hm.length || j < ht.length) {
|
|
203
213
|
const fromMine = j >= ht.length || (i < hm.length && hm[i][0] <= ht[j][0])
|
|
204
214
|
const first = fromMine ? hm[i] : ht[j]
|
|
205
215
|
let reach = fromMine ? sm[i++][1] : st[j++][1] // how far this region's edits could extend
|
|
216
|
+
let edgeRep = oneForOne(first) // every hunk ending at `reach` is one-for-one
|
|
206
217
|
const rm = fromMine ? [first] : [], rt = fromMine ? [] : [first]
|
|
207
218
|
const rs = first[0]
|
|
208
219
|
let re = first[1]
|
|
220
|
+
const take = (h, span) => {
|
|
221
|
+
re = Math.max(re, h[1])
|
|
222
|
+
if (span[1] > reach) { reach = span[1]; edgeRep = oneForOne(h) } else if (span[1] === reach) edgeRep = edgeRep && oneForOne(h)
|
|
223
|
+
}
|
|
209
224
|
for (;;) {
|
|
210
|
-
if (i < hm.length && sm[i][0]
|
|
211
|
-
if (j < ht.length && st[j][0]
|
|
225
|
+
if (i < hm.length && joins(sm[i][0], hm[i], reach, edgeRep)) { take(hm[i], sm[i]); rm.push(hm[i++]); continue }
|
|
226
|
+
if (j < ht.length && joins(st[j][0], ht[j], reach, edgeRep)) { take(ht[j], st[j]); rt.push(ht[j++]); continue }
|
|
212
227
|
break
|
|
213
228
|
}
|
|
214
229
|
for (let x = pos; x < rs; x++) out.push(b[x])
|
|
@@ -248,8 +263,84 @@ function summarizeChange(base, next) {
|
|
|
248
263
|
return { isRewrite, changedLines: churn, totalLines: total, symbols: symbolsIn(r.newLines.join('\n')) }
|
|
249
264
|
}
|
|
250
265
|
|
|
266
|
+
// A LATE SAVE AGAINST A CONFLICT BLOCK (P0-F-9, 10 Nov 2026). The author edited a clean copy; the
|
|
267
|
+
// room's copy now holds one or more blocks. Every version the room holds is a whole file: block j
|
|
268
|
+
// read as section j (a block with fewer sections repeats its last). The late save is merged into
|
|
269
|
+
// EACH of them, a merge that conflicts giving both its sides, and the distinct results are written
|
|
270
|
+
// as ONE block per region where they differ. A block is never flattened into its sections side by
|
|
271
|
+
// side: that came out clean with lines no author wrote (`a = 1 / b = 1 / a = 2 / b = 2`, P0-F p0f),
|
|
272
|
+
// and 0.14.4's one-side strip silently dropped every other version. Null: the caller's old path
|
|
273
|
+
// (a section holding markers of its own, or a diff past its work bound).
|
|
274
|
+
function blockVersions(text) {
|
|
275
|
+
const lines = text.split('\n'), segs = [] // a segment: a plain line (string) or a block {head, foot, secs}
|
|
276
|
+
let blk = null
|
|
277
|
+
for (const line of lines) {
|
|
278
|
+
if (/^<<<<<<< /.test(line)) { if (blk) return null; blk = { head: line, secs: [[]] }; continue }
|
|
279
|
+
if (blk && /^=======( |$)/.test(line)) { blk.secs.push([]); continue }
|
|
280
|
+
if (blk && /^>>>>>>> /.test(line)) { blk.foot = line; segs.push(blk); blk = null; continue }
|
|
281
|
+
if (blk) blk.secs[blk.secs.length - 1].push(line)
|
|
282
|
+
else segs.push(line)
|
|
283
|
+
}
|
|
284
|
+
if (blk) return null // an unclosed block: not ours to read
|
|
285
|
+
const blocks = segs.filter((s) => typeof s !== 'string')
|
|
286
|
+
const k = Math.max(...blocks.map((b) => b.secs.length))
|
|
287
|
+
const versions = []
|
|
288
|
+
for (let j = 0; j < k; j++) versions.push(segs.flatMap((s) => (typeof s === 'string' ? [s] : s.secs[Math.min(j, s.secs.length - 1)])).join('\n'))
|
|
289
|
+
return { versions, blocks }
|
|
290
|
+
}
|
|
291
|
+
function mergeLateSave(base, mine, theirs, opts) {
|
|
292
|
+
const bv = blockVersions(theirs)
|
|
293
|
+
if (!bv) return null
|
|
294
|
+
const variants = [], lates = []
|
|
295
|
+
const add = (t, to = variants) => { if (!variants.some((v) => v.text === t.text) && !lates.some((v) => v.text === t.text)) to.push(t) }
|
|
296
|
+
for (const T of bv.versions) {
|
|
297
|
+
if (hasConflictMarkers(T)) return null
|
|
298
|
+
const r = merge3(base, mine, T, opts)
|
|
299
|
+
if (!r.conflict) { add({ text: r.text, mine: false }); continue }
|
|
300
|
+
// The room's versions first, in the block's order, so its header and footer still name its sections;
|
|
301
|
+
// the late save's versions join after all of them.
|
|
302
|
+
add({ text: stripConflictMarkers(r.text, 'theirs'), mine: false })
|
|
303
|
+
add({ text: stripConflictMarkers(r.text, 'local'), mine: true }, lates)
|
|
304
|
+
}
|
|
305
|
+
variants.push(...lates.filter((l) => !variants.some((v) => v.text === l.text)))
|
|
306
|
+
if (variants.length === 1) return { text: variants[0].text, conflict: false }
|
|
307
|
+
// Regions, in the first version's lines, where any other version differs.
|
|
308
|
+
const anchor = variants[0].text.split('\n')
|
|
309
|
+
const hunksOf = variants.map((v, i) => (i === 0 ? [] : lineHunks(anchor, v.text.split('\n'))))
|
|
310
|
+
if (hunksOf.some((h) => h === null)) return null
|
|
311
|
+
const spans = hunksOf.flat().map((h) => [h[0], h[1]]).sort((a, b) => a[0] - b[0] || a[1] - b[1])
|
|
312
|
+
const regions = []
|
|
313
|
+
for (const [s, e] of spans) {
|
|
314
|
+
const last = regions[regions.length - 1]
|
|
315
|
+
if (last && s <= last[1]) last[1] = Math.max(last[1], e)
|
|
316
|
+
else regions.push([s, e])
|
|
317
|
+
}
|
|
318
|
+
const heads = bv.blocks.map((b) => b.head), feet = bv.blocks.map((b) => b.foot.replace(/^>>>>>>> /, '').replace(/ \(\d+ versions\)$/, ''))
|
|
319
|
+
const out = []
|
|
320
|
+
let pos = 0
|
|
321
|
+
regions.forEach(([rs, re], ri) => {
|
|
322
|
+
for (let x = pos; x < rs; x++) out.push(anchor[x])
|
|
323
|
+
const secs = []
|
|
324
|
+
variants.forEach((v, vi) => {
|
|
325
|
+
const src = v.text.split('\n')
|
|
326
|
+
const sec = sideOfRegion(anchor, rs, re, hunksOf[vi].filter((h) => h[0] >= rs && h[1] <= re), src)
|
|
327
|
+
if (!secs.some((x) => x.join('\n') === sec.join('\n'))) secs.push(sec)
|
|
328
|
+
})
|
|
329
|
+
if (secs.length === 1) out.push(...secs[0])
|
|
330
|
+
else {
|
|
331
|
+
const who = feet[Math.min(ri, feet.length - 1)]
|
|
332
|
+
const lateName = String(opts.lateName || 'local (yours)').replace(/[\r\n]/g, ' ')
|
|
333
|
+
const late = variants.some((v) => v.mine) && !who.split(', ').includes(lateName) ? `, ${lateName}` : ''
|
|
334
|
+
out.push(heads[Math.min(ri, heads.length - 1)], ...secs[0], ...secs.slice(1).flatMap((x) => ['=======', ...x]), `>>>>>>> ${who}${late} (${secs.length} versions)`)
|
|
335
|
+
}
|
|
336
|
+
pos = re
|
|
337
|
+
})
|
|
338
|
+
for (let x = pos; x < anchor.length; x++) out.push(anchor[x])
|
|
339
|
+
return { text: out.join('\n'), conflict: true }
|
|
340
|
+
}
|
|
341
|
+
|
|
251
342
|
// 3-way merge for the sync layer (disk <-> shared doc)
|
|
252
|
-
function merge3(base, mine, theirs) {
|
|
343
|
+
function merge3(base, mine, theirs, opts = {}) {
|
|
253
344
|
const norm = (s) => (s || '').replace(/\r\n/g, '\n')
|
|
254
345
|
const nm0 = norm(mine), nt0 = norm(theirs), nb0 = norm(base)
|
|
255
346
|
if (nm0 === nt0) return { text: nt0, conflict: false }
|
|
@@ -258,7 +349,17 @@ function merge3(base, mine, theirs) {
|
|
|
258
349
|
|
|
259
350
|
const cleanBase = hasConflictMarkers(base) ? stripConflictMarkers(base, 'local') : (base || '')
|
|
260
351
|
const cleanMine = hasConflictMarkers(mine) ? stripConflictMarkers(mine, 'local') : (mine || '')
|
|
261
|
-
|
|
352
|
+
// A LATE SAVE: the author edited a clean copy and the room's copy now holds a conflict block. Keep
|
|
353
|
+
// EVERY version in the block: keeping one side dropped the other authors' work (P0-F,
|
|
354
|
+
// late-save-merge-test). Only then: when the base or the author's copy holds the block too, all three
|
|
355
|
+
// are stripped alike, as before, or the extra versions read as new lines every round and pile up
|
|
356
|
+
// (merge-balloon-test).
|
|
357
|
+
const lateSave = !hasConflictMarkers(base) && !hasConflictMarkers(mine)
|
|
358
|
+
if (lateSave && hasConflictMarkers(theirs)) {
|
|
359
|
+
const r = mergeLateSave(norm(base), norm(mine), norm(theirs), opts)
|
|
360
|
+
if (r) return r
|
|
361
|
+
}
|
|
362
|
+
const cleanTheirs = hasConflictMarkers(theirs) ? stripConflictMarkers(theirs, lateSave ? 'all' : 'theirs') : (theirs || '')
|
|
262
363
|
|
|
263
364
|
const nm = norm(cleanMine), nt = norm(cleanTheirs), nb = norm(cleanBase)
|
|
264
365
|
if (nm === nt) return { text: nt, conflict: false }
|
|
@@ -266,7 +367,7 @@ function merge3(base, mine, theirs) {
|
|
|
266
367
|
if (nb === nm) return { text: nt, conflict: false }
|
|
267
368
|
|
|
268
369
|
const b = nb.split('\n')
|
|
269
|
-
const d3 = diff3Lines(b, nm.split('\n'), nt.split('\n'))
|
|
370
|
+
const d3 = diff3Lines(b, nm.split('\n'), nt.split('\n'), !!opts.sideBySide)
|
|
270
371
|
if (d3) return { text: d3.lines.join('\n'), conflict: d3.conflict }
|
|
271
372
|
// Past diff3's work bound (two sides that share almost nothing): one range per side.
|
|
272
373
|
const mr = changedRange(b, nm.split('\n'))
|
|
@@ -616,6 +717,7 @@ module.exports = {
|
|
|
616
717
|
merge3,
|
|
617
718
|
lineHunks,
|
|
618
719
|
diff3Lines,
|
|
720
|
+
blockVersions,
|
|
619
721
|
describeConflicts,
|
|
620
722
|
icrMerge3
|
|
621
723
|
}
|