@volter/supercode-orchestrator 0.5.205 → 0.5.206
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -4
- package/activation.mjs +93 -2
- package/adapters/rh2.mjs +7 -2
- package/agents.mjs +24 -2
- package/bin/agent-channel.mjs +41 -22
- package/bin/connection-runner.mjs +141 -0
- package/bin/orchestrator.mjs +11 -2
- package/bin/runtime.mjs +16 -9
- package/board/args.mjs +3 -2
- package/board/attachment-read.mjs +77 -0
- package/board/call.mjs +4 -2
- package/board/capacity.mjs +46 -9
- package/board/ci.mjs +157 -53
- package/board/cli.mjs +225 -87
- package/board/client.mjs +18 -2
- package/board/custody.mjs +109 -0
- package/board/dispatch.mjs +297 -75
- package/board/doors.mjs +47 -17
- package/board/engine.mjs +78 -24
- package/board/fleet-listeners.mjs +268 -0
- package/board/fleet-observer.mjs +79 -0
- package/board/fleet-targets.mjs +82 -0
- package/board/incidents.mjs +7 -0
- package/board/listing.mjs +5 -0
- package/board/machine-fleet.mjs +7 -0
- package/board/notice-receipts.mjs +67 -0
- package/board/owner-door-worker.mjs +61 -0
- package/board/owner-door.mjs +46 -0
- package/board/owner-health.mjs +8 -2
- package/board/published.mjs +2 -2
- package/board/record-view.mjs +2 -0
- package/board/store.mjs +55 -3
- package/board/tools.mjs +11 -4
- package/board/workflow.mjs +91 -28
- package/board/ztrack.mjs +87 -21
- package/board-input.d.ts +2 -0
- package/board-input.mjs +16 -0
- package/connection.mjs +153 -0
- package/index.d.ts +7 -5
- package/index.mjs +1 -1
- package/installed-apps.mjs +31 -20
- package/loop.mjs +91 -36
- package/mcp/bridge.mjs +12 -0
- package/mcp/hermes-kanban-tools.json +22 -14
- package/mcp/mount.mjs +10 -9
- package/ontology.d.ts +37 -0
- package/operator.mjs +72 -4
- package/orchestration.mjs +7 -2
- package/package.json +9 -5
- package/reducer.mjs +74 -5
package/board/ci.mjs
CHANGED
|
@@ -14,12 +14,92 @@ import { githubFetch } from './github-rest.mjs';
|
|
|
14
14
|
// review World's GitHub twin answers it, where gh (Go) trusts only the system keychain.
|
|
15
15
|
const githubBase = () => (process.env.GITHUB_API_URL || 'https://api.github.com').replace(/\/$/, '');
|
|
16
16
|
const githubHeaders = (token, extra = {}) => ({ accept: 'application/vnd.github+json', 'user-agent': 'supercode-board', ...(token ? { authorization: `Bearer ${token}` } : {}), ...extra });
|
|
17
|
-
let loginToken = null;
|
|
17
|
+
let loginToken = null, loginRead = null;
|
|
18
18
|
async function githubToken() {
|
|
19
19
|
const fixed = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || null;
|
|
20
|
-
if (!fixed && !loginToken)
|
|
20
|
+
if (!fixed && !loginToken) {
|
|
21
|
+
loginRead ??= run('gh', ['auth', 'token']).then((value) => loginToken = value || null, () => null).finally(() => { loginRead = null; });
|
|
22
|
+
await loginRead;
|
|
23
|
+
}
|
|
21
24
|
return fixed ?? loginToken;
|
|
22
25
|
}
|
|
26
|
+
// The provider's retry boundary is shared by this board owner's reads, including different PRs and pages.
|
|
27
|
+
// The key holds only a credential digest. Refusals retain no PR body and cannot become review evidence.
|
|
28
|
+
const githubReadContexts = new Map();
|
|
29
|
+
function admitGithubContext(key) {
|
|
30
|
+
let context = githubReadContexts.get(key);
|
|
31
|
+
if (!context) {
|
|
32
|
+
// Only idle, expired holders are disposable. Reserve before sending so a
|
|
33
|
+
// refusal always has space, even when other credentials are in flight.
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
for (const [heldKey, held] of githubReadContexts) {
|
|
36
|
+
if (!held.readers && (!held.rateLimit || now >= held.rateLimit.until)) githubReadContexts.delete(heldKey);
|
|
37
|
+
}
|
|
38
|
+
if (githubReadContexts.size >= 8) return null;
|
|
39
|
+
context = { readers: 0, rateLimit: null };
|
|
40
|
+
githubReadContexts.set(key, context);
|
|
41
|
+
}
|
|
42
|
+
context.readers += 1;
|
|
43
|
+
return context;
|
|
44
|
+
}
|
|
45
|
+
function githubRateResult(path, held, deferred = false) {
|
|
46
|
+
const until = new Date(held.until).toISOString();
|
|
47
|
+
return { status: held.status, rateLimit: { retryAt: held.until, deferred },
|
|
48
|
+
error: `GitHub ${held.status} for ${path}: ${deferred ? 'not requested; ' : ''}rate limited until ${until}; ${held.reason}` };
|
|
49
|
+
}
|
|
50
|
+
function holdGithubRateLimit(context, response, text) {
|
|
51
|
+
const retry = response.headers.get('retry-after');
|
|
52
|
+
const remaining = response.headers.get('x-ratelimit-remaining');
|
|
53
|
+
if (response.status !== 429 && !(response.status === 403
|
|
54
|
+
&& (remaining === '0' || retry != null || /rate[ -]?limit/i.test(text)))) return null;
|
|
55
|
+
const now = Date.now(), deadlines = [];
|
|
56
|
+
if (retry != null) {
|
|
57
|
+
const seconds = /^\d+(?:\.\d+)?$/.test(retry.trim()) ? Number(retry) : null;
|
|
58
|
+
const date = seconds == null ? Date.parse(retry) : now + seconds * 1000;
|
|
59
|
+
if (Number.isFinite(date) && date > now && date <= 8.64e15) deadlines.push(date);
|
|
60
|
+
}
|
|
61
|
+
if (remaining === '0') {
|
|
62
|
+
const raw = response.headers.get('x-ratelimit-reset');
|
|
63
|
+
const reset = raw != null && /^\d+$/.test(raw.trim()) ? Number(raw) * 1000 : NaN;
|
|
64
|
+
if (Number.isFinite(reset) && reset > now && reset <= 8.64e15) deadlines.push(reset);
|
|
65
|
+
}
|
|
66
|
+
const previous = context.rateLimit;
|
|
67
|
+
const failures = (previous?.failures ?? 0) + 1;
|
|
68
|
+
// With no useful provider deadline, secondary refusals wait at least a minute, increasing to fifteen minutes.
|
|
69
|
+
// The caller gets an error immediately; there is no sleeping request, timer, or automatic retry loop.
|
|
70
|
+
const until = deadlines.length ? Math.max(...deadlines) : now + Math.min(900_000, 60_000 * 2 ** Math.min(failures - 1, 4));
|
|
71
|
+
// A shorter concurrent refusal cannot shorten the shared provider boundary
|
|
72
|
+
// or replace the diagnostics that explain the longer one.
|
|
73
|
+
const held = previous && previous.until >= until ? { ...previous, failures }
|
|
74
|
+
: { status: response.status, until, failures, reason: text.slice(0, 300) };
|
|
75
|
+
context.rateLimit = held;
|
|
76
|
+
return held;
|
|
77
|
+
}
|
|
78
|
+
const GITHUB_READS = 4, GITHUB_WAITERS = 64;
|
|
79
|
+
let githubActive = 0;
|
|
80
|
+
const githubWaiting = [];
|
|
81
|
+
async function admitGithubRead() {
|
|
82
|
+
if (githubActive < GITHUB_READS) githubActive++;
|
|
83
|
+
else {
|
|
84
|
+
if (githubWaiting.length >= GITHUB_WAITERS) throw Object.assign(new Error('GitHub metadata admission is full; no read was started'), { code: 'github_metadata_capacity', deferred: true });
|
|
85
|
+
await new Promise((resolve) => githubWaiting.push(resolve));
|
|
86
|
+
}
|
|
87
|
+
return () => { const next = githubWaiting.shift(); if (next) next(); else githubActive--; };
|
|
88
|
+
}
|
|
89
|
+
async function mapGithubSources(items, work) {
|
|
90
|
+
const out = new Array(items.length);
|
|
91
|
+
let next = 0, failed = false, failure;
|
|
92
|
+
await Promise.all(Array.from({ length: Math.min(GITHUB_READS, items.length) }, async () => {
|
|
93
|
+
while (!failed && next < items.length) {
|
|
94
|
+
const index = next++;
|
|
95
|
+
try { out[index] = await work(items[index], index); }
|
|
96
|
+
catch (error) { if (!failed) { failed = true; failure = error; } }
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
99
|
+
if (failed) throw failure;
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
const readTogether = mapGithubSources;
|
|
23
103
|
/** One GitHub REST read (`all`: every page of a list). The token is the environment's, else gh's stored login: a failed
|
|
24
104
|
* read of the login is not kept, and a login token GitHub refuses (401, rotated or revoked) is read again once. */
|
|
25
105
|
const actionReads = new AsyncLocalStorage();
|
|
@@ -32,51 +112,64 @@ async function github(path, all = false, { purpose = 'validation' } = {}) {
|
|
|
32
112
|
if (!reads.has(key)) reads.set(key, githubUncached(path, all, { purpose }));
|
|
33
113
|
return reads.get(key);
|
|
34
114
|
}
|
|
35
|
-
// Bound actual HTTP/body I/O across all concurrent actions and discovery in this process.
|
|
36
|
-
let activeReads = 0;
|
|
37
|
-
const waitingReads = [];
|
|
38
115
|
async function wireRead(fn) {
|
|
39
|
-
|
|
40
|
-
try { return await fn(); }
|
|
41
|
-
finally { const next = waitingReads.shift(); if (next) next(); else activeReads--; }
|
|
42
|
-
}
|
|
43
|
-
/** Preserve input order; after a failure drain active reads and issue no more work from this batch. */
|
|
44
|
-
async function readTogether(items, read) {
|
|
45
|
-
const out = new Array(items.length); let cursor = 0, failure;
|
|
46
|
-
await Promise.all(Array.from({ length: Math.min(8, items.length) }, async () => {
|
|
47
|
-
while (!failure && cursor < items.length) {
|
|
48
|
-
const i = cursor++;
|
|
49
|
-
try { out[i] = await read(items[i], i); } catch (error) { failure ??= error; }
|
|
50
|
-
}
|
|
51
|
-
}));
|
|
52
|
-
if (failure) throw failure;
|
|
53
|
-
return out;
|
|
116
|
+
const release = await admitGithubRead();
|
|
117
|
+
try { return await fn(); } finally { release(); }
|
|
54
118
|
}
|
|
55
119
|
async function githubUncached(path, all = false, { purpose = 'validation' } = {}) {
|
|
56
|
-
const read = async () => {
|
|
57
|
-
const token = await githubToken();
|
|
58
|
-
let url = githubBase() + path, out = [];
|
|
59
|
-
for (let page = 0; url && page < 10; page += 1) {
|
|
60
|
-
const pageRead = await wireRead(async () => {
|
|
61
|
-
const r = await githubFetch(url, { headers: githubHeaders(token), signal: AbortSignal.timeout(30_000) }, { purpose });
|
|
62
|
-
if (!r.ok) return { status: r.status, error: `GitHub ${r.status} for ${path}: ${(await r.text()).slice(0, 300)}` };
|
|
63
|
-
return { body: await r.json(), next: /<([^>]+)>;\s*rel="next"/.exec(r.headers.get('link') ?? '')?.[1] ?? null };
|
|
64
|
-
});
|
|
65
|
-
if (pageRead.error) return pageRead;
|
|
66
|
-
const body = pageRead.body;
|
|
67
|
-
if (!all) return { body };
|
|
68
|
-
out = out.concat(body);
|
|
69
|
-
url = pageRead.next;
|
|
70
|
-
}
|
|
71
|
-
return { body: out };
|
|
72
|
-
};
|
|
73
120
|
const started = performance.now();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
121
|
+
const read = async () => {
|
|
122
|
+
const token = await githubToken();
|
|
123
|
+
const base = githubBase();
|
|
124
|
+
const rateKey = `${base}\0${createHash('sha256').update(token ?? '').digest('hex')}`;
|
|
125
|
+
const context = admitGithubContext(rateKey);
|
|
126
|
+
if (!context) return { code: 'github_read_admission_full', error: 'eight GitHub origin/credential contexts are active; not requested' };
|
|
127
|
+
try {
|
|
128
|
+
let url = base + path, out = [];
|
|
129
|
+
for (let page = 0; url && page < 10; page += 1) {
|
|
130
|
+
// No await separates admission from fetch. Every page observes a
|
|
131
|
+
// refusal retained while an earlier page or another read was in flight.
|
|
132
|
+
const held = context.rateLimit;
|
|
133
|
+
if (held && Date.now() < held.until) return githubRateResult(path, held, true);
|
|
134
|
+
const pageRead = await wireRead(async () => {
|
|
135
|
+
const held = context.rateLimit;
|
|
136
|
+
if (held && Date.now() < held.until) return { result: githubRateResult(path, held, true) };
|
|
137
|
+
const r = await githubFetch(url, { headers: githubHeaders(token), signal: AbortSignal.timeout(30_000) }, { purpose });
|
|
138
|
+
if (!r.ok) {
|
|
139
|
+
const text = (await r.text()).slice(0, 300), limited = holdGithubRateLimit(context, r, text);
|
|
140
|
+
return { result: limited ? githubRateResult(path, limited) : { status: r.status, token, error: `GitHub ${r.status} for ${path}: ${text}` } };
|
|
141
|
+
}
|
|
142
|
+
// An older in-flight success is not recovery from a newer refusal.
|
|
143
|
+
if (context.rateLimit && Date.now() >= context.rateLimit.until) context.rateLimit = null;
|
|
144
|
+
return { body: await r.json(), next: /<([^>]+)>;\s*rel="next"/.exec(r.headers.get('link') ?? '')?.[1] ?? null };
|
|
145
|
+
});
|
|
146
|
+
if (pageRead.result) return pageRead.result;
|
|
147
|
+
const body = pageRead.body;
|
|
148
|
+
if (!all) return { body };
|
|
149
|
+
out = out.concat(body);
|
|
150
|
+
url = pageRead.next;
|
|
151
|
+
}
|
|
152
|
+
return { body: out };
|
|
153
|
+
} finally {
|
|
154
|
+
context.readers -= 1;
|
|
155
|
+
if (!context.readers && !context.rateLimit) githubReadContexts.delete(rateKey);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
let result;
|
|
159
|
+
try { result = await read(); } catch (error) { result = { error: String(error?.message ?? error), cause: error }; }
|
|
160
|
+
if (result.status === 401 && !(process.env.GH_TOKEN || process.env.GITHUB_TOKEN) && (loginToken || loginRead)) {
|
|
161
|
+
if (loginToken === result.token) loginToken = null;
|
|
162
|
+
try { result = await read(); } catch (error) { result = { error: String(error?.message ?? error), cause: error }; }
|
|
163
|
+
}
|
|
164
|
+
noteStep('github read', performance.now() - started);
|
|
165
|
+
if (result.error) {
|
|
166
|
+
const nativeCode = result.cause?.cause?.code ?? result.cause?.code;
|
|
167
|
+
throw Object.assign(new Error(`GitHub: ${result.error.trim().slice(-500)}${nativeCode ? ` (${nativeCode})` : ''}`, { cause: result.cause }), result.rateLimit
|
|
168
|
+
? { code: 'github_rate_limited', status: result.status, retry_at: result.rateLimit.retryAt, deferred: true }
|
|
169
|
+
: result.code ? { code: result.code, deferred: result.code === 'github_read_admission_full' }
|
|
170
|
+
: result.cause?.code ? { code: result.cause.code, deferred: result.cause.code === 'GITHUB_BUDGET' } : {});
|
|
171
|
+
}
|
|
172
|
+
return result.body;
|
|
80
173
|
}
|
|
81
174
|
const repositoryOf = (url) => new URL(url).pathname.split('/').slice(1, 3).join('/');
|
|
82
175
|
const numberOf = (url) => Number(new URL(url).pathname.split('/')[4]);
|
|
@@ -141,8 +234,15 @@ const run = async (program, args, cwd) => {
|
|
|
141
234
|
}));
|
|
142
235
|
};
|
|
143
236
|
|
|
237
|
+
/** Deployment receipts were retired. Refuse them before any board or source operation. */
|
|
238
|
+
export function assertSourceReceipt(payload = {}) {
|
|
239
|
+
for (const field of ['ci_run', 'ci-run', 'deployed_sha', 'deployed-sha']) {
|
|
240
|
+
if (Object.hasOwn(payload, field)) throw new Error(`${field} is retired; board completion and review use source/PR facts, not deployment CI receipts.`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
144
244
|
export function completionEvidence(board, id) {
|
|
145
|
-
return observations.get(key(board, id)) ?? { required: false, ready: true, phase: 'source'
|
|
245
|
+
return observations.get(key(board, id)) ?? { required: false, ready: true, phase: 'source' };
|
|
146
246
|
}
|
|
147
247
|
|
|
148
248
|
export function completionPhase(board, id, entry, verb, payload, roles = []) {
|
|
@@ -472,8 +572,9 @@ export async function inspectCompletionEvidence(board, id, entry, payload = {},
|
|
|
472
572
|
return withFreshReviewReads(() => inspectCurrentCompletionEvidence(board, id, entry, payload, stage));
|
|
473
573
|
}
|
|
474
574
|
async function inspectCurrentCompletionEvidence(board, id, entry, payload, stage) {
|
|
475
|
-
const state = { required: false, ready: true, reason: null, phase: 'source'
|
|
575
|
+
const state = { required: false, ready: true, reason: null, phase: 'source' };
|
|
476
576
|
try {
|
|
577
|
+
assertSourceReceipt(payload);
|
|
477
578
|
if (stage === 'completion' || stage === 'acceptance') {
|
|
478
579
|
const supplied = payload.commit ?? payload.metadata?.commit;
|
|
479
580
|
if (supplied) {
|
|
@@ -507,7 +608,7 @@ async function inspectCurrentCompletionEvidence(board, id, entry, payload, stage
|
|
|
507
608
|
// Merged source is immutable and needs no surviving workspace. Resolve an
|
|
508
609
|
// unmerged candidate's checkout as before; a broken legacy path never
|
|
509
610
|
// prevents a verdict on already merged commits.
|
|
510
|
-
const read = await
|
|
611
|
+
const read = await mapGithubSources(urls, readPullRequest);
|
|
511
612
|
const mergedOnly = urls.length && read.every(pr => pr.state === 'MERGED');
|
|
512
613
|
const own = mergedOnly ? null : await repositoryWorkspace(board, id);
|
|
513
614
|
if (!own && !urls.length) {
|
|
@@ -522,7 +623,7 @@ async function inspectCurrentCompletionEvidence(board, id, entry, payload, stage
|
|
|
522
623
|
if (stray) throw new Error(`An unmerged PR candidate needs a repository workspace: ${stray.url} (${stray.headRefName}) is on no branch the card or its tasks declare; register it with create "<what>" --subtask-of ${id} --workspace worktree:${repositoryOf(stray.url)} --branch ${stray.headRefName}`);
|
|
523
624
|
}
|
|
524
625
|
// A verdict with no requested PR (a scratch arc) keeps its own contract. An arc's review
|
|
525
|
-
// always names every opened PR it changed, independent of
|
|
626
|
+
// always names every opened PR it changed, independent of deployment.
|
|
526
627
|
if (stage === 'review_verdict' && !asked && !urls.length) return state;
|
|
527
628
|
Object.assign(state, { required: true, ready: false });
|
|
528
629
|
if (!urls.length) throw new Error('Name each opened PR with --pr.');
|
|
@@ -531,7 +632,7 @@ async function inspectCurrentCompletionEvidence(board, id, entry, payload, stage
|
|
|
531
632
|
: payload.pr_head ?? payload.commit ?? payload.metadata?.pr_head ?? payload.metadata?.commit ?? []).filter(Boolean);
|
|
532
633
|
if (stage === 'request_review' && named.length !== urls.length) throw new Error('Name each PR head with --pr-head, one for each --pr, in the same order.');
|
|
533
634
|
if (stage === 'review_verdict' && !named.length) throw new Error('Name every reviewed PR head with --reviewed-sha.');
|
|
534
|
-
const prs = await
|
|
635
|
+
const prs = await mapGithubSources(urls, (url, index) => verifyPullRequest(board, id, entry, own, url, stage === 'request_review' ? named[index] : null, stage, read[index]));
|
|
535
636
|
if (stage === 'request_review') {
|
|
536
637
|
// A review is asked for only while some PR has a head no verdict has judged.
|
|
537
638
|
if (!await reviewDue(board, id, prs.map((p) => ({ url: p.pr.url, head: p.commit })))) throw new Error('The PRs the last verdict judged have no new head; push a new head before requesting review.');
|
|
@@ -544,19 +645,22 @@ async function inspectCurrentCompletionEvidence(board, id, entry, payload, stage
|
|
|
544
645
|
if (bound.length && (bound.length !== requested.length || bound.some((b) => !requested.some((r) => r.pr.url === b.pr.url && r.commit === b.commit)))) throw new Error('This reviewer run was launched for different PR heads.');
|
|
545
646
|
}
|
|
546
647
|
Object.assign(state, { ...prs[0], ready: true, reason: null, ...(prs.length > 1 ? { prs } : {}) });
|
|
547
|
-
} catch (error) {
|
|
548
|
-
|
|
648
|
+
} catch (error) {
|
|
649
|
+
Object.assign(state, { required: true, ready: false, reason: String(error?.message ?? error) });
|
|
650
|
+
if (error?.code) Object.assign(state, { code: error.code, retryable: error.code === 'GITHUB_BUDGET' });
|
|
651
|
+
if (['GITHUB_BUDGET', 'github_rate_limited', 'github_metadata_capacity', 'github_read_admission_full'].includes(error?.code))
|
|
652
|
+
Object.assign(state, { deferred: true, deferred_code: error.code, retry_at: error.retry_at ?? null });
|
|
653
|
+
}
|
|
549
654
|
return state;
|
|
550
655
|
}
|
|
551
656
|
|
|
552
657
|
/** One PR of an arc review, read from GitHub and, for a review request, from the card branch's checkout. */
|
|
553
|
-
async function verifyPullRequest(board, id, entry, own, url, supplied, stage) {
|
|
658
|
+
async function verifyPullRequest(board, id, entry, own, url, supplied, stage, observed) {
|
|
554
659
|
if (typeof url !== 'string' || !/^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/[1-9][0-9]*\/?$/.test(url)) throw new Error('Name the opened PR URL with --pr.');
|
|
555
660
|
const repository = new URL(url).pathname.split('/').slice(1, 3).join('/');
|
|
556
661
|
// An arc may change several repositories. A PR is read from GitHub; its pushed head is the reviewed one. The
|
|
557
662
|
// card's own checkout, and this machine's checkout of the PR's branch when it keeps one, must also hold that head
|
|
558
663
|
// with nothing uncommitted. A card on another machine is reviewed at its pushed head: its checkout is not read from here.
|
|
559
|
-
const observed = await readPullRequest(url);
|
|
560
664
|
const path = observed.state === 'MERGED' || (own && !own.here) ? null : !own ? await branchCheckout(board, id, repository, observed.headRefName) : await originOf(own.path) === repository && await run('git', ['branch', '--show-current'], own.path) === observed.headRefName ? own.path : await branchCheckout(board, id, repository, observed.headRefName);
|
|
561
665
|
// A PR merged before its review (an outage fix landed first) is still reviewed: its head is fixed for good, so the
|
|
562
666
|
// verdict is recorded on that head, and nothing about a worktree or its base can move under it any more.
|
|
@@ -567,7 +671,7 @@ async function verifyPullRequest(board, id, entry, own, url, supplied, stage) {
|
|
|
567
671
|
if (!/^[a-f0-9]{40}$/.test(head)) throw new Error('Name the full PR head commit with --pr-head (reviewer: --reviewed-sha).');
|
|
568
672
|
if (observed.headRefOid !== head) throw new Error(`The head of ${url} changed to ${observed.headRefOid}; request review of that head before recording a verdict.`);
|
|
569
673
|
const branch = observed.headRefName;
|
|
570
|
-
const mainBranch =
|
|
674
|
+
const mainBranch = observed.defaultBranch ?? 'main';
|
|
571
675
|
// A merged hotfix may have landed on another base (prod); a PR from the main branch itself is a release, never arc work.
|
|
572
676
|
if (branch === mainBranch || (!merged && observed.baseRefName !== mainBranch)) throw new Error(`${url} must propose its worktree branch to the repository main branch.`);
|
|
573
677
|
if (stage === 'request_review' && path && !merged) {
|