Repository: Agoric/agoric-sdk
Pull request #11390: miscellaneous fixes
Author: login: turadg
Labels: name: automerge:rebase
description: Automatically rebase updates, then merge
Created: 2025-05-20T22:37:37Z | Merged: 2025-05-21T18:31:20Z

refs: #451

## Description
A grab-bag of fixes and touch-ups. Largely from https://github.com/Agoric/agoric-sdk/pull/9945 but also some oldies I found in my local ta/misc branch.

Review by commit.

36c89589c55f2445b6e0d7404a39d49fd5cb493c looks like a real bug, maybe useful to @gibson042 

### Security Considerations
none

### Scaling Considerations
none

### Documentation Considerations
none

### Testing Considerations
CI

### Upgrade Considerations
nothing for chain


Diff:
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index f68598d6bc9..2fe63091b2a 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -52,7 +52,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        cli: [link-cli/yarn, registry/yarn, registry/npm, registry/npx]
+        cli: [link-cli/yarn, registry/npm, registry/npx]
     timeout-minutes: 40
     steps:
       - uses: actions/checkout@v4
diff --git a/.github/workflows/test-all-packages.yml b/.github/workflows/test-all-packages.yml
index 597e9f8b5af..5c061e450fd 100644
--- a/.github/workflows/test-all-packages.yml
+++ b/.github/workflows/test-all-packages.yml
@@ -83,7 +83,7 @@ jobs:
   ##################
   # Lint tests
   lint-primary:
-    timeout-minutes: 20
+    timeout-minutes: 25 # Since Node 22, it's sometimes longer than 20min
     needs: build
     runs-on: ubuntu-latest
     steps:
diff --git a/a3p-integration/proposals/z:acceptance/test.sh b/a3p-integration/proposals/z:acceptance/test.sh
index a44df79b583..933b179ee05 100755
--- a/a3p-integration/proposals/z:acceptance/test.sh
+++ b/a3p-integration/proposals/z:acceptance/test.sh
@@ -21,9 +21,6 @@ scripts/test-vaults.ts
 echo ACCEPTANCE TESTING recorded instances
 yarn ava recorded-retired.test.js
 
-echo ACCEPTANCE TESTING kread
-yarn ava kread.test.js
-
 echo ACCEPTANCE TESTING valueVow
 yarn ava valueVow.test.js
 
