@volter/supercode-orchestrator 0.5.205 → 0.5.207

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.
Files changed (52) hide show
  1. package/README.md +138 -4
  2. package/activation.mjs +93 -2
  3. package/adapters/rh2.mjs +7 -2
  4. package/agents.mjs +24 -2
  5. package/bin/agent-channel.mjs +41 -22
  6. package/bin/connection-runner.mjs +141 -0
  7. package/bin/orchestrator.mjs +11 -2
  8. package/bin/runtime.mjs +16 -9
  9. package/board/args.mjs +3 -2
  10. package/board/attachment-read.mjs +77 -0
  11. package/board/call.mjs +4 -2
  12. package/board/capacity.mjs +46 -9
  13. package/board/ci.mjs +157 -53
  14. package/board/cli.mjs +278 -97
  15. package/board/client.mjs +18 -2
  16. package/board/custody.mjs +109 -0
  17. package/board/dispatch.mjs +305 -80
  18. package/board/doors.mjs +47 -17
  19. package/board/engine.mjs +92 -27
  20. package/board/files.mjs +17 -3
  21. package/board/fleet-listeners.mjs +268 -0
  22. package/board/fleet-observer.mjs +79 -0
  23. package/board/fleet-targets.mjs +82 -0
  24. package/board/incidents.mjs +7 -0
  25. package/board/listing.mjs +8 -2
  26. package/board/machine-fleet.mjs +7 -0
  27. package/board/notice-receipts.mjs +67 -0
  28. package/board/notices.mjs +5 -0
  29. package/board/owner-door-worker.mjs +61 -0
  30. package/board/owner-door.mjs +46 -0
  31. package/board/owner-health.mjs +8 -2
  32. package/board/published.mjs +2 -2
  33. package/board/record-view.mjs +2 -0
  34. package/board/store.mjs +84 -3
  35. package/board/tools.mjs +11 -4
  36. package/board/workflow.mjs +96 -31
  37. package/board/ztrack.mjs +207 -32
  38. package/board-input.d.ts +2 -0
  39. package/board-input.mjs +16 -0
  40. package/connection.mjs +153 -0
  41. package/index.d.ts +7 -5
  42. package/index.mjs +1 -1
  43. package/installed-apps.mjs +31 -20
  44. package/loop.mjs +91 -36
  45. package/mcp/bridge.mjs +12 -0
  46. package/mcp/hermes-kanban-tools.json +22 -14
  47. package/mcp/mount.mjs +10 -9
  48. package/ontology.d.ts +37 -0
  49. package/operator.mjs +72 -4
  50. package/orchestration.mjs +7 -2
  51. package/package.json +9 -5
  52. 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) try { loginToken = (await run('gh', ['auth', 'token'])) || null; } catch { loginToken = null; }
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
- if (activeReads >= 8) await new Promise(resolve => waitingReads.push(resolve)); else activeReads++;
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
- let result;
75
- try { result = await read(); } catch (error) { result = { error: String(error.message), cause: error }; }
76
- if (result.status === 401 && !(process.env.GH_TOKEN || process.env.GITHUB_TOKEN) && loginToken) { loginToken = null; try { result = await read(); } catch (error) { result = { error: String(error.message), cause: error }; } }
77
- noteStep('github read', performance.now() - started);
78
- if (result.error) throw Object.assign(new Error(`GitHub: ${result.error.trim().slice(-500)}`, { cause: result.cause }), result.cause?.code ? { code: result.cause.code } : {});
79
- return result.body;
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', deployed_sha: null };
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', deployed_sha: null };
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 readTogether(urls, readPullRequest);
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 completion_ci.
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 readTogether(urls, (url, index) => verifyPullRequest(board, id, entry, own, url, stage === 'request_review' ? named[index] : null, stage));
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) { Object.assign(state, { required: true, ready: false, reason: error.message,
548
- ...(error.code ? { code: error.code, retryable: error.code === 'GITHUB_BUDGET' } : {}) }); }
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 = entry.params.completion_ci?.main_branches?.[repository] ?? 'main';
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) {