hivecode-mcp 0.12.2 → 0.12.4
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/LICENSE +21 -21
- package/README.md +187 -8
- package/core.js +91 -91
- package/docs/HIVE_RULES.md +41 -41
- package/docs/MCP.md +5 -5
- package/docs/SELF_HOSTING.md +256 -256
- package/hive-activity.js +336 -310
- package/hive-await.js +187 -0
- package/hive-branch.js +656 -0
- package/hive-busy.js +88 -88
- package/hive-coord.js +110 -110
- package/hive-gate.js +135 -135
- package/hive-hook-install.js +179 -103
- package/hive-instructions.js +74 -66
- package/hive-listen.js +81 -0
- package/hive-mailbox.js +517 -386
- package/hive-mcp.js +483 -126
- package/hive-mux-proto.js +58 -58
- package/hive-mux.js +350 -350
- package/hive-obligations.js +247 -247
- package/hive-pr-staleness.js +189 -189
- package/hive-pr.js +337 -0
- package/hive-privacy.js +327 -288
- package/hive-receipts.js +233 -233
- package/hive-relay-health.js +7 -2
- package/hive-secrets.js +89 -0
- package/hive-setup.js +548 -445
- package/hive-stdio.js +172 -168
- package/hive-stop-hook.js +290 -241
- package/hive-throttle.js +118 -118
- package/hive-update.js +58 -58
- package/hive-version.js +110 -59
- package/icr-merge.js +58 -58
- package/icr.js +121 -15
- package/lang-json.js +211 -211
- package/lang-ruby.js +230 -230
- package/lang-toml.js +136 -136
- package/lang-yaml.js +183 -183
- package/limits.js +413 -394
- package/package.json +19 -11
- package/reconcile.cjs +614 -477
- package/substrate.js +252 -252
- package/sync.js +4691 -3011
- package/grm-link.cjs +0 -141
package/hive-pr.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// hive-pr — turn a room's landed work into a pull request, with per-agent attribution intact.
|
|
2
|
+
//
|
|
3
|
+
// node hive-pr.js "<relay|room>" <YourName> --repo <path> [options]
|
|
4
|
+
//
|
|
5
|
+
// --repo <path> the git working copy the PR is opened against (required)
|
|
6
|
+
// --branch <name> branch to create (default: hivecode/<room>-<stamp>)
|
|
7
|
+
// --base <name> branch to cut from (default: the repo's current HEAD)
|
|
8
|
+
// --title <text> PR title (default: derived from the room)
|
|
9
|
+
// --only <a,b,c> only these files
|
|
10
|
+
// --dry-run print the plan, touch nothing
|
|
11
|
+
// --push push the branch to origin
|
|
12
|
+
// --pr push and open a PR with `gh` (implies --push)
|
|
13
|
+
//
|
|
14
|
+
// WHY THIS EXISTS. A room is where the work happens; a pull request is where a team already
|
|
15
|
+
// trusts work to arrive. Asking a team to abandon pull requests is an enormous ask -- they are
|
|
16
|
+
// how review, compliance and audit happen, and in many companies they are mandatory for reasons
|
|
17
|
+
// nobody on the team controls. So we do not ask. The room's OUTPUT is a PR, and everything about
|
|
18
|
+
// the team's existing pipeline (CI, required reviewers, protected branches, deploy) keeps working
|
|
19
|
+
// exactly as it did.
|
|
20
|
+
//
|
|
21
|
+
// THE PART THAT IS NOT PLUMBING. Every change in a room is signed per participant, so we know
|
|
22
|
+
// which agent landed which file -- and that survives into git as ONE COMMIT PER AGENT, each with
|
|
23
|
+
// its real author and the full signed author id in a trailer. A reviewer opening the PR sees, in
|
|
24
|
+
// `git log`, exactly which agent wrote what. No orchestration framework can produce that: they
|
|
25
|
+
// run every agent inside one process under one identity, so the attribution never existed to
|
|
26
|
+
// carry across.
|
|
27
|
+
//
|
|
28
|
+
// Attribution is per FILE, not per line. The ledger records who landed each version of a file, so
|
|
29
|
+
// a file is credited to whoever landed its FINAL state, and everyone else who touched it rides
|
|
30
|
+
// along as a Co-authored-by trailer. Claiming line-level authorship would be a stronger statement
|
|
31
|
+
// than the data supports.
|
|
32
|
+
|
|
33
|
+
import fs from 'fs'
|
|
34
|
+
import os from 'os'
|
|
35
|
+
import path from 'path'
|
|
36
|
+
import { spawnSync } from 'child_process'
|
|
37
|
+
import { fileURLToPath } from 'url'
|
|
38
|
+
import { startSync, parseLink } from './sync.js'
|
|
39
|
+
import { accountFor, verdict, explain, DIVERGED } from './hive-pr-staleness.js'
|
|
40
|
+
|
|
41
|
+
const argv = process.argv.slice(2)
|
|
42
|
+
const [LINK = '', NAME = 'hive-pr'] = argv.filter((a) => !a.startsWith('--'))
|
|
43
|
+
const flag = (k, d = '') => { const i = argv.indexOf('--' + k); return i === -1 ? d : (argv[i + 1] || '') }
|
|
44
|
+
const has = (k) => argv.includes('--' + k)
|
|
45
|
+
// --force takes a REASON, not a bare switch. A switch is typed once and never explained; a
|
|
46
|
+
// required reason has to be written down, ends up in the PR body, and is the thing somebody asks
|
|
47
|
+
// about later. hive_release already works this way for exactly the same argument.
|
|
48
|
+
const FORCE = flag('force')
|
|
49
|
+
|
|
50
|
+
const REPO = flag('repo')
|
|
51
|
+
const DRY = has('dry-run')
|
|
52
|
+
const DO_PUSH = has('push') || has('pr')
|
|
53
|
+
const DO_PR = has('pr')
|
|
54
|
+
const ONLY = flag('only') ? flag('only').split(',').map((s) => s.trim()).filter(Boolean) : null
|
|
55
|
+
|
|
56
|
+
function die(code, msg) { console.error(`${code} ${msg}`); process.exit(1) }
|
|
57
|
+
|
|
58
|
+
/** Run git in the target repo. Never throws on a non-zero exit -- the caller decides. */
|
|
59
|
+
function git(args, opts = {}) {
|
|
60
|
+
const r = spawnSync('git', args, { cwd: REPO, encoding: 'utf8', ...opts })
|
|
61
|
+
return { code: r.status, out: (r.stdout || '').trim(), err: (r.stderr || '').trim() }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The same, WITHOUT trimming. For reading file CONTENT, where a trailing newline is part of the
|
|
65
|
+
* file and not whitespace around an answer.
|
|
66
|
+
*
|
|
67
|
+
* `git()` above trims, which is right for `rev-parse` and `log` and wrong for `cat-file blob` --
|
|
68
|
+
* and wrong in the worst way: it strips the final newline, so the content hash of a healthy file
|
|
69
|
+
* never matches the room's chain, every file reads DIVERGED, and the staleness check refuses
|
|
70
|
+
* every run. Caught by the end-to-end suite on its FIRST assertion; the unit suite could not
|
|
71
|
+
* have seen it, because it passes text straight in and never goes near git. */
|
|
72
|
+
function gitRaw(args, opts = {}) {
|
|
73
|
+
const r = spawnSync('git', args, { cwd: REPO, encoding: 'utf8', ...opts })
|
|
74
|
+
return { code: r.status, out: r.stdout == null ? '' : r.stdout, err: (r.stderr || '').trim() }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The authors of a file, newest-landed first, from VERIFIED receipts only.
|
|
78
|
+
* An unverifiable receipt is not evidence of authorship, so it is dropped rather than trusted. */
|
|
79
|
+
export function authorsOf(receipts, verify) {
|
|
80
|
+
const ok = receipts.filter((r) => r && r.author && (!verify || verify(r)))
|
|
81
|
+
if (!ok.length) return { last: null, contributors: [], first: 0 }
|
|
82
|
+
const last = ok[ok.length - 1]
|
|
83
|
+
const seen = new Map()
|
|
84
|
+
for (const r of ok) if (!seen.has(r.author)) seen.set(r.author, { author: r.author, name: r.name || 'agent', at: r.at || 0 })
|
|
85
|
+
return {
|
|
86
|
+
last: { author: last.author, name: last.name || 'agent', at: last.at || 0 },
|
|
87
|
+
contributors: [...seen.values()],
|
|
88
|
+
first: Math.min(...ok.map((r) => r.at || 0)),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const mailOf = (name) => `${String(name).toLowerCase().replace(/[^a-z0-9._-]+/g, '-') || 'agent'}@agents.hivecode.local`
|
|
93
|
+
|
|
94
|
+
/** Group files by the agent who landed their final state, oldest contribution first. */
|
|
95
|
+
export function planCommits(files) {
|
|
96
|
+
const groups = new Map()
|
|
97
|
+
for (const f of files) {
|
|
98
|
+
if (!f.last) continue
|
|
99
|
+
const g = groups.get(f.last.author) || { author: f.last.author, name: f.last.name, at: f.first, files: [], others: new Map() }
|
|
100
|
+
g.files.push(f)
|
|
101
|
+
g.at = Math.min(g.at, f.first)
|
|
102
|
+
for (const c of f.contributors) if (c.author !== f.last.author) g.others.set(c.author, c)
|
|
103
|
+
groups.set(f.last.author, g)
|
|
104
|
+
}
|
|
105
|
+
return [...groups.values()]
|
|
106
|
+
.map((g) => ({ ...g, others: [...g.others.values()] }))
|
|
107
|
+
.sort((a, b) => a.at - b.at || a.name.localeCompare(b.name))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function commitMessage(g, room) {
|
|
111
|
+
const list = g.files.map((f) => f.rel).sort()
|
|
112
|
+
const head = list.length === 1 ? `${g.name}: ${list[0]}` : `${g.name}: ${list.length} files`
|
|
113
|
+
const lines = [head, '', ...list.map((x) => ` - ${x}`), '', `Room: ${room}`, `Hivecode-Author: ${g.author}`]
|
|
114
|
+
for (const o of g.others) lines.push(`Co-authored-by: ${o.name} <${mailOf(o.name)}>`)
|
|
115
|
+
return lines.join('\n')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---- main ---------------------------------------------------------------------------------
|
|
119
|
+
// COMPARED AS REAL PATHS, as hive-mcp.js does. The old test built `file:///` + argv[1], which is
|
|
120
|
+
// right only on Windows: on macOS and Linux argv[1] already starts with '/', the URL came out as
|
|
121
|
+
// `file:////...`, never matched, and the script exited 0 having done nothing at all.
|
|
122
|
+
const isMain = (() => {
|
|
123
|
+
if (!process.argv[1]) return false
|
|
124
|
+
const me = fileURLToPath(import.meta.url)
|
|
125
|
+
try { return fs.realpathSync(process.argv[1]) === fs.realpathSync(me) } catch { return path.resolve(process.argv[1]) === path.resolve(me) }
|
|
126
|
+
})()
|
|
127
|
+
if (isMain) {
|
|
128
|
+
if (!LINK || !REPO) {
|
|
129
|
+
console.error('usage: node hive-pr.js "<relay|room>" <YourName> --repo <path> [--branch b] [--dry-run] [--push] [--pr] [--force "<reason>"]')
|
|
130
|
+
process.exit(2)
|
|
131
|
+
}
|
|
132
|
+
if (!fs.existsSync(path.join(REPO, '.git'))) die('Q500', `${REPO} is not a git repository`)
|
|
133
|
+
|
|
134
|
+
const { relay, room } = parseLink(LINK)
|
|
135
|
+
const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 13)
|
|
136
|
+
const BRANCH = flag('branch') || `hivecode/${room}-${stamp}`
|
|
137
|
+
const TITLE = flag('title') || `Hivecode: work from room ${room}`
|
|
138
|
+
|
|
139
|
+
// A scratch mirror of the room, so nothing here can disturb anyone's working folder.
|
|
140
|
+
// os.tmpdir(), not $TEMP: TEMP is a Windows variable, and without it the mirror was made in the
|
|
141
|
+
// current directory -- inside the very checkout the PR is cut from.
|
|
142
|
+
const dir = path.join(os.tmpdir(), `hive-pr-${room}-${process.pid}`)
|
|
143
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
144
|
+
|
|
145
|
+
// Provenance is what this whole tool reads. Without it the ledger is empty and every
|
|
146
|
+
// commit would collapse to one anonymous blob -- which is exactly the thing we are here
|
|
147
|
+
// to avoid, so turn it on for this client rather than failing later with an empty plan.
|
|
148
|
+
process.env.HIVE_PROVENANCE = process.env.HIVE_PROVENANCE || 'on'
|
|
149
|
+
|
|
150
|
+
const hive = startSync({ relay, room, dir, name: NAME, kind: 'ai', token: process.env.HIVE_TOKEN || '', syncFiles: true, log: () => {} })
|
|
151
|
+
|
|
152
|
+
let done = false
|
|
153
|
+
// Cleared once the room answers. It used to stay armed for the whole run, so a landing that
|
|
154
|
+
// took longer than 20 s was killed as "could not reach the relay" -- mid-commit, possibly.
|
|
155
|
+
const unreachable = setTimeout(() => die('Q470', `could not reach the relay at ${relay}`), 20000)
|
|
156
|
+
hive.provider.on('sync', (s) => {
|
|
157
|
+
if (!s || done) return
|
|
158
|
+
done = true
|
|
159
|
+
clearTimeout(unreachable)
|
|
160
|
+
whenWholeRoomArrived()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// THE WHOLE ROOM, OR NOTHING. The file sub-docs arrive after the room doc, and this used to wait
|
|
164
|
+
// a flat 2.5 s for them. On a room bigger than what arrives in 2.5 s, the files still in flight
|
|
165
|
+
// were simply not in the mirror: not planned, not committed, not mentioned -- a PR that looks
|
|
166
|
+
// complete and silently leaves agents' work behind. Now the plan waits until every file the room
|
|
167
|
+
// lists is on disk here (a file's receipts travel in its own doc, so on disk means its ledger
|
|
168
|
+
// arrived too), keeps the 2.5 s floor, and refuses rather than land part of the room.
|
|
169
|
+
function whenWholeRoomArrived() {
|
|
170
|
+
const t0 = Date.now()
|
|
171
|
+
const limit = Number(process.env.HIVE_PR_WAIT_MS) || 120000
|
|
172
|
+
const tick = () => {
|
|
173
|
+
let st = null
|
|
174
|
+
try { st = hive.status() } catch { /* not ready yet */ }
|
|
175
|
+
if (st && st.filesMissing === 0 && Date.now() - t0 >= 2500) return run()
|
|
176
|
+
if (Date.now() - t0 > limit) {
|
|
177
|
+
const have = st ? `${st.filesHeld} of ${st.roomFiles}` : 'an unknown number'
|
|
178
|
+
console.error(`Q507 only ${have} of the room's files arrived in ${Math.round(limit / 1000)} s. Refusing to land part of the room: the missing files' work would be left out of the PR without anyone being told.`)
|
|
179
|
+
return finish(1)
|
|
180
|
+
}
|
|
181
|
+
setTimeout(tick, 250)
|
|
182
|
+
}
|
|
183
|
+
tick()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function walk(root, base = '') {
|
|
187
|
+
const out = []
|
|
188
|
+
for (const e of fs.readdirSync(path.join(root, base), { withFileTypes: true })) {
|
|
189
|
+
// Git's own and this client's bookkeeping (.hive.json, .hive-shadow.json, ...). Every other
|
|
190
|
+
// dotfile is project content: this skipped ALL of them, so an agent's edit to .gitignore or
|
|
191
|
+
// .github/workflows never reached the PR, and nothing said so. Only files with a verified
|
|
192
|
+
// receipt are committed either way, so bookkeeping that slipped through would still be left out.
|
|
193
|
+
if (e.name === '.git' || e.name.startsWith('.hive')) continue
|
|
194
|
+
const rel = base ? `${base}/${e.name}` : e.name
|
|
195
|
+
if (e.isDirectory()) out.push(...walk(root, rel))
|
|
196
|
+
else out.push(rel)
|
|
197
|
+
}
|
|
198
|
+
return out
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function run() {
|
|
202
|
+
let rels = walk(dir)
|
|
203
|
+
if (ONLY) rels = rels.filter((r) => ONLY.includes(r))
|
|
204
|
+
const files = []
|
|
205
|
+
for (const rel of rels) {
|
|
206
|
+
const receipts = hive.provenanceOf(rel) || []
|
|
207
|
+
const audit = hive.verifyProvenanceOf(rel) || { bad: [] }
|
|
208
|
+
const badIdx = new Set((audit.bad || []).map((b) => b.i))
|
|
209
|
+
const a = authorsOf(receipts, (r) => !badIdx.has(receipts.indexOf(r)))
|
|
210
|
+
files.push({ rel, abs: path.join(dir, rel), ...a })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const attributed = files.filter((f) => f.last)
|
|
214
|
+
const orphans = files.filter((f) => !f.last)
|
|
215
|
+
const groups = planCommits(attributed)
|
|
216
|
+
|
|
217
|
+
console.log(`room ${room}: ${files.length} files, ${attributed.length} attributed, ${groups.length} agent(s)`)
|
|
218
|
+
for (const g of groups) {
|
|
219
|
+
console.log(` ${g.name} ${g.files.length} file(s)${g.others.length ? ` (+${g.others.length} co-author)` : ''}`)
|
|
220
|
+
for (const f of g.files) console.log(` ${f.rel}`)
|
|
221
|
+
}
|
|
222
|
+
if (orphans.length) console.log(` (${orphans.length} file(s) with no verified receipt -- not committed: ${orphans.map((o) => o.rel).join(', ')})`)
|
|
223
|
+
|
|
224
|
+
if (DRY) { console.log('\n--dry-run: nothing written'); return finish(0) }
|
|
225
|
+
if (!groups.length) { console.error('Q501 nothing attributed to commit'); return finish(1) }
|
|
226
|
+
|
|
227
|
+
const base = flag('base') || git(['rev-parse', '--abbrev-ref', 'HEAD']).out || 'HEAD'
|
|
228
|
+
|
|
229
|
+
// ── HIV-28: DO NOT WRITE OVER A BRANCH CHANGE WE CANNOT ACCOUNT FOR ──────────────────
|
|
230
|
+
//
|
|
231
|
+
// Measured 19 Sep 2026: this tool produced a PR with three clean per-agent commits, real git
|
|
232
|
+
// authorship and signed trailers, which deleted the HIV-27 claim guard, the HIV-14 truncation
|
|
233
|
+
// fix and ~200 lines of tests. It looked perfect. The cause was the copyFileSync below with
|
|
234
|
+
// nothing in front of it.
|
|
235
|
+
//
|
|
236
|
+
// The check runs BEFORE `checkout -b`, so a refusal leaves no branch behind. Refusing after
|
|
237
|
+
// creating it would hand the operator a half-made artefact to clean up, and the next thing a
|
|
238
|
+
// person does with a branch that exists is push it.
|
|
239
|
+
const decisions = []
|
|
240
|
+
for (const g of groups) {
|
|
241
|
+
for (const f of g.files) {
|
|
242
|
+
// `git cat-file blob` and not `git show`: cat-file reads the object as stored, with no
|
|
243
|
+
// smudge filter. With core.autocrlf on -- true in this repo -- `show` through a working
|
|
244
|
+
// tree hands back bytes git never stored, and every content hash misses.
|
|
245
|
+
const sha = git(['rev-parse', `${base}:${f.rel}`])
|
|
246
|
+
const branchText = sha.code === 0 ? gitRaw(['cat-file', 'blob', sha.out]).out : null
|
|
247
|
+
const roomText = fs.readFileSync(f.abs, 'utf8')
|
|
248
|
+
decisions.push({ ...accountFor({ rel: f.rel, branchText, roomText, receipts: hive.provenanceOf(f.rel) || [] }), _f: f })
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const v = verdict(decisions)
|
|
252
|
+
if (v.identical.length) console.log(` (${v.identical.length} file(s) already identical on ${base} -- nothing to write)`)
|
|
253
|
+
|
|
254
|
+
if (!v.ok) {
|
|
255
|
+
const lastTouch = (rel) => git(['log', '-1', '--format=%h %an %ad %s', '--date=short', base, '--', rel]).out
|
|
256
|
+
console.error('')
|
|
257
|
+
console.error(explain(v.blocked, lastTouch))
|
|
258
|
+
if (!FORCE) return finish(1)
|
|
259
|
+
// A forced run is still reported in full, and the reason travels into the PR body. The
|
|
260
|
+
// point is not to make forcing hard, it is to make it visible.
|
|
261
|
+
console.error('')
|
|
262
|
+
console.error(`--force given: "${FORCE}" -- overwriting ${v.blocked.length} file(s) anyway. This is recorded in the PR body.`)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const co = git(['checkout', '-b', BRANCH, base])
|
|
266
|
+
if (co.code !== 0) { console.error(`Q502 could not create branch ${BRANCH}: ${co.err}`); return finish(1) }
|
|
267
|
+
|
|
268
|
+
// IDENTICAL files are dropped rather than copied. Copying one rewrites its line endings when
|
|
269
|
+
// the room and the blob differ in style -- which on this repo is most files -- producing a
|
|
270
|
+
// diff of every line for a change nobody made.
|
|
271
|
+
const skip = new Set(v.identical.map((d) => d.rel))
|
|
272
|
+
let made = 0
|
|
273
|
+
for (const g0 of groups) {
|
|
274
|
+
// THE COMMIT MESSAGE MUST DESCRIBE WHAT WAS COMMITTED. Built from g0.files it said
|
|
275
|
+
// "Builder: 2 files" and listed both while only one was written, because the other was
|
|
276
|
+
// already identical on the branch. A message that names files a commit does not contain is
|
|
277
|
+
// a small false success in the one artefact a reviewer trusts most -- and it is in `git log`
|
|
278
|
+
// forever.
|
|
279
|
+
const g = { ...g0, files: g0.files.filter((f) => !skip.has(f.rel)) }
|
|
280
|
+
if (!g.files.length) { console.log(` (nothing to write from ${g.name} — all already on ${base})`); continue }
|
|
281
|
+
for (const f of g.files) {
|
|
282
|
+
const dest = path.join(REPO, f.rel)
|
|
283
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
284
|
+
fs.copyFileSync(f.abs, dest)
|
|
285
|
+
git(['add', '--', f.rel])
|
|
286
|
+
}
|
|
287
|
+
const msg = commitMessage(g, room)
|
|
288
|
+
const r = git(['commit', '-m', msg, '--author', `${g.name} <${mailOf(g.name)}>`])
|
|
289
|
+
if (r.code !== 0) {
|
|
290
|
+
// "nothing to commit" is not a failure: the room's copy can equal what is already
|
|
291
|
+
// on the branch, and a no-op commit is the honest outcome.
|
|
292
|
+
if (/nothing to commit/i.test(r.out + r.err)) { console.log(` (no change from ${g.name})`); continue }
|
|
293
|
+
console.error(`Q503 commit failed for ${g.name}: ${r.err || r.out}`); return finish(1)
|
|
294
|
+
}
|
|
295
|
+
made++
|
|
296
|
+
}
|
|
297
|
+
console.log(`\nbranch ${BRANCH}: ${made} commit(s)`)
|
|
298
|
+
console.log(git(['log', '--format= %h %an %s', `${base}..${BRANCH}`]).out)
|
|
299
|
+
|
|
300
|
+
if (DO_PUSH) {
|
|
301
|
+
const p = git(['push', '-u', 'origin', BRANCH])
|
|
302
|
+
if (p.code !== 0) { console.error(`Q504 push failed: ${p.err}`); return finish(1) }
|
|
303
|
+
console.log(`pushed ${BRANCH}`)
|
|
304
|
+
}
|
|
305
|
+
if (DO_PR) {
|
|
306
|
+
// A forced overwrite is stated in the PR BODY, not only in the terminal of the person who
|
|
307
|
+
// ran it. The reviewer is the one who needs to know that files were written over branch
|
|
308
|
+
// content this room had never seen -- and a reviewer looking at three clean signed commits
|
|
309
|
+
// has no other way to find that out. That is the whole shape of the 19 Sep incident.
|
|
310
|
+
const forcedNote = (!v.ok && FORCE)
|
|
311
|
+
? ['',
|
|
312
|
+
'> **Forced past a staleness refusal.** ' + v.blocked.length + ' file(s) were written',
|
|
313
|
+
'> over branch content this room had never seen.',
|
|
314
|
+
'>',
|
|
315
|
+
'> Reason given: ' + JSON.stringify(FORCE),
|
|
316
|
+
'>',
|
|
317
|
+
'> ' + v.blocked.map((d) => d.rel).join(', ')]
|
|
318
|
+
: []
|
|
319
|
+
const body = [`Opened by Hivecode from room \`${room}\`.`, ...forcedNote, '', '| agent | files |', '|---|---|',
|
|
320
|
+
...groups.map((g) => `| ${g.name} | ${g.files.map((f) => '`' + f.rel + '`').join(', ')} |`),
|
|
321
|
+
'', 'Each commit carries its agent as the git author and the signed author id as a `Hivecode-Author` trailer.'].join('\n')
|
|
322
|
+
const r = spawnSync('gh', ['pr', 'create', '--title', TITLE, '--body', body, '--base', base, '--head', BRANCH],
|
|
323
|
+
{ cwd: REPO, encoding: 'utf8' })
|
|
324
|
+
if (r.status !== 0) { console.error(`Q505 gh pr create failed: ${(r.stderr || '').trim()}`); return finish(1) }
|
|
325
|
+
console.log((r.stdout || '').trim())
|
|
326
|
+
}
|
|
327
|
+
finish(0)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function finish(code) {
|
|
331
|
+
setTimeout(() => {
|
|
332
|
+
try { hive.stop() } catch { /* going away */ }
|
|
333
|
+
try { fs.rmSync(dir, { recursive: true, force: true }) } catch { /* best effort */ }
|
|
334
|
+
process.exit(code)
|
|
335
|
+
}, 400)
|
|
336
|
+
}
|
|
337
|
+
}
|