diff --git a/a3p-integration/proposals/z:acceptance/test/kread.test.js b/a3p-integration/proposals/z:acceptance/test/kread.test.js
deleted file mode 100644
index 85f22334414..00000000000
--- a/a3p-integration/proposals/z:acceptance/test/kread.test.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import test from 'ava';
-import {
-  provisionSmartWallet,
-  USER1ADDR as Alice,
-  GOV1ADDR as Bob,
-} from '@agoric/synthetic-chain';
-import {
-  buyCharacter,
-  buyItem,
-  getCharacterInventory,
-  getMarketCharactersChildren,
-  getBalanceFromPurse,
-  getMarketItem,
-  getMarketItemsChildren,
-  mintCharacter,
-  sellCharacter,
-  sellItem,
-  unequipAllItems,
-} from './test-lib/kread.js';
-
-test.serial('Alice mints a new Character', async t => {
-  await provisionSmartWallet(Alice, '200000000ubld');
-  const characterBalanceBefore = await getBalanceFromPurse(Alice, 'character');
-  t.is(characterBalanceBefore, null, 'A Character should not exist in purse');
-
-  await mintCharacter(Alice);
-
-  const characterBalanceAfter = await getBalanceFromPurse(Alice, 'character');
-  t.is(
-    characterBalanceAfter.name,
-    'ephemeral_Ace',
-    'Minted Character name should be ephemeral_Ace',
-  );
-});
-
-test.serial('Alice unequips all defaults Items', async t => {
-  const itemBalanceBefore = await getBalanceFromPurse(Alice, 'item');
-  t.is(itemBalanceBefore, null, 'An Item should not exist in purse');
-
-  const characterId = 'ephemeral_Ace';
-  const characterInventoryBefore = await getCharacterInventory(characterId);
-  t.is(
-    characterInventoryBefore.length,
-    3,
-    'Character should have 3 items in inventory',
-  );
-
-  await unequipAllItems(Alice);
-
-  const characterInventoryAfter = await getCharacterInventory(characterId);
-  t.is(
-    characterInventoryAfter.length,
-    0,
-    'Character should have 0 items in inventory',
-  );
-
-  const itemBalanceAfter = await getBalanceFromPurse(Alice, 'item');
-  t.is(
-    itemBalanceAfter.description,
-    characterInventoryBefore[0][0].description,
-    'The unequipped Item should exist in purse',
-  );
-});
-
-test.serial('Alice sells one unequipped Item', async t => {
-  const itemListBefore = await getMarketItemsChildren();
-  const itemBefore = await getBalanceFromPurse(Alice, 'item');
-
-  await sellItem(Alice);
-
-  const itemListAfter = await getMarketItemsChildren();
-  t.is(
-    itemListAfter.length,
-    itemListBefore.length + 1,
-    'Items market should have 1 more item',
-  );
-
-  const soldItemNode = itemListAfter.filter(
-    /** @param {unknown} itemNode */
-    itemNode => !itemListBefore.includes(itemNode),
-  );
-  const soldItem = await getMarketItem(soldItemNode);
-
-  t.is(
-    itemBefore.description,
-    soldItem.asset.description,
-    'Item on purse should have the same description as the one sold to market',
-  );
-});
-
-test.serial('Bob buys an Item on marketplace', async t => {
-  const itemListBefore = await getMarketItemsChildren();
-  const marketItemNode = itemListBefore[0];
-  const marketItem = await getMarketItem(marketItemNode);
-
-  await buyItem(Bob);
-
-  const itemListAfter = await getMarketItemsChildren();
-  t.is(
-    itemListAfter.length,
-    itemListBefore.length - 1,
-    'Items market should have 1 less item',
-  );
-
-  const boughtItemNode = itemListBefore.filter(
-    /** @param {unknown} itemNode */
-    itemNode => !itemListAfter.includes(itemNode),
-  );
-  t.is(
-    marketItemNode,
-    boughtItemNode[0],
-    'Item bought should have been removed from market',
-  );
-
-  const item = await getBalanceFromPurse(Bob, 'item');
-  t.is(
-    item.description,
-    marketItem.asset.description,
-    'Item on purse should have the same description as the one bought from market',
-  );
-});
-
-test.serial('Alice sells a Character', async t => {
-  const characterListBefore = await getMarketCharactersChildren();
-  t.false(
-    characterListBefore.includes('character-ephemeral_Ace'),
-    'Character should not be on market before selling',
-  );
-
-  const characterBalanceBefore = await getBalanceFromPurse(Alice, 'character');
-  t.is(
-    characterBalanceBefore.name,
-    'ephemeral_Ace',
-    'Character name should be ephemeral_Ace',
-  );
-
-  await sellCharacter(Alice);
-
-  const characterListAfter = await getMarketCharactersChildren();
-  t.true(
-    characterListAfter.includes('character-ephemeral_Ace'),
-    'Character should be on market after selling',
-  );
-
-  const characterBalanceAfter = await getBalanceFromPurse(Alice, 'character');
-  t.is(
-    characterBalanceAfter,
-    null,
-    'Character should not be in purse after selling',
-  );
-});
-
-test.serial('Bob buys a Character on marketplace', async t => {
-  await buyCharacter(Bob);
-
-  const characterListBefore = await getMarketCharactersChildren();
-  t.false(
-    characterListBefore.includes('character-ephemeral_Ace'),
-    'Character should not be on market after buying',
-  );
-
-  const characterBalance = await getBalanceFromPurse(Bob, 'character');
-  t.is(
-    characterBalance.name,
-    'ephemeral_Ace',
-    'Character name should be ephemeral_Ace',
-  );
-});
diff --git a/a3p-integration/proposals/z:acceptance/test/test-lib/kread.js b/a3p-integration/proposals/z:acceptance/test/test-lib/kread.js
deleted file mode 100644
index 1bb58b3eef1..00000000000
--- a/a3p-integration/proposals/z:acceptance/test/test-lib/kread.js
+++ /dev/null
@@ -1,361 +0,0 @@
-// @ts-nocheck FIXME
-// XXX uses agoric.follow to read data through spawned processes; replace with VstorageKit
-import assert from 'node:assert';
-
-import {
-  agoric,
-  executeOffer,
-  getContractInfo,
-  makeAgd,
-} from '@agoric/synthetic-chain';
-import { execFileSync } from 'child_process';
-import { makeCopyBag } from '@agoric/store';
-import { AmountMath } from '@agoric/ertp';
-import { boardSlottingMarshaller, makeFromBoard } from '@agoric/client-utils';
-
-const ISTunit = 1_000_000n;
-
-/** @type {(file: string, args: string[], opts: any) => string} */
-const showAndExec = (file, args, opts) => {
-  console.log('$', file, ...args);
-  return execFileSync(file, args, opts);
-};
-
-// @ts-expect-error string is not assignable to Buffer
-const agd = makeAgd({ execFileSync: showAndExec }).withOpts({
-  keyringBackend: 'test',
-});
-
-const fromBoard = makeFromBoard();
-const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal);
-
-const brandsRaw = await agoric.follow(
-  '-lF',
-  ':published.agoricNames.brand',
-  '-o',
-  'text',
-);
-const brands = Object.fromEntries(
-  marshaller.fromCapData(JSON.parse(brandsRaw)),
-);
-
-assert(brands.IST, 'Brand IST not found');
-assert(brands.timer, 'Brand timer not found');
-assert(brands.KREAdCHARACTER, 'Brand KREAdCHARACTER not found');
-assert(brands.KREAdITEM, 'Brand KREAdITEM not found');
-
-export const getMarketCharactersChildren = async () => {
-  const { children } = await agd.query([
-    'vstorage',
-    'children',
-    `published.kread.market-characters`,
-  ]);
-
-  return children;
-};
-
-export const getMarketItemsChildren = async () => {
-  const { children } = await agd.query([
-    'vstorage',
-    'children',
-    `published.kread.market-items`,
-  ]);
-
-  return children;
-};
-
-export const getMarketItem = async itemNode => {
-  const itemMarketPath = `:published.kread.market-items.${itemNode}`;
-  const rawItemData = await agoric.follow('-lF', itemMarketPath, '-o', 'text');
-  const item = marshaller.fromCapData(JSON.parse(rawItemData));
-
-  return item;
-};
-
-export const getCharacterInventory = async characterName => {
-  const inventoryPath = `kread.character.inventory-${characterName}`;
-  const characterInventory = await getContractInfo(inventoryPath, {
-    agoric,
-    prefix: 'published.',
-  });
-
-  return characterInventory;
-};
-
-export const getMarketCharacterFromVstorage = async () => {
-  const charactersMarket = await getMarketCharactersChildren();
-  const path = `:published.kread.market-characters.${charactersMarket[0]}`;
-  const rawCharacterData = await agoric.follow('-lF', path, '-o', 'text');
-  const marketCharacter = marshaller.fromCapData(JSON.parse(rawCharacterData));
-
-  return marketCharacter;
-};
-
-export const getBalanceFromPurse = async (address, type) => {
-  const walletRaw = await agoric.follow(
-    '-lF',
-    `:published.wallet.${address}.current`,
-    '-o',
-    'text',
-  );
-  const purses = marshaller.fromCapData(JSON.parse(walletRaw)).purses;
-
-  const assetBrands = {
-    character: brands.KREAdCHARACTER,
-    item: brands.KREAdITEM,
-  };
-  const assetBrand = assetBrands[type];
-  if (!assetBrand) {
-    throw new Error('Invalid type provided. Must be "character" or "item".');
-  }
-
-  const assetPurse = purses.find(
-    ({ brand }) => brand.getBoardId() === assetBrand.getBoardId(),
-  );
-
-  return assetPurse?.balance.value.payload[0]?.[0] || null;
-};
-
-/** @import {Brand, CopyBagAmount} from '@agoric/ertp/src/types.js'; */
-
-/**
- * @param {Brand} brand
- * @param {any} asset
- * @returns {CopyBagAmount<'item'>}
- */
-export const assetAsAmount = (brand, asset) => {
-  const assetValue = makeCopyBag([[asset, 1n]]);
-  const assetAmount = AmountMath.make(brand, assetValue);
-  return assetAmount;
-};
-
-export const totalAssetPriceAmount = asset => {
-  const fees = AmountMath.add(asset.platformFee, asset.royalty);
-  const price = AmountMath.add(asset.askingPrice, fees);
-  return price;
-};
-const mintCharacterOffer = async () => {
-  const body = {
-    method: 'executeOffer',
-    offer: {
-      id: 'KREAd-mint-character-acceptance-test',
-      invitationSpec: {
-        source: 'agoricContract',
-        instancePath: ['kread'],
-        callPipe: [['makeMintCharacterInvitation', []]],
-      },
-      offerArgs: { name: 'ephemeral_Ace' },
-      proposal: {
-        give: {
-          Price: {
-            brand: brands.IST,
-            value: 5n * ISTunit,
-          },
-        },
-      },
-    },
-  };
-
-  return JSON.stringify(marshaller.toCapData(harden(body)));
-};
-
-const unequipAllItemsOffer = async address => {
-  const kreadCharacter = await getBalanceFromPurse(address, 'character');
-  if (!kreadCharacter) {
-    throw new Error('Character not found on user purse');
-  }
-
-  // deal with KREAd's funky parallel character representation swap
-  const inventoryKeyId = kreadCharacter.keyId === 1 ? 2 : 1;
-  const kreadCharacter2 = { ...kreadCharacter, keyId: inventoryKeyId };
-
-  const kreadCharacterAmount = assetAsAmount(
-    brands.KREAdCHARACTER,
-    kreadCharacter,
-  );
-
-  const kreadCharacter2Amount = assetAsAmount(
-    brands.KREAdCHARACTER,
-    kreadCharacter2,
-  );
-
-  const body = {
-    method: 'executeOffer',
-    offer: {
-      id: 'KREAd-unequip-all-items-acceptance-test',
-      invitationSpec: {
-        source: 'agoricContract',
-        instancePath: ['kread'],
-        callPipe: [['makeUnequipAllInvitation', []]],
-      },
-      proposal: {
-        give: {
-          CharacterKey1: kreadCharacterAmount,
-        },
-        want: {
-          CharacterKey2: kreadCharacter2Amount,
-        },
-      },
-    },
-  };
-
-  return JSON.stringify(marshaller.toCapData(harden(body)));
-};
-
-const buyItemOffer = async () => {
-  const children = await getMarketItemsChildren();
-  const marketItem = await getMarketItem(children[0]);
-
-  const itemAmount = assetAsAmount(brands.KREAdITEM, marketItem.asset);
-  const priceAmount = totalAssetPriceAmount(marketItem);
-
-  const body = {
-    method: 'executeOffer',
-    offer: {
-      id: 'KREAd-buy-item-acceptance-test',
-      invitationSpec: {
-        source: 'agoricContract',
-        instancePath: ['kread'],
-        callPipe: [['makeBuyItemInvitation', []]],
-      },
-      offerArgs: { entryId: marketItem.id },
-      proposal: {
-        give: {
-          Price: priceAmount,
-        },
-        want: {
-          Item: itemAmount,
-        },
-      },
-    },
-  };
-
-  return JSON.stringify(marshaller.toCapData(harden(body)));
-};
-
-const sellItemOffer = async address => {
-  const kreadItem = await getBalanceFromPurse(address, 'item');
-  if (!kreadItem) {
-    throw new Error('Item not found on user purse');
-  }
-
-  const itemAmount = assetAsAmount(brands.KREAdITEM, kreadItem);
-
-  const body = {
-    method: 'executeOffer',
-    offer: {
-      id: 'KREAd-sell-item-acceptance-test',
-      invitationSpec: {
-        source: 'agoricContract',
-        instancePath: ['kread'],
-        callPipe: [['makeSellItemInvitation', []]],
-      },
-      proposal: {
-        give: {
-          Item: itemAmount,
-        },
-        want: {
-          Price: {
-            brand: brands.IST,
-            value: 5n * ISTunit,
-          },
-        },
-      },
-    },
-  };
-
-  return JSON.stringify(marshaller.toCapData(harden(body)));
-};
-
-const buyCharacterOffer = async () => {
-  const marketCharacter = await getMarketCharacterFromVstorage();
-
-  const kreadCharacterAmount = assetAsAmount(
-    brands.KREAdCHARACTER,
-    marketCharacter.asset,
-  );
-  const priceAmount = totalAssetPriceAmount(marketCharacter);
-
-  const body = {
-    method: 'executeOffer',
-    offer: {
-      id: 'KREAd-buy-character-acceptance-test',
-      invitationSpec: {
-        source: 'agoricContract',
-        instancePath: ['kread'],
-        callPipe: [['makeBuyCharacterInvitation', []]],
-      },
-      proposal: {
-        give: {
-          Price: priceAmount,
-        },
-        want: {
-          Character: kreadCharacterAmount,
-        },
-      },
-    },
-  };
-
-  return JSON.stringify(marshaller.toCapData(harden(body)));
-};
-
-const sellCharacterOffer = async address => {
-  const kreadCharacter = await getBalanceFromPurse(address, 'character');
-  if (!kreadCharacter) {
-    throw new Error('Character not found on user purse');
-  }
-
-  const kreadCharacterAmount = assetAsAmount(
-    brands.KREAdCHARACTER,
-    kreadCharacter,
-  );
-
-  const body = {
-    method: 'executeOffer',
-    offer: {
-      id: 'KREAd-sell-character-acceptance-test',
-      invitationSpec: {
-        source: 'agoricContract',
-        instancePath: ['kread'],
-        callPipe: [['makeSellCharacterInvitation', []]],
-      },
-      proposal: {
-        give: {
-          Character: kreadCharacterAmount,
-        },
-        want: {
-          Price: {
-            brand: brands.IST,
-            value: 5n * ISTunit,
-          },
-        },
-      },
-    },
-  };
-
-  return JSON.stringify(marshaller.toCapData(harden(body)));
-};
-
-export const mintCharacter = async address => {
-  return executeOffer(address, mintCharacterOffer());
-};
-
-export const unequipAllItems = async address => {
-  return executeOffer(address, unequipAllItemsOffer(address));
-};
-
-export const buyItem = async address => {
-  return executeOffer(address, buyItemOffer());
-};
-
-export const sellItem = async address => {
-  return executeOffer(address, sellItemOffer(address));
-};
-
-export const sellCharacter = async address => {
-  return executeOffer(address, sellCharacterOffer(address));
-};
-
-export const buyCharacter = async address => {
-  return executeOffer(address, buyCharacterOffer());
-};
diff --git a/package.json b/package.json
index 77e7ec58e61..83ccbd7923e 100644
--- a/package.json
+++ b/package.json
@@ -65,6 +65,7 @@
     "lint:packages": "yarn lerna run --no-bail lint",
     "lint:package-names": "./scripts/verify-package-names.js",
     "lint:types": "tsc",
