hivecode-mcp 0.6.0 → 0.8.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/README.md CHANGED
@@ -1,137 +1,68 @@
1
- <h1>Hivecode</h1>
2
-
3
- **Multiplayer for your AI coding agents — with real permissions.**
4
-
5
- Run humans and multiple AI agents on one live codebase. Folder-scoped access,
6
- approval gates, instant revoke. No git push/pull — edits sync in about a second.
7
- Open source.
8
-
9
- [**Install for VS Code**](https://marketplace.visualstudio.com/items?itemName=hivecode.hivecode)
10
- · [**Website**](https://livecode-xoss.onrender.com)
11
- · [Latest .vsix](https://github.com/GSK7024/livecode/releases)
12
-
13
- <!-- Add a demo GIF here once recorded: ![demo](docs/demo.gif) -->
14
-
15
- ---
16
-
17
- ## Why
18
-
19
- The moment you point more than one AI agent at a project, it gets messy: they
20
- overwrite each other's files, rebuild the same feature twice, and every agent can
21
- touch every file — secrets, configs, infra included. Today people juggle git
22
- worktrees and pray at merge time.
23
-
24
- Hivecode is a shared **live room** where humans and agents edit one project
25
- together — but with real access control. You decide what each agent can reach, and
26
- the relay enforces it; an out-of-scope agent never even receives the bytes.
27
-
28
- It's **agent-neutral**: Claude Code, Cursor, Windsurf, or your own bot over MCP.
29
-
30
- ---
31
-
32
- ## Quickstart
33
-
34
- 1. Install the extension (link above). It works in **VS Code, Cursor, Windsurf,
35
- and Antigravity**.
36
- 2. Open the folder you want to share (**File → Open Folder**).
37
- 3. `Ctrl/Cmd+Shift+P` → **Hivecode: Host a Secured Session**. A join link is
38
- copied to your clipboard. *(No terminal, no server to run.)*
39
- 4. **Hivecode: Invite to folders…** → pick the folders and a role (edit /
40
- read-only) for each person or agent. Send them the link.
41
- 5. They run **Hivecode: Join a Session** and paste the link. You're now editing
42
- the same project live.
43
-
44
- Use **Manage access** to re-scope or revoke anyone, and **Leave Session** to stop.
45
-
46
- ---
47
-
48
- ## Add an AI agent
49
-
50
- Agents join through the Hivecode MCP server ([`hive-mcp.js`](hive-mcp.js)). Point
51
- your agent's MCP config at it and hand it the join link; it appears in the room as
52
- an `ai` member, scoped to exactly the folders you invited it to. The agent gets
53
- tools to read chat, the task board, and members; to claim and complete tasks; to
54
- talk; and to wait for work. See [MCP.md](MCP.md) for setup.
55
-
56
- ---
57
-
58
- ## What you get
59
-
60
- **Governance**
61
- - **Folder-scoped access** — invite an agent to `frontend/` only; it never receives
62
- anything else.
63
- - **Approval gates** — a human's request to *your* agent waits for your OK;
64
- agent-to-agent coordination flows automatically.
65
- - **Read-only roles** — reviewers see everything, change nothing.
66
- - **Instant revoke** — cut access mid-session, enforced server-side on every
67
- reconnect (and it survives a relay restart).
68
-
69
- **Coordination**
70
- - **Built-in chat**, a **shared task board** (assign / complete), and **live
71
- presence** so humans and agents stay in sync.
72
- - **Co-editing heads-up** before two people touch the same file.
73
- - **Never lose work** — overlapping edits keep both versions with conflict markers;
74
- a stale rewrite can't silently delete another's lines.
75
- - **Mission control** — pause, resume, or reassign any agent; agents honor it
76
- mid-task.
77
-
78
- **Trust**
79
- - **Server-enforced auth** — signed tokens, algorithm-pinned, fail-closed.
80
- Unauthorized clients never complete the WebSocket handshake.
81
- - **Self-certifying secured rooms** — the room id embeds a fingerprint of your
82
- public key; tokens carry the key and are signed by it; the relay trusts a token
83
- only if the fingerprint matches. No account system, no shared secret. Your
84
- private key stays in the editor's secure storage.
85
- - **Dependency-free core** — the access-control layer uses only Node's `crypto`.
86
-
87
- ---
88
-
89
- ## How the security model works
90
-
91
- A secured room's id is `hs_<fp>_<rand>`, where `fp` is a base64url SHA-256
92
- fingerprint of the owner's public key. Every token for the room carries the owner's
93
- public key (claim `pk`) and is signed by the matching private key (RS256). The relay
94
- trusts a token **iff** `fingerprint(token.pk) === fingerprint-in-room-id`, then
95
- verifies the signature with that key — so trust is anchored in the room id itself
96
- and the relay stores nothing.
97
-
98
- Files use a per-file subdocument model: the room is `<base>`, each file is a
99
- separate document at `<base>␁<path>`. The relay authorizes per path, so a scoped
100
- client can only open the files it may load, and out-of-scope content never reaches
101
- its disk. Paths are guarded against traversal, absolute paths, drive letters, and
102
- control characters on both the client (before writing) and the relay.
103
-
104
- A full threat/edge-case audit is in [EDGE-CASES.md](EDGE-CASES.md); the RBAC design
105
- is in [RBAC.md](RBAC.md).
106
-
107
- ---
108
-
109
- ## Self-hosting the relay
110
-
111
- The hosted relay is the default (`wss://livecode-xoss.onrender.com`) and needs no
112
- setup. To run your own, deploy [`server.js`](server.js) anywhere Node runs and set
113
- `hivecode.relayUrl` in the extension. The relay holds no truth of its own — it just
114
- passes CRDT updates between clients in the same room, and enforces access at the
115
- handshake. See [DEPLOY.md](DEPLOY.md).
116
-
117
- ---
118
-
119
- ## Development
120
-
121
- ```bash
122
- npm install
123
- node test.js # unit tests
124
- node hive-edge-test.js # adversarial / edge-case live tests
125
- ```
126
-
127
- Other live suites: `hive-secure-test.js`, `hive-scope-test.js`, `hive-auth-test.js`,
128
- `hive-readonly-test.js`, `hive-resume-test.js`, `hive-control-test.js`,
129
- `hive-durable-revoke-test.js`, `hive-relay-robust-test.js`.
130
-
131
- The extension source is in [`extension/`](extension/).
132
-
133
- ---
134
-
135
- ## License
136
-
137
- MIT.
1
+ <h1>Hivecode</h1>
2
+
3
+ **Oversight and control for your AI coding agents.**
4
+
5
+ Run AI agents on your codebase and actually stay in control: **watch** every edit
6
+ live, **fence** each agent to the folders it's allowed in, **approve** the risky
7
+ moves, and **undo** any agent instantly. No git push/pull. Open source.
8
+
9
+ [**Install for VS Code**](https://marketplace.visualstudio.com/items?itemName=hivecode.hivecode)
10
+ · [**Website**](https://livecode-xoss.onrender.com)
11
+ · [Latest .vsix](https://github.com/GSK7024/livecode/releases)
12
+
13
+ ---
14
+
15
+ ## Why
16
+
17
+ You can't trust an AI agent blindly. The faster it edits, the more you have to
18
+ check — and the moment you run more than one, it gets worse: they touch files they
19
+ shouldn't, overwrite each other, and you lose track of who changed what. Today's
20
+ answer is to isolate each agent in a git worktree and pray at merge time — which
21
+ doesn't help you *trust* the output, it just defers the mess.
22
+
23
+ Hivecode adds the missing layer: **oversight and control.**
24
+
25
+ - **See everything** — a live Control Room (browser or phone) shows every agent and every file it touches, in real time.
26
+ - **Fence them in** — folder-scoped access the relay enforces; an out-of-scope agent never even receives the code.
27
+ - **Approve what matters** — gate risky work so nothing lands without your OK.
28
+ - **Undo anything** — instant rollback: restore any file, or revert *everything* one agent did.
29
+
30
+ It's **agent-neutral**: Claude Code, Cursor, Windsurf, or your own bot over MCP. No
31
+ git push/pull — edits sync in about a second.
32
+
33
+ ---
34
+
35
+ ## Documentation
36
+
37
+ To keep this README clean, we've modularized the documentation into separate files. Click below to read more about each specific topic:
38
+
39
+ - **[🔌 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.
40
+ - **[🤖 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.
41
+ - **[🏛️ Hivecode Architecture](docs/HIVECODE_EXPLAINED.md)** - A breakdown of the three core components, the CRDT data model, and how it scales.
42
+ - **[🧠 ICR (Intent-Centric Resolution)](docs/ICR_EXPLAINED.md)** - How our semantic AST-aware merge algorithm guarantees no lines are lost during multi-agent collisions.
43
+ - **[🔐 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.
44
+ - **[🔗 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.
45
+
46
+ ---
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ npm install
52
+ node test.js # unit tests
53
+ node hive-edge-test.js # adversarial / edge-case live tests
54
+
55
+ npm run test:substrate:all # the substrate: 4 suites (core + live + enforcement + content authority)
56
+ ```
57
+
58
+ Other live suites: `hive-secure-test.js`, `hive-scope-test.js`, `hive-auth-test.js`,
59
+ `hive-readonly-test.js`, `hive-resume-test.js`, `hive-control-test.js`,
60
+ `hive-durable-revoke-test.js`, `hive-relay-robust-test.js`.
61
+
62
+ The extension source is in [`extension/`](extension/).
63
+
64
+ ---
65
+
66
+ ## License
67
+
68
+ MIT.
package/hive-coord.js ADDED
@@ -0,0 +1,72 @@
1
+ // hive-coord.js — the Hive coordination layer (v0 prototype).
2
+ //
3
+ // The bet: AI coding agents work fast and BLIND — they collide because, unlike a human
4
+ // team, they have no way to coordinate BEFORE they edit. Merging (ICR) cleans up after a
5
+ // collision. This prevents the collision in the first place.
6
+ //
7
+ // The design is a real hive: NO central controller, no conductor, no extra agent. Order
8
+ // emerges from each agent following one local rule against a shared medium — exactly how
9
+ // bees coordinate via traces in the environment (stigmergy), and how millions of Ethernet
10
+ // devices share one wire with no master (carrier-sense / collision-detect / backoff).
11
+ //
12
+ // The shared medium is a CRDT map living IN the codebase doc (a Y.Map in Hivecode; a plain
13
+ // Map in tests). Each region of work (a file, or a unit) can hold one CLAIM:
14
+ // { by: agentId, intent: string, at: time, ttl: ms }
15
+ //
16
+ // An agent's entire protocol — no coordinator anywhere:
17
+ // 1. SENSE — read the shared map. Is my target region claimed and fresh?
18
+ // 2. FLOW — if taken, move to open space (pick another free region). This is the
19
+ // emergent load-balancing: agents spread across the codebase on their own.
20
+ // 3. CLAIM — write my claim to the free region.
21
+ // 4. VERIFY — re-read (collision-detect): if a concurrent claim beat mine, BACK OFF and
22
+ // retry later. (Like Ethernet: listen, send, detect collision, back off.)
23
+ // 5. RELEASE — when done, delete my claim and (optionally) leave a "done" trace.
24
+ //
25
+ // EVAPORATION: claims carry a TTL. A crashed or stalled agent's claim simply expires, and
26
+ // the region frees itself — no central garbage collector, no deadlock. (The pheromone fades.)
27
+ //
28
+ // `shared` is any object with get(key)/set(key,val)/delete(key)/entries() — so the same
29
+ // code runs on a JS Map (tests) and a Yjs Y.Map (production). `now` is injectable so tests
30
+ // can drive a deterministic virtual clock.
31
+
32
+ export function makeCoordinator(shared, self, opts = {}) {
33
+ const ttl = opts.ttl != null ? opts.ttl : 5000
34
+ const now = opts.now || (() => Date.now())
35
+ const expired = (c) => !c || (now() - c.at) >= c.ttl
36
+
37
+ // SENSE: the live, non-expired claim on a region (or null if open).
38
+ const sense = (region) => { const c = shared.get(region); return expired(c) ? null : c }
39
+
40
+ // Is this region open to ME? (free, expired, or already mine)
41
+ const free = (region) => { const c = shared.get(region); return expired(c) || c.by === self }
42
+
43
+ const claim = (region, intent) => { shared.set(region, { by: self, intent, at: now(), ttl }) }
44
+ const release = (region) => { const c = shared.get(region); if (c && c.by === self) shared.delete(region) }
45
+
46
+ // ACQUIRE one region: sense → claim → verify. Returns true iff I now hold it.
47
+ // The verify step is the collision-detector: if a concurrent writer won the region,
48
+ // I see their id on re-read and concede.
49
+ const acquire = (region, intent) => {
50
+ const c = shared.get(region)
51
+ if (c && !expired(c) && c.by !== self) return false // sensed an owner → defer
52
+ claim(region, intent)
53
+ const after = shared.get(region)
54
+ return !(after && after.by !== self) // verify: did my claim survive?
55
+ }
56
+
57
+ // FLOW to open space: try my preferred regions in order, take the first I can acquire.
58
+ // This is what makes the swarm spread out with nobody assigning work.
59
+ const acquireAny = (candidates, intent) => {
60
+ for (const r of candidates) if (acquire(r, intent)) return r
61
+ return null
62
+ }
63
+
64
+ // Everything currently claimed (live), as a snapshot — for awareness / a Control Room.
65
+ const board = () => {
66
+ const out = []
67
+ for (const [region, c] of shared.entries()) if (!expired(c)) out.push({ region, ...c })
68
+ return out
69
+ }
70
+
71
+ return { sense, free, claim, release, acquire, acquireAny, board }
72
+ }
package/hive-mcp.js CHANGED
@@ -25,6 +25,13 @@ import crypto from 'crypto'
25
25
  import { Server } from '@modelcontextprotocol/sdk/server/index.js'
26
26
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
27
27
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
28
+ // Substrate defaults for MCP agents: signed provenance + the silent-fork gate are ON
29
+ // unless explicitly disabled. Agents register this server with arbitrary (often empty)
30
+ // env, so relying on per-registration env vars left rooms in mixed signed/unsigned mode —
31
+ // which is exactly the condition that disabled fork detection in live testing. sync.js
32
+ // reads these when startSync() runs (not at import), so setting them here covers every join.
33
+ if (!process.env.HIVE_PROVENANCE) process.env.HIVE_PROVENANCE = 'on'
34
+ if (!process.env.HIVE_FORK_GATE) process.env.HIVE_FORK_GATE = 'on'
28
35
  import { startSync, parseLink } from './sync.js'
29
36
 
30
37
  const DEFAULT_RELAY = process.env.HIVE_RELAY || 'wss://livecode-xoss.onrender.com'
@@ -42,6 +49,21 @@ function resolveRoom(link, dir) {
42
49
  return { relay: DEFAULT_RELAY, room: 'room-' + crypto.randomBytes(13).toString('base64url'), token: '', mode: 'HOSTED (new room)' }
43
50
  }
44
51
 
52
+ // Session persistence: keyed to this process's cwd, so each agent runtime (which starts
53
+ // its own MCP process from its own directory) resumes only its own identity. Lets the
54
+ // session survive MCP host restarts, which otherwise silently drop the agent from the room.
55
+ const SESSION_FILE = path.join(process.cwd(), '.hive-mcp-session.json')
56
+ function persistSession({ dir, name, owner }) {
57
+ try { fs.writeFileSync(SESSION_FILE, JSON.stringify({ dir, name, owner })) } catch { /* best-effort */ }
58
+ }
59
+ async function tryResumeSession() {
60
+ try {
61
+ if (!fs.existsSync(SESSION_FILE)) return
62
+ const s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'))
63
+ if (s && s.dir && s.name && fs.existsSync(path.join(path.resolve(s.dir), '.hive.json'))) await joinRoom(s)
64
+ } catch { /* resume is best-effort; the agent can always hive_join explicitly */ }
65
+ }
66
+
45
67
  async function joinRoom({ link = '', dir = './workspace', name = `agent-${crypto.randomBytes(2).toString('hex')}`, owner = '', token = '' }) {
46
68
  if (session) { try { session.hive.stop() } catch {} ; session = null }
47
69
  const ROOT = path.resolve(dir)
@@ -54,6 +76,7 @@ async function joinRoom({ link = '', dir = './workspace', name = `agent-${crypto
54
76
  fs.writeFileSync(path.join(ROOT, '.hive.json'), JSON.stringify({ relay, room, ...(useToken ? { token: useToken } : {}) }, null, 2))
55
77
  const hive = startSync({ relay, room, dir, name, kind: 'ai', owner, token: useToken, log: () => {} })
56
78
  session = { hive, room, relay, dir, name }
79
+ persistSession({ dir, name, owner }) // survive MCP host restarts (see tryResumeSession)
57
80
  // wait for the first sync so members/board/rules are ready
58
81
  await new Promise((resolve) => {
59
82
  let done = false
@@ -61,10 +84,24 @@ async function joinRoom({ link = '', dir = './workspace', name = `agent-${crypto
61
84
  hive.provider.on('sync', (s) => s && finish())
62
85
  setTimeout(finish, 8000) // don't hang if the relay is cold
63
86
  })
64
- hive.say(`${name} joined and is ready to work.`)
65
- const rules = (() => { try { return fs.readFileSync(path.join(ROOT, 'HIVE_RULES.md'), 'utf8') } catch { return '(rules not yet written)' } })()
66
87
  const others = hive.members().filter((m) => m.name !== name).map((m) => `${m.name}(${m.kind})`)
67
- return `${mode}\nroom: ${room}\nrelay: ${relay}\nfolder: ${ROOT}\nothers here: ${others.join(', ') || 'none yet'}\ninvite link: ${relay}|${room}\n\n--- FOLLOW THESE RULES ---\n${rules}`
88
+
89
+ // Event-Driven Wakeups: notify the MCP client the millisecond the DCO state changes
90
+ hive.doc.getMap('swarm_state').observe(() => {
91
+ try {
92
+ server.notification({ method: 'notifications/hive_state_changed', params: { state: hive.getState() } })
93
+ } catch (e) {
94
+ // client might not support notifications, swallow error
95
+ }
96
+ })
97
+
98
+ // The sync engine writes HIVE_RULES.md into the folder on first sync — return it inline
99
+ // so the agent gets the rules with its join. (Fixes a ReferenceError: `rules` was never
100
+ // defined, which made every hive_join return "error: rules is not defined".)
101
+ let rulesText = ''
102
+ try { rulesText = fs.readFileSync(path.join(ROOT, 'HIVE_RULES.md'), 'utf8') } catch {}
103
+ if (!rulesText) rulesText = 'Read HIVE_RULES.md in this folder. In short: read chat + board before editing; CLAIM a file with hive_claim before you edit it and hive_release when done; announce intent with hive_say; prefer small patches; act only on accepted tasks; resolve <<<<<<< conflict markers rather than blindly overwriting.'
104
+ return `${mode}\nroom: ${room}\nrelay: ${relay}\nfolder: ${ROOT}\nothers here: ${others.join(', ') || 'none yet'}\ninvite link: ${relay}|${room}\n\n--- FOLLOW THESE RULES ---\n${rulesText}`
68
105
  }
69
106
 
70
107
  function requireSession() { if (!session) throw new Error('not in a room — call hive_join first'); return session }
@@ -142,22 +179,44 @@ const TOOLS = [
142
179
  inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'how many recent messages (default 30)' } } } },
143
180
  { name: 'hive_read_board', description: 'Read recent whole-file rewrites (who rewrote what, which symbols). Read this before editing a file — if it was just rewritten, re-read the file first.',
144
181
  inputSchema: { type: 'object', properties: {} } },
182
+ { name: 'hive_claim', description: 'CLAIM a file (or region) BEFORE you edit it, so other agents flow around you instead of colliding. Returns whether you got it — if false, someone else holds it: work on something else. Claims auto-expire, so call hive_release when done (or it frees itself).',
183
+ inputSchema: { type: 'object', properties: { region: { type: 'string', description: 'the file path (or region id) you want to edit' }, intent: { type: 'string', description: 'short note on what you intend to do' } }, required: ['region'] } },
184
+ { name: 'hive_release', description: 'Release a file/region you claimed, so other agents can take it. Call this when you finish editing it.',
185
+ inputSchema: { type: 'object', properties: { region: { type: 'string', description: 'the file path (or region id) to release' } }, required: ['region'] } },
186
+ { name: 'hive_claims', description: 'See what every agent is currently working on (the live claim board). Read this to pick open work and avoid taken regions.',
187
+ inputSchema: { type: 'object', properties: {} } },
145
188
  { name: 'hive_members', description: 'List who is currently in the room (humans and AI agents).', inputSchema: { type: 'object', properties: {} } },
146
189
  { name: 'hive_status', description: 'Show the current session (room, relay, folder, name).', inputSchema: { type: 'object', properties: {} } },
147
190
  { name: 'hive_leave', description: 'Leave the room and stop syncing.', inputSchema: { type: 'object', properties: {} } },
191
+ { name: 'hive_set_state', description: 'PM ONLY: Set a global project state flag (e.g. key="contract", val="READY"). This physically unblocks/blocks other agents. WARNING: ONLY use short enum values. Do NOT use this to store paragraphs of text or code, as it will bloat the context window. Use files for text.', inputSchema: { type: 'object', properties: { key: { type: 'string' }, val: { type: 'string' } }, required: ['key', 'val'] } },
192
+ { name: 'hive_read_state', description: 'Read the global project state flags.', inputSchema: { type: 'object', properties: {} } },
193
+ { name: 'hive_set_schema', description: 'PM ONLY: Define strict JSON schema validation for a global state key to prevent agents from hallucinating values (e.g. key="contract", rules={"enum": ["PLANNING", "READY", "APPROVED"]}).', inputSchema: { type: 'object', properties: { key: { type: 'string' }, rules: { type: 'object' } }, required: ['key', 'rules'] } },
148
194
  ]
149
195
 
150
- const server = new Server({ name: 'hivecode', version: '0.1.0' }, { capabilities: { tools: {} } })
196
+ function enforceDcoLock() {
197
+ const state = requireSession().hive.getState()
198
+ const name = requireSession().name
199
+ if (state[`${name}_status`] === 'LOCKED' || state[`${name}_locked`] === 'true') {
200
+ throw new Error(`[DCO Enforced] You are locked by the global state machine. Tool execution denied. Block on hive_wait until your status changes.`)
201
+ }
202
+ }
203
+
204
+ const server = new Server({ name: 'hivecode', version: '0.8.0' }, { capabilities: { tools: {} } })
151
205
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
152
206
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
153
207
  const { name, arguments: args = {} } = req.params
154
208
  try {
209
+ // AUTO-RESUME: the MCP host may restart this server process at any time, which
210
+ // wipes the in-memory session and silently kicks the agent out of its room. If a
211
+ // persisted session exists for this cwd, transparently rejoin before any tool runs —
212
+ // the agent (and its human) never see a "not in a room" flake again.
213
+ if (!session && name !== 'hive_join' && name !== 'hive_leave') await tryResumeSession()
155
214
  switch (name) {
156
215
  case 'hive_join': return text(await joinRoom(args))
157
216
  case 'hive_say': { requireSession().hive.say(String(args.text || '')); return text(`sent: ${args.text}`) }
158
217
  case 'hive_read_chat': { const n = args.limit || 30; const msgs = chatArray().slice(-n).map((m) => `${m.at} ${m.by}(${m.kind}): ${m.text}`); return text(msgs.join('\n') || '(no messages yet)') }
159
218
  case 'hive_read_board': { const b = boardMap(); return text(b.length ? b.map((e) => `${e.at} ${e.by} rewrote ${e.file} (${e.churn}) — touched: ${(e.symbols || []).join(', ')}`).join('\n') : '(no rewrites logged)') }
160
- case 'hive_assign': { const id = requireSession().hive.assign(String(args.to || ''), String(args.text || '')); return id ? text(`assigned task ${id} to ${args.to} (pending approval if they have an owner)`) : err('need to and text') }
219
+ case 'hive_assign': { enforceDcoLock(); const id = requireSession().hive.assign(String(args.to || ''), String(args.text || '')); return id ? text(`assigned task ${id} to ${args.to} (pending approval if they have an owner)`) : err('need to and text') }
161
220
  case 'hive_read_tasks': {
162
221
  const all = taskList(); const me = requireSession().name
163
222
  const mine = all.filter((t) => t.to === me)
@@ -167,6 +226,9 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
167
226
  case 'hive_wait': {
168
227
  const ms = Math.min(Math.max(Number(args.timeoutSeconds) || 60, 1), 300) * 1000
169
228
  const r = await waitForWork(ms)
229
+ const globalState = requireSession().hive.getState()
230
+ const stateStr = Object.keys(globalState).length ? `\n[GLOBAL PROJECT STATE]\n${JSON.stringify(globalState, null, 2)}\n` : ''
231
+
170
232
  if (r && r.paused) {
171
233
  if (r.mentioned) {
172
234
  const parts = []
@@ -174,21 +236,34 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
174
236
  if (r.tasks && r.tasks.length) parts.push(`📌 Directed task(s):\n${r.tasks.map(fmtTask).join('\n')}`)
175
237
  return text(`⏸ You are PAUSED, but were directly addressed. Handle ONLY this, then call hive_wait again (you stay paused until a human resumes you — don't pick up other queued work):\n\n${parts.join('\n\n')}`)
176
238
  }
177
- return text('⏸ You are PAUSED by mission control. Stop new work and wait. You will be woken if someone @mentions you or assigns you a task; otherwise just call hive_wait again.')
239
+ return text(`⏸ You are PAUSED by mission control. Stop new work and wait. You will be woken if someone @mentions you or assigns you a task; otherwise just call hive_wait again.${stateStr}`)
178
240
  }
179
- if (!r) return text('(nothing yet — no approved work or new messages in the wait window. Call hive_wait again to keep waiting.)')
241
+ if (!r) return text(`(nothing yet — no approved work or new messages in the wait window. Call hive_wait again to keep waiting.)${stateStr}`)
180
242
  const parts = []
181
243
  if (r.resumed) parts.push('▶ You were RESUMED by mission control — continue.')
182
244
  if (r.tasks.length) parts.push(`APPROVED WORK — do these now, then hive_complete each:\n${r.tasks.map(fmtTask).join('\n')}`)
183
245
  if (r.newChat.length) parts.push(`NEW MESSAGES:\n${r.newChat.map((m) => `${m.at} ${m.by}(${m.kind}): ${m.text}`).join('\n')}`)
184
- return text(parts.join('\n\n') || '(resumed — no queued work yet. Call hive_wait again.)')
246
+ return text(`${stateStr}\n` + (parts.join('\n\n') || '(resumed — no queued work yet. Call hive_wait again.)'))
185
247
  }
248
+ case 'hive_set_state': { requireSession().hive.setState(String(args.key), String(args.val)); return text(`Set global state [${args.key}] to [${args.val}]`) }
249
+ case 'hive_read_state': { return text(requireSession().hive.getState()) }
250
+ case 'hive_set_schema': { requireSession().hive.setSchema(String(args.key), args.rules); return text(`Set schema validation for [${args.key}]`) }
186
251
  case 'hive_approve': { const r = requireSession().hive.decide(String(args.id), true); return r.ok ? text(`approved ${args.id}`) : err(r.error) }
187
252
  case 'hive_deny': { const r = requireSession().hive.decide(String(args.id), false); return r.ok ? text(`denied ${args.id}`) : err(r.error) }
188
- case 'hive_complete': { const r = requireSession().hive.complete(String(args.id), String(args.note || '')); return r.ok ? text(`completed ${args.id}`) : err(r.error) }
253
+ case 'hive_complete': { enforceDcoLock(); const r = requireSession().hive.complete(String(args.id), String(args.note || '')); return r.ok ? text(`completed ${args.id}`) : err(r.error) }
254
+ case 'hive_claim': {
255
+ enforceDcoLock();
256
+ const region = String(args.region || ''); if (!region) return err('need a region (file path)')
257
+ const got = requireSession().hive.claim(region, String(args.intent || 'edit'))
258
+ if (got) return text(`claimed ${region} — it's yours. Edit it, then call hive_release.`)
259
+ const c = requireSession().hive.senseClaim(region)
260
+ return text(`could NOT claim ${region}${c ? ` — held by ${c.by}${c.intent ? ` (${c.intent})` : ''}` : ''}. Work on something else; check hive_claims for open files.`)
261
+ }
262
+ case 'hive_release': { const region = String(args.region || ''); if (!region) return err('need a region'); requireSession().hive.release(region); return text(`released ${region}`) }
263
+ case 'hive_claims': { const b = requireSession().hive.claimsBoard(); return text(b.length ? b.map((c) => `${c.region} — ${c.by}${c.intent ? ` (${c.intent})` : ''}`).join('\n') : '(nothing claimed right now — all open)') }
189
264
  case 'hive_members': { const now = Date.now(); return text(requireSession().hive.members().map((m) => `${m.name} (${m.kind})${m.editing && now - m.editing.at < 15000 ? ` — editing ${m.editing.file}` : ''}`).join('\n') || '(none)') }
190
265
  case 'hive_status': { const s = requireSession(); return text({ room: s.room, relay: s.relay, dir: s.dir, name: s.name }) }
191
- case 'hive_leave': { requireSession().hive.stop(); session = null; return text('left the room') }
266
+ case 'hive_leave': { requireSession().hive.stop(); session = null; try { fs.rmSync(SESSION_FILE) } catch {}; return text('left the room') }
192
267
  default: return err(`unknown tool: ${name}`)
193
268
  }
194
269
  } catch (e) { return err(`error: ${e.message}`) }
package/icr-merge.js ADDED
@@ -0,0 +1,58 @@
1
+ // icr-merge.js — the bridge that brings ICR into the live product, SAFELY.
2
+ //
3
+ // Hivecode's reconcile() merges concurrent edits with merge3() (a line-level 3-way
4
+ // merge). merge3 is convergent: across many peers it always settles on one canonical
5
+ // text. ICR's structural merge produces a DIFFERENT canonical form (it may reorder or
6
+ // reformat). Mixing two merge algorithms in a live multi-peer sync means the same logical
7
+ // content has two byte-forms, and peers ping-pong between them — which corrupts files
8
+ // (duplicated/*dropped* lines). So we do NOT change merge bytes with ICR here.
9
+ //
10
+ // Instead ICR runs in ADVISORY mode: merge3 still produces the bytes (full convergence
11
+ // preserved, identical to pre-ICR behavior), and ICR runs alongside purely to DETECT the
12
+ // meaning-level problems a line merge can't see — a dangling reference, a rename, a
13
+ // same-declaration clash — and surface them as a warning on the activity feed. This ships
14
+ // ICR's core value (catching semantic breaks) with zero divergence risk.
15
+ //
16
+ // ICR's full structural/rename AUTO-MERGE lives in the library (icr.js) and is proven by
17
+ // the test suite + fuzzer; turning it on in the product is a later step that needs a
18
+ // format-preserving, single-canonical-form merge so peers can't diverge.
19
+
20
+ import { structuralMerge, supports } from './icr.js'
21
+ import { merge3 } from './core.js'
22
+
23
+ // Turn ICR's machine conflict keys into a human sentence for the activity feed / chat.
24
+ function describeConflicts(conflicts) {
25
+ return (conflicts || []).map((c) => {
26
+ if (c.startsWith('ref:')) return `'${c.slice(4)}' was removed or renamed but is still used`
27
+ if (c.startsWith('fn:')) return `both sides changed function ${c.slice(3)}`
28
+ if (c.startsWith('class:')) return `both sides changed class ${c.slice(6)}`
29
+ if (c.startsWith('var:')) return `both sides changed ${c.slice(4)}`
30
+ return `both sides changed ${c}`
31
+ }).join('; ')
32
+ }
33
+
34
+ // Same shape as merge3 — { text, conflict } — plus an `icr` tag ('structural'|'rename')
35
+ // and an optional `icrWarning`. ICR's format-preserving merge is convergent (symmetric +
36
+ // fixed-point, verified by icr-converge-test.js), so its 'auto' result is used as the
37
+ // merged bytes for supported files; merge3 covers conflicts, fallbacks, and other languages.
38
+ export function icrMerge3(base, mine, theirs, relPath) {
39
+ const lm = merge3(base, mine, theirs)
40
+ if (!supports(relPath)) return lm
41
+
42
+ let r
43
+ try { r = structuralMerge(base, mine, theirs, { filename: relPath }) }
44
+ catch { return lm } // ICR must never break a sync
45
+
46
+ if (r.status === 'auto') {
47
+ const renamed = r.renames && r.renames.length
48
+ return {
49
+ text: r.text, conflict: false, icr: renamed ? 'rename' : 'structural',
50
+ icrWarning: renamed ? `auto-applied rename ${r.renames.join(', ')} — call sites updated` : undefined,
51
+ }
52
+ }
53
+ // A meaning-level conflict the line merge can't represent (dangling ref / clash): keep
54
+ // the safe line-merge bytes, but surface what ICR caught so it isn't shipped silently.
55
+ if (r.status === 'semantic-conflict')
56
+ return { ...lm, icrWarning: describeConflicts(r.conflicts) }
57
+ return lm // 'fallback'
58
+ }