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 +68 -149
- package/hive-mcp.js +70 -10
- package/icr.js +6 -1
- package/lang-brace.js +174 -0
- package/lang-python.js +149 -0
- package/package.json +13 -5
- package/substrate.js +252 -0
- package/sync.js +427 -8
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
+
]
|