Merge origin/main (v1.91.278.0) into eval-select-review-floor-followups; stamp v1.91.279.0

FILES: CHANGELOG.md VERSION agents-digest/gstack-AGENTS.md package.json
MERGE_CONDITION: sha=64684de144e5c5b8934408514b44434559487b18
NEW_VERSION=1.91.279.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-27 17:02:49 +00:00
commit c7f6daa7a4
30 files changed
+654 -85

No files matched your search

+22 -4
View File
@@ -5,7 +5,8 @@
* head's paid slices start only after (1) the PR has been quiet for the
* window (no newer push since this run was created; `vars.EVALS_QUIET_MINUTES`,
* default 10; the `evals-no-debounce` label sets it to 0) and (2) Free Tests
* (free-tests.yml) has concluded success on the same head SHA. Selection,
* (free-tests.yml) has concluded success on the same head SHA, as GitHub's
* pull_request run or as a workflow_dispatch run on the branch. Selection,
* trials and verdict rules are unchanged: only the start time moves.
*
* A newer push cancels the run through the workflow's cancel-in-progress
@@ -40,9 +41,25 @@ export type ReadinessDecision =
| { kind: 'superseded'; by: string }
| { kind: 'blocked'; reason: string };
/** The newest Free Tests pull_request run on this exact head decides; a rerun updates that run in place. */
/**
* The Free Tests run events that count as this head's CI, in order of
* precedence: GitHub's own pull_request run (it builds the merge ref, the tree
* a merge would test), then a workflow_dispatch run on the branch (the
* dispatch route in docs/ship-policy.md for a head whose only conflict with
* the base is the CHANGELOG, which GitHub builds no merge ref for and so never
* runs pull_request CI on; #3508). A dispatched run builds the head alone, so
* it decides only for a head with no pull_request run: it never overrides a red
* pull_request run (rerun that run), and a lane's extra dispatch never holds a
* head its pull_request CI cleared. A push run is a branch build, not this
* head's gate.
*/
export const FREE_TESTS_EVENTS: readonly string[] = ['pull_request', 'workflow_dispatch'];
/** The newest Free Tests run on this exact head under the first event present decides; a rerun updates that run in place. */
export function freeTestsState(runs: FreeTestsRun[], headSha: string): FreeTestsState {
const newest = runs.filter(run => run.head_sha === headSha && run.event === 'pull_request').sort((a, b) => b.id - a.id)[0];
const atHead = runs.filter(run => run.head_sha === headSha);
const event = FREE_TESTS_EVENTS.find(e => atHead.some(run => run.event === e));
const newest = atHead.filter(run => run.event === event).sort((a, b) => b.id - a.id)[0];
if (!newest) return { state: 'missing' };
if (newest.status !== 'completed') return { state: 'running', runId: newest.id };
if (newest.conclusion === 'success') return { state: 'success', runId: newest.id };
@@ -111,8 +128,9 @@ export function waitForReadiness(o: WaitOptions): { decision: ReadinessDecision;
if (quietUntil > now()) sleep(quietUntil - now());
let lastError = '';
for (;;) {
// Every event at the head; freeTestsState keeps the pull_request and workflow_dispatch runs.
const listed = ghJson<{ workflow_runs?: FreeTestsRun[] }>(gh, ['api', '-X', 'GET', `repos/${o.repo}/actions/workflows/free-tests.yml/runs`,
'-f', `head_sha=${o.headSha}`, '-f', 'event=pull_request', '-f', 'per_page=20']);
'-f', `head_sha=${o.headSha}`, '-f', 'per_page=20']);
const pr = ghJson<{ head?: { sha?: string } }>(gh, ['api', `repos/${o.repo}/pulls/${o.pr}`]);
lastError = !listed.ok ? listed.error : !pr.ok ? pr.error : '';
if (!lastError && listed.ok && pr.ok) {
+23 -5
View File
@@ -26,7 +26,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import { normalizeChangelog } from '../lib/changelog-check';
import { normalizeLockfile } from '../lib/evidence-bundle';
import { rewriteStamps, versionAtRef } from '../lib/restamp';
import { stampMentions, rewriteStamps, versionAtRef } from '../lib/restamp';
import { loadPolicyFromHead, matchesAny, type ShipPolicy } from '../lib/ship-policy';
import type { BisectPrefixRecord, TrainGateRecord } from '../lib/ship-queue-train-verdict';
import type { CasePanel } from '../lib/ship-queue-train-record';
@@ -132,20 +132,38 @@ export function fullGateCases(rootDir = ROOT): Map<string, string> {
return plannedCases(buildRunManifest({ tier: 'gate', profile: 'full', sliceCount: 1, evalsAll: true, env: { EVALS_ALL: '1' }, rootDir }), rootDir);
}
/**
* A release output or stamp path without its stamps: every version mentioned
* in a stamp form (the built-in forms and the policy's `stamp_patterns`
* templates) collapses to one placeholder. Normalising only the commit's own
* version left a tentative `*_KEY_VERSION = '<next minor>'` as written on the
* unstamped prefix and normalised once the slot stamp moved it to the released
* version, so the two digests differed and train-backstop found no covering
* record for main after that train (#3512), and a base-version mention on a
* line the base already carried was normalised on the prefix and kept on the
* stamped main. Every stamp-form mention collapsing makes the digest blind to
* which version a stamp names; the receipt (`stampOnlyLine`) is what judges a
* stamp commit's content.
*/
export function stampFreeText(text: string, templates: readonly string[]): string {
let out = text;
for (const v of [...stampMentions(text, templates)].sort((a, b) => b.length - a.length)) out = rewriteStamps(out, v, '\0VERSION', templates).text;
return out;
}
/**
* A commit's tree modulo stamps: every path with its mode and blob, except the
* files a restamp rewrites, which contribute their content without the stamp
* (the version file nothing, package.json without its version, lockfiles
* without their root version, the CHANGELOG with its headings normalized,
* release outputs and stamp paths with their own version replaced). Two
* commits that differ only by a restamp have the same digest.
* release outputs and stamp paths with every stamp-form version mention
* replaced). Two commits that differ only by a restamp have the same digest.
*/
export function stampFreeTree(git: Git, commit: string, repoRoot: string, policy: ShipPolicy): string | { error: string } {
const listed = git(['ls-tree', '-r', '-z', '--full-tree', commit]);
if (!listed.ok) return { error: `cannot list ${commit.slice(0, 12)} (${listed.out.split('\n')[0]})` };
const at = versionAtRef(repoRoot, commit);
const versionRel = 'rel' in at ? at.rel : 'VERSION';
const version = 'version' in at ? at.version : null;
const lines: string[] = [];
for (const item of listed.out.split('\0').filter(Boolean)) {
const [meta, rel] = [item.slice(0, item.indexOf('\t')), item.slice(item.indexOf('\t') + 1)];
@@ -161,7 +179,7 @@ export function stampFreeTree(git: Git, commit: string, repoRoot: string, policy
if (stamp === 'manifest') { try { const parsed = JSON.parse(text); delete parsed.version; text = JSON.stringify(parsed); } catch { /* not JSON: kept verbatim */ } }
else if (stamp === 'lockfile') text = normalizeLockfile(base, text);
else if (stamp === 'changelog') text = normalizeChangelog(text);
else if (version) text = rewriteStamps(text, version, '\0VERSION', policy.stamp_patterns).text;
else text = stampFreeText(text, policy.stamp_patterns);
lines.push(`${rel}\0${mode}\0${createHash('sha256').update(text).digest('hex')}`);
}
return createHash('sha256').update(lines.sort().join('\n')).digest('hex');