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-staleness.js
CHANGED
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
// hive-pr-staleness.js — do not write a file over a branch change you cannot account for.
|
|
2
|
-
//
|
|
3
|
-
// THE INCIDENT, MEASURED 19 Sep 2026. hive-pr generated a PR that looked perfect: three commits,
|
|
4
|
-
// one per agent, each agent as the real git author, signed ids in `Hivecode-Author:` trailers,
|
|
5
|
-
// files with no verified receipt refused rather than committed. A reviewer would have had every
|
|
6
|
-
// reason to trust it.
|
|
7
|
-
//
|
|
8
|
-
// Diffed against the branch it was cut from:
|
|
9
|
-
//
|
|
10
|
-
// hive-linear.js -77
|
|
11
|
-
// tests/hive-linear-test.mjs -114
|
|
12
|
-
// tests/hive-mcp-http-test.mjs -49
|
|
13
|
-
// tests/hive-governor-test.mjs -32
|
|
14
|
-
// sync.js -39
|
|
15
|
-
//
|
|
16
|
-
// It deleted the HIV-27 claim guard, deleted the HIV-14 truncation fix, deleted ~200 lines of
|
|
17
|
-
// tests, and restored the 10x dashboard bug -- the one where thirty minutes of runtime displays
|
|
18
|
-
// as 3.0. Filed HIV-28.
|
|
19
|
-
//
|
|
20
|
-
// THE CAUSE IS ONE LINE. hive-pr.js did `fs.copyFileSync(f.abs, dest)` and then `git add`. It
|
|
21
|
-
// treats the room as truth and writes it over the branch. Grepping that file for `ahead`,
|
|
22
|
-
// `behind`, `stale`, `diverged`, `merge-base` or `rev-list` returned nothing: it never asked
|
|
23
|
-
// whether the branch had moved.
|
|
24
|
-
//
|
|
25
|
-
// This is not an exotic case, it is the NORMAL one. Any commit reaching the branch by another
|
|
26
|
-
// route puts it ahead -- a hotfix, a second developer, CI, or a human doing exactly the
|
|
27
|
-
// review-then-push workflow this tool exists to serve. In the measured incident the other route
|
|
28
|
-
// was me, working in the git checkout instead of my room folder.
|
|
29
|
-
//
|
|
30
|
-
// WHY "AHEAD/BEHIND" IS THE WRONG QUESTION. There is no merge-base between a Yjs room and a git
|
|
31
|
-
// branch; they are not two git histories. `git rev-list --count` cannot answer this, and reaching
|
|
32
|
-
// for it would produce a check that looks rigorous and measures nothing.
|
|
33
|
-
//
|
|
34
|
-
// THE QUESTION THAT IS ANSWERABLE, per file:
|
|
35
|
-
//
|
|
36
|
-
// has the room ever SEEN what is currently on the branch?
|
|
37
|
-
//
|
|
38
|
-
// It can be answered exactly, from state that already exists. Every landed change appends a
|
|
39
|
-
// receipt carrying `parent` (the contentHash the author built on) and `contentHash` (the result),
|
|
40
|
-
// so a file's ledger IS a hash chain of every state the room has passed through. `provenanceOf()`
|
|
41
|
-
// already returns it -- hive-pr reads it for attribution and then ignores it for safety.
|
|
42
|
-
//
|
|
43
|
-
// If the branch's current bytes hash to something in that chain, the room descends from the
|
|
44
|
-
// branch and writing is continuing the line. If they do not, the branch holds a change the room
|
|
45
|
-
// has never seen, and writing would delete it. That is the whole check.
|
|
46
|
-
|
|
47
|
-
import { contentHash } from './substrate.js'
|
|
48
|
-
|
|
49
|
-
/** Nothing to write: the branch already holds exactly this. */
|
|
50
|
-
export const IDENTICAL = 'IDENTICAL'
|
|
51
|
-
/** The file is not on the branch at all, so there is nothing to revert. */
|
|
52
|
-
export const NEW = 'NEW'
|
|
53
|
-
/** The branch's bytes are in the room's chain: the room descends from them. Safe. */
|
|
54
|
-
export const DESCENDS = 'DESCENDS'
|
|
55
|
-
/** The branch holds bytes the room has never seen. REFUSE. */
|
|
56
|
-
export const DIVERGED = 'DIVERGED'
|
|
57
|
-
/** The room has no chain for this file, so nothing can be proved either way. REFUSE. */
|
|
58
|
-
export const UNPROVEN = 'UNPROVEN'
|
|
59
|
-
|
|
60
|
-
/** The only two states that may be written. A list, so widening it is a visible edit. */
|
|
61
|
-
export const WRITABLE = [NEW, DESCENDS]
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* LINE ENDINGS ARE PRESENTATION, AND ON THIS REPO THEY DIFFER BY DEFAULT.
|
|
65
|
-
*
|
|
66
|
-
* Measured here: `hive-mcp.js` and `sync.js` are CRLF in the working tree and LF in the blob,
|
|
67
|
-
* because `core.autocrlf` is true. So the bytes git stored and the bytes the room holds are not
|
|
68
|
-
* equal for most files in this project, while the file is by any useful definition the same file.
|
|
69
|
-
*
|
|
70
|
-
* A byte comparison would therefore mark nearly everything DIVERGED and refuse every run. That is
|
|
71
|
-
* worse than no check: a guard that fires on the HEALTHY path is the one that gets a `--force`
|
|
72
|
-
* alias in somebody's shell history within a week, and then it is not a guard at all.
|
|
73
|
-
*
|
|
74
|
-
* So both the equality test and the chain lookup are done modulo line endings. Three variants, not
|
|
75
|
-
* a normalisation, because the chain's hashes were computed by substrate.js over whatever text the
|
|
76
|
-
* room actually held at the time -- there is no single canonical form to normalise TO.
|
|
77
|
-
*/
|
|
78
|
-
const variants = (t) => {
|
|
79
|
-
const lf = String(t).replace(/\r\n/g, '\n')
|
|
80
|
-
return [String(t), lf, lf.replace(/\n/g, '\r\n')]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Same file, ignoring how the lines end. */
|
|
84
|
-
export const sameText = (a, b) =>
|
|
85
|
-
a != null && b != null && String(a).replace(/\r\n/g, '\n') === String(b).replace(/\r\n/g, '\n')
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Every content state this file has passed through in the room.
|
|
89
|
-
*
|
|
90
|
-
* Both fields go in. `parent` is the state an author BUILT ON -- which may be a state no receipt
|
|
91
|
-
* ever produced, because the first write's parent is the hash of the empty string and a rejoin
|
|
92
|
-
* can land on a state whose own receipt lives on another peer. Taking only `contentHash` would
|
|
93
|
-
* miss exactly those, and each miss reads as DIVERGED: a refusal on a healthy file, which is the
|
|
94
|
-
* failure mode that gets a safety check disabled.
|
|
95
|
-
*/
|
|
96
|
-
export function chainOf(receipts) {
|
|
97
|
-
const seen = new Set()
|
|
98
|
-
for (const r of receipts || []) {
|
|
99
|
-
if (!r) continue
|
|
100
|
-
if (r.parent) seen.add(r.parent)
|
|
101
|
-
if (r.contentHash) seen.add(r.contentHash)
|
|
102
|
-
}
|
|
103
|
-
return seen
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Decide whether the room's copy may be written over the branch's.
|
|
108
|
-
*
|
|
109
|
-
* `branchText` is null when the path does not exist on the branch. It must be the RAW blob --
|
|
110
|
-
* read with `git cat-file blob`, never `git show` through a working tree, or a checkout with
|
|
111
|
-
* `core.autocrlf` hands back bytes git never stored and every hash misses.
|
|
112
|
-
*/
|
|
113
|
-
export function accountFor({ rel, branchText, roomText, receipts }) {
|
|
114
|
-
if (branchText == null) return { rel, state: NEW, why: 'not on the branch yet — nothing can be reverted' }
|
|
115
|
-
// Modulo line endings on purpose. A file that differs ONLY in how its lines end must not be
|
|
116
|
-
// rewritten: it would produce a diff of every line in the file, for no change anybody made.
|
|
117
|
-
if (sameText(branchText, roomText)) return { rel, state: IDENTICAL, why: 'the branch already holds this (line endings aside)' }
|
|
118
|
-
|
|
119
|
-
const chain = chainOf(receipts)
|
|
120
|
-
if (!chain.size) {
|
|
121
|
-
return {
|
|
122
|
-
rel, state: UNPROVEN,
|
|
123
|
-
why: 'the room has no provenance chain for this file, so there is no way to tell whether its copy is newer than the branch or older',
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const hashes = variants(branchText).map(contentHash)
|
|
128
|
-
if (hashes.some((h) => chain.has(h))) {
|
|
129
|
-
return { rel, state: DESCENDS, why: 'the room has passed through the branch\'s current content, so its copy is a descendant' }
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
rel, state: DIVERGED, branchHash: hashes[0],
|
|
133
|
-
why: 'the branch holds content this room has never seen — writing the room\'s copy would delete it',
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** May this decision be written? Never inline `state !== DIVERGED` — that admits UNPROVEN. */
|
|
138
|
-
export const writable = (d) => WRITABLE.includes(d.state)
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* The run-level verdict.
|
|
142
|
-
*
|
|
143
|
-
* ALL OR NOTHING, and that is the design. Committing the safe files and skipping the rest produces
|
|
144
|
-
* a PR that looks complete and is not -- the same false success the tool already produced once,
|
|
145
|
-
* one level up. A refusal a human can read beats a branch they have to audit.
|
|
146
|
-
*/
|
|
147
|
-
export function verdict(decisions) {
|
|
148
|
-
const blocked = decisions.filter((d) => !writable(d) && d.state !== IDENTICAL)
|
|
149
|
-
return {
|
|
150
|
-
ok: blocked.length === 0,
|
|
151
|
-
blocked,
|
|
152
|
-
writable: decisions.filter(writable),
|
|
153
|
-
identical: decisions.filter((d) => d.state === IDENTICAL),
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* What the operator reads. `lastTouch` is supplied by the caller (hive-pr runs the git command)
|
|
159
|
-
* so this module stays pure and testable without a repository.
|
|
160
|
-
*
|
|
161
|
-
* It names the commit and the person, because "diverged" alone sends someone to `git log` to work
|
|
162
|
-
* out whether this is their own forgotten hotfix or somebody else's live work -- and that lookup
|
|
163
|
-
* is the moment a tired operator reaches for --force.
|
|
164
|
-
*/
|
|
165
|
-
export function explain(blocked, lastTouch = () => '') {
|
|
166
|
-
// Q506, the next free code in hive-pr's own Q500-Q505 block. Q443 would have read naturally
|
|
167
|
-
// here -- it is the other "you are being handed an incomplete picture" refusal -- and it is
|
|
168
|
-
// already the Linear truncation code. Two meanings behind one code is the duplicated-wire
|
|
169
|
-
// -contract defect wearing a three-digit hat.
|
|
170
|
-
const lines = ['Q506 REFUSING TO WRITE — the branch has moved under these files:', '']
|
|
171
|
-
for (const d of blocked) {
|
|
172
|
-
const touch = lastTouch(d.rel)
|
|
173
|
-
lines.push(` ${d.rel}`)
|
|
174
|
-
lines.push(` ${d.why}`)
|
|
175
|
-
if (touch) lines.push(` last changed on the branch by: ${touch}`)
|
|
176
|
-
}
|
|
177
|
-
lines.push('')
|
|
178
|
-
lines.push('Nothing was written and no branch was created. Writing the room\'s copy over these')
|
|
179
|
-
lines.push('would silently revert whatever reached the branch another way — a hotfix, a second')
|
|
180
|
-
lines.push('developer, CI, or your own work done outside the room. That has happened once on this')
|
|
181
|
-
lines.push('project and the resulting PR looked perfect.')
|
|
182
|
-
lines.push('')
|
|
183
|
-
lines.push('Fix it one of two ways:')
|
|
184
|
-
lines.push(' - bring the branch\'s version INTO the room (edit it there, or hive_edit it), so the')
|
|
185
|
-
lines.push(' room\'s copy descends from what is on the branch. Then run this again.')
|
|
186
|
-
lines.push(' - --force "<reason>" if you have decided the room\'s copy should win. The reason is')
|
|
187
|
-
lines.push(' recorded in the PR body, because somebody will ask.')
|
|
188
|
-
return lines.join('\n')
|
|
189
|
-
}
|
|
1
|
+
// hive-pr-staleness.js — do not write a file over a branch change you cannot account for.
|
|
2
|
+
//
|
|
3
|
+
// THE INCIDENT, MEASURED 19 Sep 2026. hive-pr generated a PR that looked perfect: three commits,
|
|
4
|
+
// one per agent, each agent as the real git author, signed ids in `Hivecode-Author:` trailers,
|
|
5
|
+
// files with no verified receipt refused rather than committed. A reviewer would have had every
|
|
6
|
+
// reason to trust it.
|
|
7
|
+
//
|
|
8
|
+
// Diffed against the branch it was cut from:
|
|
9
|
+
//
|
|
10
|
+
// hive-linear.js -77
|
|
11
|
+
// tests/hive-linear-test.mjs -114
|
|
12
|
+
// tests/hive-mcp-http-test.mjs -49
|
|
13
|
+
// tests/hive-governor-test.mjs -32
|
|
14
|
+
// sync.js -39
|
|
15
|
+
//
|
|
16
|
+
// It deleted the HIV-27 claim guard, deleted the HIV-14 truncation fix, deleted ~200 lines of
|
|
17
|
+
// tests, and restored the 10x dashboard bug -- the one where thirty minutes of runtime displays
|
|
18
|
+
// as 3.0. Filed HIV-28.
|
|
19
|
+
//
|
|
20
|
+
// THE CAUSE IS ONE LINE. hive-pr.js did `fs.copyFileSync(f.abs, dest)` and then `git add`. It
|
|
21
|
+
// treats the room as truth and writes it over the branch. Grepping that file for `ahead`,
|
|
22
|
+
// `behind`, `stale`, `diverged`, `merge-base` or `rev-list` returned nothing: it never asked
|
|
23
|
+
// whether the branch had moved.
|
|
24
|
+
//
|
|
25
|
+
// This is not an exotic case, it is the NORMAL one. Any commit reaching the branch by another
|
|
26
|
+
// route puts it ahead -- a hotfix, a second developer, CI, or a human doing exactly the
|
|
27
|
+
// review-then-push workflow this tool exists to serve. In the measured incident the other route
|
|
28
|
+
// was me, working in the git checkout instead of my room folder.
|
|
29
|
+
//
|
|
30
|
+
// WHY "AHEAD/BEHIND" IS THE WRONG QUESTION. There is no merge-base between a Yjs room and a git
|
|
31
|
+
// branch; they are not two git histories. `git rev-list --count` cannot answer this, and reaching
|
|
32
|
+
// for it would produce a check that looks rigorous and measures nothing.
|
|
33
|
+
//
|
|
34
|
+
// THE QUESTION THAT IS ANSWERABLE, per file:
|
|
35
|
+
//
|
|
36
|
+
// has the room ever SEEN what is currently on the branch?
|
|
37
|
+
//
|
|
38
|
+
// It can be answered exactly, from state that already exists. Every landed change appends a
|
|
39
|
+
// receipt carrying `parent` (the contentHash the author built on) and `contentHash` (the result),
|
|
40
|
+
// so a file's ledger IS a hash chain of every state the room has passed through. `provenanceOf()`
|
|
41
|
+
// already returns it -- hive-pr reads it for attribution and then ignores it for safety.
|
|
42
|
+
//
|
|
43
|
+
// If the branch's current bytes hash to something in that chain, the room descends from the
|
|
44
|
+
// branch and writing is continuing the line. If they do not, the branch holds a change the room
|
|
45
|
+
// has never seen, and writing would delete it. That is the whole check.
|
|
46
|
+
|
|
47
|
+
import { contentHash } from './substrate.js'
|
|
48
|
+
|
|
49
|
+
/** Nothing to write: the branch already holds exactly this. */
|
|
50
|
+
export const IDENTICAL = 'IDENTICAL'
|
|
51
|
+
/** The file is not on the branch at all, so there is nothing to revert. */
|
|
52
|
+
export const NEW = 'NEW'
|
|
53
|
+
/** The branch's bytes are in the room's chain: the room descends from them. Safe. */
|
|
54
|
+
export const DESCENDS = 'DESCENDS'
|
|
55
|
+
/** The branch holds bytes the room has never seen. REFUSE. */
|
|
56
|
+
export const DIVERGED = 'DIVERGED'
|
|
57
|
+
/** The room has no chain for this file, so nothing can be proved either way. REFUSE. */
|
|
58
|
+
export const UNPROVEN = 'UNPROVEN'
|
|
59
|
+
|
|
60
|
+
/** The only two states that may be written. A list, so widening it is a visible edit. */
|
|
61
|
+
export const WRITABLE = [NEW, DESCENDS]
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* LINE ENDINGS ARE PRESENTATION, AND ON THIS REPO THEY DIFFER BY DEFAULT.
|
|
65
|
+
*
|
|
66
|
+
* Measured here: `hive-mcp.js` and `sync.js` are CRLF in the working tree and LF in the blob,
|
|
67
|
+
* because `core.autocrlf` is true. So the bytes git stored and the bytes the room holds are not
|
|
68
|
+
* equal for most files in this project, while the file is by any useful definition the same file.
|
|
69
|
+
*
|
|
70
|
+
* A byte comparison would therefore mark nearly everything DIVERGED and refuse every run. That is
|
|
71
|
+
* worse than no check: a guard that fires on the HEALTHY path is the one that gets a `--force`
|
|
72
|
+
* alias in somebody's shell history within a week, and then it is not a guard at all.
|
|
73
|
+
*
|
|
74
|
+
* So both the equality test and the chain lookup are done modulo line endings. Three variants, not
|
|
75
|
+
* a normalisation, because the chain's hashes were computed by substrate.js over whatever text the
|
|
76
|
+
* room actually held at the time -- there is no single canonical form to normalise TO.
|
|
77
|
+
*/
|
|
78
|
+
const variants = (t) => {
|
|
79
|
+
const lf = String(t).replace(/\r\n/g, '\n')
|
|
80
|
+
return [String(t), lf, lf.replace(/\n/g, '\r\n')]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Same file, ignoring how the lines end. */
|
|
84
|
+
export const sameText = (a, b) =>
|
|
85
|
+
a != null && b != null && String(a).replace(/\r\n/g, '\n') === String(b).replace(/\r\n/g, '\n')
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Every content state this file has passed through in the room.
|
|
89
|
+
*
|
|
90
|
+
* Both fields go in. `parent` is the state an author BUILT ON -- which may be a state no receipt
|
|
91
|
+
* ever produced, because the first write's parent is the hash of the empty string and a rejoin
|
|
92
|
+
* can land on a state whose own receipt lives on another peer. Taking only `contentHash` would
|
|
93
|
+
* miss exactly those, and each miss reads as DIVERGED: a refusal on a healthy file, which is the
|
|
94
|
+
* failure mode that gets a safety check disabled.
|
|
95
|
+
*/
|
|
96
|
+
export function chainOf(receipts) {
|
|
97
|
+
const seen = new Set()
|
|
98
|
+
for (const r of receipts || []) {
|
|
99
|
+
if (!r) continue
|
|
100
|
+
if (r.parent) seen.add(r.parent)
|
|
101
|
+
if (r.contentHash) seen.add(r.contentHash)
|
|
102
|
+
}
|
|
103
|
+
return seen
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Decide whether the room's copy may be written over the branch's.
|
|
108
|
+
*
|
|
109
|
+
* `branchText` is null when the path does not exist on the branch. It must be the RAW blob --
|
|
110
|
+
* read with `git cat-file blob`, never `git show` through a working tree, or a checkout with
|
|
111
|
+
* `core.autocrlf` hands back bytes git never stored and every hash misses.
|
|
112
|
+
*/
|
|
113
|
+
export function accountFor({ rel, branchText, roomText, receipts }) {
|
|
114
|
+
if (branchText == null) return { rel, state: NEW, why: 'not on the branch yet — nothing can be reverted' }
|
|
115
|
+
// Modulo line endings on purpose. A file that differs ONLY in how its lines end must not be
|
|
116
|
+
// rewritten: it would produce a diff of every line in the file, for no change anybody made.
|
|
117
|
+
if (sameText(branchText, roomText)) return { rel, state: IDENTICAL, why: 'the branch already holds this (line endings aside)' }
|
|
118
|
+
|
|
119
|
+
const chain = chainOf(receipts)
|
|
120
|
+
if (!chain.size) {
|
|
121
|
+
return {
|
|
122
|
+
rel, state: UNPROVEN,
|
|
123
|
+
why: 'the room has no provenance chain for this file, so there is no way to tell whether its copy is newer than the branch or older',
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const hashes = variants(branchText).map(contentHash)
|
|
128
|
+
if (hashes.some((h) => chain.has(h))) {
|
|
129
|
+
return { rel, state: DESCENDS, why: 'the room has passed through the branch\'s current content, so its copy is a descendant' }
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
rel, state: DIVERGED, branchHash: hashes[0],
|
|
133
|
+
why: 'the branch holds content this room has never seen — writing the room\'s copy would delete it',
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** May this decision be written? Never inline `state !== DIVERGED` — that admits UNPROVEN. */
|
|
138
|
+
export const writable = (d) => WRITABLE.includes(d.state)
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The run-level verdict.
|
|
142
|
+
*
|
|
143
|
+
* ALL OR NOTHING, and that is the design. Committing the safe files and skipping the rest produces
|
|
144
|
+
* a PR that looks complete and is not -- the same false success the tool already produced once,
|
|
145
|
+
* one level up. A refusal a human can read beats a branch they have to audit.
|
|
146
|
+
*/
|
|
147
|
+
export function verdict(decisions) {
|
|
148
|
+
const blocked = decisions.filter((d) => !writable(d) && d.state !== IDENTICAL)
|
|
149
|
+
return {
|
|
150
|
+
ok: blocked.length === 0,
|
|
151
|
+
blocked,
|
|
152
|
+
writable: decisions.filter(writable),
|
|
153
|
+
identical: decisions.filter((d) => d.state === IDENTICAL),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* What the operator reads. `lastTouch` is supplied by the caller (hive-pr runs the git command)
|
|
159
|
+
* so this module stays pure and testable without a repository.
|
|
160
|
+
*
|
|
161
|
+
* It names the commit and the person, because "diverged" alone sends someone to `git log` to work
|
|
162
|
+
* out whether this is their own forgotten hotfix or somebody else's live work -- and that lookup
|
|
163
|
+
* is the moment a tired operator reaches for --force.
|
|
164
|
+
*/
|
|
165
|
+
export function explain(blocked, lastTouch = () => '') {
|
|
166
|
+
// Q506, the next free code in hive-pr's own Q500-Q505 block. Q443 would have read naturally
|
|
167
|
+
// here -- it is the other "you are being handed an incomplete picture" refusal -- and it is
|
|
168
|
+
// already the Linear truncation code. Two meanings behind one code is the duplicated-wire
|
|
169
|
+
// -contract defect wearing a three-digit hat.
|
|
170
|
+
const lines = ['Q506 REFUSING TO WRITE — the branch has moved under these files:', '']
|
|
171
|
+
for (const d of blocked) {
|
|
172
|
+
const touch = lastTouch(d.rel)
|
|
173
|
+
lines.push(` ${d.rel}`)
|
|
174
|
+
lines.push(` ${d.why}`)
|
|
175
|
+
if (touch) lines.push(` last changed on the branch by: ${touch}`)
|
|
176
|
+
}
|
|
177
|
+
lines.push('')
|
|
178
|
+
lines.push('Nothing was written and no branch was created. Writing the room\'s copy over these')
|
|
179
|
+
lines.push('would silently revert whatever reached the branch another way — a hotfix, a second')
|
|
180
|
+
lines.push('developer, CI, or your own work done outside the room. That has happened once on this')
|
|
181
|
+
lines.push('project and the resulting PR looked perfect.')
|
|
182
|
+
lines.push('')
|
|
183
|
+
lines.push('Fix it one of two ways:')
|
|
184
|
+
lines.push(' - bring the branch\'s version INTO the room (edit it there, or hive_edit it), so the')
|
|
185
|
+
lines.push(' room\'s copy descends from what is on the branch. Then run this again.')
|
|
186
|
+
lines.push(' - --force "<reason>" if you have decided the room\'s copy should win. The reason is')
|
|
187
|
+
lines.push(' recorded in the PR body, because somebody will ask.')
|
|
188
|
+
return lines.join('\n')
|
|
189
|
+
}
|