hivecode-mcp 0.7.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,149 +1,68 @@
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
- <!-- Add a demo GIF here once recorded: ![demo](docs/demo.gif) -->
14
-
15
- ---
16
-
17
- ## Why
18
-
19
- You can't trust an AI agent blindly. The faster it edits, the more you have to
20
- check — and the moment you run more than one, it gets worse: they touch files they
21
- shouldn't, overwrite each other, and you lose track of who changed what. Today's
22
- answer is to isolate each agent in a git worktree and pray at merge time — which
23
- doesn't help you *trust* the output, it just defers the mess.
24
-
25
- Hivecode adds the missing layer: **oversight and control.**
26
-
27
- - **See everything** — a live Control Room (browser or phone) shows every agent and every file it touches, in real time.
28
- - **Fence them in** — folder-scoped access the relay enforces; an out-of-scope agent never even receives the code.
29
- - **Approve what matters** — gate risky work so nothing lands without your OK.
30
- - **Undo anything** — instant rollback: restore any file, or revert *everything* one agent did.
31
-
32
- It's **agent-neutral**: Claude Code, Cursor, Windsurf, or your own bot over MCP. No
33
- git push/pull — edits sync in about a second.
34
-
35
- ---
36
-
37
- ## Quickstart
38
-
39
- 1. Install the extension (link above). It works in **VS Code, Cursor, Windsurf,
40
- and Antigravity**.
41
- 2. Open the folder you want to share (**File → Open Folder**).
42
- 3. `Ctrl/Cmd+Shift+P` → **Hivecode: Host a Secured Session**. A join link is
43
- copied to your clipboard. *(No terminal, no server to run.)*
44
- 4. **Hivecode: Invite to folders…** → pick the folders and a role (edit /
45
- read-only) for each person or agent. Send them the link.
46
- 5. They run **Hivecode: Join a Session** and paste the link. You're now editing
47
- the same project live.
48
-
49
- Use **Manage access** to re-scope or revoke anyone, and **Leave Session** to stop.
50
-
51
- ---
52
-
53
- ## Add an AI agent
54
-
55
- Agents join through the Hivecode MCP server ([`hive-mcp.js`](hive-mcp.js)). Set it
56
- up once, then just hand the agent a join link; it appears in the room as an `ai`
57
- member, scoped to exactly the folders you invited it to.
58
-
59
- - **[JOIN_WITH_AGENT.md](JOIN_WITH_AGENT.md)** — step-by-step for a *human*: set up MCP, then bring your agent in.
60
- - **[AGENT_MANUAL.md](AGENT_MANUAL.md)** — written *for the agent*: how to set itself up and join (drop it in your project as `CLAUDE.md` so the agent reads it automatically).
61
- - **[MCP.md](MCP.md)** — the full tool reference.
62
-
63
- ---
64
-
65
- ## What you get
66
-
67
- **Oversight & control** *(the point of the whole thing)*
68
- - **Live Control Room** — watch every agent and every file it touches in real time,
69
- from any browser or your phone. No more "what is it even doing right now?"
70
- - **Folder-scoped access** — invite an agent to `frontend/` only; it never receives
71
- anything else (relay-enforced, not trust-based).
72
- - **Approval gates** — risky work waits for your OK; agent-to-agent coordination
73
- flows automatically.
74
- - **Instant rollback** — restore any file to an earlier point, or **revert
75
- *everything* one agent did** in a click. Undo a rogue agent without touching the
76
- rest of the team's work.
77
- - **Mission control** — pause, resume, or reassign any agent; agents honor it
78
- mid-task.
79
- - **Instant revoke** — cut access mid-session, enforced server-side on every
80
- reconnect (and it survives a relay restart).
81
- - **Read-only roles** — reviewers see everything, change nothing.
82
-
83
- **Coordination**
84
- - **Built-in chat**, a **shared task board** (assign / complete), and **live
85
- presence** so humans and agents stay in sync.
86
- - **Co-editing heads-up** before two people touch the same file.
87
- - **Never lose work** — overlapping edits keep both versions with conflict markers;
88
- a stale rewrite can't silently delete another's lines.
89
-
90
- **Trust**
91
- - **Server-enforced auth** — signed tokens, algorithm-pinned, fail-closed.
92
- Unauthorized clients never complete the WebSocket handshake.
93
- - **Self-certifying secured rooms** — the room id embeds a fingerprint of your
94
- public key; tokens carry the key and are signed by it; the relay trusts a token
95
- only if the fingerprint matches. No account system, no shared secret. Your
96
- private key stays in the editor's secure storage.
97
- - **Dependency-free core** — the access-control layer uses only Node's `crypto`.
98
-
99
- ---
100
-
101
- ## How the security model works
102
-
103
- A secured room's id is `hs_<fp>_<rand>`, where `fp` is a base64url SHA-256
104
- fingerprint of the owner's public key. Every token for the room carries the owner's
105
- public key (claim `pk`) and is signed by the matching private key (RS256). The relay
106
- trusts a token **iff** `fingerprint(token.pk) === fingerprint-in-room-id`, then
107
- verifies the signature with that key — so trust is anchored in the room id itself
108
- and the relay stores nothing.
109
-
110
- Files use a per-file subdocument model: the room is `<base>`, each file is a
111
- separate document at `<base>␁<path>`. The relay authorizes per path, so a scoped
112
- client can only open the files it may load, and out-of-scope content never reaches
113
- its disk. Paths are guarded against traversal, absolute paths, drive letters, and
114
- control characters on both the client (before writing) and the relay.
115
-
116
- A full threat/edge-case audit is in [EDGE-CASES.md](EDGE-CASES.md); the RBAC design
117
- is in [RBAC.md](RBAC.md).
118
-
119
- ---
120
-
121
- ## Self-hosting the relay
122
-
123
- The hosted relay is the default (`wss://livecode-xoss.onrender.com`) and needs no
124
- setup. To run your own, deploy [`server.js`](server.js) anywhere Node runs and set
125
- `hivecode.relayUrl` in the extension. The relay holds no truth of its own — it just
126
- passes CRDT updates between clients in the same room, and enforces access at the
127
- handshake. See [DEPLOY.md](DEPLOY.md).
128
-
129
- ---
130
-
131
- ## Development
132
-
133
- ```bash
134
- npm install
135
- node test.js # unit tests
136
- node hive-edge-test.js # adversarial / edge-case live tests
137
- ```
138
-
139
- Other live suites: `hive-secure-test.js`, `hive-scope-test.js`, `hive-auth-test.js`,
140
- `hive-readonly-test.js`, `hive-resume-test.js`, `hive-control-test.js`,
141
- `hive-durable-revoke-test.js`, `hive-relay-robust-test.js`.
142
-
143
- The extension source is in [`extension/`](extension/).
144
-
145
- ---
146
-
147
- ## License
148
-
149
- 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-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 }
@@ -151,19 +188,35 @@ const TOOLS = [
151
188
  { name: 'hive_members', description: 'List who is currently in the room (humans and AI agents).', inputSchema: { type: 'object', properties: {} } },
152
189
  { name: 'hive_status', description: 'Show the current session (room, relay, folder, name).', inputSchema: { type: 'object', properties: {} } },
153
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'] } },
154
194
  ]