+    "lint:scripts": "yarn eslint scripts/*js",
     "test": "yarn lerna run --no-bail test",
     "test:xs": "yarn workspaces run test:xs",
     "build": "yarn workspaces run build && scripts/agd-builder.sh stamp yarn-built",
diff --git a/packages/ERTP/package.json b/packages/ERTP/package.json
index e65f503492c..1c6332ffbc5 100644
--- a/packages/ERTP/package.json
+++ b/packages/ERTP/package.json
@@ -53,6 +53,7 @@
   },
   "devDependencies": {
     "@agoric/swingset-vat": "^0.32.2",
+    "@agoric/xsnap": "^0.14.2",
     "@endo/bundle-source": "^4.0.0",
     "@fast-check/ava": "^1.1.5",
     "ava": "^5.3.0",
diff --git a/packages/SwingSet/src/types-external.js b/packages/SwingSet/src/types-external.js
index 41f8c4e2806..a5ae65125ab 100644
--- a/packages/SwingSet/src/types-external.js
+++ b/packages/SwingSet/src/types-external.js
@@ -32,7 +32,7 @@ export {};
  * @typedef {'getExport' | 'nestedEvaluate' | 'endoZipBase64'} BundleFormat
  * @typedef { { moduleFormat: 'getExport', source: string, sourceMap?: string } } GetExportBundle
  * @typedef { { moduleFormat: 'nestedEvaluate', source: string, sourceMap?: string } } NestedEvaluateBundle
- * @typedef { { moduleFormat: 'test' } } TestBundle
+ * @typedef { { moduleFormat: 'test', [x: symbol]: Record<PropertyKey, unknown> } } TestBundle
  * @typedef { EndoZipBase64Bundle | GetExportBundle | NestedEvaluateBundle | TestBundle} Bundle
  */
 
diff --git a/packages/agoric-cli/src/commands/perf.js b/packages/agoric-cli/src/commands/perf.js
index 599020509ec..db6c19c291d 100644
--- a/packages/agoric-cli/src/commands/perf.js
+++ b/packages/agoric-cli/src/commands/perf.js
@@ -64,15 +64,13 @@ export const makePerfCommand = logger => {
 
       const spec = `:published.wallet.${opts.from}`;
 
-      const leaderOptions = makeLeaderOptions({
-        sleep: SLEEP_SECONDS,
-        jitter: 0,
-        log: () => undefined,
-      });
-
       const leader = makeLeaderFromRpcAddresses(
         networkConfig.rpcAddrs,
-        leaderOptions,
+        makeLeaderOptions({
+          sleep: SLEEP_SECONDS,
+          jitter: 0,
+          log: console.warn,
+        }),
       );
 
       logger.warn('Following', spec);
@@ -87,9 +85,10 @@ export const makePerfCommand = logger => {
             if (status.error) {
               console.error(status.error);
               exit(1);
-            } else if (status.numWantsSatisfied)
+            } else if (status.numWantsSatisfied) {
               process.stdout.write(`satisfied: ${status.numWantsSatisfied}\n`);
-            exit(0);
+              exit(0);
+            }
           }
         }
       };
