@volter/supercode-orchestrator 0.5.57 → 0.5.59
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/board/ci.mjs +18 -11
- package/board/dispatch.mjs +11 -7
- package/package.json +1 -1
package/board/ci.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { readFileSync, realpathSync, writeFileSync, renameSync, statSync } from 'node:fs';
|
|
5
5
|
import { dirname, join, relative } from 'node:path';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
|
-
import { branchCheckouts, checkoutOf, resolveWorkspace } from './workspace.mjs';
|
|
7
|
+
import { branchCheckouts, checkoutOf, isLocalWorkspace, resolveWorkspace } from './workspace.mjs';
|
|
8
8
|
|
|
9
9
|
// GitHub is read through its REST API with Node's own fetch, not the gh binary: Node takes a World's trust bundle and
|
|
10
10
|
// injector (NODE_EXTRA_CA_CERTS, NODE_OPTIONS), so a review World's GitHub twin answers it, where gh (Go) trusts only
|
|
@@ -128,17 +128,23 @@ export const candidatePullRequests = (ci) => ci?.prs ?? (ci?.pr ? [{ repository:
|
|
|
128
128
|
// The repository a checkout declares (remote.origin.url), not where a URL rewrite sends its transport.
|
|
129
129
|
const originOf = (path) => /(?:github\.com[:/])([^/]+\/[^/]+?)(?:\.git)?$/.exec(run('git', ['config', '--get', 'remote.origin.url'], path))?.[1] ?? null;
|
|
130
130
|
|
|
131
|
+
/** The card's checkout as its machine answers for it (path, repository, branch), and whether it is on this machine:
|
|
132
|
+
* git runs only in a checkout here, and another machine's is known by what its door said. */
|
|
131
133
|
function repositoryWorkspace(board, id) {
|
|
132
134
|
const task = board.db.prepare('SELECT t.*,c.subtask_of,c.workspace_anchor,c.machine FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(id);
|
|
133
135
|
const parent = task?.subtask_of && board.db.prepare('SELECT t.*,c.workspace_anchor,c.machine FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(task.subtask_of);
|
|
134
136
|
const workspace = task?.workspace_kind === 'scratch' ? parent : task;
|
|
135
|
-
|
|
137
|
+
if (!workspace || workspace.workspace_kind === 'scratch') return null;
|
|
138
|
+
const here = isLocalWorkspace(workspace);
|
|
139
|
+
if (workspace.workspace_anchor) return { ...resolveWorkspace(workspace, false), here };
|
|
140
|
+
return workspace.workspace_path ? { path: workspace.workspace_path, here } : null;
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
const pullRequestLists = new Map();
|
|
139
144
|
|
|
140
145
|
function branchCheckout(board, id, repository, branch) {
|
|
141
146
|
const task = board.db.prepare('SELECT c.machine FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(id);
|
|
147
|
+
if (!isLocalWorkspace(task ?? {})) return null;
|
|
142
148
|
try { return checkoutOf(task?.machine, repository, branch)?.path ?? null; } catch { return null; }
|
|
143
149
|
}
|
|
144
150
|
|
|
@@ -179,9 +185,9 @@ export function openReviewPullRequests(board, id) {
|
|
|
179
185
|
const own = repositoryWorkspace(board, id);
|
|
180
186
|
if (!own) return null;
|
|
181
187
|
const task = board.db.prepare('SELECT branch_name FROM tasks WHERE id=?').get(id);
|
|
182
|
-
const ownRepository = originOf(own);
|
|
188
|
+
const ownRepository = own.repository ?? (own.here ? originOf(own.path) : null);
|
|
183
189
|
if (!ownRepository) return null;
|
|
184
|
-
const current = task?.branch_name ? null : run('git', ['branch', '--show-current'], own);
|
|
190
|
+
const current = task?.branch_name ? null : own.branch ?? (own.here ? run('git', ['branch', '--show-current'], own.path) : null);
|
|
185
191
|
let branch = task?.branch_name ?? (current || null);
|
|
186
192
|
const registered = registeredBranches(board, id);
|
|
187
193
|
const mine = (repository, head) => (repository === ownRepository && head === branch) || namedWorkBranch(id, head)
|
|
@@ -200,13 +206,13 @@ export function openReviewPullRequests(board, id) {
|
|
|
200
206
|
}
|
|
201
207
|
// The repository's default branch is not a work branch, declared or checked out (a card working in a `dir` on main).
|
|
202
208
|
if (repository === ownRepository && branch) {
|
|
203
|
-
const defaultBranch = list.items.find((pr) => pr.defaultBranch)?.defaultBranch ?? (() => { try { return run('git', ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], own).replace(/^origin\//, ''); } catch { return null; } })();
|
|
209
|
+
const defaultBranch = list.items.find((pr) => pr.defaultBranch)?.defaultBranch ?? (() => { try { return run('git', ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], own.path).replace(/^origin\//, ''); } catch { return null; } })();
|
|
204
210
|
if (branch === defaultBranch) branch = null;
|
|
205
211
|
}
|
|
206
212
|
// a draft is not handed over yet: its review starts when it is marked ready; a PR to another base (main→prod) is a release, not the arc's work
|
|
207
213
|
for (const pr of list.items) if (!pr.isCrossRepository && !pr.isDraft && pr.baseRefName === pr.defaultBranch && mine(repository, pr.headRefName)) prs.push({ url: pr.url, head: pr.headRefOid });
|
|
208
214
|
}
|
|
209
|
-
if (!prs.length) return branch ? null : { waits: `the card has no work branch (${task?.branch_name ? `it declares ${task.branch_name}, its repository's default` : current ? `its checkout ${own} is on ${current}, its repository's default` : `its checkout ${own} is on no branch`}): give it one with --branch, or open its PR from a wt/${id} branch` };
|
|
215
|
+
if (!prs.length) return branch ? null : { waits: `the card has no work branch (${task?.branch_name ? `it declares ${task.branch_name}, its repository's default` : current ? `its checkout ${own.path} is on ${current}, its repository's default` : `its checkout ${own.path} is on no branch`}): give it one with --branch, or open its PR from a wt/${id} branch` };
|
|
210
216
|
return { pr: prs.map((p) => p.url), pr_head: prs.map((p) => p.head), branch };
|
|
211
217
|
}
|
|
212
218
|
|
|
@@ -214,8 +220,9 @@ export function openReviewPullRequests(board, id) {
|
|
|
214
220
|
* beyond its repository's default branch, locally or on origin, its checkout holds no uncommitted work, no PR was
|
|
215
221
|
* ever opened from it, and it registered no other branch. Answers what its review reads instead of a diff, or null when the arc carries (or may carry) code. */
|
|
216
222
|
export function noCodeChange(board, id) {
|
|
217
|
-
const
|
|
218
|
-
if (!
|
|
223
|
+
const checkout = repositoryWorkspace(board, id);
|
|
224
|
+
if (!checkout?.here) return null;
|
|
225
|
+
const own = checkout.path;
|
|
219
226
|
try {
|
|
220
227
|
const repository = originOf(own);
|
|
221
228
|
const task = board.db.prepare('SELECT branch_name FROM tasks WHERE id=?').get(id);
|
|
@@ -291,7 +298,7 @@ export function inspectCompletionEvidence(board, id, entry, payload = {}, stage
|
|
|
291
298
|
if (stage === 'completion' || stage === 'acceptance') {
|
|
292
299
|
const supplied = payload.commit ?? payload.metadata?.commit;
|
|
293
300
|
if (supplied) {
|
|
294
|
-
const path = repositoryWorkspace(board, id);
|
|
301
|
+
const path = repositoryWorkspace(board, id)?.path;
|
|
295
302
|
if (!path) throw new Error('The named commit needs a resolved repository workspace.');
|
|
296
303
|
Object.assign(state, { repository: originOf(path), commit: run('git', ['rev-parse', '--verify', `${supplied}^{commit}`], path) });
|
|
297
304
|
}
|
|
@@ -351,9 +358,9 @@ function verifyPullRequest(board, id, entry, own, url, supplied, stage) {
|
|
|
351
358
|
const repository = new URL(url).pathname.split('/').slice(1, 3).join('/');
|
|
352
359
|
// An arc may change several repositories. A PR is read from GitHub; its pushed head is the reviewed one. The
|
|
353
360
|
// card's own checkout, and this machine's checkout of the PR's branch when it keeps one, must also hold that head
|
|
354
|
-
// with nothing uncommitted.
|
|
361
|
+
// with nothing uncommitted. A card on another machine is reviewed at its pushed head: its checkout is not read from here.
|
|
355
362
|
const observed = readPullRequest(url);
|
|
356
|
-
const path = originOf(own) === repository && run('git', ['branch', '--show-current'], own) === observed.headRefName ? own : branchCheckout(board, id, repository, observed.headRefName);
|
|
363
|
+
const path = !own.here ? null : originOf(own.path) === repository && run('git', ['branch', '--show-current'], own.path) === observed.headRefName ? own.path : branchCheckout(board, id, repository, observed.headRefName);
|
|
357
364
|
// A PR merged before its review (an outage fix landed first) is still reviewed: its head is fixed for good, so the
|
|
358
365
|
// verdict is recorded on that head, and nothing about a worktree or its base can move under it any more.
|
|
359
366
|
const merged = observed.state === 'MERGED';
|
package/board/dispatch.mjs
CHANGED
|
@@ -640,7 +640,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
|
|
|
640
640
|
const candidates = [];
|
|
641
641
|
const waiting = (task, why) => {
|
|
642
642
|
report.queued.push([task.id, why]);
|
|
643
|
-
if (!dry) tx(board, db => noteGuard(db, task.id, why));
|
|
643
|
+
if (!dry && !guardNoted(board.db, task.id, why)) tx(board, db => noteGuard(db, task.id, why));
|
|
644
644
|
};
|
|
645
645
|
for (const task of everyCard) {
|
|
646
646
|
if (only && task.id !== only) continue;
|
|
@@ -673,7 +673,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
|
|
|
673
673
|
status: c.plan.target, stored: c.plan.target, model: c.task.model_override ?? c.plan.lane?.model ?? held.defaults?.[c.plan.lane?.harness] ?? null, harness: c.plan.lane?.harness ?? null });
|
|
674
674
|
const waitingEntries = candidates.map(entryOf);
|
|
675
675
|
// a dry run (dispatch --dry-run) plans the same way and writes nothing: no notes, no moves, no launches
|
|
676
|
-
const note = (id, why) => { if (!dry) tx(board, (db) => noteGuard(db, id, why)); };
|
|
676
|
+
const note = (id, why) => { if (!dry && !guardNoted(board.db, id, why)) tx(board, (db) => noteGuard(db, id, why)); };
|
|
677
677
|
for (const [i, c] of candidates.entries()) {
|
|
678
678
|
const { task, plan, scope } = c;
|
|
679
679
|
if (!plan.profile) { report.unassigned.push(task.id); note(task.id, 'no profile runs it (no assignee, and the workflow names none)'); continue; }
|
|
@@ -714,7 +714,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
|
|
|
714
714
|
} catch (error) {
|
|
715
715
|
const reason = error.refusal ?? String(error.message).slice(-300);
|
|
716
716
|
// another round's start is that round's to say; this one writes nothing for it
|
|
717
|
-
if (!error.claimedElsewhere) tx(board, (db) => noteGuard(db, task.id, reason));
|
|
717
|
+
if (!error.claimedElsewhere && !guardNoted(board.db, task.id, reason)) tx(board, (db) => noteGuard(db, task.id, reason));
|
|
718
718
|
report.queued.push([task.id, reason]);
|
|
719
719
|
continue;
|
|
720
720
|
}
|
|
@@ -1247,12 +1247,16 @@ function memoryPressure() {
|
|
|
1247
1247
|
|
|
1248
1248
|
/** Record a guard reason once per change, not once per tick. */
|
|
1249
1249
|
function noteGuard(db, id, reason) {
|
|
1250
|
-
|
|
1251
|
-
|
|
1250
|
+
if (guardNoted(db, id, reason)) return;
|
|
1251
|
+
event(db, id, 'respawn_guarded', { reason });
|
|
1252
|
+
}
|
|
1253
|
+
/** Whether `reason` is already noted: once per reason per entry into the status (a card back in ready, a new `since`, is
|
|
1254
|
+
* noted again, so the workflow's after-timers that read the reason, card.queue_reason, see it). Read before a write
|
|
1255
|
+
* is opened too: a card that waits for the same reason round after round costs no transaction. */
|
|
1256
|
+
function guardNoted(db, id, reason) {
|
|
1252
1257
|
const since = Number(db.prepare('SELECT since FROM supercode_cards WHERE task_id = ?').get(id)?.since ?? 0);
|
|
1253
1258
|
const last = db.prepare("SELECT payload FROM task_events WHERE task_id = ? AND kind IN ('respawn_guarded', 'claimed') AND created_at >= ? ORDER BY id DESC LIMIT 1").get(id, since);
|
|
1254
|
-
|
|
1255
|
-
event(db, id, 'respawn_guarded', { reason });
|
|
1259
|
+
return json(last?.payload, {})?.reason === reason;
|
|
1256
1260
|
}
|
|
1257
1261
|
|
|
1258
1262
|
/** Hermes's emergency stop: the `ESTOP` sentinel in the home or in `~/.hermes` stops new starts (running ones go on). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.59",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
|
|
6
6
|
"exports": {
|