155
195
 
156
- 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: {} } })
157
205
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
158
206
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
159
207
  const { name, arguments: args = {} } = req.params
160
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()
161
214
  switch (name) {
162
215
  case 'hive_join': return text(await joinRoom(args))
163
216
  case 'hive_say': { requireSession().hive.say(String(args.text || '')); return text(`sent: ${args.text}`) }
164
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)') }
165
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)') }
166
- 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') }
167
220
  case 'hive_read_tasks': {
168
221
  const all = taskList(); const me = requireSession().name
169
222
  const mine = all.filter((t) => t.to === me)
@@ -173,6 +226,9 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
173
226
  case 'hive_wait': {
174
227
  const ms = Math.min(Math.max(Number(args.timeoutSeconds) || 60, 1), 300) * 1000
175
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
+
176
232
  if (r && r.paused) {
177
233
  if (r.mentioned) {
178
234
  const parts = []
@@ -180,19 +236,23 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
180
236
  if (r.tasks && r.tasks.length) parts.push(`📌 Directed task(s):\n${r.tasks.map(fmtTask).join('\n')}`)
181
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')}`)
182
238
  }
183
- 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}`)
184
240
  }
185
- 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}`)
186
242
  const parts = []
187
243
  if (r.resumed) parts.push('▶ You were RESUMED by mission control — continue.')
188
244
  if (r.tasks.length) parts.push(`APPROVED WORK — do these now, then hive_complete each:\n${r.tasks.map(fmtTask).join('\n')}`)
189
245
  if (r.newChat.length) parts.push(`NEW MESSAGES:\n${r.newChat.map((m) => `${m.at} ${m.by}(${m.kind}): ${m.text}`).join('\n')}`)
190
- 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.)'))
191
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}]`) }
192
251
  case 'hive_approve': { const r = requireSession().hive.decide(String(args.id), true); return r.ok ? text(`approved ${args.id}`) : err(r.error) }
193
252
  case 'hive_deny': { const r = requireSession().hive.decide(String(args.id), false); return r.ok ? text(`denied ${args.id}`) : err(r.error) }
194
- 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) }
195
254
  case 'hive_claim': {
255
+ enforceDcoLock();
196
256
  const region = String(args.region || ''); if (!region) return err('need a region (file path)')
197
257
  const got = requireSession().hive.claim(region, String(args.intent || 'edit'))
198
258
  if (got) return text(`claimed ${region} — it's yours. Edit it, then call hive_release.`)