diff --git a/packages/agoric-cli/src/commands/wallet.js b/packages/agoric-cli/src/commands/wallet.js
index 1b169dca6ac..1e6fd1cbb3c 100644
--- a/packages/agoric-cli/src/commands/wallet.js
+++ b/packages/agoric-cli/src/commands/wallet.js
@@ -278,15 +278,13 @@ export const makeWalletCommand = async command => {
     .action(async function ({ from }) {
       const spec = `:published.wallet.${from}`;
 
-      const leaderOptions = makeLeaderOptions({
-        sleep: SLEEP_SECONDS,
-        jitter: 0,
-        log: () => undefined,
-      });
-
       const leader = makeLeaderFromRpcAddresses(
         networkConfig.rpcAddrs,
-        leaderOptions,
+        makeLeaderOptions({
+          sleep: SLEEP_SECONDS,
+          jitter: 0,
+          log: console.warn,
+        }),
       );
 
       console.warn('Following', spec);
diff --git a/packages/agoric-cli/src/follow.js b/packages/agoric-cli/src/follow.js
index 745afc5830f..e294d6e64b8 100644
--- a/packages/agoric-cli/src/follow.js
+++ b/packages/agoric-cli/src/follow.js
@@ -126,16 +126,17 @@ export default async function followerMain(progname, rawArgs, powers, opts) {
     });
   }
 
-  const leaderOptions = makeLeaderOptions({
-    sleep,
-    jitter,
-    log: verbose ? console.warn : () => undefined,
-  });
-
   const [_cmd, ...specs] = rawArgs;
 
   verbose && console.warn('Creating leader for', bootstrap);
-  const leader = makeLeader(bootstrap, leaderOptions);
+  const leader = makeLeader(
+    bootstrap,
+    makeLeaderOptions({
+      sleep,
+      jitter,
+      log: verbose ? console.warn : () => undefined,
+    }),
+  );
   const iterate = opts.lossy ? iterateLatest : iterateEach;
   await Promise.all(
     specs.map(async spec => {
diff --git a/packages/casting/src/defaults.js b/packages/casting/src/defaults.js
index 321e9eec29a..e0b9ec2ef74 100644
--- a/packages/casting/src/defaults.js
+++ b/packages/casting/src/defaults.js
@@ -13,6 +13,11 @@ export const DEFAULT_POLL_WITH_EVENTS_SECONDS = 600;
 
 export const DEFAULT_KEEP_POLLING_SECONDS = 5;
 
+/**
+ * Default backoff is the block interval.
+ */
+export const DEFAULT_MAX_BACKOFF = 6_000;
+
 /**
  * Resolve a Promise after a given number of milliseconds.
  *
@@ -34,9 +39,13 @@ export const randomBackoff = (range, cap = range) => {
 /**
  * @param {number} [attempt]
  * @param {number} [base]
- * @param {number} [cap]
+ * @param {number} [cap=DEFAULT_MAX_BACKOFF]
  */
-export const exponentialBackoff = (attempt = 0, base = 1_000, cap = 30_000) => {
+export const exponentialBackoff = (
+  attempt = 0,
+  base = 1_000,
+  cap = DEFAULT_MAX_BACKOFF,
+) => {
   return randomBackoff(2 ** attempt * base, cap);
 };
 
diff --git a/packages/xsnap/README.md b/packages/xsnap/README.md
index ea228d103d0..e83fae208d3 100644
--- a/packages/xsnap/README.md
+++ b/packages/xsnap/README.md
@@ -47,6 +47,8 @@ The parent and child communicate using "commands".
 
 ![state diagram](doc/xsnap-states.svg)
 
+<!-- FIXME this stopped working some time ago (was never in CI)
+https://github.com/Agoric/agoric-sdk/issues/9955
 # xsrepl
 
 With `xsnap` comes an `xsrepl` command line tool.
@@ -75,3 +77,4 @@ xs> x
 42
 xs> quit
 ```
+ -->
diff --git a/packages/xsnap/package.json b/packages/xsnap/package.json
index fbf3f11991a..130c58eb2c0 100644
--- a/packages/xsnap/package.json
+++ b/packages/xsnap/package.json
@@ -12,11 +12,11 @@
   },
   "scripts": {
     "repl": "node src/xsrepl.js",
-    "build:bin": "if test -d ./test; then node src/build.js; else yarn build:from-env; fi",
+    "build:bin": "/bin/sh -c 'if test -d ./test; then node src/build.js; else yarn build:from-env; fi'",
     "build:env": "node src/build.js --show-env > build.env",
     "build:from-env": "{ cat build.env; echo node src/build.js; } | xargs env",
     "build": "yarn build:bin && yarn build:env",
-    "check-version": "xsnap_version=$(./scripts/get_xsnap_version.sh); if test \"${npm_package_version}\" != \"${xsnap_version}\"; then echo \"xsnap version mismatch; expected '${npm_package_version}', got '${xsnap_version}'\"; exit 1; fi",
+    "check-version": "/bin/sh -c 'if test \"${npm_package_version}\" != \"$(./scripts/get_xsnap_version.sh)\"; then echo \"xsnap version mismatch; expected '${npm_package_version}'\"; exit 1; fi'",
     "postinstall": "npm run build:from-env",
     "clean": "rm -rf xsnap-native/xsnap/build",
     "lint": "run-s --continue-on-error lint:*",
diff --git a/packages/zoe/package.json b/packages/zoe/package.json
index bddd98e57a4..d982169d1fb 100644
--- a/packages/zoe/package.json
+++ b/packages/zoe/package.json
@@ -70,8 +70,9 @@
     "yargs-parser": "^21.1.1"
   },
   "devDependencies": {
-    "@endo/init": "^1.1.9",
     "@agoric/kmarshal": "^0.1.0",
+    "@agoric/xsnap": "^0.14.2",
+    "@endo/init": "^1.1.9",
     "ava": "^5.3.0",
     "c8": "^10.1.3",
     "import-meta-resolve": "^4.1.0",
diff --git a/scripts/check-untested-packages.mjs b/scripts/check-untested-packages.mjs
index b97465426bc..2cc724df3cf 100755
--- a/scripts/check-untested-packages.mjs
+++ b/scripts/check-untested-packages.mjs
@@ -3,6 +3,7 @@
 import fs from 'fs';
 import path from 'path';
 import { execFileSync } from 'child_process';
+// eslint-disable-next-line import/no-relative-packages -- okay for dev script
 import { listWorkspaces } from '../packages/agoric-cli/src/lib/packageManager.js';
 
 const parent = new URL('..', import.meta.url).pathname;
diff --git a/scripts/create-agoric-cli.cjs b/scripts/create-agoric-cli.cjs
index bc692b72955..3dfb3eb0bf9 100755
--- a/scripts/create-agoric-cli.cjs
+++ b/scripts/create-agoric-cli.cjs
@@ -1,4 +1,5 @@
 #! /usr/bin/env node
+/* eslint-env node */
 
 const fs = require('fs');
 const path = require('path');
@@ -37,7 +38,7 @@ try {
 # AUTOMATICALLY GENERATED by ${process.argv[1]}
 # Maybe execute the checked-out Agoric CLI with the --sdk flag.
 opts=\${AGORIC_CLI_OPTS-"--sdk"}
-exec ${cli} \$opts \${1+"\$@"}
+exec ${cli} $opts \${1+"$@"}
 `;
 
   console.log(`creating ${script}`);
diff --git a/scripts/migrate-test-names.mjs b/scripts/migrate-test-names.mjs
index d2f16998867..5ab264957c6 100755
--- a/scripts/migrate-test-names.mjs
+++ b/scripts/migrate-test-names.mjs
@@ -26,6 +26,7 @@ Then for the couple deeper packages
   yarn lerna exec --concurrency 1 --no-bail '../../../scripts/migrate-test-names.mjs'
 
 `;
+console.log(usage);
 
 const packageObj = fs.readJsonSync('package.json');
 console.log(`${chalk.bold(`Migrating test names: ${packageObj.name}`)}\n`);
diff --git a/scripts/registry.sh b/scripts/registry.sh
index 4b0827f490e..7ae4adcdbc2 100755
--- a/scripts/registry.sh
+++ b/scripts/registry.sh
@@ -123,10 +123,6 @@ integrationTest() {
       # Install on demand. "legacy-peer-deps" like above.
       persistVar AGORIC_CMD "[\"npx\",\"--legacy-peer-deps\",\"agoric@$DISTTAG\"]"
       ;;
-    *)
-      yarn global add "agoric@$DISTTAG"
-      persistVar AGORIC_CMD "[\"$(yarn global bin)/agoric\"]"
-      ;;
   esac
 
   test -z "${DISTTAG-}" || {
diff --git a/scripts/update-typedoc-functions-path.cjs b/scripts/update-typedoc-functions-path.cjs
index 0942b4c67d1..9109f0ec07a 100755
--- a/scripts/update-typedoc-functions-path.cjs
+++ b/scripts/update-typedoc-functions-path.cjs
@@ -1,4 +1,5 @@
 #!/usr/bin/env node
+/* eslint-env node */
 
 /**
  * Help us workaround a limitation in Cloudflare Pages that prevents us from


Reviews:
user: login: dckc
body: thanks for tidying up around the shop
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-20T23:16:06Z

Review comments:
diff_hunk: @@ -12,11 +12,11 @@
   },
   "scripts": {
     "repl": "node src/xsrepl.js",
-    "build:bin": "if test -d ./test; then node src/build.js; else yarn build:from-env; fi",
+    "build:bin": "/bin/sh -c 'if test -d ./test; then node src/build.js; else yarn build:from-env; fi'",
     "build:env": "node src/build.js --show-env > build.env",
     "build:from-env": "{ cat build.env; echo node src/build.js; } | xargs env",
     "build": "yarn build:bin && yarn build:env",
-    "check-version": "xsnap_version=$(./scripts/get_xsnap_version.sh); if test \"${npm_package_version}\" != \"${xsnap_version}\"; then echo \"xsnap version mismatch; expected '${npm_package_version}', got '${xsnap_version}'\"; exit 1; fi",
+    "check-version": "/bin/sh -c 'if test \"${npm_package_version}\" != \"$(./scripts/get_xsnap_version.sh)\"; then echo \"xsnap version mismatch; expected '${npm_package_version}'\"; exit 1; fi'",
path: packages/xsnap/package.json
user: login: dckc
body: quoting starts to get hairy :-/
created_at: 2025-05-20T23:14:30Z
author_association: MEMBER
diff_hunk: @@ -83,7 +83,7 @@ jobs:
   ##################
   # Lint tests
   lint-primary:
-    timeout-minutes: 20
+    timeout-minutes: 25 # Since Node 22, it's sometimes longer than 20min
path: .github/workflows/test-all-packages.yml
user: login: dckc
body: ouch!!
created_at: 2025-05-20T23:15:42Z
author_association: MEMBER

PR comments:
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-05-20T22:40:21Z
body: ## Deploying agoric-sdk with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>feb6aa9</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://a5c81dde.agoric-sdk.pages.dev'>https://a5c81dde.agoric-sdk.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://ta-misc.agoric-sdk.pages.dev'>https://ta-misc.agoric-sdk.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/0c4635effffcd7f36d1b9f0425a4367a/pages/view/agoric-sdk/a5c81dde-bf04-4326-b509-f952315e889e)

author_association: NONE
user: login: mergify[bot]
created_at: 2025-05-21T02:36:20Z
body: This pull request has been removed from the queue for the following reason: <code>checks failed</code>.

The merge conditions cannot be satisfied due to failing checks:
- ❌ [integration-test-result](https://github.com/Agoric/agoric-sdk/runs/42599602988)
- ☑️ [linear-history](https://github.com/Agoric/agoric-sdk/actions/runs/15149266136/job/42592140262)


You may have to fix your CI before adding the pull request to the queue again.
If you update this pull request, to fix the CI, it will automatically be requeued once the queue conditions match again.
If you think this was a flaky issue instead, you can requeue the pull request, without updating it, by posting a `@mergifyio requeue` comment.

author_association: CONTRIBUTOR
user: login: mergify[bot]
created_at: 2025-05-21T16:52:17Z
body: This pull request has been removed from the queue for the following reason: <code>checks failed</code>.

The merge conditions cannot be satisfied due to failing checks:
- ❌ [integration-test-result](https://github.com/Agoric/agoric-sdk/runs/42649234669)
- ☑️ [linear-history](https://github.com/Agoric/agoric-sdk/actions/runs/15149266136/job/42592140262)


You may have to fix your CI before adding the pull request to the queue again.
If you update this pull request, to fix the CI, it will automatically be requeued once the queue conditions match again.
If you think this was a flaky issue instead, you can requeue the pull request, without updating it, by posting a `@mergifyio requeue` comment.

author_association: CONTRIBUTOR