hivecode-mcp 0.13.0 → 0.13.1
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 +11 -6
- 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 +64 -5
- 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/package.json +3 -2
- package/sync.js +214 -12
package/sync.js
CHANGED
|
@@ -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
|
|
@@ -3198,18 +3200,51 @@ export function startSync({ relay = 'ws://localhost:1234', room = 'default', dir
|
|
|
3198
3200
|
// So agents coordinate among themselves, but a human can never make an agent
|
|
3199
3201
|
// act without that agent's owner approving it.
|
|
3200
3202
|
const newId = () => 't' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
|
|
3203
|
+
|
|
3204
|
+
const getMemberNames = () => {
|
|
3205
|
+
const names = new Set()
|
|
3206
|
+
if (provider && provider.awareness) {
|
|
3207
|
+
for (const s of provider.awareness.getStates().values()) {
|
|
3208
|
+
const u = s && s.user
|
|
3209
|
+
if (u && u.name) names.add(u.name)
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
// Union with durable activity map (non-gone keys)
|
|
3213
|
+
try {
|
|
3214
|
+
const act = doc.getMap('activity')
|
|
3215
|
+
if (act) {
|
|
3216
|
+
for (const [k, v] of act.entries()) {
|
|
3217
|
+
if (v && !v.gone && k) names.add(k)
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
} catch { }
|
|
3221
|
+
// Union with recent chat senders
|
|
3222
|
+
try {
|
|
3223
|
+
const chatArr = doc.getArray('chat')
|
|
3224
|
+
if (chatArr) {
|
|
3225
|
+
for (const msg of chatArr.toArray()) {
|
|
3226
|
+
if (msg && msg.by) names.add(msg.by)
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
} catch { }
|
|
3230
|
+
names.add(name)
|
|
3231
|
+
return [...names]
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3201
3234
|
function assign(to, text) {
|
|
3202
3235
|
if (!to || !text) return null
|
|
3236
|
+
const normTo = String(to).trim() === '@all' ? 'all' : String(to).trim()
|
|
3203
3237
|
const id = newId()
|
|
3204
|
-
const ownerOf = owners.get(
|
|
3238
|
+
const ownerOf = owners.get(normTo)
|
|
3205
3239
|
// 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
|
|
3240
|
+
// or the asker IS the target's owner, or an @all task.
|
|
3241
|
+
const auto = kind === 'ai' || !ownerOf || name === ownerOf || normTo === 'all'
|
|
3208
3242
|
const status = auto ? 'accepted' : 'pending'
|
|
3209
3243
|
const decidedBy = auto ? (kind === 'ai' ? `${name} (AI coordination)` : name) : null
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3244
|
+
const membersAt = getMemberNames()
|
|
3245
|
+
tasks.set(id, { id, to: normTo, by: name, byKind: kind, text: String(text), status, decidedBy, at: fmtTime(), membersAt, createdAt: Date.now() })
|
|
3246
|
+
if (auto) say(`@${normTo}: ${text} (task ${id} — ${kind === 'ai' ? 'AI coordination, proceeding' : 'from owner, proceeding'})`)
|
|
3247
|
+
else say(`@${normTo}: ${text} (task ${id} — ${ownerOf} must approve: do it or ignore?)`)
|
|
3213
3248
|
return id
|
|
3214
3249
|
}
|
|
3215
3250
|
// A decision is about a task that is WAITING for one. Approving an accepted or done task used to
|
|
@@ -3228,19 +3263,182 @@ export function startSync({ relay = 'ws://localhost:1234', room = 'default', dir
|
|
|
3228
3263
|
say(`${accept ? `@${t.to} ` : ''}task ${id} ${accept ? 'APPROVED' : 'denied'} by ${by}: "${t.text}"`)
|
|
3229
3264
|
return { ok: true }
|
|
3230
3265
|
}
|
|
3231
|
-
// Only the ASSIGNEE finishes a task, and only work it was cleared to do.
|
|
3232
|
-
//
|
|
3233
|
-
//
|
|
3266
|
+
// Only the ASSIGNEE finishes a task, and only work it was cleared to do.
|
|
3267
|
+
// For @all tasks (Claude addition iii), any member can complete it provided they
|
|
3268
|
+
// were in the room when the task was created, or hold a claim / signed receipt / review on the work.
|
|
3234
3269
|
function complete(id, note = '') {
|
|
3235
3270
|
const t = tasks.get(id); if (!t) return { error: 'no such task' }
|
|
3236
|
-
|
|
3271
|
+
const isAll = t.to === 'all' || t.to === '@all'
|
|
3272
|
+
if (!isAll && t.to !== name) return { error: `task ${id} is ${t.to}'s -- only ${t.to} can complete it` }
|
|
3237
3273
|
if (t.status === 'done') return { ok: true, already: true }
|
|
3238
3274
|
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
|
-
|
|
3275
|
+
|
|
3276
|
+
if (isAll) {
|
|
3277
|
+
const wasPresent = Array.isArray(t.membersAt) ? t.membersAt.includes(name) : (t.by === name)
|
|
3278
|
+
const hasClaim = coordinator ? coordinator.board().some((c) => c && c.by === name) : false
|
|
3279
|
+
let hasReceipt = false
|
|
3280
|
+
for (const e of fileDocs.values()) {
|
|
3281
|
+
if (e && e.ledger) {
|
|
3282
|
+
const rs = e.ledger.toArray()
|
|
3283
|
+
if (rs.some((r) => r && (r.name === name || (identity && r.author === identity.id)))) {
|
|
3284
|
+
hasReceipt = true
|
|
3285
|
+
break
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
let hasReview = false
|
|
3290
|
+
try {
|
|
3291
|
+
for (const r of getReviews('')) {
|
|
3292
|
+
if (r && r.by === name) { hasReview = true; break }
|
|
3293
|
+
}
|
|
3294
|
+
if (!hasReview) {
|
|
3295
|
+
for (const [k, r] of reviews.entries()) {
|
|
3296
|
+
if (r && r.by === name) { hasReview = true; break }
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
} catch { }
|
|
3300
|
+
if (!wasPresent && !hasClaim && !hasReceipt && !hasReview) {
|
|
3301
|
+
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' }
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
tasks.set(id, { ...t, status: 'done', completedBy: name, decidedBy: t.decidedBy })
|
|
3240
3306
|
say(`task ${id} done by ${name}${note ? ': ' + note : ''}`)
|
|
3241
3307
|
return { ok: true }
|
|
3242
3308
|
}
|
|
3243
|
-
const myTasks = () => [...tasks.values()].filter((t) => t.to === name)
|
|
3309
|
+
const myTasks = () => [...tasks.values()].filter((t) => t.to === name || t.to === 'all' || t.to === '@all')
|
|
3310
|
+
|
|
3311
|
+
/**
|
|
3312
|
+
* Room review: record a non-writer review verdict on files in the room.
|
|
3313
|
+
* Stored in Y.Map 'reviews' as an append-only list per file: [{ by, at, verdict, note, hash }].
|
|
3314
|
+
*/
|
|
3315
|
+
async function review(files, verdict, note = '') {
|
|
3316
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
3317
|
+
return { error: 'files must be a non-empty array' }
|
|
3318
|
+
}
|
|
3319
|
+
const v = String(verdict || '').toLowerCase()
|
|
3320
|
+
if (v !== 'pass' && v !== 'fail') {
|
|
3321
|
+
return { error: "verdict must be 'pass' or 'fail'" }
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// 1. Validate manifest presence (Claude addition i)
|
|
3325
|
+
for (const rel of files) {
|
|
3326
|
+
if (!inManifest(rel)) {
|
|
3327
|
+
return { error: `Refused: ${rel} is not in room manifest` }
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
// Ensure all target files are synced
|
|
3332
|
+
for (const rel of files) {
|
|
3333
|
+
const fe = openFile(rel)
|
|
3334
|
+
if (!fe.synced && fe.provider) {
|
|
3335
|
+
await new Promise((res) => {
|
|
3336
|
+
const t = setTimeout(res, 3000)
|
|
3337
|
+
fe.provider.on('sync', (isSynced) => {
|
|
3338
|
+
if (isSynced) {
|
|
3339
|
+
clearTimeout(t)
|
|
3340
|
+
res()
|
|
3341
|
+
}
|
|
3342
|
+
})
|
|
3343
|
+
})
|
|
3344
|
+
}
|
|
3345
|
+
if (!fe.synced) {
|
|
3346
|
+
return { error: `Refused: ${rel} has not finished syncing yet` }
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
// 2. Validate non-contributor status
|
|
3351
|
+
for (const rel of files) {
|
|
3352
|
+
const fe = openFile(rel)
|
|
3353
|
+
const curText = fe.text ? fe.text.toString() : ''
|
|
3354
|
+
const rs = fe.ledger ? fe.ledger.toArray() : []
|
|
3355
|
+
const authors = authorsOf(rs, verifyReceipt, curText)
|
|
3356
|
+
const isContributor = authors.contributors.some(
|
|
3357
|
+
(c) => c && (c.name === name || (identity && c.author === identity.id))
|
|
3358
|
+
)
|
|
3359
|
+
const head = fe.head ? fe.head.get('cur') : null
|
|
3360
|
+
const isHeadLander = head && head.by === name && head.hash === contentHash(curText)
|
|
3361
|
+
const boardEntry = board.get(rel)
|
|
3362
|
+
const isBoardAuthor = boardEntry && boardEntry.by === name
|
|
3363
|
+
const claimEntry = coordinator.sense(rel)
|
|
3364
|
+
const isClaimant = claimEntry && claimEntry.by === name
|
|
3365
|
+
if (isContributor || isHeadLander || isBoardAuthor || isClaimant) {
|
|
3366
|
+
return { error: `Refused: @${name} contributed to ${rel}'s held content; review must come from a non-contributor` }
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
// 3. Record review on each file
|
|
3371
|
+
const stamp = fmtTime()
|
|
3372
|
+
for (const rel of files) {
|
|
3373
|
+
const fe = openFile(rel)
|
|
3374
|
+
const curText = fe.text ? fe.text.toString() : ''
|
|
3375
|
+
const h = contentHash(curText)
|
|
3376
|
+
const entry = {
|
|
3377
|
+
file: rel,
|
|
3378
|
+
by: name,
|
|
3379
|
+
at: stamp,
|
|
3380
|
+
ts: Date.now(),
|
|
3381
|
+
verdict: v,
|
|
3382
|
+
note: String(note || ''),
|
|
3383
|
+
hash: h,
|
|
3384
|
+
}
|
|
3385
|
+
// ONLY per-review key to prevent concurrent clobbers under LWW
|
|
3386
|
+
reviews.set(`${rel}::${entry.ts}::${newId()}`, entry)
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
say(`review ${v.toUpperCase()} on ${files.join(', ')} by ${name}${note ? ': ' + note : ''}`)
|
|
3390
|
+
return { ok: true, files, verdict: v }
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
function getReviews(relPath) {
|
|
3394
|
+
const mapEntries = []
|
|
3395
|
+
const prefix = relPath ? relPath + '::' : ''
|
|
3396
|
+
for (const [k, r] of reviews.entries()) {
|
|
3397
|
+
if (relPath && k === relPath && Array.isArray(r)) {
|
|
3398
|
+
for (const item of r) if (item && typeof item === 'object') mapEntries.push(item)
|
|
3399
|
+
} else if (r && (!relPath || r.file === relPath || k.startsWith(prefix))) {
|
|
3400
|
+
mapEntries.push(r)
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
const seen = new Set()
|
|
3404
|
+
const unique = []
|
|
3405
|
+
for (const r of mapEntries) {
|
|
3406
|
+
const key = `${r.file}|${r.by}|${r.ts}|${r.hash}|${r.verdict}`
|
|
3407
|
+
if (!seen.has(key)) {
|
|
3408
|
+
seen.add(key)
|
|
3409
|
+
unique.push(r)
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
return unique.sort((a, b) => (a.ts || 0) - (b.ts || 0))
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
function getReviewStatus(relPath) {
|
|
3416
|
+
const fe = openFile(relPath)
|
|
3417
|
+
const curText = fe.text ? fe.text.toString() : ''
|
|
3418
|
+
const curHash = contentHash(curText)
|
|
3419
|
+
const all = getReviews(relPath)
|
|
3420
|
+
|
|
3421
|
+
// Ignore reviews authored by contributors to the current held content
|
|
3422
|
+
const rs = fe.ledger ? fe.ledger.toArray() : []
|
|
3423
|
+
const authors = authorsOf(rs, verifyReceipt, curText)
|
|
3424
|
+
const contributorNames = new Set(authors.contributors.map((c) => c.name).filter(Boolean))
|
|
3425
|
+
const head = fe.head ? fe.head.get('cur') : null
|
|
3426
|
+
if (head && head.by && head.hash === curHash) contributorNames.add(head.by)
|
|
3427
|
+
|
|
3428
|
+
const validReviews = all.filter((r) => r && !contributorNames.has(r.by))
|
|
3429
|
+
const matching = validReviews.filter((r) => r.hash === curHash)
|
|
3430
|
+
if (!matching.length) {
|
|
3431
|
+
return { verdict: 'unreviewed', hash: curHash, stale: validReviews.length > 0, reviews: validReviews }
|
|
3432
|
+
}
|
|
3433
|
+
let lastFail = -1
|
|
3434
|
+
let lastPass = -1
|
|
3435
|
+
for (let i = 0; i < matching.length; i++) {
|
|
3436
|
+
if (matching[i].verdict === 'fail') lastFail = i
|
|
3437
|
+
if (matching[i].verdict === 'pass') lastPass = i
|
|
3438
|
+
}
|
|
3439
|
+
const finalVerdict = (lastPass > lastFail) ? 'pass' : 'fail'
|
|
3440
|
+
return { verdict: finalVerdict, hash: curHash, stale: false, reviews: matching }
|
|
3441
|
+
}
|
|
3244
3442
|
|
|
3245
3443
|
// --- mission control: pause / resume / steer an agent ---
|
|
3246
3444
|
// A human (or an owner) can PAUSE an agent: it should finish its current step,
|
|
@@ -4584,6 +4782,10 @@ export function startSync({ relay = 'ws://localhost:1234', room = 'default', dir
|
|
|
4584
4782
|
forkInfo, // the active fork on a file as a resolvable object (base + versions + signed intents), or null
|
|
4585
4783
|
resolveFork, // propose the reconciled file; ICR-validated (parse + dangling) before it lands, refused otherwise
|
|
4586
4784
|
edit: editFile, // first-class authored write: intent + transition stamped race-free, then normal reconcile
|
|
4785
|
+
// --- non-writer review & approval ---
|
|
4786
|
+
review,
|
|
4787
|
+
reviews: getReviews,
|
|
4788
|
+
reviewStatus: getReviewStatus,
|
|
4587
4789
|
// --- convergent substrate: provenance (I1) ---
|
|
4588
4790
|
identity: () => identity ? { id: identity.id, name: identity.name, pk: identity.pk } : null, // never exposes the private key
|
|
4589
4791
|
provenanceOf: (relPath) => { const e = fileDocs.get(relPath); return e && e.ledger ? e.ledger.toArray() : [] }, // signed receipts for a file
|