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 +68 -137
- package/hive-coord.js +72 -0
- package/hive-mcp.js +85 -10
- package/icr-merge.js +58 -0
- package/icr.js +272 -0
- package/lang-brace.js +174 -0
- package/lang-js.js +418 -0
- package/lang-python.js +149 -0
- package/package.json +18 -5
- package/substrate.js +252 -0
- package/sync.js +711 -19
- package/token.js +20 -0
package/README.md
CHANGED
|
@@ -1,137 +1,68 @@
|
|
|
1
|
-
<h1>Hivecode</h1>
|
|
2
|
-
|
|
3
|
-
**
|
|
4
|
-
|
|
5
|
-
Run
|
|
6
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
the relay enforces
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
##
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
+
}
|