hivecode-mcp 0.13.0 → 0.13.1
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/docs/GITHUB_WORKFLOW.md +60 -0
- package/docs/TROUBLESHOOTING.md +18 -0
- package/hive-authors.js +44 -0
- package/hive-await.js +11 -6
- package/hive-branch.js +79 -158
- package/hive-github.js +40 -0
- package/hive-land.js +691 -0
- package/hive-listen.js +2 -2
- package/hive-mcp.js +64 -5
- package/hive-pr.js +201 -38
- package/hive-repos.js +430 -0
- package/hive-room-mode.js +1668 -0
- package/hive-room-watcher.js +561 -0
- package/hive-stop-hook.js +1 -1
- package/package.json +3 -2
- package/sync.js +214 -12
package/docs/GITHUB_WORKFLOW.md
CHANGED
|
@@ -164,6 +164,66 @@ Notifications are optional: one Slack or Discord webhook URL. They are sent when
|
|
|
164
164
|
|
|
165
165
|
Nothing is sent for each comment.
|
|
166
166
|
|
|
167
|
+
## Room mode: multi-agent issues in a shared room
|
|
168
|
+
|
|
169
|
+
Room Mode hands labelled issues directly to agents already collaborating in a shared room bound to a branch.
|
|
170
|
+
Rather than spinning up isolated single-agent runs, the room's agents claim files, build, and review each other concurrently.
|
|
171
|
+
When work is complete and reviewed, Hivecode runs the project tests and opens or updates a single pull request.
|
|
172
|
+
|
|
173
|
+
### Setup
|
|
174
|
+
|
|
175
|
+
1. **Bind a room to a branch:**
|
|
176
|
+
```bash
|
|
177
|
+
npx -y hivecode-mcp@latest branch <branch-name>
|
|
178
|
+
```
|
|
179
|
+
This creates or binds a branch room on the relay and prints its join link (`wss://...|room-...`).
|
|
180
|
+
|
|
181
|
+
2. **Agents join the room:**
|
|
182
|
+
Connect your agents (Claude Code, Codex, Antigravity, or human developers) into the room using `hive_join` or the room link. Each agent gets its own workspace folder and display name.
|
|
183
|
+
|
|
184
|
+
3. **Start the room watcher:**
|
|
185
|
+
```bash
|
|
186
|
+
npx -y hivecode-mcp@latest github start --room "<room link>" [--fallback-agent "<command>"] [--ack-minutes 10]
|
|
187
|
+
```
|
|
188
|
+
The watcher polls GitHub for labelled issues and manages task dispatch and landings for that room.
|
|
189
|
+
|
|
190
|
+
### What the person sees on GitHub
|
|
191
|
+
|
|
192
|
+
Every event is posted in order on the issue or the pull request. Room identifiers and relay URLs are never posted on GitHub.
|
|
193
|
+
|
|
194
|
+
| event | where | what you see |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| Issue labelled `hivecode` | Issue comment | `Sent to room for branch <name>. In the room now: <agents>` (lists active agents and humans separately; skips passive bots). |
|
|
197
|
+
| Tests fail during landing | Issue comment | `tests failed, sent back to the room (attempt 1 of 2)`. The failing test output is posted as a task into the room. Unchanged code is not re-tested. |
|
|
198
|
+
| Work complete and reviewed | Pull request opened | PR titled after the issue (`<Title> (#1)`), body with `Closes #1`, summary of changed files, test verification (`Tests: npm test passed on <sha>, the exact commit pushed`), and one commit per author. |
|
|
199
|
+
| Issue updated with PR | Issue comment | `landed as PR #N` linking the pull request. |
|
|
200
|
+
| PR review feedback | PR comment & Room task | Comments on the PR become a single room task naming the blamed author (`Sent 1 review comment to the agents`). When fixed and reviewed, fixes are pushed to the same PR (`fixes pushed`). |
|
|
201
|
+
| Unresponsive or stuck | Issue comment & Label | If `--max-minutes` passes or retries exhaust (`--max-runs`), the issue is labelled `hivecode:needs-human` with the last room transcript lines. |
|
|
202
|
+
|
|
203
|
+
### `/hivecode stop` and `/hivecode retry`
|
|
204
|
+
|
|
205
|
+
- **/hivecode stop:** A comment starting with `/hivecode stop` on the issue or PR stops work immediately. It works mid-work: the room is notified, the active task is withdrawn, and nothing lands. A stop from someone who cannot push, or from before the run started, is ignored.
|
|
206
|
+
- **/hivecode retry:** A comment starting with `/hivecode retry` re-queues the issue. The watcher increments the attempt number and posts a new task (`Issue #12 (attempt 2): <title>`), matching the updated attempt so the room receives the retry cleanly without duplicate task suppression.
|
|
207
|
+
|
|
208
|
+
### Fallback agent and token cost
|
|
209
|
+
|
|
210
|
+
- **Opt-in only:** `--fallback-agent "<command>"` is strictly opt-in. Unattended agents never run unless explicitly configured.
|
|
211
|
+
- **Reachability & engagement gate:** If reachable agents are present in the room when an issue arrives, no fallback starts before `--ack-minutes` (default 10). If any agent engages (claims a file or chats intent), fallback is prevented. If 0 reachable agents exist at post time, fallback spawns immediately.
|
|
212
|
+
- **Budget caps:** Fallback runs are counted against `--max-runs-per-day` (default 20 across the repo) and run at most once per issue. When the daily limit is reached, no fallback spawns, and an issue comment notes that the limit was reached.
|
|
213
|
+
|
|
214
|
+
### The review rule and lone-agent waiver
|
|
215
|
+
|
|
216
|
+
- **Non-writer review required:** A task marked done will not land until a non-writer agent submits a passing review (`hive_review({ files, verdict: 'pass' })`). Reviews authored by contributors to the current held files are ignored.
|
|
217
|
+
- **Lone-agent waiver:** If only one agent is present in the room and no second agent arrives before `--ack-minutes` (default 10), Hivecode waives the second-agent requirement and lands the PR.
|
|
218
|
+
- **PR waiver notice:** The PR body explicitly states: `"not reviewed by a second agent"`, making the single-agent status transparent to human reviewers on GitHub. A `VERDICT: FAIL` review is never waived.
|
|
219
|
+
|
|
220
|
+
### Known limits
|
|
221
|
+
|
|
222
|
+
- **One issue at a time:** The watcher processes one issue at a time per room; subsequent labelled issues queue and wait for the active PR to be merged or closed.
|
|
223
|
+
- **One open PR:** The room maintains at most one open PR at a time to keep branch history and CRDT file states consistent.
|
|
224
|
+
- **Squash-merge history:** When PRs are squash-merged on GitHub, git commits are condensed to one, but GitHub preserves individual contributor names as `Co-authored-by:` trailers.
|
|
225
|
+
- **Antigravity multi-window MCP session sharing:** In Antigravity IDE, multiple windows on the same machine share a single background MCP server process. Parallel agents must use separate workspace folders and distinct agent names (e.g. `antigravity-2`, `antigravity-4`) or standalone sessions to avoid `.hive-mcp-session.json` identity collisions.
|
|
226
|
+
|
|
167
227
|
## Why `gh` and not a GitHub App, for now
|
|
168
228
|
|
|
169
229
|
- **With `gh`, nothing new is exposed.**
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -85,3 +85,21 @@ This guide explains every error code (`Qxxx`) emitted by Hivecode CLI commands,
|
|
|
85
85
|
| `Q642` | `hive-notify.js` | Running `notify` | Missing `--for <name>` argument or non-positive `--every` interval. | Specify who to notify with `--for <name>` and positive minutes with `--every <minutes>`. |
|
|
86
86
|
| `Q643` | `hive-notify.js` | Running `notify` | The webhook endpoint refused the initial test post. | Check the webhook URL and ensure the receiving service is accessible. |
|
|
87
87
|
| `Q644` | `hive-notify.js` | Running `notify` | A notifier is already running for this room (lock file held by another process). | Stop the other running notifier process or let it run. |
|
|
88
|
+
| `Q575` | `hive-land.js` | Landing branch changes into a pull request (`hive_land` / `landIssue`) | A file claim is still held by a participant (or appeared during landing). All files must be released before landing. | Release all held files using `hive_release`, or wait for the holder to finish and release their claims. |
|
|
89
|
+
| `Q576` | `hive-land.js` | Running project tests during landing verification (`--test "<command>"`) | The verification tests failed, timed out, or test checkout was not at the exact commit being landed. | Inspect test error output in room task or log. If tests require uncommitted files or dependencies, pass `--test-copy <files>` or `--test-setup <cmd>`, or fix failing test code. |
|
|
90
|
+
| `Q577` | `hive-land.js` | Landing branch changes into a pull request | The room holds no files or landing would delete most branch files (>50% mass deletion). Indicates relay restarted or room was wiped. | Reconnect agents to reseed the room, reseed from branch (`branch <name> --reseed`), or pass `--allow-mass-delete` if intentional. |
|
|
91
|
+
| `Q578` | `hive-land.js` | Pushing landed commits to remote origin (`git push origin <branch>`) | Pre-push checks failed: `origin` remote missing or unreachable, remote branch ahead, workflow files modified without `--allow-workflow-changes`, or secrets detected. | Ensure `origin` is configured and accessible, pull latest branch commits, or remove secrets/workflow changes from landed files. |
|
|
92
|
+
| `Q579` | `hive-land.js` | Starting a landing run for a branch | Another landing run for this branch is currently active (landing lock held). | Wait for the concurrent landing to finish, or remove stale landing lock if the prior process crashed. |
|
|
93
|
+
| `Q582` | `hive-repos.js` | Running `pr` from within a room | The room's repository folder is not located on this local machine. | Run `pr` from the machine where the repository clone resides, or register the repository with `hive-repos`. |
|
|
94
|
+
| `Q583` | `hive-repos.js`, `hive-branch.js` | Running `pr` to land changes from a room | No local git repository is registered or bound to this room on this machine. | In your repository checkout, run `npx hivecode-mcp branch <branch>` to bind the room to your repository. |
|
|
95
|
+
| `Q584` | `hive-repos.js` | Looking up registered repository for room | Repository registration is stale (folder moved/deleted) or multiple local clones are registered for the same room. | Re-bind the correct repository clone using `npx hivecode-mcp branch <branch>`, or remove obsolete entries in `~/.hivecode/repos.json`. |
|
|
96
|
+
| `Q585` | `hive-repos.js` | Calling `hive_pr` | Not currently connected to a room (`hive_join` has not been called). | Join a room first using `hive_join` before calling `hive_pr`. |
|
|
97
|
+
| `Q586` | `hive-repos.js` | Calling `hive_pr` | Room connection is currently offline or disconnected from relay. | Wait for the room connection to reconnect to the relay before running `hive_pr`. |
|
|
98
|
+
| `Q650` | `hive-room-mode.js` | Starting room-mode watcher (`github start --room "<link>"`) | Missing, empty, or malformed room join link. | Provide a valid room join link (`wss://<relay>|room-<id>`) or use `github start --branch <name>`. |
|
|
99
|
+
| `Q651` | `hive-room-mode.js` | Starting room-mode watcher | Target directory is not a git repository root (or is the user's home directory). | Run the watcher from inside a valid git repository checkout. |
|
|
100
|
+
| `Q652` | `hive-room-mode.js` | Starting room-mode watcher | The specified room is not bound to a git branch in this repository, or multiple branches match. | Run `npx hivecode-mcp branch <branch>` to bind the branch, or launch with `github start --branch <branch>`. |
|
|
101
|
+
| `Q653` | `hive-room-mode.js` | Preflight check for room-mode watcher | GitHub CLI (`gh`) is not installed, not in `PATH`, or not authenticated. | Install `gh` and run `gh auth login` with write permissions to the repository. |
|
|
102
|
+
| `Q654` | `hive-room-mode.js` | Preflight member version check for room mode | An active AI agent in the room is running a Hivecode version older than 0.13.0, or has no version declared. | Update all participating AI agent packages to `hivecode-mcp@latest` (>= 0.13.0) and reconnect. |
|
|
103
|
+
| `Q655` | `hive-room-mode.js` | Starting room-mode watcher | Another room-mode watcher is already active for this room (watcher lock held). | Stop the other running watcher process, or remove the stale lock file in `.git/hive-room-watcher.lock` if crashed. |
|
|
104
|
+
| `Q656` | `hive-room-mode.js` | Room-mode watcher connecting to relay | Could not connect to relay at specified URL, or connection timed out. | Verify internet connectivity, check relay status, and ensure the relay URL is correct. |
|
|
105
|
+
| `Q657` | `hive-room-mode.js` | Dispatching an issue task or chat into room | Delivery verification failed: task assignment or chat message was not confirmed in room within timeout. | Check relay stability, ensure room has reachable participants, and retry with `/hivecode retry`. |
|
package/hive-authors.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// hive-authors.js -- who wrote what the room holds: the ONE authorsOf, shared by sync.js and hive-pr.js. A receipt counts only when verify(r) === true or verify(r).ok === true (a truthy {ok:false} used to count).
|
|
2
|
+
import { contentHash } from './substrate.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check whether a receipt is valid according to the optional verify function.
|
|
6
|
+
* Handles both boolean returns and { ok: boolean } result objects (verifyReceipt).
|
|
7
|
+
*/
|
|
8
|
+
export function isReceiptValid(verify, r) {
|
|
9
|
+
if (!verify) return true
|
|
10
|
+
const v = verify(r)
|
|
11
|
+
return v === true || Boolean(v && v.ok)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The authors of a file's held content, newest-landed first, from verified receipts only.
|
|
16
|
+
*/
|
|
17
|
+
export function authorsOf(receipts, verify, roomText) {
|
|
18
|
+
const ok = (receipts || []).filter((r) => r && r.author && isReceiptValid(verify, r))
|
|
19
|
+
if (!ok.length) return { last: null, contributors: [], first: 0, receipts: [] }
|
|
20
|
+
const want = typeof roomText === 'string' ? contentHash(roomText) : null
|
|
21
|
+
let idx = -1
|
|
22
|
+
if (want) {
|
|
23
|
+
for (let i = ok.length - 1; i >= 0; i--) {
|
|
24
|
+
if (ok[i].contentHash === want) {
|
|
25
|
+
idx = i
|
|
26
|
+
break
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const held = idx >= 0 ? ok.slice(0, idx + 1) : ok
|
|
31
|
+
const last = held[held.length - 1]
|
|
32
|
+
const seen = new Map()
|
|
33
|
+
for (const r of held) {
|
|
34
|
+
if (!seen.has(r.author)) {
|
|
35
|
+
seen.set(r.author, { author: r.author, name: r.name || 'agent', at: r.at || 0 })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
last: last ? { author: last.author, name: last.name || 'agent', at: last.at || 0 } : null,
|
|
40
|
+
contributors: [...seen.values()],
|
|
41
|
+
first: Math.min(...ok.map((r) => r.at || 0)),
|
|
42
|
+
receipts: held,
|
|
43
|
+
}
|
|
44
|
+
}
|
package/hive-await.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// hive-await — block until someone speaks to you, then exit. The CLI's `hive_wait`.
|
|
3
3
|
//
|
|
4
|
-
// npx -y -p hivecode-mcp@latest hivecode-await "<relay|room>" <YourName> [idleMinutes]
|
|
4
|
+
// npx -y -p hivecode-mcp@latest hivecode-await "<relay|room>" <YourName> [idleMinutes] [app]
|
|
5
5
|
//
|
|
6
6
|
// Run it in the BACKGROUND and let its exit wake you. Exits 0 in both cases; read `reason` on stdout:
|
|
7
7
|
// {"reason":"mention","messages":[...]} someone said @YourName or @all -> read, reply, run it again
|
|
@@ -37,15 +37,20 @@ import { startSync, parseLink } from './sync.js'
|
|
|
37
37
|
import * as RC from './hive-receipts.js'
|
|
38
38
|
import { awaitDir, armedFile, BEAT_MS } from './hive-listen.js'
|
|
39
39
|
|
|
40
|
-
const [, , LINK = '', NAME = '', IDLE_MIN = '10'] = process.argv
|
|
40
|
+
const [, , LINK = '', NAME = '', IDLE_MIN = '10', APP_ARG = ''] = process.argv
|
|
41
41
|
if (!LINK || !NAME) {
|
|
42
|
-
console.error('usage: npx -y -p hivecode-mcp@latest hivecode-await "<relay|room>" <YourName> [idleMinutes]')
|
|
42
|
+
console.error('usage: npx -y -p hivecode-mcp@latest hivecode-await "<relay|room>" <YourName> [idleMinutes] [app]')
|
|
43
43
|
process.exit(2)
|
|
44
44
|
}
|
|
45
45
|
const { relay, room } = parseLink(LINK)
|
|
46
46
|
const dir = awaitDir(NAME)
|
|
47
|
+
// Which app will be woken when this listener exits: passed by listenCommand, or inferred from the
|
|
48
|
+
// session environment when the agent ran `node hive-await.js <link> <name> <min>` directly. Without
|
|
49
|
+
// `app` here, a listener running while the MCP connection is closed or restarting leaves `app`
|
|
50
|
+
// undefined in awareness, and hive-activity.js misreports the agent as "not woken by it".
|
|
51
|
+
const APP = APP_ARG || process.env.HIVE_APP || (process.env.CLAUDE_CODE_SESSION_ID ? 'claude-code' : (process.env.ANTIGRAVITY_CONVERSATION_ID || process.env.ANTIGRAVITY_AGENT ? 'antigravity' : ''))
|
|
47
52
|
|
|
48
|
-
const hive = startSync({ relay, room, dir, name: NAME, kind: 'ai', token: process.env.HIVE_TOKEN || '', syncFiles: false, listener: true, log: () => {} })
|
|
53
|
+
const hive = startSync({ relay, room, dir, name: NAME, kind: 'ai', token: process.env.HIVE_TOKEN || '', syncFiles: false, listener: true, app: APP, log: () => {} })
|
|
49
54
|
|
|
50
55
|
// ARMED, AND THE NEWEST LISTENER WINS (4 Oct 2026; see hive-listen.js). This file is how the Stop
|
|
51
56
|
// hook and the MCP server know, with no network call, that the agent will hear the room after its
|
|
@@ -55,7 +60,7 @@ const hive = startSync({ relay, room, dir, name: NAME, kind: 'ai', token: proces
|
|
|
55
60
|
// listener that finds the file naming another process leaves: that one was started after it.
|
|
56
61
|
const ARMED = armedFile(NAME, room)
|
|
57
62
|
const SESSION = process.env.CLAUDE_CODE_SESSION_ID || process.env.ANTIGRAVITY_CONVERSATION_ID || ''
|
|
58
|
-
const AGAIN = `"${process.execPath.replace(/\\/g, '/')}" "${path.resolve(process.argv[1]).replace(/\\/g, '/')}" "${LINK}" "${NAME}" ${IDLE_MIN}`
|
|
63
|
+
const AGAIN = `"${process.execPath.replace(/\\/g, '/')}" "${path.resolve(process.argv[1]).replace(/\\/g, '/')}" "${LINK}" "${NAME}" ${IDLE_MIN}${APP_ARG ? ` "${APP_ARG}"` : ''}`
|
|
59
64
|
const startedAt = Date.now()
|
|
60
65
|
let synced = false
|
|
61
66
|
const readArmed = () => { try { return JSON.parse(fs.readFileSync(ARMED, 'utf8')) } catch { return null } }
|
|
@@ -77,7 +82,7 @@ const newerThanMe = (rec) => {
|
|
|
77
82
|
function beat() {
|
|
78
83
|
try {
|
|
79
84
|
fs.mkdirSync(path.dirname(ARMED), { recursive: true })
|
|
80
|
-
const body = JSON.stringify({ pid: process.pid, name: NAME, room, link: LINK, session: SESSION, at: startedAt, beat: Date.now(), synced })
|
|
85
|
+
const body = JSON.stringify({ pid: process.pid, name: NAME, room, link: LINK, session: SESSION, ...(APP ? { app: APP } : {}), at: startedAt, beat: Date.now(), synced })
|
|
81
86
|
const part = `${ARMED}.${process.pid}.tmp`
|
|
82
87
|
try { fs.writeFileSync(part, body); fs.renameSync(part, ARMED) } catch {
|
|
83
88
|
try { fs.rmSync(part, { force: true }) } catch {}
|
package/hive-branch.js
CHANGED
|
@@ -31,7 +31,6 @@ import os from 'os'
|
|
|
31
31
|
import path from 'path'
|
|
32
32
|
import crypto from 'crypto'
|
|
33
33
|
import { spawnSync } from 'child_process'
|
|
34
|
-
import { fileURLToPath } from 'url'
|
|
35
34
|
import ignore from 'ignore'
|
|
36
35
|
import { startSync, parseLink, ALWAYS_IGNORE, SKIP } from './sync.js'
|
|
37
36
|
import { PUBLIC_DEFAULT_RELAY, currentRelayFor } from './hive-privacy.js'
|
|
@@ -40,6 +39,10 @@ import { validateMemberName, suggestMemberName } from './hive-mailbox.js'
|
|
|
40
39
|
import { isSafeRelPath } from './token.js'
|
|
41
40
|
|
|
42
41
|
const TEXT_MAX = 1_000_000 // sync.js MAX_BYTES: a larger file never crosses the relay
|
|
42
|
+
|
|
43
|
+
// For hive-land.js, the one landing path. Declared below; exported here so the landing reads the
|
|
44
|
+
// binding, the branch and the room with exactly the rules this module binds and seeds with.
|
|
45
|
+
export { projectRoot, validBranch, revParse, coded, safeSegment, isOwnFile, git as gitIn, waitFor }
|
|
43
46
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
44
47
|
const coded = (code, msg) => Object.assign(new Error(`${code} ${msg}`), { code })
|
|
45
48
|
|
|
@@ -327,6 +330,13 @@ async function seedRoom({ root, branch, commit, link, you, log }) {
|
|
|
327
330
|
throw coded('Q566', `a fresh joiner did not receive the whole branch: ${missing.length} of ${expected.size} files are missing or different (first: ${missing.slice(0, 5).join(', ')}). Nothing was bound; agents would have built on a partial tree.`)
|
|
328
331
|
}
|
|
329
332
|
const short = commit.slice(0, 7)
|
|
333
|
+
// Who holds this room's repo, for display only: an agent on another machine asked to open a PR
|
|
334
|
+
// is told whom to ask. Never a path; what lands is decided on the landing machine alone.
|
|
335
|
+
try { const R = await import('./hive-repos.js'); R.writeRepoHolder(seeder.doc, { branch, holder: you, host: os.hostname() }) } catch { /* display only */ }
|
|
336
|
+
// THE COMMIT THE ROOM STARTS FROM, for a teammate's `--join`: without it their clone cannot tell
|
|
337
|
+
// a file the room deleted from one added to the branch later, and landing from there skipped the
|
|
338
|
+
// room's deletions -- and the Q572 guard -- without a word (antigravity, 5 Nov 2026).
|
|
339
|
+
try { seeder.doc.getMap('seed').set('commit', commit); seeder.doc.getMap('seed').set('branch', branch) } catch { /* older relay docs */ }
|
|
330
340
|
seeder.say(`Room for git branch \`${branch}\` @ ${short}, seeded by ${you} with ${expected.size} file(s). Work here lands on \`${branch}\` as one commit per agent via \`npx -y hivecode-mcp@latest pr ${branch}\`. Room folders are not git checkouts on purpose: edit files, do not commit.`)
|
|
331
341
|
await sleep(600) // let the announcement leave before the socket closes
|
|
332
342
|
return { dir, count: expected.size, total: files.length }
|
|
@@ -355,6 +365,25 @@ async function roomPaths(link, you) {
|
|
|
355
365
|
}
|
|
356
366
|
const roomFileCount = async (link, you) => { const p = await roomPaths(link, you); return p ? p.size : null }
|
|
357
367
|
|
|
368
|
+
/** The commit the room was seeded from, as its seeder recorded it ({ commit, branch }), or null. */
|
|
369
|
+
async function roomSeed(link, you) {
|
|
370
|
+
const { relay, room } = parseLink(link)
|
|
371
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hive-peek-'))
|
|
372
|
+
const peek = startSync({ relay, room, dir, name: `${you}-peek`, kind: 'ai', passive: true, syncFiles: false, log: () => {} })
|
|
373
|
+
try {
|
|
374
|
+
let synced = false
|
|
375
|
+
peek.provider.on('sync', (s) => { if (s) synced = true })
|
|
376
|
+
if (!(await waitFor(() => synced, 15000))) return null
|
|
377
|
+
await sleep(300)
|
|
378
|
+
const m = peek.doc.getMap('seed')
|
|
379
|
+
const commit = m.get('commit')
|
|
380
|
+
return typeof commit === 'string' && /^[0-9a-f]{40}$/i.test(commit) ? { commit, branch: String(m.get('branch') || '') } : null
|
|
381
|
+
} finally {
|
|
382
|
+
try { peek.stop() } catch { }
|
|
383
|
+
try { fs.rmSync(dir, { recursive: true, force: true }) } catch { }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
358
387
|
/** The branch a PR from `name` goes into: the checkout's current branch, else origin's default. */
|
|
359
388
|
function defaultBase(root, name) {
|
|
360
389
|
const cur = git(root, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
|
|
@@ -383,6 +412,15 @@ export function removeRoomFolder(dir, room) {
|
|
|
383
412
|
fs.rmSync(abs, { recursive: true, force: true })
|
|
384
413
|
}
|
|
385
414
|
|
|
415
|
+
const projectRootOrNull = (cwd) => { try { return projectRoot(cwd) } catch { return null } }
|
|
416
|
+
|
|
417
|
+
// THIS MACHINE'S RECORD OF WHICH REPO HOLDS WHICH ROOM (hive-repos.js), so an agent that joined by
|
|
418
|
+
// link -- not by branch -- can still land from here with hive_pr. Imported late: hive-repos imports
|
|
419
|
+
// this module. Best effort: a registry that cannot be written never fails a bind, and hive_pr then
|
|
420
|
+
// says exactly that the room is not bound on this machine (Q583), with the command that fixes it.
|
|
421
|
+
async function registerLocal(root, branch, link) {
|
|
422
|
+
try { const R = await import('./hive-repos.js'); return !!R.registerRepo({ root, branch, link }) } catch { return false }
|
|
423
|
+
}
|
|
386
424
|
function youFrom(root, given) {
|
|
387
425
|
const raw = given || git(root, ['config', 'user.name']).out || os.userInfo().username || 'dev'
|
|
388
426
|
return validateMemberName(raw).ok ? raw : suggestMemberName(raw)
|
|
@@ -403,14 +441,17 @@ export async function bindBranch({ cwd, branch, from = '', relay = '', you = '',
|
|
|
403
441
|
const { relay: jr, room } = parseLink(join)
|
|
404
442
|
if (!jr || !room) throw coded('Q567', `"${join}" is not a room link (it looks like wss://relay|room-...).`)
|
|
405
443
|
if (existing && existing.link !== join) throw coded('Q567', `branch "${name}" is already bound to a different room here (${existing.link}). Remove it with: git config --remove-section "hivecode.${name}"`)
|
|
406
|
-
|
|
407
|
-
|
|
444
|
+
const seed = await roomSeed(join, me)
|
|
445
|
+
writeBinding(root, name, { room: join, base: seed && seed.commit ? seed.commit : '' })
|
|
446
|
+
await registerLocal(root, name, join)
|
|
447
|
+
return { root, branch: name, link: join, you: me, joined: true, base: seed && seed.commit ? seed.commit : '' }
|
|
408
448
|
}
|
|
409
449
|
|
|
410
450
|
if (existing && !reseed) {
|
|
411
451
|
const n = await roomFileCount(existing.link, me)
|
|
412
452
|
// Empty is only news when the seed was not: a branch with nothing a room can carry seeds nothing.
|
|
413
453
|
const lost = n === 0 && Number(existing.files || 1) > 0
|
|
454
|
+
await registerLocal(root, name, existing.link) // a binding made before the registry existed gets its entry
|
|
414
455
|
return { root, branch: name, link: existing.link, you: me, from: existing.from, existing: true, roomFiles: n, lost }
|
|
415
456
|
}
|
|
416
457
|
|
|
@@ -459,166 +500,27 @@ export async function bindBranch({ cwd, branch, from = '', relay = '', you = '',
|
|
|
459
500
|
throw e
|
|
460
501
|
}
|
|
461
502
|
writeBinding(root, name, { room: link, from: fromName, base: tip, seed: seeded.dir, files: String(seeded.count) })
|
|
503
|
+
await registerLocal(root, name, link)
|
|
462
504
|
return { root, branch: name, link, you: me, from: fromName, commit: tip, created, reseeded: reseed, files: seeded.count, total: seeded.total, seedDir: seeded.dir }
|
|
463
505
|
}
|
|
464
506
|
|
|
465
507
|
// ---- landing: the room's work becomes commits on the branch ---------------------------------
|
|
466
508
|
|
|
467
|
-
const HIVE_PR = path.join(path.dirname(fileURLToPath(import.meta.url)), 'hive-pr.js')
|
|
468
|
-
const reEsc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* THE ROOM'S DELETIONS, which hive-pr does not land: it copies the files a room holds, so a file an
|
|
472
|
-
* agent deleted would simply stay on the branch, in a PR that looks complete. A room bound to a
|
|
473
|
-
* branch starts as the whole branch, so "on the branch but no longer in the room" is a deletion --
|
|
474
|
-
* but only when the room provably held THAT version of the file. For each such path:
|
|
475
|
-
* - last changed on the branch by a landing from this room (its commit says "Room: <id>") -> delete
|
|
476
|
-
* - unchanged since the seed commit -> delete
|
|
477
|
-
* - in the seed, but changed on the branch since by someone else -> REFUSE (Q572):
|
|
478
|
-
* deleting it would erase a change the room never saw
|
|
479
|
-
* - not in the seed and not landed from this room: added to the branch outside the room -> keep
|
|
480
|
-
*/
|
|
481
|
-
async function roomDeletions({ root, link, base, tip }) {
|
|
482
|
-
if (!base) return []
|
|
483
|
-
const paths = await roomPaths(link, 'hive-land')
|
|
484
|
-
if (!paths) throw coded('Q565', `could not reach the relay to read the room's file list, so its deletions cannot be accounted for. Nothing was changed.`)
|
|
485
|
-
const room = parseLink(link).room
|
|
486
|
-
const atTip = treeFiles(root, tip)
|
|
487
|
-
const carries = roomCarries(atTip)
|
|
488
|
-
const baseSha = new Map(treeFiles(root, base).map((f) => [f.rel, f.sha]))
|
|
489
|
-
const gone = [], diverged = []
|
|
490
|
-
for (const f of atTip) {
|
|
491
|
-
if (!carries(f) || paths.has(f.rel)) continue
|
|
492
|
-
const last = git(root, ['log', '-1', '--format=%B', tip, '--', f.rel]).out
|
|
493
|
-
if (new RegExp(`^Room: ${reEsc(room)}$`, 'm').test(last)) gone.push(f.rel)
|
|
494
|
-
else if (baseSha.get(f.rel) === f.sha) gone.push(f.rel)
|
|
495
|
-
else if (baseSha.has(f.rel)) diverged.push(f.rel)
|
|
496
|
-
}
|
|
497
|
-
if (diverged.length) {
|
|
498
|
-
const who = (rel) => git(root, ['log', '-1', '--format=%h %an %ad %s', '--date=short', tip, '--', rel]).out
|
|
499
|
-
throw coded('Q572', `the room deleted ${diverged.length} file(s) that were changed on the branch since it was seeded, by a change the room never saw:\n${diverged.map((r) => ` ${r} last changed by: ${who(r)}`).join('\n')}\nDeleting them would erase that change. Nothing was changed. Restore the file in the room (or delete it on the branch yourself), then run pr again.`)
|
|
500
|
-
}
|
|
501
|
-
return gone
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function worktreesOn(root, branch) {
|
|
505
|
-
const r = git(root, ['worktree', 'list', '--porcelain'])
|
|
506
|
-
const out = []
|
|
507
|
-
let cur = null
|
|
508
|
-
for (const line of r.out.split('\n')) {
|
|
509
|
-
if (line.startsWith('worktree ')) cur = line.slice(9)
|
|
510
|
-
else if (line === `branch refs/heads/${branch}` && cur) out.push(path.resolve(cur))
|
|
511
|
-
}
|
|
512
|
-
return out
|
|
513
|
-
}
|
|
514
|
-
|
|
515
509
|
/**
|
|
516
|
-
* `pr <name
|
|
510
|
+
* `pr <name>` used to land here. Landing now has ONE path, hive-land.js landAndOpen, shared by the
|
|
511
|
+
* `pr` command, the hive_pr tool and the GitHub watcher: every check (claims, an empty room, workflow
|
|
512
|
+
* files, keys, tests, origin ahead, the lock) protects all three. Two copies of the landing had
|
|
513
|
+
* already drifted once (the deletion accounting lived only here).
|
|
517
514
|
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
* the worktree was cut from, so a commit that reached the branch meanwhile is refused, never lost.
|
|
515
|
+
* landBranch keeps its old contract for the watcher, which still runs its own workflow, key and test
|
|
516
|
+
* checks after landing, and whose agent has exited by the time it lands (so its claims are moot):
|
|
517
|
+
* { ok:true, landed, tip, deletions } | { ok:false, code } with the reason on logErr.
|
|
522
518
|
*/
|
|
523
519
|
export async function landBranch({ cwd, branch, dry = false, push = false, pr = false, base = '', force = '', only = '', title = '', log = console.log, logErr = console.error }) {
|
|
524
|
-
const
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const tip = revParse(root, `refs/heads/${name}`)
|
|
529
|
-
if (!tip) throw coded('Q563', `branch "${name}" does not exist in ${root}. Fetch it first: git fetch origin ${name}:${name}`)
|
|
530
|
-
|
|
531
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'hive-land-'))
|
|
532
|
-
const temp = `hivecode-land/${safeSegment(name)}-${process.pid}-${Date.now()}`
|
|
533
|
-
const cleanup = () => {
|
|
534
|
-
git(root, ['worktree', 'remove', '--force', tmp])
|
|
535
|
-
try { fs.rmSync(tmp, { recursive: true, force: true }) } catch { }
|
|
536
|
-
git(root, ['worktree', 'prune'])
|
|
537
|
-
if (revParse(root, `refs/heads/${temp}`)) git(root, ['branch', '-D', temp])
|
|
538
|
-
}
|
|
539
|
-
const wt = git(root, noHooks(['worktree', 'add', '--detach', tmp, tip]))
|
|
540
|
-
if (wt.code !== 0) { cleanup(); throw coded('Q563', `could not make a temporary worktree: ${wt.err}`) }
|
|
541
|
-
|
|
542
|
-
try {
|
|
543
|
-
// Accounted for BEFORE anything is written, so a refusal (Q572) leaves the branch as it was.
|
|
544
|
-
const deletions = await roomDeletions({ root, link: b.link, base: b.base, tip })
|
|
545
|
-
|
|
546
|
-
const args = [HIVE_PR, b.link, 'hive-pr', '--repo', tmp, '--branch', temp, '--base', 'HEAD']
|
|
547
|
-
if (dry) args.push('--dry-run')
|
|
548
|
-
if (force) args.push('--force', force)
|
|
549
|
-
if (only) args.push('--only', only)
|
|
550
|
-
const r = spawnSync(process.execPath, args, { cwd: tmp, encoding: 'utf8', env: noHooksEnv({ ...process.env, HIVE_PROVENANCE: 'on' }), maxBuffer: 64 * 1024 * 1024, windowsHide: true })
|
|
551
|
-
if (r.stdout) log(r.stdout.replace(/\s+$/, ''))
|
|
552
|
-
// Q501 is hive-pr saying no file in the room carries an agent's receipt: nothing was changed or
|
|
553
|
-
// added. For a branch room that is an answer, not a failure -- and deletions may still land.
|
|
554
|
-
const nothingChanged = r.status !== 0 && /^Q501 /m.test(r.stderr || '')
|
|
555
|
-
if (r.stderr && !nothingChanged) logErr(r.stderr.replace(/\s+$/, ''))
|
|
556
|
-
if (r.status !== 0 && !nothingChanged) return { ok: false, code: r.status || 1 }
|
|
557
|
-
if (dry) {
|
|
558
|
-
if (deletions.length) log(`would delete ${deletions.length} file(s) the room deleted: ${deletions.join(', ')}`)
|
|
559
|
-
return { ok: true, dry: true, deletions }
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (deletions.length) {
|
|
563
|
-
// hive-pr left the temporary worktree on its branch when it committed; when it had nothing to
|
|
564
|
-
// commit it never made one, so make it here, at the same tip.
|
|
565
|
-
if (!revParse(root, `refs/heads/${temp}`)) git(tmp, noHooks(['checkout', '-q', '-b', temp]))
|
|
566
|
-
const rm = git(tmp, ['rm', '-q', '--', ...deletions])
|
|
567
|
-
if (rm.code !== 0) throw coded('Q563', `could not stage the room's deletions: ${rm.err}`)
|
|
568
|
-
const msg = [`Room: delete ${deletions.length === 1 ? deletions[0] : `${deletions.length} files`}`, '', ...deletions.map((d) => ` - ${d}`), '', `Room: ${parseLink(b.link).room}`].join('\n')
|
|
569
|
-
const c = git(tmp, noHooks(['commit', '-q', '-m', msg, '--author', 'hivecode-room <room@agents.hivecode.local>']))
|
|
570
|
-
if (c.code !== 0) throw coded('Q563', `could not commit the room's deletions: ${c.err || c.out}`)
|
|
571
|
-
}
|
|
572
|
-
// NOTHING NEW TO LAND IS NOT "DONE" WHEN A PUSH OR A PR WAS ASKED FOR. The usual sequence is a
|
|
573
|
-
// plain `pr` to look, then `pr --pr`; the second run lands nothing, and returning here skipped
|
|
574
|
-
// the push and the PR while printing success (1 Nov 2026). Push and open what is already there.
|
|
575
|
-
let nothingNew = false
|
|
576
|
-
if (nothingChanged && !deletions.length) { log(`nothing to land yet: no agent has changed a file in the room for ${name}.`); nothingNew = true }
|
|
577
|
-
const newTip = nothingNew ? tip : revParse(root, `refs/heads/${temp}`)
|
|
578
|
-
if (!nothingNew && (!newTip || newTip === tip)) { log(`nothing to land: ${name} already has everything the room holds.`); nothingNew = true }
|
|
579
|
-
if (nothingNew && !push && !pr) return { ok: true, landed: 0 }
|
|
580
|
-
if (nothingNew) return finish('')
|
|
581
|
-
|
|
582
|
-
// Move the branch. Checked out somewhere: only a clean fast-forward of that checkout, which is
|
|
583
|
-
// what `git pull` would do; with local changes it is refused, because moving the branch under a
|
|
584
|
-
// dirty checkout makes its edits look like reversals of the landed work.
|
|
585
|
-
const on = worktreesOn(root, name).filter((p) => p !== path.resolve(tmp))
|
|
586
|
-
if (on.length) {
|
|
587
|
-
const where = on[0]
|
|
588
|
-
const dirty = git(where, ['status', '--porcelain', '--untracked-files=no'])
|
|
589
|
-
if (dirty.out) throw coded('Q571', `${name} is checked out in ${where} with uncommitted changes. Commit or stash them there (or switch that checkout to another branch), then run pr again. Nothing was changed.`)
|
|
590
|
-
const ff = git(where, ['merge', '--ff-only', '--quiet', newTip])
|
|
591
|
-
if (ff.code !== 0) throw coded('Q570', `could not fast-forward ${name} in ${where}: ${ff.err || 'git refused'}. Nothing was changed; if the branch moved there, run pr again.`)
|
|
592
|
-
} else {
|
|
593
|
-
const cas = git(root, ['update-ref', '-m', 'hivecode: land room work', `refs/heads/${name}`, newTip, tip])
|
|
594
|
-
if (cas.code !== 0) throw coded('Q570', `${name} moved while this was landing (it is no longer ${tip.slice(0, 7)}). Nothing was changed; run pr again so the new commits are accounted for.`)
|
|
595
|
-
}
|
|
596
|
-
const made = git(root, ['log', '--format= %h %an %s', `${tip}..${newTip}`]).out
|
|
597
|
-
log(`\nlanded on ${name}:\n${made}`)
|
|
598
|
-
return finish(made)
|
|
599
|
-
|
|
600
|
-
// `made` is '' when nothing new landed this run: the push and the PR are of what is already on
|
|
601
|
-
// the branch, and the PR body lists the branch's commits since its base instead.
|
|
602
|
-
function finish(made) {
|
|
603
|
-
if (push) {
|
|
604
|
-
const p = git(root, ['push', '-u', 'origin', name])
|
|
605
|
-
if (p.code !== 0) throw coded('Q573', `the work ${made ? 'landed' : 'is'} on ${name} locally, but the push failed: ${p.err}`)
|
|
606
|
-
log(`pushed ${name}`)
|
|
607
|
-
}
|
|
608
|
-
if (pr) {
|
|
609
|
-
const into = base || b.from
|
|
610
|
-
if (!into) throw coded('Q574', `the work is on ${name}, but there is no branch to open the PR into; pass --base <branch>.`)
|
|
611
|
-
const listed = made || git(root, ['log', '--format= %h %an %s', `${into}..${name}`]).out
|
|
612
|
-
const body = [`Opened by Hivecode from the room bound to \`${name}\`.`, '', 'Each commit carries its agent as the git author and the signed author id as a `Hivecode-Author` trailer.', '', '```', listed, '```'].join('\n')
|
|
613
|
-
const g = spawnSync('gh', ['pr', 'create', '--base', into, '--head', name, '--title', title || `Hivecode: ${name}`, '--body', body], { cwd: root, encoding: 'utf8', windowsHide: true })
|
|
614
|
-
if (g.status !== 0) throw coded('Q574', `the work is on ${name}, but gh pr create failed: ${(g.stderr || (g.error && g.error.message) || '').toString().trim()}`)
|
|
615
|
-
log((g.stdout || '').trim())
|
|
616
|
-
}
|
|
617
|
-
return { ok: true, landed: made ? made.split('\n').filter(Boolean).length : 0, tip: made ? newTip : tip, deletions }
|
|
618
|
-
}
|
|
619
|
-
} finally {
|
|
620
|
-
cleanup()
|
|
621
|
-
}
|
|
520
|
+
const { landAndOpen } = await import('./hive-land.js')
|
|
521
|
+
const r = await landAndOpen({ cwd, branch, dry: !!dry, push, pr, base, force, only, title, log, logErr, claims: false, checks: false, by: 'the Hivecode GitHub watcher' })
|
|
522
|
+
if (!r.ok) { logErr(r.why); return { ok: false, code: 1, refused: r.code } }
|
|
523
|
+
return r.dry ? { ok: true, dry: true, deletions: r.deletions } : { ok: true, landed: r.landed, tip: r.tip, deletions: r.deletions }
|
|
622
524
|
}
|
|
623
525
|
|
|
624
526
|
// ---- CLI ----------------------------------------------------------------------------------
|
|
@@ -630,7 +532,7 @@ function flags(argv) {
|
|
|
630
532
|
const a = argv[i]
|
|
631
533
|
if (!a.startsWith('--')) { pos.push(a); continue }
|
|
632
534
|
const k = a.slice(2)
|
|
633
|
-
if (['dry-run', 'push', 'pr', 'reseed'].includes(k)) f[k] = true
|
|
535
|
+
if (['dry-run', 'push', 'pr', 'reseed', 'allow-workflow-changes', 'allow-mass-delete', 'save'].includes(k)) f[k] = true
|
|
634
536
|
else f[k] = argv[++i] || ''
|
|
635
537
|
}
|
|
636
538
|
return { pos, f }
|
|
@@ -640,7 +542,8 @@ const USAGE = `usage:
|
|
|
640
542
|
npx -y hivecode-mcp@latest branch <name> [--from <ref>] [--name <you>] [--relay <url>]
|
|
641
543
|
npx -y hivecode-mcp@latest branch <name> --join "<link>"
|
|
642
544
|
npx -y hivecode-mcp@latest branch <name> --reseed
|
|
643
|
-
npx -y hivecode-mcp@latest pr <name> [--dry-run] [--push] [--pr] [--base <branch>] [--
|
|
545
|
+
npx -y hivecode-mcp@latest pr <name> [--dry-run] [--push] [--pr] [--base <branch>] [--test "<command>" [--test-copy <file,...>] [--test-setup "<cmd>"] [--save]] [--test-minutes <n>]
|
|
546
|
+
[--allow-workflow-changes] [--allow-mass-delete] [--title "<t>"] [--force "<reason>"]`
|
|
644
547
|
|
|
645
548
|
/** `argv` starts at the subcommand: ['branch', 'fix-x', ...] or ['pr', 'fix-x', ...]. Returns an exit code. */
|
|
646
549
|
export async function main(argv, { cwd = process.cwd(), log = console.log, logErr = console.error } = {}) {
|
|
@@ -650,8 +553,26 @@ export async function main(argv, { cwd = process.cwd(), log = console.log, logEr
|
|
|
650
553
|
if (!name || (cmd !== 'branch' && cmd !== 'pr')) { logErr(USAGE); return 2 }
|
|
651
554
|
try {
|
|
652
555
|
if (cmd === 'pr') {
|
|
653
|
-
const
|
|
654
|
-
|
|
556
|
+
const { landAndOpen, testCommandFor, saveTestCommand } = await import('./hive-land.js')
|
|
557
|
+
let test = f.test || ''
|
|
558
|
+
// --test-copy a,b and --test-setup "<cmd>": what the tests need beyond git (see hive-land.js).
|
|
559
|
+
const testCopy = 'test-copy' in f ? String(f['test-copy'] || '').split(',').map((s) => s.trim()).filter(Boolean) : undefined
|
|
560
|
+
const testSetup = 'test-setup' in f ? String(f['test-setup'] || '') : undefined
|
|
561
|
+
if (f.save) {
|
|
562
|
+
if (!test) { logErr(`--save keeps a test command: pass it with --test "<command>"`); return 2 }
|
|
563
|
+
saveTestCommand(cwd, name, test, { copy: testCopy ?? null, setup: testSetup ?? null })
|
|
564
|
+
log(`saved: landing ${name} now runs ${testSetup ? `\`${testSetup}\`, then ` : ''}\`${test}\` first${testCopy && testCopy.length ? `, with ${testCopy.join(', ')} copied in` : ''}, from any caller (pr, hive_pr, room mode). Change it the same way.`)
|
|
565
|
+
}
|
|
566
|
+
// No --test: the command saved for this branch, if any. (The project's own `npm test` is the
|
|
567
|
+
// hive_pr tool's fallback; here the developer is present, so nothing runs they did not choose.)
|
|
568
|
+
if (!test) { const t = testCommandFor(cwd, name); if (t.from === 'saved') { test = t.command; log(`tests: \`${test}\` (saved for ${name})`) } }
|
|
569
|
+
const r = await landAndOpen({
|
|
570
|
+
cwd, branch: name, dry: !!f['dry-run'], push: !!(f.push || f.pr), pr: !!f.pr, base: f.base || '', force: f.force || '', only: f.only || '', title: f.title || '',
|
|
571
|
+
test, testMinutes: Number(f['test-minutes']) || 0, testCopy, testSetup, allowWorkflowChanges: !!f['allow-workflow-changes'], allowMassDelete: !!f['allow-mass-delete'],
|
|
572
|
+
by: youFrom(projectRootOrNull(cwd) || cwd, ''), log, logErr,
|
|
573
|
+
})
|
|
574
|
+
if (!r.ok) { logErr(r.why); return 1 }
|
|
575
|
+
return 0
|
|
655
576
|
}
|
|
656
577
|
const r = await bindBranch({ cwd, branch: name, from: f.from || '', relay: f.relay || '', you: f.name || '', join: f.join || '', reseed: !!f.reseed })
|
|
657
578
|
const agentLine = `Use hive_join with branch "${r.branch}", repo "${r.root.replace(/\\/g, '/')}", as a name of your own (each agent picks a different one)`
|
package/hive-github.js
CHANGED
|
@@ -1632,8 +1632,17 @@ function flags(argv) {
|
|
|
1632
1632
|
|
|
1633
1633
|
const USAGE = `usage:
|
|
1634
1634
|
npx -y hivecode-mcp@latest github start --agent "<agent command>" [options]
|
|
1635
|
+
npx -y hivecode-mcp@latest github start --room "<room link>" [--fallback-agent "<command>"] [--ack-minutes 10]
|
|
1636
|
+
npx -y hivecode-mcp@latest github start --branch <name> [--fallback-agent "<command>"] [--ack-minutes 10]
|
|
1635
1637
|
npx -y hivecode-mcp@latest github status
|
|
1636
1638
|
|
|
1639
|
+
--room: hand each labelled issue to the agents ALREADY in that room (a room bound to a branch of
|
|
1640
|
+
this repo: npx -y hivecode-mcp@latest branch <name>). They split it, build it and review each other;
|
|
1641
|
+
Hivecode runs the tests and opens one pull request. One issue at a time.
|
|
1642
|
+
--branch <name> bind or use the room for this branch, and run room mode
|
|
1643
|
+
--fallback-agent "<command>" if nobody in the room takes an issue, start this agent in the room
|
|
1644
|
+
(opt-in: it spends that agent's tokens; capped by --max-runs-per-day)
|
|
1645
|
+
|
|
1637
1646
|
The agent command reads its task on stdin. For Claude Code:
|
|
1638
1647
|
--agent "claude -p --output-format json --allowedTools Bash Edit Write Read Glob Grep mcp__hivecode"
|
|
1639
1648
|
|
|
@@ -1686,6 +1695,16 @@ export async function main(argv, { cwd: here = process.cwd(), log = console.log,
|
|
|
1686
1695
|
if (sub === 'status') {
|
|
1687
1696
|
const s = loadState(file, log, { peek: true })
|
|
1688
1697
|
log(describe(s, s.config || {}))
|
|
1698
|
+
// ROOM MODE keeps its own state (room-<room>.json, never github.json): describe each one, so
|
|
1699
|
+
// "why isn't it picking up my issue?" has an answer here too.
|
|
1700
|
+
try {
|
|
1701
|
+
const dir = path.join(gitDir, 'hivecode')
|
|
1702
|
+
const rooms = fs.existsSync(dir) ? fs.readdirSync(dir).filter((x) => /^room-.+\.json$/.test(x)) : []
|
|
1703
|
+
if (rooms.length) {
|
|
1704
|
+
const RW = await import('./hive-room-watcher.js')
|
|
1705
|
+
for (const x of rooms) log(`\n${RW.describeRoomState(RW.loadRoomState(path.join(dir, x)))}`)
|
|
1706
|
+
}
|
|
1707
|
+
} catch (e) { log(`(room mode state could not be read: ${e.message})`) }
|
|
1689
1708
|
return 0
|
|
1690
1709
|
}
|
|
1691
1710
|
|
|
@@ -1719,6 +1738,27 @@ export async function main(argv, { cwd: here = process.cwd(), log = console.log,
|
|
|
1719
1738
|
return busy ? EXIT_AGAIN : EXIT_FIX
|
|
1720
1739
|
}
|
|
1721
1740
|
if (info.permissions && info.permissions.push === false) { logErr(`Q601 the GitHub account gh is logged in as cannot push to ${id.owner}/${id.repo}.`); return EXIT_FIX }
|
|
1741
|
+
// ROOM MODE: issues go to the agents ALREADY in a room (hive-room-watcher.js), and no agent of ours
|
|
1742
|
+
// runs unless --fallback-agent asks for one. Everything below this line is the other mode.
|
|
1743
|
+
if (opt('room') || ('branch' in f)) {
|
|
1744
|
+
if (f['dry-run']) { logErr('Q602 --dry-run does not work with --room yet. Use: npx -y hivecode-mcp@latest github status'); return EXIT_FIX }
|
|
1745
|
+
let roomLink = opt('room')
|
|
1746
|
+
if (!roomLink && ('branch' in f)) {
|
|
1747
|
+
const branchName = String(opt('branch') || '').trim()
|
|
1748
|
+
if (!branchName || branchName === 'true') { logErr('Q602 say which branch to watch, with --branch <name>.'); return EXIT_FIX }
|
|
1749
|
+
const HB = await import('./hive-branch.js')
|
|
1750
|
+
try {
|
|
1751
|
+
const bound = await HB.bindBranch({ cwd: root, branch: branchName, relay: opt('relay') || '', log })
|
|
1752
|
+
roomLink = bound.link
|
|
1753
|
+
f['room'] = roomLink
|
|
1754
|
+
} catch (e) {
|
|
1755
|
+
logErr(isQ(e) ? e.message : `Q603 ${e.message}`)
|
|
1756
|
+
return EXIT_FIX
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
const RW = await import('./hive-room-watcher.js')
|
|
1760
|
+
return RW.runRoomMode({ f, opt, gh, id, info, root, gitDir, label: opt('label') || LABEL, log, logErr })
|
|
1761
|
+
}
|
|
1722
1762
|
const agent = opt('agent')
|
|
1723
1763
|
if (!agent) { logErr(`Q602 say which agent runs the work, with --agent "<command>".\n\n${USAGE}`); return EXIT_FIX }
|
|
1724
1764
|
if (!commandExists(agent)) { logErr(`Q602 "${String(agent).split(/\s+/)[0]}" is not installed here (not on PATH).`); return EXIT_FIX }
|