hivecode-mcp 0.13.0 → 0.14.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/docs/GITHUB_WORKFLOW.md +60 -0
- package/docs/TROUBLESHOOTING.md +18 -0
- package/hive-authors.js +44 -0
- package/hive-await.js +34 -11
- package/hive-branch.js +79 -158
- package/hive-github.js +40 -0
- package/hive-land.js +691 -0
- package/hive-listen.js +2 -2
- package/hive-mcp.js +67 -5
- package/hive-poll-proto.js +50 -0
- package/hive-pr.js +201 -38
- package/hive-repos.js +430 -0
- package/hive-room-mode.js +1668 -0
- package/hive-room-watcher.js +561 -0
- package/hive-stop-hook.js +1 -1
- package/hive-transport.js +353 -0
- package/package.json +5 -2
- package/sync.js +218 -14
package/sync.js
CHANGED
|
@@ -22,7 +22,7 @@ import crypto from 'crypto'
|
|
|
22
22
|
import * as Y from 'yjs'
|
|
23
23
|
import { WebsocketProvider } from 'y-websocket'
|
|
24
24
|
import { createFileTransport } from './hive-mux.js' // ONE SOCKET PER PARTICIPANT: every file doc on one physical socket when the relay offers it
|
|
25
|
-
import {
|
|
25
|
+
import { HiveSocket, transportFor } from './hive-transport.js' // a WebSocket, or HTTPS long-poll where WebSockets are refused
|
|
26
26
|
import ignore from 'ignore'
|
|
27
27
|
import { applyDiff, summarizeChange, changedRange, hasConflictMarkers, merge3 } from './core.js'
|
|
28
28
|
import { icrMerge3 } from './icr-merge.js' // structure/intent-aware merge; falls back to line merge3
|
|
@@ -31,6 +31,7 @@ import { gateConfig, runGate, verdictRecord, refusalMessage, forcedMessage } fro
|
|
|
31
31
|
import { fileRoom, decodeUnsafe, scopeForRoom, pathAllowed, writeAllowed, isSafeRelPath } from './token.js'
|
|
32
32
|
import { validateMemberName } from './hive-mailbox.js' // a name the mention parser cannot produce is not a name
|
|
33
33
|
import { genIdentity, authorChange, verifyReceipt, contentHash } from './substrate.js' // convergent-substrate: signed provenance for every landed change
|
|
34
|
+
import { authorsOf } from './hive-authors.js'
|
|
34
35
|
// `languageFor` belongs in this list and was missing: reconcile() calls it on the
|
|
35
36
|
// clean-rewrite path, so a local whole-file rewrite of a supported file threw
|
|
36
37
|
// ReferenceError and killed the sync process outright. Nothing caught it -- the client
|
|
@@ -584,6 +585,7 @@ export function startSync({ relay = 'ws://localhost:1234', room = 'default', dir
|
|
|
584
585
|
const claims = doc.getMap('claims')
|
|
585
586
|
const swarmState = doc.getMap('swarm_state')
|
|
586
587
|
const swarmSchema = doc.getMap('swarm_schema')
|
|
588
|
+
const reviews = doc.getMap('reviews') // relPath -> Array of { by, at, verdict: 'pass'|'fail', note, hash }
|
|
587
589
|
// ATTESTATION LEDGER — path -> { <peerName>: { bytes, lines, at } }.
|
|
588
590
|
//
|
|
589
591
|
// The one thing a four-agent build could not do without a human relaying numbers by hand: tell
|
|
@@ -1119,7 +1121,9 @@ export function startSync({ relay = 'ws://localhost:1234', room = 'default', dir
|
|
|
1119
1121
|
if (name) wsParams.name = String(name).slice(0, 48)
|
|
1120
1122
|
if (kind) wsParams.kind = kind
|
|
1121
1123
|
if (passive) wsParams.passive = 'true'
|
|
1122
|
-
|
|
1124
|
+
// HiveSocket: a WebSocket where the network allows one, HTTPS long-poll where it refuses the upgrade
|
|
1125
|
+
// (Antigravity's cloud VMs, many company proxies). Same bytes, same relay gates, either way.
|
|
1126
|
+
const wsOpts = { WebSocketPolyfill: HiveSocket, disableBc: true, params: Object.keys(wsParams).length ? wsParams : undefined }
|
|
1123
1127
|
// ONE SOCKET PER PARTICIPANT. When the relay offers it (it says so in /health), file docs stop
|
|
1124
1128
|
// costing a socket each: they ride one physical socket as channels, and the relay admits each
|
|
1125
1129
|
// one through the same gates a per-file socket passes. Against a relay that does not offer it,
|
|
@@ -3198,18 +3202,51 @@ export function startSync({ relay = 'ws://localhost:1234', room = 'default', dir
|
|
|
3198
3202
|
// So agents coordinate among themselves, but a human can never make an agent
|
|
3199
3203
|
// act without that agent's owner approving it.
|
|
3200
3204
|
const newId = () => 't' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
|
|
3205
|
+
|
|
3206
|
+
const getMemberNames = () => {
|
|
3207
|
+
const names = new Set()
|
|
3208
|
+
if (provider && provider.awareness) {
|
|
3209
|
+
for (const s of provider.awareness.getStates().values()) {
|
|
3210
|
+
const u = s && s.user
|
|
3211
|
+
if (u && u.name) names.add(u.name)
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
// Union with durable activity map (non-gone keys)
|
|
3215
|
+
try {
|
|
3216
|
+
const act = doc.getMap('activity')
|
|
3217
|
+
if (act) {
|
|
3218
|
+
for (const [k, v] of act.entries()) {
|
|
3219
|
+
if (v && !v.gone && k) names.add(k)
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
} catch { }
|
|
3223
|
+
// Union with recent chat senders
|
|
3224
|
+
try {
|
|
3225
|
+
const chatArr = doc.getArray('chat')
|
|
3226
|
+
if (chatArr) {
|
|
3227
|
+
for (const msg of chatArr.toArray()) {
|
|
3228
|
+
if (msg && msg.by) names.add(msg.by)
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
} catch { }
|
|
3232
|
+
names.add(name)
|
|
3233
|
+
return [...names]
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3201
3236
|
function assign(to, text) {
|
|
3202
3237
|
if (!to || !text) return null
|
|
3238
|
+
const normTo = String(to).trim() === '@all' ? 'all' : String(to).trim()
|
|
3203
3239
|
const id = newId()
|
|
3204
|
-
const ownerOf = owners.get(
|
|
3240
|
+
const ownerOf = owners.get(normTo)
|
|
3205
3241
|
// auto-accept when it's AI->AI coordination, the target has no owner to ask,
|
|
3206
|
-
// or the asker IS the target's owner
|
|
3207
|
-
const auto = kind === 'ai' || !ownerOf || name === ownerOf
|
|
3242
|
+
// or the asker IS the target's owner, or an @all task.
|
|
3243
|
+
const auto = kind === 'ai' || !ownerOf || name === ownerOf || normTo === 'all'
|
|
3208
3244
|
const status = auto ? 'accepted' : 'pending'
|
|
3209
3245
|
const decidedBy = auto ? (kind === 'ai' ? `${name} (AI coordination)` : name) : null
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3246
|
+
const membersAt = getMemberNames()
|
|
3247
|
+
tasks.set(id, { id, to: normTo, by: name, byKind: kind, text: String(text), status, decidedBy, at: fmtTime(), membersAt, createdAt: Date.now() })
|
|
3248
|
+
if (auto) say(`@${normTo}: ${text} (task ${id} — ${kind === 'ai' ? 'AI coordination, proceeding' : 'from owner, proceeding'})`)
|
|
3249
|
+
else say(`@${normTo}: ${text} (task ${id} — ${ownerOf} must approve: do it or ignore?)`)
|
|
3213
3250
|
return id
|
|
3214
3251
|
}
|
|
3215
3252
|
// A decision is about a task that is WAITING for one. Approving an accepted or done task used to
|
|
@@ -3228,19 +3265,182 @@ export function startSync({ relay = 'ws://localhost:1234', room = 'default', dir
|
|
|
3228
3265
|
say(`${accept ? `@${t.to} ` : ''}task ${id} ${accept ? 'APPROVED' : 'denied'} by ${by}: "${t.text}"`)
|
|
3229
3266
|
return { ok: true }
|
|
3230
3267
|
}
|
|
3231
|
-
// Only the ASSIGNEE finishes a task, and only work it was cleared to do.
|
|
3232
|
-
//
|
|
3233
|
-
//
|
|
3268
|
+
// Only the ASSIGNEE finishes a task, and only work it was cleared to do.
|
|
3269
|
+
// For @all tasks (Claude addition iii), any member can complete it provided they
|
|
3270
|
+
// were in the room when the task was created, or hold a claim / signed receipt / review on the work.
|
|
3234
3271
|
function complete(id, note = '') {
|
|
3235
3272
|
const t = tasks.get(id); if (!t) return { error: 'no such task' }
|
|
3236
|
-
|
|
3273
|
+
const isAll = t.to === 'all' || t.to === '@all'
|
|
3274
|
+
if (!isAll && t.to !== name) return { error: `task ${id} is ${t.to}'s -- only ${t.to} can complete it` }
|
|
3237
3275
|
if (t.status === 'done') return { ok: true, already: true }
|
|
3238
3276
|
if (t.status !== 'accepted') return { error: `task ${id} is ${t.status}: only accepted work can be completed${t.status === 'pending' ? ` (it is waiting for ${owners.get(t.to) || 'its owner'} to approve)` : ''}` }
|
|
3239
|
-
|
|
3277
|
+
|
|
3278
|
+
if (isAll) {
|
|
3279
|
+
const wasPresent = Array.isArray(t.membersAt) ? t.membersAt.includes(name) : (t.by === name)
|
|
3280
|
+
const hasClaim = coordinator ? coordinator.board().some((c) => c && c.by === name) : false
|
|
3281
|
+
let hasReceipt = false
|
|
3282
|
+
for (const e of fileDocs.values()) {
|
|
3283
|
+
if (e && e.ledger) {
|
|
3284
|
+
const rs = e.ledger.toArray()
|
|
3285
|
+
if (rs.some((r) => r && (r.name === name || (identity && r.author === identity.id)))) {
|
|
3286
|
+
hasReceipt = true
|
|
3287
|
+
break
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
let hasReview = false
|
|
3292
|
+
try {
|
|
3293
|
+
for (const r of getReviews('')) {
|
|
3294
|
+
if (r && r.by === name) { hasReview = true; break }
|
|
3295
|
+
}
|
|
3296
|
+
if (!hasReview) {
|
|
3297
|
+
for (const [k, r] of reviews.entries()) {
|
|
3298
|
+
if (r && r.by === name) { hasReview = true; break }
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
} catch { }
|
|
3302
|
+
if (!wasPresent && !hasClaim && !hasReceipt && !hasReview) {
|
|
3303
|
+
return { error: 'Refused: completing an @all task requires having been in the room when it was created, or having a claim/receipt/review on the work' }
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
tasks.set(id, { ...t, status: 'done', completedBy: name, decidedBy: t.decidedBy })
|
|
3240
3308
|
say(`task ${id} done by ${name}${note ? ': ' + note : ''}`)
|
|
3241
3309
|
return { ok: true }
|
|
3242
3310
|
}
|
|
3243
|
-
const myTasks = () => [...tasks.values()].filter((t) => t.to === name)
|
|
3311
|
+
const myTasks = () => [...tasks.values()].filter((t) => t.to === name || t.to === 'all' || t.to === '@all')
|
|
3312
|
+
|
|
3313
|
+
/**
|
|
3314
|
+
* Room review: record a non-writer review verdict on files in the room.
|
|
3315
|
+
* Stored in Y.Map 'reviews' as an append-only list per file: [{ by, at, verdict, note, hash }].
|
|
3316
|
+
*/
|
|
3317
|
+
async function review(files, verdict, note = '') {
|
|
3318
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
3319
|
+
return { error: 'files must be a non-empty array' }
|
|
3320
|
+
}
|
|
3321
|
+
const v = String(verdict || '').toLowerCase()
|
|
3322
|
+
if (v !== 'pass' && v !== 'fail') {
|
|
3323
|
+
return { error: "verdict must be 'pass' or 'fail'" }
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
// 1. Validate manifest presence (Claude addition i)
|
|
3327
|
+
for (const rel of files) {
|
|
3328
|
+
if (!inManifest(rel)) {
|
|
3329
|
+
return { error: `Refused: ${rel} is not in room manifest` }
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
// Ensure all target files are synced
|
|
3334
|
+
for (const rel of files) {
|
|
3335
|
+
const fe = openFile(rel)
|
|
3336
|
+
if (!fe.synced && fe.provider) {
|
|
3337
|
+
await new Promise((res) => {
|
|
3338
|
+
const t = setTimeout(res, 3000)
|
|
3339
|
+
fe.provider.on('sync', (isSynced) => {
|
|
3340
|
+
if (isSynced) {
|
|
3341
|
+
clearTimeout(t)
|
|
3342
|
+
res()
|
|
3343
|
+
}
|
|
3344
|
+
})
|
|
3345
|
+
})
|
|
3346
|
+
}
|
|
3347
|
+
if (!fe.synced) {
|
|
3348
|
+
return { error: `Refused: ${rel} has not finished syncing yet` }
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
// 2. Validate non-contributor status
|
|
3353
|
+
for (const rel of files) {
|
|
3354
|
+
const fe = openFile(rel)
|
|
3355
|
+
const curText = fe.text ? fe.text.toString() : ''
|
|
3356
|
+
const rs = fe.ledger ? fe.ledger.toArray() : []
|
|
3357
|
+
const authors = authorsOf(rs, verifyReceipt, curText)
|
|
3358
|
+
const isContributor = authors.contributors.some(
|
|
3359
|
+
(c) => c && (c.name === name || (identity && c.author === identity.id))
|
|
3360
|
+
)
|
|
3361
|
+
const head = fe.head ? fe.head.get('cur') : null
|
|
3362
|
+
const isHeadLander = head && head.by === name && head.hash === contentHash(curText)
|
|
3363
|
+
const boardEntry = board.get(rel)
|
|
3364
|
+
const isBoardAuthor = boardEntry && boardEntry.by === name
|
|
3365
|
+
const claimEntry = coordinator.sense(rel)
|
|
3366
|
+
const isClaimant = claimEntry && claimEntry.by === name
|
|
3367
|
+
if (isContributor || isHeadLander || isBoardAuthor || isClaimant) {
|
|
3368
|
+
return { error: `Refused: @${name} contributed to ${rel}'s held content; review must come from a non-contributor` }
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
// 3. Record review on each file
|
|
3373
|
+
const stamp = fmtTime()
|
|
3374
|
+
for (const rel of files) {
|
|
3375
|
+
const fe = openFile(rel)
|
|
3376
|
+
const curText = fe.text ? fe.text.toString() : ''
|
|
3377
|
+
const h = contentHash(curText)
|
|
3378
|
+
const entry = {
|
|
3379
|
+
file: rel,
|
|
3380
|
+
by: name,
|
|
3381
|
+
at: stamp,
|
|
3382
|
+
ts: Date.now(),
|
|
3383
|
+
verdict: v,
|
|
3384
|
+
note: String(note || ''),
|
|
3385
|
+
hash: h,
|
|
3386
|
+
}
|
|
3387
|
+
// ONLY per-review key to prevent concurrent clobbers under LWW
|
|
3388
|
+
reviews.set(`${rel}::${entry.ts}::${newId()}`, entry)
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
say(`review ${v.toUpperCase()} on ${files.join(', ')} by ${name}${note ? ': ' + note : ''}`)
|
|
3392
|
+
return { ok: true, files, verdict: v }
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
function getReviews(relPath) {
|
|
3396
|
+
const mapEntries = []
|
|
3397
|
+
const prefix = relPath ? relPath + '::' : ''
|
|
3398
|
+
for (const [k, r] of reviews.entries()) {
|
|
3399
|
+
if (relPath && k === relPath && Array.isArray(r)) {
|
|
3400
|
+
for (const item of r) if (item && typeof item === 'object') mapEntries.push(item)
|
|
3401
|
+
} else if (r && (!relPath || r.file === relPath || k.startsWith(prefix))) {
|
|
3402
|
+
mapEntries.push(r)
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
const seen = new Set()
|
|
3406
|
+
const unique = []
|
|
3407
|
+
for (const r of mapEntries) {
|
|
3408
|
+
const key = `${r.file}|${r.by}|${r.ts}|${r.hash}|${r.verdict}`
|
|
3409
|
+
if (!seen.has(key)) {
|
|
3410
|
+
seen.add(key)
|
|
3411
|
+
unique.push(r)
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
return unique.sort((a, b) => (a.ts || 0) - (b.ts || 0))
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
function getReviewStatus(relPath) {
|
|
3418
|
+
const fe = openFile(relPath)
|
|
3419
|
+
const curText = fe.text ? fe.text.toString() : ''
|
|
3420
|
+
const curHash = contentHash(curText)
|
|
3421
|
+
const all = getReviews(relPath)
|
|
3422
|
+
|
|
3423
|
+
// Ignore reviews authored by contributors to the current held content
|
|
3424
|
+
const rs = fe.ledger ? fe.ledger.toArray() : []
|
|
3425
|
+
const authors = authorsOf(rs, verifyReceipt, curText)
|
|
3426
|
+
const contributorNames = new Set(authors.contributors.map((c) => c.name).filter(Boolean))
|
|
3427
|
+
const head = fe.head ? fe.head.get('cur') : null
|
|
3428
|
+
if (head && head.by && head.hash === curHash) contributorNames.add(head.by)
|
|
3429
|
+
|
|
3430
|
+
const validReviews = all.filter((r) => r && !contributorNames.has(r.by))
|
|
3431
|
+
const matching = validReviews.filter((r) => r.hash === curHash)
|
|
3432
|
+
if (!matching.length) {
|
|
3433
|
+
return { verdict: 'unreviewed', hash: curHash, stale: validReviews.length > 0, reviews: validReviews }
|
|
3434
|
+
}
|
|
3435
|
+
let lastFail = -1
|
|
3436
|
+
let lastPass = -1
|
|
3437
|
+
for (let i = 0; i < matching.length; i++) {
|
|
3438
|
+
if (matching[i].verdict === 'fail') lastFail = i
|
|
3439
|
+
if (matching[i].verdict === 'pass') lastPass = i
|
|
3440
|
+
}
|
|
3441
|
+
const finalVerdict = (lastPass > lastFail) ? 'pass' : 'fail'
|
|
3442
|
+
return { verdict: finalVerdict, hash: curHash, stale: false, reviews: matching }
|
|
3443
|
+
}
|
|
3244
3444
|
|
|
3245
3445
|
// --- mission control: pause / resume / steer an agent ---
|
|
3246
3446
|
// A human (or an owner) can PAUSE an agent: it should finish its current step,
|
|
@@ -4584,6 +4784,10 @@ export function startSync({ relay = 'ws://localhost:1234', room = 'default', dir
|
|
|
4584
4784
|
forkInfo, // the active fork on a file as a resolvable object (base + versions + signed intents), or null
|
|
4585
4785
|
resolveFork, // propose the reconciled file; ICR-validated (parse + dangling) before it lands, refused otherwise
|
|
4586
4786
|
edit: editFile, // first-class authored write: intent + transition stamped race-free, then normal reconcile
|
|
4787
|
+
// --- non-writer review & approval ---
|
|
4788
|
+
review,
|
|
4789
|
+
reviews: getReviews,
|
|
4790
|
+
reviewStatus: getReviewStatus,
|
|
4587
4791
|
// --- convergent substrate: provenance (I1) ---
|
|
4588
4792
|
identity: () => identity ? { id: identity.id, name: identity.name, pk: identity.pk } : null, // never exposes the private key
|
|
4589
4793
|
provenanceOf: (relPath) => { const e = fileDocs.get(relPath); return e && e.ledger ? e.ledger.toArray() : [] }, // signed receipts for a file
|