hivecode-mcp 0.11.5 → 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 +433 -12
package/hive-mailbox.js
CHANGED
|
@@ -28,14 +28,227 @@
|
|
|
28
28
|
*/
|
|
29
29
|
export function mentionsName(text, me) {
|
|
30
30
|
if (!text || !me) return false
|
|
31
|
+
if (mentionsAll(text)) return true // @all reaches everyone by definition
|
|
31
32
|
const escaped = String(me).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
32
|
-
//
|
|
33
|
-
|
|
33
|
+
// Both boundaries, and the same ones the extractor uses. If these two ever disagree, a message
|
|
34
|
+
// can be extracted as addressing someone who does not match it, or match someone it was never
|
|
35
|
+
// extracted for -- which is how a mailbox and a transcript come to tell different stories.
|
|
36
|
+
return new RegExp(`${MENTION_START}@${escaped}${MENTION_END}`, 'i').test(String(text))
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
/**
|
|
40
|
+
* `@all` — the deliberate broadcast.
|
|
41
|
+
*
|
|
42
|
+
* Before this there was no way to reach the whole room on purpose. An unaddressed message goes
|
|
43
|
+
* into every agent's ambient digest, where the rules tell them to skip it -- correct for
|
|
44
|
+
* chatter, useless for "everyone stop, the suite is red". So the only two options were name
|
|
45
|
+
* eight agents by hand or be ignored.
|
|
46
|
+
*
|
|
47
|
+
* It wakes everyone. It deliberately does NOT create a reply debt (see unansweredFor): one
|
|
48
|
+
* broadcast obliging eight replies is how a coordination channel becomes a spam channel, and
|
|
49
|
+
* that is the exact failure this room already had once.
|
|
50
|
+
*/
|
|
51
|
+
export function mentionsAll(text) {
|
|
52
|
+
// CODE SPANS ARE STRIPPED FIRST, AND ONLY HERE. Every other false mention is handled by the
|
|
53
|
+
// boundary rules below plus `obligationTargets`, which refuses to open a debt against somebody
|
|
54
|
+
// who is not in the room. `@all` is the one case neither can save: it is not a member, so the
|
|
55
|
+
// membership gate does not apply to it, and it wakes EVERYONE. So writing `@all` inside
|
|
56
|
+
// backticks -- which is how anybody documents this feature -- paged the entire room, every time.
|
|
57
|
+
//
|
|
58
|
+
// This is deliberately NOT a Markdown parser. Nested spans, unclosed fences and indented blocks
|
|
59
|
+
// are all out of scope, because the question is not "what is code here" but the much smaller
|
|
60
|
+
// "is this one token wrapped in backticks". A tokenizer would be the wrong tool and a permanent
|
|
61
|
+
// source of edge cases; one replace is the whole fix.
|
|
62
|
+
return /@all(?![\w-])/i.test(stripCodeSpans(text))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Inline code spans blanked out, so a token quoted as code is not read as an address. */
|
|
66
|
+
const stripCodeSpans = (text) => String(text || '').replace(/`[^`]*`/g, ' ')
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* WHERE A MENTION MAY BEGIN, AND WHERE IT MAY NOT.
|
|
70
|
+
*
|
|
71
|
+
* Measured in room `night` on 20 Sep 2026, against the old bare /@([\w-]+)/g:
|
|
72
|
+
*
|
|
73
|
+
* "email me at ganesh@gmail.com" -> ["gmail"] every email address
|
|
74
|
+
* "blame line: author@example.org" -> ["example"] every commit trailer
|
|
75
|
+
* "npm i @modelcontextprotocol/sdk" -> ["modelcontextprotocol"] every scoped package
|
|
76
|
+
* "see `@all` in the docs" -> broadcast to everyone every doc about this feature
|
|
77
|
+
*
|
|
78
|
+
* After three hours the obligation ledger held 44 entries of which 32% were debts against members
|
|
79
|
+
* who had never existed, and a debt can only be discharged by the asker, so nothing in the system
|
|
80
|
+
* could ever have closed them. The ledger is what catches an agent going silent; it was becoming
|
|
81
|
+
* unreadable through ordinary use.
|
|
82
|
+
*
|
|
83
|
+
* LEADING: start of string, whitespace, an opening bracket, a quote, a dash, or a colon. Quotes
|
|
84
|
+
* are INCLUDED on purpose. A quoted example ("the parser reads "@antigravity"") is then extracted
|
|
85
|
+
* for the transcript and creates no debt, because `obligationTargets` gates on membership. The
|
|
86
|
+
* opposite choice would silently drop a real mention that opens a quoted sentence, and a dropped
|
|
87
|
+
* mention is the worse failure: the sender believes it was delivered.
|
|
88
|
+
*
|
|
89
|
+
* TRAILING: not a word character, a hyphen, or a SLASH. The slash is what distinguishes
|
|
90
|
+
* `@scope/package` from `@person`. It is written as one lookahead rather than a separate check so
|
|
91
|
+
* the regex cannot backtrack into a shorter name to satisfy it -- `[\w-]+` followed by `(?!\/)`
|
|
92
|
+
* alone would happily match "modelcontextprotoco" and declare victory.
|
|
93
|
+
*/
|
|
94
|
+
const MENTION_START = '(?:^|[\\s(\\[{<"\'—–:])'
|
|
95
|
+
const MENTION_END = '(?![\\w\\-/])'
|
|
96
|
+
|
|
36
97
|
/** Everyone addressed in a message, so a broadcast can be told from a direct call. */
|
|
37
98
|
export function mentionedNames(text) {
|
|
38
|
-
|
|
99
|
+
const re = new RegExp(`${MENTION_START}@([\\w-]+)${MENTION_END}`, 'g')
|
|
100
|
+
return [...new Set([...String(text || '').matchAll(re)].map((m) => m[1]))]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* WHO ACTUALLY OWES AN ANSWER — which is not the same question as who was named.
|
|
105
|
+
*
|
|
106
|
+
* Extraction and obligation were one decision and they are two. A transcript should show what the
|
|
107
|
+
* text said, including a name that is merely quoted; a DEBT is a claim about the present, and
|
|
108
|
+
* opening one against somebody who is not here produces an entry nobody can ever discharge, since
|
|
109
|
+
* only the asker may close it and the debtor does not exist.
|
|
110
|
+
*
|
|
111
|
+
* The distinction that makes this safe is which way time points. Resolving a HISTORICAL mention
|
|
112
|
+
* against who is present now would be the Historical Transcript Fallacy -- the past decided by the
|
|
113
|
+
* present -- and that is why extraction deliberately does NOT do it. An obligation is not history:
|
|
114
|
+
* it is live state about who owes me an answer right now, so checking present membership is
|
|
115
|
+
* exactly the right question for it.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} text
|
|
118
|
+
* @param {{ me?: string, present?: Iterable<string> }} ctx `present` is lowercased names
|
|
119
|
+
*/
|
|
120
|
+
export function obligationTargets(text, { me = '', present = [] } = {}) {
|
|
121
|
+
const here = new Set([...present].map((n) => String(n).toLowerCase()))
|
|
122
|
+
const self = String(me).toLowerCase()
|
|
123
|
+
return mentionedNames(text).filter((n) => {
|
|
124
|
+
const low = n.toLowerCase()
|
|
125
|
+
// @all wakes the room and obliges nobody -- one broadcast owing eight replies is how a
|
|
126
|
+
// coordination channel becomes a spam channel. Unchanged from the original behaviour.
|
|
127
|
+
if (low === 'all' || low === self) return false
|
|
128
|
+
return here.has(low)
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A NAME THAT CANNOT BE ADDRESSED IS NOT A NAME. Refuse it at the door.
|
|
134
|
+
*
|
|
135
|
+
* On 20 Sep 2026 an agent joined room `night` as "antigravity 1". `mentionedNames` above splits
|
|
136
|
+
* on /@([\w-]+)/g, so "@antigravity 1" reads as "@antigravity", and no such member existed. Its
|
|
137
|
+
* mail, its obligations and its wake-on-mention were all silently dead for an hour. It saw
|
|
138
|
+
* messages only because it happened to be tailing the chat log. Nothing errored; the room said
|
|
139
|
+
* "@antigravity is NOT in the room", which was true and useless, because the member WAS present
|
|
140
|
+
* under a name this parser cannot produce.
|
|
141
|
+
*
|
|
142
|
+
* WHY REFUSE RATHER THAN NORMALISE. Silently rewriting "coder 1" to "coder-1" collides with a
|
|
143
|
+
* real "coder-1" and merges two processes into one participant — one awareness entry, one cursor,
|
|
144
|
+
* one mailbox — with neither told. And the agent's own prompt still says its name is the original,
|
|
145
|
+
* so it stops recognising messages about itself.
|
|
146
|
+
*
|
|
147
|
+
* WHY REFUSE RATHER THAN RESOLVE @mentions AGAINST THE MEMBER LIST. That would make a departed
|
|
148
|
+
* member break the historical record: a fact about the past decided by who happens to be present
|
|
149
|
+
* now. Same defect as the receipts work in HIV-P2.
|
|
150
|
+
*
|
|
151
|
+
* WHY THIS CHARSET AND NOT A NICER ONE. It is not taste, it is arithmetic. The extractor above
|
|
152
|
+
* only recognises [\w-]. A name outside that set CANNOT be produced by it, however carefully the
|
|
153
|
+
* sender types. Constraining names to the charset the parser already uses makes the two
|
|
154
|
+
* consistent by construction instead of by coincidence.
|
|
155
|
+
*
|
|
156
|
+
* KNOWN LIMIT, stated because it is a product decision and not an oversight: `\w` is ASCII, so a
|
|
157
|
+
* name in Devanagari, Han or Cyrillic is refused. That is honest rather than good — those names
|
|
158
|
+
* are equally unaddressable today, and refusing at least says so out loud. Widening it means
|
|
159
|
+
* widening the extractor first; the two move together or not at all.
|
|
160
|
+
*
|
|
161
|
+
* @param {any} name
|
|
162
|
+
* @returns {{ ok: boolean, error?: string, suggested?: string }}
|
|
163
|
+
*/
|
|
164
|
+
export function validateMemberName(name) {
|
|
165
|
+
if (name == null || typeof name !== 'string' || name.length === 0) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
error: 'Member name must be a non-empty string so that the participant can be addressed via @mentions.',
|
|
169
|
+
suggested: 'agent-1',
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Whitespace is checked BEFORE the charset so the message names the actual problem. "contains
|
|
173
|
+
// whitespace" tells you what to do; "contains invalid characters" makes you hunt for which one.
|
|
174
|
+
if (/\s/.test(name)) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
error: `Member name "${name}" contains whitespace. Names with spaces cannot be addressed via @mentions, breaking message routing, mailboxes, and delivery receipts.`,
|
|
178
|
+
suggested: suggestMemberName(name),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!/^[\w-]+$/.test(name)) {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
error: `Member name "${name}" contains invalid characters. Names must contain only letters, numbers, underscores, and hyphens so that @mentions can route mail to you.`,
|
|
185
|
+
suggested: suggestMemberName(name),
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { ok: true }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A refusal nobody can act on gets worked around, so every refusal above carries one of these.
|
|
193
|
+
*
|
|
194
|
+
* It deliberately validates its own output before returning it: a suggestion that is itself
|
|
195
|
+
* invalid is worse than no suggestion, because the caller retries, fails again, and now distrusts
|
|
196
|
+
* the error message. Whitespace-only and non-ASCII names both collapse to nothing here, which is
|
|
197
|
+
* why the fallback exists rather than being defensive padding.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} name
|
|
200
|
+
* @returns {string}
|
|
201
|
+
*/
|
|
202
|
+
export function suggestMemberName(name) {
|
|
203
|
+
if (!name || typeof name !== 'string') return 'agent-1'
|
|
204
|
+
let s = name.replace(/[\s]+/g, '-').replace(/[^\w-]+/g, '-')
|
|
205
|
+
s = s.replace(/-+/g, '-').replace(/^[-_]+|[-_]+$/g, '')
|
|
206
|
+
return /^[\w-]+$/.test(s) ? s : 'agent-1'
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Who has directly addressed `me` and not been answered since?
|
|
211
|
+
*
|
|
212
|
+
* "Answered" is deliberately cheap to satisfy: any later message from me that names them. It is
|
|
213
|
+
* not trying to judge whether the reply was any good -- only that the room never goes silent on
|
|
214
|
+
* someone who asked a direct question, which is the failure that makes agents give up on the
|
|
215
|
+
* channel and start tailing files instead.
|
|
216
|
+
*
|
|
217
|
+
* Derived from the chat log rather than stored, so it cannot drift out of sync with what was
|
|
218
|
+
* actually said, and needs no new state to persist or migrate.
|
|
219
|
+
*
|
|
220
|
+
* THREE THINGS DO NOT COUNT AS A DEBT, all for the same reason -- an obligation that fires on
|
|
221
|
+
* traffic nobody chose to send is noise wearing a rule's clothes:
|
|
222
|
+
* - `@all` broadcasts (see mentionsAll)
|
|
223
|
+
* - system messages, and the client's own auto-posted conflict/adopt notices
|
|
224
|
+
* - my own messages, however many times I say my own name
|
|
225
|
+
*
|
|
226
|
+
* @param {Array<{by?: string, kind?: string, text?: string, at?: string}>} messages
|
|
227
|
+
* @param {string} me
|
|
228
|
+
* @returns {Array<{from: string, at: string, text: string}>} oldest first
|
|
229
|
+
*/
|
|
230
|
+
export function unansweredFor(messages, me) {
|
|
231
|
+
const list = Array.isArray(messages) ? messages : []
|
|
232
|
+
if (!me) return []
|
|
233
|
+
const low = (s) => String(s || '').toLowerCase()
|
|
234
|
+
const mine = low(me)
|
|
235
|
+
const AUTO = /^(⚠ MERGE CONFLICT|✓ conflict|↓ )/ // posted by the sync client, not by a person
|
|
236
|
+
|
|
237
|
+
const owed = new Map() // sender (lowercased) -> the message I owe a reply to
|
|
238
|
+
for (const m of list) {
|
|
239
|
+
if (!m || m.kind === 'system' || AUTO.test(String(m.text || ''))) continue
|
|
240
|
+
if (low(m.by) === mine) {
|
|
241
|
+
// Anything I name is discharged. A broadcast from me discharges everything: I have
|
|
242
|
+
// spoken to the room, and holding a debt open after that is pedantry, not coordination.
|
|
243
|
+
if (mentionsAll(m.text)) { owed.clear(); continue }
|
|
244
|
+
for (const n of mentionedNames(m.text)) owed.delete(low(n))
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
// Directly addressed, not merely broadcast to.
|
|
248
|
+
if (mentionsAll(m.text)) continue
|
|
249
|
+
if (mentionsName(m.text, me)) owed.set(low(m.by), { from: m.by, at: m.at || '', text: m.text || '' })
|
|
250
|
+
}
|
|
251
|
+
return [...owed.values()]
|
|
39
252
|
}
|
|
40
253
|
|
|
41
254
|
/**
|