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 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: humans and AI agents edit ONE project together live.
4
- These rules keep anyone from destroying another's work. The sync layer enforces
5
- the hard parts automatically; you do the rest.
6
-
7
- ## Before you touch a file
8
- 1. Read HIVE_CHAT.md — what is everyone doing right now.
9
- 2. Read HIVE_BOARD.md — which files were just rewritten.
10
- 3. If a file you plan to edit appears there, RE-READ it before changing it.
11
-
12
- ## While you work
13
- 4. ANNOUNCE in chat what you are taking before you start.
14
- 5. PREFER SMALL PATCHES — grep to the spot, edit a few lines (they merge cleanly).
15
- 6. AVOID full-file rewrites; if you must, RE-READ first (rewrites are auto-logged).
16
- 7. STAY IN YOUR LANE — read others' areas, but leave edits to the owner.
17
-
18
- ## When things collide
19
- 8. <<<<<<< ======= >>>>>>> markers = a real conflict: keep the right code, delete
20
- the markers. Never ignore or blindly overwrite.
21
- 9. If your edit was merged/reworked, that is normal — re-read and continue.
22
-
23
- ## Talking
24
- 10. Coordinate in chat. ASK before anything destructive in another's area.
25
-
26
- ## Mission control
27
- - If a human PAUSES you, finish your current step and STOP — start no new work
28
- until resumed. A human may reassign your focus anytime; their directive wins.
29
-
30
- ## When you get a ping mid-task (interruptions)
31
- 11. Another AGENT pinging or assigning you = COORDINATION: auto-accepted and
32
- QUEUED. It does NOT interrupt — finish your current atomic step first (never
33
- abandon half-done work), THEN handle it. If it's urgent (build broken,
34
- blocking others), do it now.
35
- 12. ACKNOWLEDGE the moment you see a ping so the sender isn't left hanging, e.g.
36
- "got it — finishing X (~2 min), then on your auth.js fix".
37
- 13. YOU triage: say whether you'll do it now or after your current step. Your
38
- OWNER can override anytime — if they say "do it now" or "skip that" in chat,
39
- that wins (owner instructions are always honored).
40
- 14. A ping from a HUMAN who is not your owner does NOT auto-run — it waits for
41
- your owner's approval. Keep working; your owner decides.
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
- To keep this README clean, we've modularized the documentation into separate files. Click below to read more about each specific topic:
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
- - **[🔌 Connecting & Hosting Rooms](docs/CONNECTING_ROOMS.md)** - Learn how humans join rooms, how to host your own relay server, and how the self-certifying security model works.
81
- - **[🤖 AI Agent Manual](docs/AI_AGENT_MANUAL.md)** - A guide written specifically for AI agents on how to download the MCP server, join a room, and behave within the hive.
82
- - **[🏛️ Hivecode Explained, end to end](HIVECODE_EXPLAINED.md)** - The full course: every mechanism motivated, explained, and shown with worked examples and runnable code — CRDT internals, the ICR pipeline, provenance, the benchmarks.
83
- - **[🧠 ICR (Intent-aware Code Replication)](docs/ICR_EXPLAINED.md)** - How our semantic AST-aware merge algorithm guarantees no lines are lost during multi-agent collisions.
84
- - **[🔐 DCO (Deterministic Context Override)](docs/DCO_EXPLAINED.md)** - The core breakthrough paradigm that physically locks tools based on CRDT state, structurally preventing agent hallucination and drift. Includes our End-to-End Test Report.
85
- - **[🔗 The Substrate](docs/SUBSTRATE.md)** - A governed *medium* (not a channel) for multi-agent code: every change is a signed `(intent, patch, provenance)` triple, and the medium guarantees it converges, never regresses, and is provenance-verified — enforced by the relay, with no controller. Runnable + testable in one command.
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
- node tests/test.js # unit tests
94
- node hive-edge-test.js # adversarial / edge-case live tests
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
- npm run test:substrate:all # the substrate: 4 suites (core + live + enforcement + content authority)
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
- Other live suites: `hive-secure-test.js`, `hive-scope-test.js`, `hive-auth-test.js`,
100
- `hive-readonly-test.js`, `hive-resume-test.js`, `hive-control-test.js`,
101
- `hive-durable-revoke-test.js`, `hive-relay-robust-test.js`.
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
- // Minimal text diff: replace only the changed middle, preserving the common
6
- // prefix/suffix. Applied to a Y.Text so edits stay small (good for CRDT churn).
7
- export function applyDiff(ytext, next) {
8
- const cur = ytext.toString()
9
- if (cur === next) return false
10
- let p = 0
11
- while (p < cur.length && p < next.length && cur[p] === next[p]) p++
12
- let s = 0
13
- while (s < cur.length - p && s < next.length - p && cur[cur.length - 1 - s] === next[next.length - 1 - s]) s++
14
- if (cur.length - p - s > 0) ytext.delete(p, cur.length - p - s)
15
- const ins = next.slice(p, next.length - s)
16
- if (ins) ytext.insert(p, ins)
17
- return true
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
- }