hivecode-mcp 0.11.4 → 0.11.6
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 +114 -28
- package/docs/MCP.md +33 -0
- package/hive-busy.js +88 -0
- package/hive-coord.js +14 -3
- package/hive-gate.js +135 -0
- package/hive-hook-install.js +103 -0
- package/hive-instructions.js +66 -0
- package/hive-mailbox.js +216 -3
- package/hive-mcp.js +468 -28
- package/hive-obligations.js +247 -0
- package/hive-receipts.js +233 -0
- package/hive-stop-hook.js +241 -0
- package/hive-throttle.js +118 -0
- package/package.json +29 -6
- package/sync.js +438 -24
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* THE ANTI-IDLE GATE.
|
|
4
|
+
*
|
|
5
|
+
* A rule an agent is told to follow is advice. This is the same rule as a lock.
|
|
6
|
+
*
|
|
7
|
+
* THE PROBLEM. hive_wait exists so an agent never goes unreachable: block until someone needs
|
|
8
|
+
* you, do the work, block again. It is in the tool description and it is now in the system
|
|
9
|
+
* prompt (hive-instructions.js). Both are still instructions, and an agent that decides it is
|
|
10
|
+
* finished simply emits a final answer instead of another tool call. Its process is alive, its
|
|
11
|
+
* socket is up, and it is deaf. Everyone waiting on it waits forever, and nothing tells them.
|
|
12
|
+
*
|
|
13
|
+
* You cannot fix that with better wording, for the same reason a claim board that only warns
|
|
14
|
+
* does not stop a write: it is enforcement or it is a suggestion.
|
|
15
|
+
*
|
|
16
|
+
* WHAT THIS IS. A Claude Code `Stop` hook. Claude Code runs it when the main agent is about to
|
|
17
|
+
* end its turn. Returning {"decision":"block","reason":...} makes Claude Code refuse to stop and
|
|
18
|
+
* feed `reason` back as the next input. The agent does not get a vote. So an agent that tries to
|
|
19
|
+
* go idle while it has unread mail or unfinished accepted work is simply not allowed to.
|
|
20
|
+
*
|
|
21
|
+
* WHY IT READS DISK AND NOT THE RELAY. This runs on every turn boundary and has a hard timeout.
|
|
22
|
+
* A network round trip inside that budget would make the hook the least reliable thing in the
|
|
23
|
+
* session. It does not need one: the sync client already mirrors the room to HIVE_CHAT.md and
|
|
24
|
+
* HIVE_TASKS.md on local disk. Reading those is microseconds, works with the relay unreachable,
|
|
25
|
+
* and cannot itself desync the room because it only ever reads.
|
|
26
|
+
*
|
|
27
|
+
* IT BLOCKS ON THREE THINGS, and the middle one is the subtle one:
|
|
28
|
+
*
|
|
29
|
+
* - unread mail addressed to you
|
|
30
|
+
* - a question you DID read and never answered ("read" is not "answered": the cursor advances
|
|
31
|
+
* the moment a message is handed over, so an agent can absorb a direct question mid-work and
|
|
32
|
+
* end its turn having left someone hanging, with nothing unread to block on)
|
|
33
|
+
* - an accepted task still open
|
|
34
|
+
*
|
|
35
|
+
* FOUR GUARDS AGAINST TRAPPING THE AGENT IN A LOOP. A hook that blocks forever is worse than
|
|
36
|
+
* no hook -- it burns the user's tokens and they cannot get their agent back:
|
|
37
|
+
*
|
|
38
|
+
* 1. stop_hook_active. Claude Code sets this when the stop it is asking about is ALREADY the
|
|
39
|
+
* result of a previous block. We always allow then, so a block is never immediately
|
|
40
|
+
* re-applied on top of itself.
|
|
41
|
+
* 2. The watermark advances even when we block. A given message can cause exactly one block,
|
|
42
|
+
* ever. If the agent ignores it, that is the human's problem to see, not ours to enforce.
|
|
43
|
+
* 3. `nudged` -- the same one-block-per-message rule extended across BOTH reasons, so a
|
|
44
|
+
* message cannot charge once as unread mail and again as an unanswered question. Keyed on
|
|
45
|
+
* sender+timestamp, so a genuinely new question still gets its own single nudge.
|
|
46
|
+
* 4. MAX_TASK_BLOCKS. Unlike a message, an open task stays open, so it would re-block on every
|
|
47
|
+
* turn until completed -- including when it CANNOT be completed. After a few consecutive
|
|
48
|
+
* task-only blocks we stand down and let the agent stop.
|
|
49
|
+
*
|
|
50
|
+
* Any failure at all -- no session, unreadable file, malformed JSON, unexpected shape -- exits 0
|
|
51
|
+
* and allows the stop. The hook may make an agent more reliable; it must never make a session
|
|
52
|
+
* unusable.
|
|
53
|
+
*
|
|
54
|
+
* Install with: npx hivecode-hook install
|
|
55
|
+
*/
|
|
56
|
+
import fs from 'fs'
|
|
57
|
+
import path from 'path'
|
|
58
|
+
import { mentionsName, unansweredFor } from './hive-mailbox.js'
|
|
59
|
+
|
|
60
|
+
// Consecutive turns we will block purely because a task is still open, before standing down.
|
|
61
|
+
// Messages are not subject to this: the watermark already makes each one block at most once.
|
|
62
|
+
const MAX_TASK_BLOCKS = 3
|
|
63
|
+
|
|
64
|
+
const SESSION_FILE = '.hive-mcp-session.json' // written by hive-mcp.js on join
|
|
65
|
+
const STATE_FILE = '.hive-stop-hook.json' // ours: watermark, task blocks, questions already nudged
|
|
66
|
+
|
|
67
|
+
// A chat line: "- 19:07:27 Eve (ai): text". Anything else is a continuation of the message
|
|
68
|
+
// above it (messages may contain newlines) or a header comment.
|
|
69
|
+
const CHAT_LINE = /^- (\d{2}:\d{2}:\d{2})\s+(.+?) \((human|ai|system)\): ([\s\S]*)$/
|
|
70
|
+
// A task line: "- [accepted] tmtofacav4mf Ganesh -> PM: do the thing (by Ganesh)"
|
|
71
|
+
const TASK_LINE = /^- \[(\w+)\]\s+(\S+)\s+(.+?) -> (.+?): ([\s\S]*)$/
|
|
72
|
+
|
|
73
|
+
const readJson = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')) } catch { return null } }
|
|
74
|
+
|
|
75
|
+
/** Fold continuation lines into their message so a multi-line post counts once. */
|
|
76
|
+
export function parseChat(src) {
|
|
77
|
+
const out = []
|
|
78
|
+
for (const line of String(src || '').split(/\r?\n/)) {
|
|
79
|
+
const m = CHAT_LINE.exec(line)
|
|
80
|
+
if (m) out.push({ at: m[1], by: m[2], kind: m[3], text: m[4] })
|
|
81
|
+
else if (out.length && line.trim() && !line.startsWith('#')) out[out.length - 1].text += '\n' + line
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Accepted tasks pointed at `me` that nobody has marked done. */
|
|
87
|
+
export function openTasksFor(src, me) {
|
|
88
|
+
const out = []
|
|
89
|
+
for (const line of String(src || '').split(/\r?\n/)) {
|
|
90
|
+
const m = TASK_LINE.exec(line)
|
|
91
|
+
// `done` and `completed` both appear in the wild depending on how the task was closed;
|
|
92
|
+
// anything that is not explicitly accepted-and-open is not work we should block on.
|
|
93
|
+
if (m && m[1] === 'accepted' && m[4].trim().toLowerCase() === String(me).toLowerCase()) {
|
|
94
|
+
out.push({ id: m[2], from: m[3], text: m[5] })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The whole decision, as a pure function of what is on disk, so the test can drive it without a
|
|
102
|
+
* Claude Code session, a relay, or a room.
|
|
103
|
+
*
|
|
104
|
+
* Returns { block: boolean, reason?: string, seen: number, taskBlocks: number }.
|
|
105
|
+
*/
|
|
106
|
+
export function decide({ chat, tasks, me, seen, taskBlocks, nudged = [] }) {
|
|
107
|
+
const msgs = parseChat(chat)
|
|
108
|
+
const nowSeen = msgs.length
|
|
109
|
+
// A cursor past the end means this is a different (rebuilt or switched) room -- start over
|
|
110
|
+
// rather than treating the whole history as unread and blocking on all of it.
|
|
111
|
+
const from = seen > nowSeen ? 0 : seen
|
|
112
|
+
const fresh = msgs.slice(from)
|
|
113
|
+
|
|
114
|
+
const mail = fresh.filter((m) =>
|
|
115
|
+
m.kind !== 'system' &&
|
|
116
|
+
String(m.by).toLowerCase() !== String(me).toLowerCase() && // never block on our own post
|
|
117
|
+
mentionsName(m.text, me))
|
|
118
|
+
|
|
119
|
+
if (mail.length) {
|
|
120
|
+
const quoted = mail.slice(0, 5).map((m) => ` ${m.by}: ${m.text.replace(/\s+/g, ' ').slice(0, 220)}`)
|
|
121
|
+
const more = mail.length > 5 ? `\n ... and ${mail.length - 5} more` : ''
|
|
122
|
+
return {
|
|
123
|
+
block: true, seen: nowSeen, taskBlocks: 0,
|
|
124
|
+
// Blocking here also spends this message's ONE nudge, so the debt branch below cannot
|
|
125
|
+
// charge for the same message a second time. One block per message, whatever the reason.
|
|
126
|
+
nudged: [...nudged, ...mail.map((m) => m.by + '|' + m.at)].slice(-200),
|
|
127
|
+
reason: `You are in a Hivecode room and ${mail.length} message${mail.length > 1 ? 's' : ''} ` +
|
|
128
|
+
`addressed to you arrived while you were working:\n\n${quoted.join('\n')}${more}\n\n` +
|
|
129
|
+
`Do not stop -- they are waiting on a reply from you. Answer with hive_say (put @Name on ` +
|
|
130
|
+
`it), then go back to hive_wait. hive_wait is your idle state; ending your turn makes you ` +
|
|
131
|
+
`unreachable and nobody in the room finds out.`,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A DEBT IS NOT THE SAME AS UNREAD MAIL, and this is the gap the watermark alone leaves. The
|
|
136
|
+
// cursor advances the instant a message is handed over, so an agent can read a direct
|
|
137
|
+
// question, get absorbed in the work, and end its turn having left someone hanging -- with
|
|
138
|
+
// nothing unread and nothing to block on. The person waiting has no way to find that out.
|
|
139
|
+
//
|
|
140
|
+
// ONE NUDGE PER QUESTION, EVER -- the same rule mail already follows, and it has to be the
|
|
141
|
+
// same rule. A debt survives until it is answered, so anything weaker than remembering which
|
|
142
|
+
// ones we already raised means the identical question blocks turn after turn, and a gate that
|
|
143
|
+
// nags is a gate the user switches off. Keyed on sender+timestamp rather than on sender, so a
|
|
144
|
+
// NEW question from someone we have already nudged about still gets its own one nudge.
|
|
145
|
+
const owed = unansweredFor(msgs, me)
|
|
146
|
+
const unnudged = owed.filter((o) => !nudged.includes(o.from + '|' + o.at))
|
|
147
|
+
if (unnudged.length) {
|
|
148
|
+
const quoted = unnudged.slice(0, 3).map((o) => ` ${o.from}: ${o.text.replace(/\s+/g, ' ').slice(0, 200)}`)
|
|
149
|
+
return {
|
|
150
|
+
block: true, seen: nowSeen, taskBlocks,
|
|
151
|
+
// Recorded BEFORE returning, exactly as the watermark is, so a question cannot block a
|
|
152
|
+
// second time even if whatever happens next goes wrong. Capped so the state file cannot
|
|
153
|
+
// grow without bound in a long-lived room.
|
|
154
|
+
nudged: [...nudged, ...unnudged.map((o) => o.from + '|' + o.at)].slice(-200),
|
|
155
|
+
reason: `You are in a Hivecode room and ${unnudged.length} participant${unnudged.length > 1 ? 's are' : ' is'} ` +
|
|
156
|
+
`still waiting on a reply from you:\n\n${quoted.join('\n')}\n\n` +
|
|
157
|
+
`Answer with hive_say and name them. "Not yet — I am on X, I will come back to you" is a ` +
|
|
158
|
+
`complete answer; silence is the one thing they cannot act on, and nothing tells them you ` +
|
|
159
|
+
`stopped. Then call hive_wait.`,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const open = openTasksFor(tasks, me)
|
|
164
|
+
if (open.length && taskBlocks < MAX_TASK_BLOCKS) {
|
|
165
|
+
const quoted = open.slice(0, 3).map((t) => ` [${t.id}] from ${t.from}: ${t.text.replace(/\s+/g, ' ').slice(0, 220)}`)
|
|
166
|
+
return {
|
|
167
|
+
block: true, seen: nowSeen, taskBlocks: taskBlocks + 1, nudged,
|
|
168
|
+
reason: `You are in a Hivecode room with ${open.length} accepted task${open.length > 1 ? 's' : ''} ` +
|
|
169
|
+
`still open:\n\n${quoted.join('\n')}\n\n` +
|
|
170
|
+
`Finish the work and call hive_complete with the task id, or say why you are blocked with ` +
|
|
171
|
+
`hive_say (@ the person who assigned it). Then call hive_wait. If you genuinely cannot ` +
|
|
172
|
+
`proceed, say so in the room first -- do not just stop, nobody would learn that you had.`,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Standing down on something we have already blocked on MAX_TASK_BLOCKS times is not the same
|
|
177
|
+
// as it being resolved, so hold the counter rather than resetting it while it is still there.
|
|
178
|
+
// Debts and tasks share the counter deliberately: they are the same kind of obligation, and
|
|
179
|
+
// two separate counters would let an agent be pinned for 2x MAX_TASK_BLOCKS turns.
|
|
180
|
+
return { block: false, seen: nowSeen, taskBlocks: open.length ? taskBlocks : 0, nudged }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Walk up from `start` looking for the session file; the hook's cwd is usually but not always
|
|
184
|
+
* the folder the MCP server was launched in. */
|
|
185
|
+
function findSession(start) {
|
|
186
|
+
let dir = path.resolve(start)
|
|
187
|
+
for (let i = 0; i < 4; i++) {
|
|
188
|
+
const p = path.join(dir, SESSION_FILE)
|
|
189
|
+
if (fs.existsSync(p)) return { root: dir, session: readJson(p) }
|
|
190
|
+
const up = path.dirname(dir)
|
|
191
|
+
if (up === dir) break
|
|
192
|
+
dir = up
|
|
193
|
+
}
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function main() {
|
|
198
|
+
let input = ''
|
|
199
|
+
for await (const chunk of process.stdin) input += chunk
|
|
200
|
+
const hook = (() => { try { return JSON.parse(input) } catch { return {} } })()
|
|
201
|
+
|
|
202
|
+
// GUARD 1: this stop is already the product of a block. Never stack another on it.
|
|
203
|
+
if (hook.stop_hook_active) return
|
|
204
|
+
|
|
205
|
+
const found = findSession(hook.cwd || process.cwd())
|
|
206
|
+
if (!found || !found.session || !found.session.dir || !found.session.name) return // not in a room
|
|
207
|
+
|
|
208
|
+
const { root, session } = found
|
|
209
|
+
const dir = path.isAbsolute(session.dir) ? session.dir : path.join(root, session.dir)
|
|
210
|
+
const read = (f) => { try { return fs.readFileSync(path.join(dir, f), 'utf8') } catch { return '' } }
|
|
211
|
+
|
|
212
|
+
const chat = read('HIVE_CHAT.md')
|
|
213
|
+
if (!chat) return // room folder not mirrored yet; nothing to be sure about, so allow
|
|
214
|
+
|
|
215
|
+
const statePath = path.join(root, STATE_FILE)
|
|
216
|
+
const prev = readJson(statePath) || {}
|
|
217
|
+
// Keyed by room: a folder rejoined against a different room must not inherit a watermark from
|
|
218
|
+
// unrelated history -- the same reasoning as the persisted sync ancestor being room-keyed.
|
|
219
|
+
const key = String(session.room || session.dir)
|
|
220
|
+
const st = prev[key] || { seen: 0, taskBlocks: 0, nudged: [] }
|
|
221
|
+
|
|
222
|
+
const d = decide({
|
|
223
|
+
chat, tasks: read('HIVE_TASKS.md'), me: session.name,
|
|
224
|
+
seen: Number(st.seen) || 0, taskBlocks: Number(st.taskBlocks) || 0,
|
|
225
|
+
nudged: Array.isArray(st.nudged) ? st.nudged : [],
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// GUARD 2: write the watermark BEFORE returning the block, so this message can never block a
|
|
229
|
+
// second time even if what happens next goes wrong.
|
|
230
|
+
try {
|
|
231
|
+
prev[key] = { seen: d.seen, taskBlocks: d.taskBlocks, nudged: d.nudged || [], at: Date.now() }
|
|
232
|
+
fs.writeFileSync(statePath, JSON.stringify(prev, null, 2))
|
|
233
|
+
} catch { /* a watermark we cannot persist is a reason to allow, never to fail */ }
|
|
234
|
+
|
|
235
|
+
if (d.block) process.stdout.write(JSON.stringify({ decision: 'block', reason: d.reason }))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Only run when executed as the hook, so the test can import decide()/parseChat() cleanly.
|
|
239
|
+
if (process.argv[1] && /hive-stop-hook\.js$/.test(process.argv[1])) {
|
|
240
|
+
main().catch(() => { }).finally(() => process.exit(0))
|
|
241
|
+
}
|
package/hive-throttle.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// hive-throttle.js — say it again only if they can no longer see it.
|
|
2
|
+
//
|
|
3
|
+
// TWO OPPOSITE WASTES, ONE CAUSE. Hivecode repeats some blocks to an agent that still has them
|
|
4
|
+
// verbatim, and never repeats others to an agent that lost them hours ago. Both are the same
|
|
5
|
+
// missing question: *is this still in their context?*
|
|
6
|
+
//
|
|
7
|
+
// [GLOBAL PROJECT STATE] rebuilt and returned on EVERY hive_wait, unchanged, forever.
|
|
8
|
+
// An agent waiting in a loop is handed the same JSON hundreds of
|
|
9
|
+
// times, and pays for it hundreds of times.
|
|
10
|
+
//
|
|
11
|
+
// HIVE_RULES.md 6,288 bytes handed over ONCE, in the hive_join tool result. It sits
|
|
12
|
+
// in the transcript, so it is exactly what compaction throws away
|
|
13
|
+
// first -- and it is the ROOM-specific half of the rules: lanes, gate
|
|
14
|
+
// commands, who owns what. Six hours in, it is simply gone.
|
|
15
|
+
//
|
|
16
|
+
// WHAT IS *NOT* IN SCOPE, AND THIS WAS WORTH CHECKING BEFORE WRITING ANYTHING. `INSTRUCTIONS`
|
|
17
|
+
// (2,477 chars) is passed to `new Server(..., { instructions })`, which puts it in the agent's
|
|
18
|
+
// SYSTEM PROMPT. A system prompt is not part of the compactable transcript, so those rules are
|
|
19
|
+
// present on every single turn and re-sending them would be the precise waste this file exists
|
|
20
|
+
// to remove. Half of an earlier draft of this module did exactly that.
|
|
21
|
+
//
|
|
22
|
+
// THE PROXY, AND ITS HONEST LIMIT. There is no way to ask an agent what is still in its context.
|
|
23
|
+
// Agent Relay's broker faces the same wall and answers it with a measurement it *does* have --
|
|
24
|
+
// bytes of PTY output since the last reminder:
|
|
25
|
+
//
|
|
26
|
+
// "Output volume is the proxy for 'the agent actually worked and may have compacted the
|
|
27
|
+
// earlier reminder out of its context'. An agent that only sat at the prompt receiving
|
|
28
|
+
// messages still has the previous reminder verbatim in context, so repeating it only
|
|
29
|
+
// burns tokens." -- crates/broker/src/broker/injection_format.rs
|
|
30
|
+
//
|
|
31
|
+
// There is no PTY here. The equivalent measurement is the one thing this server controls exactly:
|
|
32
|
+
// BYTES IT HAS HANDED THE AGENT since the last send. Every tool result goes out through one
|
|
33
|
+
// `text()`, so the count is precise rather than sampled.
|
|
34
|
+
//
|
|
35
|
+
// This is a proxy and nothing more. It does not detect compaction; it estimates that enough has
|
|
36
|
+
// happened for compaction to be plausible. It will sometimes repeat something still in context,
|
|
37
|
+
// and sometimes stay quiet about something already gone. Both failures are cheap. Pretending the
|
|
38
|
+
// number means more than it does would not be.
|
|
39
|
+
//
|
|
40
|
+
// THREE RULES, IN PRIORITY ORDER, AND THE ORDER IS THE DESIGN:
|
|
41
|
+
//
|
|
42
|
+
// 1. never sent -> SEND. They have not seen it.
|
|
43
|
+
// 2. content changed -> SEND. Correctness outranks economy: a suppressed state change is a
|
|
44
|
+
// wrong answer, while a repeated one is only a cost.
|
|
45
|
+
// 3. otherwise -> send only if BOTH the cooldown has elapsed AND enough bytes have gone
|
|
46
|
+
// out. Either gate alone is wrong. Time alone nags an idle agent that
|
|
47
|
+
// still has the text. Bytes alone fires mid-burst on an agent that read
|
|
48
|
+
// it ninety seconds ago.
|
|
49
|
+
|
|
50
|
+
/** Long enough that a burst of work does not trigger a repeat, short enough to catch a stall. */
|
|
51
|
+
export const COOLDOWN_MS = 5 * 60000
|
|
52
|
+
|
|
53
|
+
/** Agent Relay's number, and a reasonable one: ~16k tokens of traffic since they last saw it. */
|
|
54
|
+
export const OUTPUT_BYTES = 64 * 1024
|
|
55
|
+
|
|
56
|
+
/** Cheap, order-insensitive change detection. Not a hash -- a hash would be more code for a
|
|
57
|
+
* comparison that is already exact on the strings these callers pass. */
|
|
58
|
+
const norm = (c) => (typeof c === 'string' ? c : JSON.stringify(c === undefined ? null : c))
|
|
59
|
+
|
|
60
|
+
export function makeThrottle({ now = Date.now, cooldownMs = COOLDOWN_MS, bytes = OUTPUT_BYTES } = {}) {
|
|
61
|
+
// key -> { at, content, bytesSince, sends }
|
|
62
|
+
const seen = new Map()
|
|
63
|
+
let noted = 0 // total bytes handed to the agent, for reporting only
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Record output handed to the agent.
|
|
67
|
+
*
|
|
68
|
+
* Counting for a key starts only once that key has been sent at least once -- the volume only
|
|
69
|
+
* means anything relative to "since you last saw this". Agent Relay makes the same choice and
|
|
70
|
+
* for the same reason; a counter running before the first send would make the first repeat fire
|
|
71
|
+
* immediately on a busy agent.
|
|
72
|
+
*/
|
|
73
|
+
function note(n) {
|
|
74
|
+
const add = Math.max(0, Number(n) || 0)
|
|
75
|
+
noted += add
|
|
76
|
+
for (const e of seen.values()) e.bytesSince += add
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function should(key, content) {
|
|
80
|
+
const e = seen.get(key)
|
|
81
|
+
if (!e) return { send: true, why: 'first' }
|
|
82
|
+
if (norm(content) !== e.content) return { send: true, why: 'changed' }
|
|
83
|
+
const waited = now() - e.at
|
|
84
|
+
if (waited < cooldownMs) return { send: false, why: 'recent', waited }
|
|
85
|
+
if (e.bytesSince < bytes) return { send: false, why: 'quiet', bytesSince: e.bytesSince }
|
|
86
|
+
return { send: true, why: 'likely-compacted', waited, bytesSince: e.bytesSince }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sent(key, content) {
|
|
90
|
+
const prev = seen.get(key)
|
|
91
|
+
seen.set(key, { at: now(), content: norm(content), bytesSince: 0, sends: (prev ? prev.sends : 0) + 1 })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** should() and sent() in one call, for the common site: `if (send(k, c)) include(c)`. */
|
|
95
|
+
function send(key, content) {
|
|
96
|
+
const d = should(key, content)
|
|
97
|
+
if (d.send) sent(key, content)
|
|
98
|
+
return d.send
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** What was actually saved, so the claim is measured rather than asserted. */
|
|
102
|
+
function stats() {
|
|
103
|
+
const out = { keys: {}, bytesHandled: noted }
|
|
104
|
+
for (const [k, e] of seen.entries()) out.keys[k] = { sends: e.sends, bytesSince: e.bytesSince }
|
|
105
|
+
return out
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { note, should, sent, send, stats }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Cheap enough to be worth repeating; says nothing an agent's system prompt already carries.
|
|
112
|
+
* Deliberately a POINTER, not a copy: the file is on the agent's own disk, and a second
|
|
113
|
+
* hand-written summary of it here would be one more description to drift. */
|
|
114
|
+
export const RULES_NUDGE =
|
|
115
|
+
'[ROOM RULES] You have been working a while — the room rules you were handed when you joined ' +
|
|
116
|
+
'are probably out of your context by now. They are ROOM-specific (lanes, gate command, who ' +
|
|
117
|
+
'owns what), so the general rules you still have do not cover them. Re-read HIVE_RULES.md in ' +
|
|
118
|
+
'your own folder before your next claim.'
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hivecode-mcp",
|
|
3
|
-
"version": "0.11.
|
|
4
|
-
"description": "MCP server that lets any AI coding agent join a Hivecode room
|
|
3
|
+
"version": "0.11.6",
|
|
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,19 @@
|
|
|
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",
|
|
30
31
|
"hive-mailbox.js",
|
|
32
|
+
"hive-obligations.js",
|
|
33
|
+
"hive-receipts.js",
|
|
34
|
+
"hive-throttle.js",
|
|
35
|
+
"hive-instructions.js",
|
|
36
|
+
"hive-busy.js",
|
|
37
|
+
"hive-stop-hook.js",
|
|
38
|
+
"hive-hook-install.js",
|
|
31
39
|
"sync.js",
|
|
32
40
|
"core.js",
|
|
33
41
|
"limits.js",
|
|
@@ -43,6 +51,7 @@
|
|
|
43
51
|
"lang-toml.js",
|
|
44
52
|
"icr-merge.js",
|
|
45
53
|
"hive-coord.js",
|
|
54
|
+
"hive-gate.js",
|
|
46
55
|
"substrate.js",
|
|
47
56
|
"HIVE_RULES.md",
|
|
48
57
|
"docs/MCP.md",
|
|
@@ -58,14 +67,20 @@
|
|
|
58
67
|
"relay": "node server.js",
|
|
59
68
|
"sync:engine": "node sync-engine.mjs",
|
|
60
69
|
"check:engine": "node sync-engine.mjs --check",
|
|
61
|
-
"pretest": "node scripts/sync-engine.mjs --check && node scripts/check-files.mjs",
|
|
70
|
+
"pretest": "node scripts/sync-engine.mjs --check && node scripts/check-files.mjs && node scripts/check-deploy.mjs",
|
|
62
71
|
"test": "node tests/test.js",
|
|
72
|
+
"test:features": "node tests/features-test.mjs",
|
|
73
|
+
"test:features:shape": "node tests/features-test.mjs --shape",
|
|
74
|
+
"test:learnings": "node tests/learnings-test.mjs",
|
|
75
|
+
"test:extension": "node tests/extension-parity-test.mjs",
|
|
76
|
+
"test:ceiling": "node tests/refused-visible-test.mjs && node tests/ceiling-no-data-loss-test.mjs",
|
|
63
77
|
"test:identity": "node tests/identity-audit-test.mjs",
|
|
64
78
|
"test:billing": "node tests/billing-test.mjs",
|
|
65
79
|
"test:license": "node tests/hive-license-test.js",
|
|
66
80
|
"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
81
|
"check:web": "tsc --noEmit",
|
|
68
82
|
"build:web": "esbuild web/src/main.ts --bundle --minify --format=iife --target=es2019 --outfile=public/app.js",
|
|
83
|
+
"build:crdt": "esbuild web/crdt.js --bundle --minify --format=esm --target=es2019 --outfile=public/hive-crdt.js",
|
|
69
84
|
"test:substrate": "node substrate-test.js",
|
|
70
85
|
"test:substrate:live": "node substrate-live-test.js",
|
|
71
86
|
"test:substrate:enforce": "node substrate-enforce-test.js",
|
|
@@ -73,9 +88,10 @@
|
|
|
73
88
|
"test:substrate:all": "node substrate-test.js && node substrate-live-test.js && node substrate-enforce-test.js && node substrate-authority-test.js",
|
|
74
89
|
"test:recover": "node tests/recover-test.mjs",
|
|
75
90
|
"test:mailbox": "node tests/mailbox-test.mjs && node tests/mcp-wait-test.mjs",
|
|
91
|
+
"test:stophook": "node tests/stop-hook-test.mjs",
|
|
76
92
|
"test:limits": "node tests/limits-test.mjs",
|
|
77
93
|
"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",
|
|
94
|
+
"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
95
|
"test:meter": "node tests/usage-meter-test.mjs",
|
|
80
96
|
"test:subscribe": "node tests/subscribe-test.mjs",
|
|
81
97
|
"test:cred": "node tests/credential-split-test.mjs",
|
|
@@ -89,7 +105,13 @@
|
|
|
89
105
|
"demo:all": "node demo/record.mjs && node demo/record-panel.mjs",
|
|
90
106
|
"check:files": "node scripts/check-files.mjs",
|
|
91
107
|
"prepublishOnly": "node scripts/check-files.mjs",
|
|
92
|
-
"demo:cast": "node demo/cast.mjs --dry-run"
|
|
108
|
+
"demo:cast": "node demo/cast.mjs --dry-run",
|
|
109
|
+
"check:deploy": "node scripts/check-deploy.mjs",
|
|
110
|
+
"test:coordination": "node tests/coordination-test.mjs",
|
|
111
|
+
"test:panel": "node tests/control-panel-test.mjs",
|
|
112
|
+
"test:map": "node tests/hive-map-test.mjs",
|
|
113
|
+
"test:gate": "node tests/hive-gate-test.mjs",
|
|
114
|
+
"bench:ab": "node bench/coordination-ab.mjs"
|
|
93
115
|
},
|
|
94
116
|
"engines": {
|
|
95
117
|
"node": ">=18"
|
|
@@ -99,6 +121,7 @@
|
|
|
99
121
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
100
122
|
"acorn": "^8.17.0",
|
|
101
123
|
"ignore": "^7.0.5",
|
|
124
|
+
"lib0": "^0.2.117",
|
|
102
125
|
"ws": "^8.18.0",
|
|
103
126
|
"y-websocket": "^2.0.4",
|
|
104
127
|
"yjs": "^13.6.18",
|