@@ -203,7 +263,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
203
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)') }
204
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)') }
205
265
  case 'hive_status': { const s = requireSession(); return text({ room: s.room, relay: s.relay, dir: s.dir, name: s.name }) }
206
- 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') }
207
267
  default: return err(`unknown tool: ${name}`)
208
268
  }
209
269
  } catch (e) { return err(`error: ${e.message}`) }
package/icr.js CHANGED
@@ -26,9 +26,14 @@
26
26
  // providers for every language behind this same interface.
27
27
 
28
28
  import { javascript } from './lang-js.js'
29
+ import { braceLanguages } from './lang-brace.js'
30
+ import { python } from './lang-python.js'
29
31
 
30
32
  // --- language registry ----------------------------------------------------------
31
- const LANGUAGES = [javascript]
33
+ // JavaScript (acorn, full intent layer) first; then structural providers for the
34
+ // C-family (TypeScript, Go, Rust, Java, C/C++, C#, Swift, Kotlin, …) and Python.
35
+ // These cover disjoint extensions, so order only matters on clashes (none today).
36
+ const LANGUAGES = [javascript, ...braceLanguages, python]
32
37
 
33
38
  // Register an additional language provider. Must expose the provider contract:
34
39
  // { id, exts:[...], parses, units, declaredNames, usedIdentifiers, declBody,
