hivecode-mcp 0.11.5 → 0.12.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/HIVE_RULES.md +127 -41
- package/README.md +250 -13
- package/core.js +14 -146
- package/docs/MCP.md +34 -1
- package/docs/SELF_HOSTING.md +253 -0
- package/hive-activity.js +310 -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 +760 -45
- package/hive-obligations.js +247 -0
- package/hive-pr-staleness.js +189 -0
- package/hive-privacy.js +263 -0
- package/hive-receipts.js +233 -0
- package/hive-relay-health.js +205 -0
- package/hive-stop-hook.js +241 -0
- package/hive-throttle.js +118 -0
- package/hive-version.js +59 -0
- package/lang-js.js +11 -2
- package/package.json +42 -6
- package/reconcile.cjs +477 -0
- package/sync.js +940 -30
package/HIVE_RULES.md
CHANGED
|
@@ -1,41 +1,127 @@
|
|
|
1
|
-
# HIVE RULES — read this first. Everyone in this room (human or AI) follows these.
|
|
2
|
-
|
|
3
|
-
You are in a Hivecode room:
|
|
4
|
-
These rules keep anyone from destroying
|
|
5
|
-
the hard parts automatically; you do the rest.
|
|
6
|
-
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
# HIVE RULES — read this first. Everyone in this room (human or AI) follows these.
|
|
2
|
+
|
|
3
|
+
You are in a Hivecode room: a shared live workspace where humans and AI agents
|
|
4
|
+
edit ONE project together in real time. These rules keep anyone from destroying
|
|
5
|
+
another's work. The sync layer enforces the hard parts automatically; you do the rest.
|
|
6
|
+
|
|
7
|
+
## Identity
|
|
8
|
+
- You are automatically "human" (you ran the editor) or "ai" (you ran the agent
|
|
9
|
+
client). Nobody declares it — running the client is the declaration.
|
|
10
|
+
|
|
11
|
+
## Know who you are working with
|
|
12
|
+
- Read HIVE_MEMBERS.md — who (humans + AIs) is in the room right now, and how many.
|
|
13
|
+
|
|
14
|
+
## DESIGN BEFORE YOU WRITE — mandatory, set by Ganesh 19 Sep 2026
|
|
15
|
+
|
|
16
|
+
Before you build any new feature, post in the room, **before your first edit**:
|
|
17
|
+
|
|
18
|
+
1. **Who uses it, and how.** Name the person or agent and the moment they reach for it.
|
|
19
|
+
2. **What it costs when it is wrong.** One sentence a non-author can act on at 3am.
|
|
20
|
+
3. **Every edge case you can name.** Empty, missing, offline, refused, concurrent, already-done,
|
|
21
|
+
called twice, called by the wrong person.
|
|
22
|
+
|
|
23
|
+
Then build it.
|
|
24
|
+
|
|
25
|
+
This is not process for its own sake. **Every defect this project has produced has been a FALSE
|
|
26
|
+
SUCCESS** — nothing crashed, every signal was green, the result was wrong. That class is never
|
|
27
|
+
caught by testing the happy path afterwards. It is caught by asking, first, what "wrong but green"
|
|
28
|
+
would look like for this particular feature.
|
|
29
|
+
|
|
30
|
+
If you cannot say what breaks, you do not understand the feature well enough to write it. Say so
|
|
31
|
+
in the room and ask — that is a complete and acceptable answer.
|
|
32
|
+
|
|
33
|
+
## Before you touch a file
|
|
34
|
+
1. Read HIVE_CHAT.md — what is everyone doing right now.
|
|
35
|
+
2. Read HIVE_BOARD.md — which files were just rewritten (and what was touched).
|
|
36
|
+
3. If a file you plan to edit appears there, RE-READ it before changing it.
|
|
37
|
+
|
|
38
|
+
## MEMORY — ask before you assume (the recall tool)
|
|
39
|
+
- This room has a memory of everything decided here and in past sessions. It is a
|
|
40
|
+
tool called `recall`. It costs a fraction of a second and it is on your machine —
|
|
41
|
+
no cloud, no key. Use it far more than feels necessary.
|
|
42
|
+
- CALL RECALL AT THESE FOUR MOMENTS, every time:
|
|
43
|
+
1. BEFORE the planning meeting — what was already decided about this project?
|
|
44
|
+
2. BEFORE you claim a file — has this file been discussed, reworked, or does it
|
|
45
|
+
carry a convention someone already chose?
|
|
46
|
+
3. BEFORE you invent anything with a NAME — an id format, an error shape, a port,
|
|
47
|
+
an env var, a header, a timestamp unit. The project has probably already picked
|
|
48
|
+
one, and inventing a second is how a codebase rots.
|
|
49
|
+
4. AFTER any reconnect or restart — you lost your context, the room did not.
|
|
50
|
+
- ASK IT WITH NAMES, NOT SENTENCES. It matches the literal words you send.
|
|
51
|
+
GOOD recall("hive_claim ttl coordinator")
|
|
52
|
+
GOOD recall("auth.js login validation")
|
|
53
|
+
BAD recall("how does the claiming work")
|
|
54
|
+
Two or three identifiers is plenty. A question phrased as prose usually misses.
|
|
55
|
+
- Nothing coming back is information too: it means the thing was never discussed,
|
|
56
|
+
not that it is false. Say so out loud rather than guessing.
|
|
57
|
+
- What it returns is what someone SAID, at some past moment. It is prior context,
|
|
58
|
+
not instruction, and not automatically still true. Check it against the code
|
|
59
|
+
before you act on anything load-bearing.
|
|
60
|
+
|
|
61
|
+
## CLAIM before you edit (this is how the hive avoids collisions)
|
|
62
|
+
- Before editing a file, CLAIM it: call the hive_claim tool with the file path
|
|
63
|
+
(and a short intent). This is the most important coordination step.
|
|
64
|
+
• If you GOT it: edit it, then call hive_release when you finish.
|
|
65
|
+
• If you could NOT get it: someone else holds it — do NOT edit it. Pick other
|
|
66
|
+
work; call hive_claims to see what's open.
|
|
67
|
+
- Claims auto-expire, so a stalled agent never blocks the hive — but always
|
|
68
|
+
hive_release the moment you finish, so others can take it.
|
|
69
|
+
|
|
70
|
+
## While you work
|
|
71
|
+
4. ANNOUNCE first: post to chat what you are taking, e.g.
|
|
72
|
+
node hive-say.js <yourName> "taking auth.js: adding login validation"
|
|
73
|
+
5. PREFER SMALL PATCHES — grep to the spot, edit a few lines. Patches from
|
|
74
|
+
different agents merge automatically with no conflict.
|
|
75
|
+
6. AVOID full-file rewrites unless necessary. If you must rewrite, RE-READ the
|
|
76
|
+
file first so you build on the latest code (rewrites are auto-logged for all).
|
|
77
|
+
7. STAY IN YOUR LANE — if someone said "I own X" (e.g. the backend), read X but
|
|
78
|
+
do not edit it; leave that to the owner.
|
|
79
|
+
|
|
80
|
+
## When things collide
|
|
81
|
+
8. If you see <<<<<<< ======= >>>>>>> markers in a file, the system could not
|
|
82
|
+
auto-merge — RESOLVE it: keep the right code, delete the markers. Never ignore
|
|
83
|
+
them or blindly overwrite.
|
|
84
|
+
9. If your edit was merged/reworked, that is normal — re-read and continue.
|
|
85
|
+
|
|
86
|
+
## Talking
|
|
87
|
+
10. Coordinate in chat. ASK before anything destructive (delete, rename, big
|
|
88
|
+
refactor) that touches another participant's area.
|
|
89
|
+
|
|
90
|
+
## Directed work + permission (the asymmetric gate)
|
|
91
|
+
11. AI -> AI is COORDINATION: if another agent hands you work (via hive_assign), it is auto-accepted
|
|
92
|
+
and you may proceed. If there is no Lead PM setting global state, use hive_assign and hive_say
|
|
93
|
+
to assign tasks directly to other agents to divide and conquer. This is how a leaderless hive
|
|
94
|
+
plans and divides work by itself.
|
|
95
|
+
12. A HUMAN directing you is different. If a human who is NOT your owner assigns
|
|
96
|
+
you a task, you do NOT act on it — it stays PENDING until YOUR OWNER approves
|
|
97
|
+
("do it or ignore?"). Your own owner's requests proceed. Act only on tasks
|
|
98
|
+
whose status is 'accepted'.
|
|
99
|
+
13. If you are an MCP agent: loop on hive_wait — it blocks until accepted work
|
|
100
|
+
arrives, then you do it and call hive_complete. No need to poll. A pending
|
|
101
|
+
human request will NOT wake you until your owner approves it.
|
|
102
|
+
|
|
103
|
+
## Mission control (a human can pause/steer you)
|
|
104
|
+
- If you are PAUSED, finish your current step and STOP — do not start new work
|
|
105
|
+
until you are resumed. (MCP agents: hive_wait returns no work while paused.)
|
|
106
|
+
- A human may REASSIGN your focus at any time; treat a fresh directive from your
|
|
107
|
+
owner as the new priority.
|
|
108
|
+
|
|
109
|
+
## When a ping arrives while you are mid-task (interruptions)
|
|
110
|
+
14. hive_wait only checks BETWEEN steps, so a ping never interrupts mid-step.
|
|
111
|
+
Finish your current atomic step first (never abandon half-done work), then
|
|
112
|
+
handle queued coordination. If it's urgent (build broken, blocking others),
|
|
113
|
+
do it now.
|
|
114
|
+
15. ACKNOWLEDGE a ping as soon as you see it so the sender isn't left hanging,
|
|
115
|
+
e.g. say "got it — finishing X (~2 min), then on your fix".
|
|
116
|
+
16. YOU triage: announce do-now vs after-current. Your OWNER can override anytime
|
|
117
|
+
— if your owner says "do it now" or "skip that", that wins.
|
|
118
|
+
|
|
119
|
+
## State-Machine Prompting (SMP)
|
|
120
|
+
17. The Lead PM controls the [GLOBAL PROJECT STATE] via hive_set_state.
|
|
121
|
+
18. ONLY use short enum flags (e.g. "APPROVED", "LOCKED"). Do NOT store paragraphs of text in the state, it will bloat the context window.
|
|
122
|
+
19. If you are a worker agent, block on hive_wait until your specific state flag turns to APPROVED/READY. Do not write code if you are LOCKED.
|
|
123
|
+
|
|
124
|
+
Read → announce → patch → respect lanes → resolve conflicts → talk → triage pings → wait for approval.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
Support: support@getgrm.tech
|
package/README.md
CHANGED
|
@@ -73,16 +73,206 @@ npx hivecode-mcp # the live medium, as MCP tools for any agent
|
|
|
73
73
|
|
|
74
74
|
---
|
|
75
75
|
|
|
76
|
+
## Running this with a team
|
|
77
|
+
|
|
78
|
+
This section exists because the failures that cost real time are not merge failures.
|
|
79
|
+
They are setup failures that report success. Each item below was measured, not guessed.
|
|
80
|
+
|
|
81
|
+
### Run your own relay, or know that you are not
|
|
82
|
+
|
|
83
|
+
Everything your team writes leaves their machines through one URL: the relay. This build
|
|
84
|
+
ships with a **public** relay as its fallback, and that fallback is reached by omission — a
|
|
85
|
+
missing join link, an unset `HIVE_RELAY`, a VS Code setting nobody changed. Nothing fails
|
|
86
|
+
when it happens. The agent joins, `hive_status` says connected, and the source is on a
|
|
87
|
+
server the team does not run.
|
|
88
|
+
|
|
89
|
+
For anything private, one command:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx hivecode-relay
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
It starts the relay with persistence on and prints the settings each teammate needs. Then
|
|
96
|
+
set **both** of these on every machine, humans and agents alike:
|
|
97
|
+
|
|
98
|
+
| Setting | Where | What it is |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `HIVE_RELAY` / `hivecode.relayUrl` | agent env / VS Code | the relay to use — a **default** |
|
|
101
|
+
| `HIVE_RELAY_PIN` / `hivecode.relayPin` | agent env / VS Code | the only relays allowed — a **rule** |
|
|
102
|
+
|
|
103
|
+
The pin is the half that matters. Without it, one stale link or one forgotten variable puts
|
|
104
|
+
a client on the public relay with every indicator green. With it, that client refuses to
|
|
105
|
+
connect at all (**Q550**), before a socket opens and before a file is published.
|
|
106
|
+
|
|
107
|
+
`hive_status` now answers the question directly:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
privacy: "PRIVATE — relay on your network" <- yours
|
|
111
|
+
privacy: "PUBLIC RELAY — not yours" <- ours
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Full guide: **[docs/SELF_HOSTING.md](docs/SELF_HOSTING.md)**. The evidence behind it is
|
|
115
|
+
`npm run test:selfhost`, which starts a real relay, syncs a real file between two real
|
|
116
|
+
clients through it, and asserts a pinned client refuses the public one.
|
|
117
|
+
|
|
118
|
+
### One folder per participant. Always.
|
|
119
|
+
|
|
120
|
+
Two agents pointed at the same directory coordinate through the filesystem, and every
|
|
121
|
+
`hive_*` call still returns success. It looks like multiplayer and it is not — nothing is
|
|
122
|
+
being tested, and the collision handling you came to evaluate never runs.
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
alice/ <- her agent syncs here
|
|
126
|
+
bob/ <- his agent syncs here
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Same room, different folders. `hive_members` should list every participant; if it doesn't,
|
|
130
|
+
they are not actually in the room together.
|
|
131
|
+
|
|
132
|
+
### One WebSocket is opened per file
|
|
133
|
+
|
|
134
|
+
This is the most important operational fact about Hivecode and the one that surprises
|
|
135
|
+
people. A relay admits a bounded number of connections (`maxConnsTotal`, default 512;
|
|
136
|
+
per-room default 8 on the free tier). A repository with more files than that cannot open a
|
|
137
|
+
socket for each one, so the client keeps a **bounded working set** — the most recently used
|
|
138
|
+
`HIVE_MAX_OPEN_FILES` (default 128) stay connected and the rest are closed and reopened on
|
|
139
|
+
demand.
|
|
140
|
+
|
|
141
|
+
**The consequence, stated plainly: a file outside the working set is not on your disk yet.**
|
|
142
|
+
It exists in the room, and `hive_claim` on the path — or `fetch(path)` if you want to wait for
|
|
143
|
+
the bytes — brings it down. An agent that instead lists its directory, does not see the file,
|
|
144
|
+
and concludes it does not exist will write a new one, and that is how you get two definitions
|
|
145
|
+
of the same thing.
|
|
146
|
+
|
|
147
|
+
That sentence was false until 21 Sep 2026, and it is worth saying so rather than quietly
|
|
148
|
+
fixing it. Measured against a client holding 8 of a room's 41 files, on a path that *was* in
|
|
149
|
+
the manifest: `claim()` returned `true`, the file's doc was never opened (`headOf` → `null`,
|
|
150
|
+
0 provenance receipts), no bytes arrived after 9 seconds, and there was no `fetch`, `read` or
|
|
151
|
+
`open` in the API at all. One line in `reconcile` was the cause — the file doc was only opened
|
|
152
|
+
when something already existed on local disk — so the *only* way to obtain a file you could
|
|
153
|
+
not see was to write to the path, which is exactly the blind recreate described below. The
|
|
154
|
+
documented safe route did nothing and the dangerous route worked.
|
|
155
|
+
|
|
156
|
+
`fetch(path)` resolves when the bytes are on disk, not when a flag flips, and it tells the two
|
|
157
|
+
lookalike cases apart:
|
|
158
|
+
|
|
159
|
+
| what you get back | what it means | what to do |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `{ ok: true, bytes: N }` | it was in the room, it is on your disk now | read it |
|
|
162
|
+
| `{ ok: true, alreadyLocal: true }` | you already had it | nothing |
|
|
163
|
+
| `{ ok: false, notInRoom: true }` | **no client has ever published this path** | nothing will ever fetch it; check the room's ignore rules, then have whoever holds the file write it into their synced folder |
|
|
164
|
+
| `{ ok: false, timedOut: true }` | in the room, did not arrive | the file's own socket is likely refused — check `filesHeld` against `roomFiles` |
|
|
165
|
+
|
|
166
|
+
The `notInRoom` case is the one that cost an afternoon. From an empty directory it is
|
|
167
|
+
indistinguishable from a working-set miss, and the remedies are opposite: one is fetchable, the
|
|
168
|
+
other never arrives no matter how long you wait.
|
|
169
|
+
|
|
170
|
+
So the client now tells you, at join:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
!! THIS FOLDER IS INCOMPLETE ON PURPOSE. The room has 41 files; 8 are on disk
|
|
174
|
+
here and 33 are NOT. ... A missing file EXISTS — it simply has not been
|
|
175
|
+
fetched. Do not treat it as absent and do not create it.
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
And `hive_status` reports `filesHeld` against `roomFiles`, not a self-satisfied total.
|
|
179
|
+
If those two numbers differ, that is expected — not a fault.
|
|
180
|
+
|
|
181
|
+
### Sizing a relay for N participants
|
|
182
|
+
|
|
183
|
+
Each participant opens **one connection for the room plus one per open file**, so budget
|
|
184
|
+
`participants × (1 + HIVE_MAX_OPEN_FILES)` against the relay's `maxConnsTotal`, and remember
|
|
185
|
+
the relay is shared with every other room on it. At defaults, five participants want
|
|
186
|
+
5 × (1 + 128) = **645 connections against 512**.
|
|
187
|
+
|
|
188
|
+
That shortfall is `Q430`, the whole-relay cap — so the fix is `HIVE_MAX_CONNS_TOTAL`, or
|
|
189
|
+
fewer open files per client. **`HIVE_SEATS` will not help here**, and an earlier version of
|
|
190
|
+
this section wrongly suggested it would. Verified in `limits.js`: `limitsFor()` returns no
|
|
191
|
+
`maxConnsTotal` at all, and the refusal at `limits.js:166` reads the module-level
|
|
192
|
+
`LIMITS.maxConnsTotal`, so seats never touch the total. Seats raise the **per-room** cap,
|
|
193
|
+
which is a different ceiling that bites in a different place — see the table below.
|
|
194
|
+
|
|
195
|
+
Check headroom before you start:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
curl -s https://your-relay/health
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Two different refusals with two different fixes, which is worth knowing before you debug
|
|
202
|
+
the wrong one:
|
|
203
|
+
|
|
204
|
+
| Code | Meaning | What actually fixes it |
|
|
205
|
+
|---|---|---|
|
|
206
|
+
| `Q430` | whole relay at `maxConnsTotal` | raise `HIVE_MAX_CONNS_TOTAL`, or sync fewer files |
|
|
207
|
+
| `Q431` | one room past its per-room cap | set `HIVE_SEATS` — raising the total will **not** help |
|
|
208
|
+
|
|
209
|
+
`roomConns` is keyed on the **file**-room (`baseRoom + FILE_SEP + relPath`), so a room's
|
|
210
|
+
per-room count is *how many participants have that one file open*, not how many files the
|
|
211
|
+
folder has. Five participants sharing a file is 5 against a free-tier 8. `Q431` therefore
|
|
212
|
+
needs roughly nine or more participants on the *same* file — which is why a five-person team
|
|
213
|
+
hits `Q430` first, and why reaching for `HIVE_SEATS` is the wrong instinct. Measured by
|
|
214
|
+
@Antigravity while designing the five-participant capacity suite; it corrected this section.
|
|
215
|
+
|
|
216
|
+
If files stop syncing, the client fetches `/health` itself and tells you which of these it
|
|
217
|
+
is, or says plainly that capacity is not the cause. It will not guess.
|
|
218
|
+
|
|
219
|
+
### When an agent goes quiet
|
|
220
|
+
|
|
221
|
+
An agent whose turn has ended is still connected, so "is it online" is the wrong question.
|
|
222
|
+
The room answers the one that is decidable — is it inside a blocking `hive_wait` right now:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
Alice (ai) — WORKING: last action 2 min ago (hive_claim). Reachable.
|
|
226
|
+
Bob (ai) — WAITING: blocked in hive_wait, wakes the moment anything arrives. Reachable.
|
|
227
|
+
Carol (ai) — PARKED: connected, but NOT inside hive_wait and silent for 22 min.
|
|
228
|
+
It will not wake on its own — anything you send it now will sit
|
|
229
|
+
unread until a human prompts it.
|
|
230
|
+
Dave (ai) — GONE: not connected. Last action 41 min ago (hive_say).
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`hive_say` warns you at send time if you address a PARKED or GONE agent, and `hive_owed`
|
|
234
|
+
marks a debt owed by one as `[UNREACHABLE]` rather than pending.
|
|
235
|
+
|
|
236
|
+
### Known limits
|
|
237
|
+
|
|
238
|
+
- **Marketplace extension**: loses offline edits on rejoin. Not fixed — it needs a real
|
|
239
|
+
VS Code host to verify, and we will not claim it without one. The `sync.js` engine is
|
|
240
|
+
covered; the extension is not.
|
|
241
|
+
- **Bounded working set**: described above. A real behaviour change, deliberately taken.
|
|
242
|
+
- **`git`-vs-room drift**: a room checkout and a git branch can diverge.
|
|
243
|
+
`scripts/check-drift.mjs` reports it.
|
|
244
|
+
- **Relay deploy**: pushing this repo does not redeploy a hosted relay unless auto-deploy
|
|
245
|
+
is on. Check `/health` uptime against your last deploy.
|
|
246
|
+
- **Dead-heat file creation** (HIV-P17, fixed in `929323b`). If two participants create the
|
|
247
|
+
*same* path in the same instant, the creation-race path now runs `fusionReason()` — the same
|
|
248
|
+
check the normal merge path uses. When both inputs parse but the merged output does not (or
|
|
249
|
+
has structural duplication), the merge is REFUSED: nothing is written, both copies stay
|
|
250
|
+
intact, and the room is told via `say()`. Disjoint functions that parse cleanly when merged
|
|
251
|
+
are still unioned as before. Non-code files (no language support) are unaffected.
|
|
252
|
+
`tests/genesis-collision-test.mjs` pins the fix (6/6).
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
76
256
|
## Documentation
|
|
77
257
|
|
|
78
|
-
|
|
258
|
+
- **[🔌 Connecting & Hosting Rooms](docs/CONNECTING_ROOMS.md)** — how humans join rooms, how to host your own relay, and the self-certifying security model.
|
|
259
|
+
- **[🤖 AI Agent Manual](docs/AI_AGENT_MANUAL.md)** — written for agents: download the MCP server, join a room, behave in the hive.
|
|
260
|
+
- **[🏛️ Hivecode Explained, end to end](docs/HIVECODE_EXPLAINED.md)** — the full course: CRDT internals, the ICR pipeline, provenance, the benchmarks.
|
|
261
|
+
- **[🧠 ICR (Intent-aware Code Replication)](docs/ICR_EXPLAINED.md)** — how the AST-aware merge guarantees no lines are lost in a multi-agent collision.
|
|
262
|
+
- **[🔐 DCO (Deterministic Context Override)](docs/DCO_EXPLAINED.md)** — tools locked by CRDT state, structurally preventing drift.
|
|
263
|
+
- **[🔗 The Substrate](docs/SUBSTRATE.md)** — a governed *medium*: every change a signed `(intent, patch, provenance)` triple, enforced by the relay, with no controller.
|
|
264
|
+
- **[🏗️ Architecture](docs/ARCHITECTURE.md)** · **[🚀 Self-hosting](docs/SELF_HOSTING.md)** · **[⚙️ Setup](docs/SETUP.md)**
|
|
265
|
+
|
|
266
|
+
### `learnings/` — read this one if you read nothing else
|
|
79
267
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
268
|
+
Eight defect classes, every one found on this repo, every one having already recurred at
|
|
269
|
+
least once. They are written as classes rather than bug reports, and
|
|
270
|
+
[`tests/learnings-test.mjs`](tests/learnings-test.mjs) fails if a document cites a file that
|
|
271
|
+
has moved — a stale learning is worse than none, because it is read with the authority of
|
|
272
|
+
something that was once checked.
|
|
273
|
+
|
|
274
|
+
The recurring shape is **false success**: nothing crashes, every signal is green, the result
|
|
275
|
+
is wrong. [`learnings/false-success.md`](learnings/false-success.md) is the index into it.
|
|
86
276
|
|
|
87
277
|
---
|
|
88
278
|
|
|
@@ -90,15 +280,62 @@ To keep this README clean, we've modularized the documentation into separate fil
|
|
|
90
280
|
|
|
91
281
|
```bash
|
|
92
282
|
npm install
|
|
93
|
-
|
|
94
|
-
|
|
283
|
+
npm test # unit + property suites; pretest runs the packaging and deploy guards
|
|
284
|
+
npm run test:hardening # every live suite below, in one command, against real relays
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`test:hardening` is the one to run if you are evaluating this. It spawns real relays, drives
|
|
288
|
+
real clients, and every assertion in it reads a channel the product actually writes — bytes on
|
|
289
|
+
disk and the room's own chat — rather than a status flag. It takes a few minutes because the
|
|
290
|
+
failures it looks for only appear at real timings.
|
|
291
|
+
|
|
292
|
+
`pretest` is not optional and is worth understanding: it runs
|
|
293
|
+
[`scripts/check-files.mjs`](scripts/check-files.mjs), which refuses the package if any
|
|
294
|
+
relative import is missing from `package.json` `files[]`. That guard has caught a published
|
|
295
|
+
tarball that would have thrown `ERR_MODULE_NOT_FOUND` on its first call.
|
|
95
296
|
|
|
96
|
-
|
|
297
|
+
### Never pack or publish from a synced room folder
|
|
298
|
+
|
|
299
|
+
This one is specific to Hivecode and it bites its own maintainers. A room folder holds a
|
|
300
|
+
**bounded working set**, so a file listed in `files[]` may simply not be on disk there.
|
|
301
|
+
Measured 21 Sep 2026: of 35 `files[]` entries, `night/room` had 25 missing and
|
|
302
|
+
`night/deepmind` 26, while the repo itself had none.
|
|
303
|
+
|
|
304
|
+
`npm publish` is safe — `prepublishOnly` runs the guard, which fails with
|
|
305
|
+
`files[] lists "hive-mailbox.js" but it does not exist on disk`, one line per entry.
|
|
306
|
+
|
|
307
|
+
**`npm pack` is not.** On npm 9.6.1 a real `npm pack` runs *no* lifecycle script — not
|
|
308
|
+
`prepack`, not `prepare` — so it cannot be guarded that way. It omits a `files[]` entry it
|
|
309
|
+
cannot find and says nothing at all: no warning, no non-zero exit, just a smaller tarball
|
|
310
|
+
that installs cleanly and throws on the user's first import. Verified with a two-file probe
|
|
311
|
+
where one listed file did not exist.
|
|
312
|
+
|
|
313
|
+
So: pack from the repository, never from a participant folder, and check the tarball
|
|
314
|
+
against `files[]` rather than trusting the count.
|
|
315
|
+
|
|
316
|
+
Live suites spawn a real relay on a pid-derived port, so they can run concurrently:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
node tests/agent-liveness-test.mjs # WORKING / WAITING / PARKED / GONE, real dispatch
|
|
320
|
+
node tests/relay-diagnosis-test.mjs # a real relay made genuinely full, and the advice it gets
|
|
321
|
+
node tests/unreachable-recipient-test.mjs # send-time warnings for unreachable recipients
|
|
322
|
+
node tests/open-file-cap-test.mjs # the bounded working set, and the trade it makes
|
|
323
|
+
node tests/ceiling-no-data-loss-test.mjs # a full relay must not cost you your files
|
|
324
|
+
node tests/first-contact-refusal-test.mjs # clone-then-join must not clobber local work
|
|
325
|
+
node tests/genesis-claim-test.mjs # a claim whose bytes could never leave is refused
|
|
326
|
+
node tests/blind-recreate-test.mjs # writing a file you cannot see, that the room already has
|
|
327
|
+
node tests/genesis-collision-test.mjs # HIV-P17: two whole-module rewrites in a creation race are refused
|
|
328
|
+
node tests/join-honesty-test.mjs # what a join says when the relay refused it
|
|
329
|
+
node tests/mention-boundary-test.mjs # who owes an answer, and who cannot be asked
|
|
330
|
+
node tests/coordination-test.mjs # claims, leases, flow-around under contention
|
|
331
|
+
|
|
332
|
+
npm run test:ceiling · test:gate · test:learnings · test:features · test:coordination
|
|
97
333
|
```
|
|
98
334
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
335
|
+
Everything under [`tests/`](tests/) is runnable directly with `node`. Suites assert on
|
|
336
|
+
observable behaviour — bytes on disk, tool output, relay refusals — rather than on source
|
|
337
|
+
text, for the reason set out in
|
|
338
|
+
[`learnings/testing-the-consumer-instead-of-the-derivation.md`](learnings/testing-the-consumer-instead-of-the-derivation.md).
|
|
102
339
|
|
|
103
340
|
The extension source is in [`extension/`](extension/).
|
|
104
341
|
|
package/core.js
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
// in test.js. Keeping these here (instead of copy-pasted) means a fix in one
|
|
3
3
|
// place fixes everywhere, and the tests exercise the REAL code.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
5
|
+
import reconcilePkg from './reconcile.cjs'
|
|
6
|
+
|
|
7
|
+
export const {
|
|
8
|
+
applyDiff,
|
|
9
|
+
hasConflictMarkers,
|
|
10
|
+
stripConflictMarkers,
|
|
11
|
+
merge3,
|
|
12
|
+
summarizeChange,
|
|
13
|
+
changedRange,
|
|
14
|
+
symbolsIn,
|
|
15
|
+
icrMerge3,
|
|
16
|
+
createReconciler,
|
|
17
|
+
reconcile
|
|
18
|
+
} = reconcilePkg
|
|
19
19
|
|
|
20
20
|
// Optimistic concurrency: is a write based on `expected` still safe to apply?
|
|
21
21
|
// Returns the new version on success, or { stale, current } if the region moved.
|
|
@@ -89,135 +89,3 @@ export function mergeEdit(base, mine, current) {
|
|
|
89
89
|
return { conflict: true } // same lines changed two ways -> re-reason on `current`
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// Detect REAL git-style conflict markers in a file. Line-anchored on purpose:
|
|
93
|
-
// it requires an opening `<<<<<<< ` AND a closing `>>>>>>> ` each at the start
|
|
94
|
-
// of a line. This is what merge3 emits. A naive `text.includes('<<<<<<<')`
|
|
95
|
-
// false-positives on any file that merely MENTIONS the markers (e.g. a README
|
|
96
|
-
// documenting conflict resolution), which would announce a phantom conflict on
|
|
97
|
-
// every sync. (Found by a live two-agent test, 2026-06-19.)
|
|
98
|
-
export function hasConflictMarkers(text) {
|
|
99
|
-
return /^<<<<<<< /m.test(text || '') && /^>>>>>>> /m.test(text || '')
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Strip and de-nest conflict markers to prevent exponential 2^N code ballooning
|
|
103
|
-
export function stripConflictMarkers(text, prefer = 'local') {
|
|
104
|
-
if (!text || !hasConflictMarkers(text)) return text || ''
|
|
105
|
-
const lines = text.split('\n')
|
|
106
|
-
const out = []
|
|
107
|
-
let inConflict = false
|
|
108
|
-
let inTheirs = false
|
|
109
|
-
const localBlock = []
|
|
110
|
-
const theirsBlock = []
|
|
111
|
-
|
|
112
|
-
for (let i = 0; i < lines.length; i++) {
|
|
113
|
-
const line = lines[i]
|
|
114
|
-
if (/^<<<<<<< /.test(line)) {
|
|
115
|
-
inConflict = true
|
|
116
|
-
inTheirs = false
|
|
117
|
-
localBlock.length = 0
|
|
118
|
-
theirsBlock.length = 0
|
|
119
|
-
continue
|
|
120
|
-
}
|
|
121
|
-
if (/^=======$/.test(line) && inConflict) {
|
|
122
|
-
inTheirs = true
|
|
123
|
-
continue
|
|
124
|
-
}
|
|
125
|
-
if (/^>>>>>>> /.test(line) && inConflict) {
|
|
126
|
-
inConflict = false
|
|
127
|
-
out.push(...(prefer === 'theirs' ? theirsBlock : localBlock))
|
|
128
|
-
continue
|
|
129
|
-
}
|
|
130
|
-
if (inConflict) {
|
|
131
|
-
if (inTheirs) theirsBlock.push(line)
|
|
132
|
-
else localBlock.push(line)
|
|
133
|
-
} else {
|
|
134
|
-
out.push(line)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return out.join('\n')
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// --- 3-way merge for the SYNC layer (disk <-> shared doc) ---
|
|
141
|
-
// Same idea as mergeEdit, but instead of bailing with { conflict } it always
|
|
142
|
-
// returns text: disjoint edits merge cleanly; overlapping edits are wrapped in
|
|
143
|
-
// git-style conflict markers so BOTH versions survive and a human/AI resolves.
|
|
144
|
-
// De-nests existing conflict blocks to prevent exponential duplication.
|
|
145
|
-
// merge3(base, mine, theirs) -> { text, conflict }
|
|
146
|
-
export function merge3(base, mine, theirs) {
|
|
147
|
-
if (mine === theirs) return { text: mine, conflict: false }
|
|
148
|
-
if (base === theirs) return { text: mine, conflict: false } // only I changed
|
|
149
|
-
if (base === mine) return { text: theirs, conflict: false } // only they changed
|
|
150
|
-
|
|
151
|
-
// De-nest stale conflict markers so conflict blocks never wrap exponentially
|
|
152
|
-
const cleanBase = hasConflictMarkers(base) ? stripConflictMarkers(base, 'local') : (base || '')
|
|
153
|
-
const cleanMine = hasConflictMarkers(mine) ? stripConflictMarkers(mine, 'local') : (mine || '')
|
|
154
|
-
const cleanTheirs = hasConflictMarkers(theirs) ? stripConflictMarkers(theirs, 'theirs') : (theirs || '')
|
|
155
|
-
|
|
156
|
-
if (cleanMine === cleanTheirs) return { text: cleanMine, conflict: false }
|
|
157
|
-
if (cleanBase === cleanTheirs) return { text: cleanMine, conflict: false }
|
|
158
|
-
if (cleanBase === cleanMine) return { text: cleanTheirs, conflict: false }
|
|
159
|
-
|
|
160
|
-
const b = cleanBase.split('\n')
|
|
161
|
-
const mr = changedRange(b, cleanMine.split('\n'))
|
|
162
|
-
const tr = changedRange(b, cleanTheirs.split('\n'))
|
|
163
|
-
if (mr.endBase <= tr.startBase || tr.endBase <= mr.startBase) {
|
|
164
|
-
const out = b.slice()
|
|
165
|
-
for (const e of [mr, tr].sort((x, y) => y.startBase - x.startBase)) {
|
|
166
|
-
out.splice(e.startBase, e.endBase - e.startBase, ...e.newLines)
|
|
167
|
-
}
|
|
168
|
-
return { text: out.join('\n'), conflict: false }
|
|
169
|
-
}
|
|
170
|
-
// Overlap: reconstruct each side's lines over the union region [start,end).
|
|
171
|
-
const start = Math.min(mr.startBase, tr.startBase)
|
|
172
|
-
const end = Math.max(mr.endBase, tr.endBase)
|
|
173
|
-
const mineBlock = [...b.slice(start, mr.startBase), ...mr.newLines, ...b.slice(mr.endBase, end)]
|
|
174
|
-
const theirsBlock = [...b.slice(start, tr.startBase), ...tr.newLines, ...b.slice(tr.endBase, end)]
|
|
175
|
-
const out = [
|
|
176
|
-
...b.slice(0, start),
|
|
177
|
-
'<<<<<<< local (yours)',
|
|
178
|
-
...mineBlock,
|
|
179
|
-
'=======',
|
|
180
|
-
...theirsBlock,
|
|
181
|
-
'>>>>>>> incoming (theirs)',
|
|
182
|
-
...b.slice(end),
|
|
183
|
-
]
|
|
184
|
-
return { text: out.join('\n'), conflict: true }
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// --- Rewrite detection for the auto-board ---
|
|
188
|
-
// Classify a change as a small PATCH (grep-and-edit, the common case) vs a
|
|
189
|
-
// wholesale REWRITE, and pull out the symbol names the new version defines in
|
|
190
|
-
// the changed region. The sync layer uses this to AUTO-record what an agent did
|
|
191
|
-
// on a rewrite — so other agents learn of it even when the agent forgets to say.
|
|
192
|
-
// summarizeChange(base, next) -> { isRewrite, changedLines, totalLines, symbols }
|
|
193
|
-
export function summarizeChange(base, next) {
|
|
194
|
-
const b = (base || '').split('\n')
|
|
195
|
-
const n = (next || '').split('\n')
|
|
196
|
-
if (!base) return { isRewrite: false, changedLines: n.length, totalLines: n.length, symbols: symbolsIn(next || '') }
|
|
197
|
-
const r = changedRange(b, n)
|
|
198
|
-
const churn = Math.max(r.endBase - r.startBase, r.newLines.length) // lines replaced or added
|
|
199
|
-
const total = Math.max(b.length, n.length)
|
|
200
|
-
// a rewrite = at least half the file churned (and more than a couple of lines)
|
|
201
|
-
const isRewrite = total > 0 && churn >= 4 && churn / total >= 0.5
|
|
202
|
-
return { isRewrite, changedLines: churn, totalLines: total, symbols: symbolsIn(r.newLines.join('\n')) }
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Best-effort "what was defined here" across common languages — for the board.
|
|
206
|
-
function symbolsIn(text) {
|
|
207
|
-
const names = new Set()
|
|
208
|
-
const add = (re) => { let m; while ((m = re.exec(text)) && names.size < 8) names.add(m[1]) }
|
|
209
|
-
add(/\bfunction\s+(\w+)/g) // function foo
|
|
210
|
-
add(/\bclass\s+(\w+)/g) // class Foo
|
|
211
|
-
add(/\bdef\s+(\w+)/g) // def foo (python)
|
|
212
|
-
add(/\b(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=/g) // const foo =
|
|
213
|
-
return [...names].slice(0, 8)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Which base line-range an edit replaced, and the replacement lines.
|
|
217
|
-
export function changedRange(base, other) {
|
|
218
|
-
let p = 0
|
|
219
|
-
while (p < base.length && p < other.length && base[p] === other[p]) p++
|
|
220
|
-
let s = 0
|
|
221
|
-
while (s < base.length - p && s < other.length - p && base[base.length - 1 - s] === other[other.length - 1 - s]) s++
|
|
222
|
-
return { startBase: p, endBase: base.length - s, newLines: other.slice(p, other.length - s) }
|
|
223
|
-
}
|