package/lang-brace.js ADDED
@@ -0,0 +1,174 @@
1
+ // Brace-family language provider for ICR — a dependency-free, language-agnostic
2
+ // structural splitter for C-style languages (TypeScript, Go, Rust, Java, C/C++,
3
+ // C#, Swift, Kotlin, Scala, PHP, Dart, …). It implements the ICR provider contract
4
+ // { id, exts, parses, units, declaredNames, splitUnit, parsesUnit } so the SAME
5
+ // merge engine in icr.js merges these languages by structure — different top-level
6
+ // declarations merge cleanly, the same declaration edited two ways is a named
7
+ // conflict, and edits to different members inside one class/function merge via the
8
+ // engine's finer-grained inner merge (splitUnit).
9
+ //
10
+ // It is a heuristic scanner, not a full parser: it tracks strings, line/block
11
+ // comments, and bracket depth to find top-level declarations and their byte ranges.
12
+ // This gives real structural merge and an approximate parses() (balanced delimiters)
13
+ // — honest and useful today; a tree-sitter provider can later replace it behind the
14
+ // same interface with zero engine changes.
15
+
16
+ const LINE = ['//']
17
+ const OPEN = { '{': '}', '(': ')', '[': ']' }
18
+ const CLOSE = { '}': '{', ')': '(', ']': '[' }
19
+
20
+ // Walk `src` once. `at(i)` returns the index just past a string or comment starting
21
+ // at i, or -1 if none starts there — so callers can skip over them uniformly.
22
+ function scannerAt(src, strings) {
23
+ const n = src.length
24
+ return function at(i) {
25
+ if (src[i] === '/' && src[i + 1] === '*') { let j = i + 2; while (j < n && !(src[j] === '*' && src[j + 1] === '/')) j++; return Math.min(n, j + 2) }
26
+ for (const lc of LINE) if (src.startsWith(lc, i)) { let j = i + lc.length; while (j < n && src[j] !== '\n') j++; return j }
27
+ if (strings.includes(src[i])) { const q = src[i]; let j = i + 1; while (j < n) { if (src[j] === '\\') { j += 2; continue } if (src[j] === q) { j++; break } j++ } return j }
28
+ return -1
29
+ }
30
+ }
31
+
32
+ const isWS = (c) => c === ' ' || c === '\t' || c === '\r' || c === '\n'
33
+
34
+ // Extract top-level units [{start,end}] from src[from..to]. A unit is a maximal
35
+ // declaration/statement at bracket depth 0: either a block (signature + balanced
36
+ // {…}) or a statement ending at ';' or end-of-line.
37
+ function topUnits(src, strings, from = 0, to = src.length) {
38
+ const at = scannerAt(src, strings)
39
+ const units = []
40
+ let i = from
41
+ while (i < to) {
42
+ while (i < to && isWS(src[i])) i++
43
+ if (i >= to) break
44
+ // A standalone comment before a declaration belongs to the GAP (kept verbatim by
45
+ // the engine's splice), so skip it without starting a unit.
46
+ if ((src[i] === '/' && (src[i + 1] === '/' || src[i + 1] === '*'))) { const s = at(i); if (s > i) { i = s; continue } }
47
+ const start = i
48
+ let depth = 0, sawBrace = false
49
+ while (i < to) {
50
+ const s = at(i)
51
+ if (s > i) { i = s; continue } // skip string / comment
52
+ const c = src[i]
53
+ if (OPEN[c]) { if (c === '{') sawBrace = true; depth++; i++; continue }
54
+ if (CLOSE[c]) { depth--; i++; if (depth <= 0 && sawBrace) { while (i < to && (src[i] === ' ' || src[i] === '\t')) i++; if (src[i] === ';') i++; break } continue }
55
+ if (depth === 0) {
56
+ if (c === ';') { i++; break }
57
+ if (c === '\n' && !sawBrace) break
58
+ }
59
+ i++
60
+ }
61
+ units.push({ start, end: i })
62
+ }
63
+ return units
64
+ }
65
+
66
+ const DECL = /\b(function|func|fn|def|fun|class|interface|struct|enum|trait|impl|type|namespace|module|package|object|record|protocol|actor|extension)\s+([A-Za-z_$][\w$]*)/
67
+ const VARD = /\b(const|let|var|val|static|final)\s+([A-Za-z_$][\w$]*)/
68
+ // Method/function signature, including TYPE-FIRST forms (`int a()`, `public static
69
+ // void main(String[] a)`, `std::string name()`): optional decorators + any number of
70
+ // modifiers + an optional return-type token, then NAME(args){. The captured name is
71
+ // the identifier directly before `(`.
72
+ const METH = /^(?:@[\w.()]+\s+)*(?:(?:public|private|protected|internal|static|async|final|override|open|pub|suspend|inline|virtual|export|default|abstract|sealed|unsafe|extern|synchronized|native|constexpr)\s+)*(?:(?:function|fn|func|fun)\s+)?(?:[A-Za-z_$][\w$<>\[\].,:&*\s]*?\s+)?([A-Za-z_$][\w$]*)\s*(?:<[^>]*>)?\s*\([^;{]*\)\s*(?:->[^{]+|:[^={]+)?\s*\{/
73
+ // control-flow headers also look like `name (…) {` — never key them as methods
74
+ const CTRL = new Set(['if', 'for', 'while', 'switch', 'catch', 'do', 'else', 'try', 'return', 'new', 'throw', 'match', 'select', 'when', 'until', 'lock', 'using', 'foreach', 'with'])
75
+
76
+ function keyOf(text) {
77
+ const head = text.replace(/^\s+/, '')
78
+ let m = head.match(DECL); if (m) return m[1] + ':' + m[2]
79
+ m = head.match(METH); if (m && !CTRL.has(m[1])) return 'method:' + m[1]
80
+ m = head.match(VARD); if (m) return 'var:' + m[2]
81
+ return 'stmt:' + head.split('\n')[0].trim().replace(/\s+/g, ' ')
82
+ }
83
+
84
+ // De-duplicate keys deterministically (repeated statements / same-named units) so the
85
+ // engine's keyed merge never silently drops one. Order is stable → convergent.
86
+ function keyedUnits(raw, src) {
87
+ const seen = new Map(), out = []
88
+ for (const u of raw) {
89
+ const text = src.slice(u.start, u.end)
90
+ let k = keyOf(text)
91
+ const c = (seen.get(k) || 0) + 1; seen.set(k, c)
92
+ if (c > 1) k += '#' + c
93
+ out.push({ key: k, text, start: u.start, end: u.end })
94
+ }
95
+ return out
96
+ }
97
+
98
+ // Approximate parse check: delimiters balanced and no unterminated string/comment,
99
+ // ignoring the contents of strings and comments.
100
+ function balanced(src, strings) {
101
+ const at = scannerAt(src, strings)
102
+ const stack = []
103
+ let i = 0
104
+ while (i < src.length) {
105
+ const s = at(i)
106
+ if (s > i) { if (s > src.length) return false; i = s; continue }
107
+ const c = src[i]
108
+ if (OPEN[c]) stack.push(c)
109
+ else if (CLOSE[c]) { if (stack.pop() !== CLOSE[c]) return false }
110
+ i++
111
+ }
112
+ return stack.length === 0
113
+ }
114
+
115
+ export function makeBraceLanguage({ id, exts, strings = ['"', "'", '`'] }) {
116
+ const provider = {
117
+ id,
118
+ exts,
119
+ parses: (src) => balanced(String(src == null ? '' : src), strings),
120
+ units: (src) => keyedUnits(topUnits(String(src), strings), String(src)),
121
+ declaredNames: (src) => {
122
+ const names = new Set()
123
+ for (const u of keyedUnits(topUnits(String(src), strings), String(src))) {
124
+ const m = u.key.match(/^(?:[a-z]+):([A-Za-z_$][\w$]*)/)
125
+ if (m && !u.key.startsWith('stmt:')) names.add(m[1])
126
+ }
127
+ return names
128
+ },
129
+ parsesUnit: (src) => balanced(String(src), strings),
130
+ // Finer granularity: descend one level into a block declaration so two agents
131
+ // editing DIFFERENT members of the same class/function merge instead of clashing.
132
+ splitUnit: (text) => {
133
+ const src = String(text)
134
+ // Only descend into NAMED containers (class/struct/impl/…), whose members are
135
+ // keyed by name — so editing different members merges. A function/method body is
136
+ // just statements (no stable keys); descending there would falsely merge two
137
+ // edits to the same code, so we DON'T — the whole unit becomes a clean conflict.
138
+ if (!/\b(class|interface|struct|enum|trait|impl|namespace|module|object|record|extension|protocol|actor)\b/.test(src.split('{')[0]))
139
+ return null
140
+ const at = scannerAt(src, strings)
141
+ let i = 0, open = -1
142
+ while (i < src.length) { const s = at(i); if (s > i) { i = s; continue } if (src[i] === '{') { open = i; break } i++ }
143
+ if (open < 0) return null
144
+ // find matching close
145
+ let depth = 0, j = open, close = -1
146
+ while (j < src.length) { const s = at(j); if (s > j) { j = s; continue } const c = src[j]; if (c === '{') depth++; else if (c === '}') { depth--; if (depth === 0) { close = j; break } } j++ }
147
+ if (close < 0) return null
148
+ const sig = src.slice(0, open)
149
+ const body = src.slice(open + 1, close)
150
+ const inner = keyedUnits(topUnits(body, strings), body)
151
+ // re-base inner ranges into `body` coordinates are already correct (topUnits used body)
152
+ return { sig, open: '{', close: src.slice(close), body, units: inner, join: '' }
153
+ },
154
+ }
155
+ return provider
156
+ }
157
+
158
+ // Ready-made providers covering the common C-family languages. One provider can claim
159
+ // many extensions; comment/string syntax defaults fit the whole family (edge cases like
160
+ // Rust raw strings / lifetimes are tolerated — worst case a unit is merged coarsely).
161
+ export const braceLanguages = [
162
+ makeBraceLanguage({ id: 'ts', exts: ['.ts', '.tsx', '.mts', '.cts'] }),
163
+ makeBraceLanguage({ id: 'jsx', exts: ['.jsx'] }),
164
+ makeBraceLanguage({ id: 'go', exts: ['.go'], strings: ['"', '`'] }),
165
+ makeBraceLanguage({ id: 'rust', exts: ['.rs'] }),
166
+ makeBraceLanguage({ id: 'java', exts: ['.java'] }),
167
+ makeBraceLanguage({ id: 'c', exts: ['.c', '.h', '.cpp', '.hpp', '.cc', '.hh', '.cxx'] }),
168
+ makeBraceLanguage({ id: 'csharp', exts: ['.cs'] }),
169
+ makeBraceLanguage({ id: 'swift', exts: ['.swift'] }),
170
+ makeBraceLanguage({ id: 'kotlin', exts: ['.kt', '.kts'] }),
171
+ makeBraceLanguage({ id: 'scala', exts: ['.scala'] }),
172
+ makeBraceLanguage({ id: 'php', exts: ['.php'] }),
173
+ makeBraceLanguage({ id: 'dart', exts: ['.dart'] }),
174
+ ]