@edgehero/pi-dispatch 3.0.0 → 3.1.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgehero/pi-dispatch",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "type": "module",
5
5
  "description": "The pi-dispatch worker and CLI: runs the pi coding agent as a self-hosted service, one locked down Docker or Podman container per job, with spend caps checked before anything is spent, plus init, up, doctor and service install.",
6
6
  "keywords": [
@@ -105,7 +105,7 @@
105
105
  "start": "node src/cli.mjs worker"
106
106
  },
107
107
  "dependencies": {
108
- "@earendil-works/pi-ai": "0.99.1",
108
+ "@earendil-works/pi-ai": "1.0.3",
109
109
  "@octokit/auth-app": "8.2.0",
110
110
  "@octokit/rest": "22.0.1",
111
111
  "bullmq": "5.80.4",
package/src/config.mjs CHANGED
@@ -195,7 +195,7 @@ function forwardEnvList(raw, egressArmed = false) {
195
195
  if (owned.length > 0) {
196
196
  throw configError(`PI_FORWARD_ENV must not forward ${owned.join(", ")} -- the worker writes ${owned.length === 1 ? "it" : "them"} into every job's container itself, and a forwarded host value would replace the per-job one (a forwarded PI_MODEL runs a model the pre-spend checks never saw)`);
197
197
  }
198
- // Issue #500: the two names the runner sets in its own environment for its descendants (RUNNER_ENV_NAMES). A forwarded
198
+ // Issue #500: the names the runner sets in its own environment for its descendants (RUNNER_ENV_NAMES). A forwarded
199
199
  // host value would sit in the runner's environment before the runner sets its own.
200
200
  const runnerOwned = names.filter((n) => RUNNER_ENV_NAMES.has(n));
201
201
  if (runnerOwned.length > 0) {
package/src/doctor.mjs CHANGED
@@ -66,7 +66,7 @@ import { isTransientOverlayRead, overlayProviderProblem } from "./model-catalog.
66
66
  import { EMPTY_USD_FINGERPRINT, usdFingerprint } from "./dollar-fingerprint.mjs";
67
67
  import { splitModelEntry } from "./model-ref.mjs";
68
68
  import { ignoredOutputCapModels, outputCapView, outputUnboundable } from "./output-cap.mjs";
69
- import { KEYLESS_API_KEY, KEYLESS_HOW, MODEL_ENDPOINTS_FILE_NAME, MODEL_ENDPOINTS_INCLUDE_NAME, MODEL_ENDPOINT_ID_RE, OVERLAY_LINK_FIX, OVERLAY_NOT_A_FILE_FIX, baseUrlTarget, keylessVerdict, loadModelEndpoints, readOverlayModels, renderEndpointsInclude, unreportedUsageModels } from "./model-endpoints.mjs";
69
+ import { KEYLESS_API_KEY, KEYLESS_HOW, MODEL_ENDPOINTS_FILE_NAME, MODEL_ENDPOINTS_INCLUDE_NAME, MODEL_ENDPOINT_ID_RE, OVERLAY_LINK_FIX, OVERLAY_NOT_A_FILE_FIX, RENAMED_PROVIDERS, baseUrlTarget, keylessVerdict, loadModelEndpoints, providerRenameHint, readOverlayModels, renderEndpointsInclude, unreportedUsageModels } from "./model-endpoints.mjs";
70
70
  import { declaredEndpointsIn, endpointsDeclaredIn, reloadCommand, rulesFileIncludes, rulesPredateEndpointsLine } from "./egress-cli.mjs";
71
71
  import { loadPauseWindows, parseScopeString } from "./pause-windows.mjs";
72
72
  import { WAIT_AFTER_MAX_DEFAULT_MS, afterInstantMs, parseWaitProfiles } from "./wait-for.mjs";
@@ -2554,7 +2554,17 @@ export async function collectChecks(shellVars, seams) {
2554
2554
  if (typeof error?.code === "string") keylessUnreadable = error.code;
2555
2555
  }
2556
2556
  }
2557
- const keyCheck = providerKeyCheck({ provider, env: { ...env, ...keyFromFile }, agentDir: keyAgentDir, oracle, nodeOk: checks[0]?.ok, keyless: { endpoints: keylessEndpoints, models: keylessModels, unreadable: keylessUnreadable } });
2557
+ // Issue #587's review: the rename hint is judged on the overlay itself, read whenever there is one, endpoint or not.
2558
+ let hintModels = keylessModels;
2559
+ let hintUnread = false;
2560
+ if (unknownToPi && hintModels === null && Object.hasOwn(RENAMED_PROVIDERS, provider) && typeof env.PI_GLOBAL_PI_DIR === "string" && isAbsolute(env.PI_GLOBAL_PI_DIR)) {
2561
+ try {
2562
+ hintModels = readOverlayModels(env.PI_GLOBAL_PI_DIR, keylessIo);
2563
+ } catch {
2564
+ hintUnread = true;
2565
+ }
2566
+ }
2567
+ const keyCheck = providerKeyCheck({ provider, env: { ...env, ...keyFromFile }, agentDir: keyAgentDir, oracle, nodeOk: checks[0]?.ok, keyless: { endpoints: keylessEndpoints, models: keylessModels, unreadable: keylessUnreadable, hintModels, hintUnread } });
2558
2568
  const keyNamed = Object.keys(keyFromFile).filter((name) => keyCheck.label?.includes(`: ${name})`));
2559
2569
  // Issue #481 (PR #485 review round 2): a key found nowhere doctor can look is one an env-setup script may export, which
2560
2570
  // is the documented home for a provider key fetched from a secrets manager (docs/secrets.md).
@@ -2692,7 +2702,20 @@ export async function collectChecks(shellVars, seams) {
2692
2702
  // `overlayProviderProblem`). pi drops that whole entry, so the worker refuses every job on the provider.
2693
2703
  const providers = overlayModels?.providers;
2694
2704
  for (const name of providers !== null && typeof providers === "object" && !Array.isArray(providers) ? Object.keys(providers) : []) {
2695
- const problem = overlayProviderProblem(providers[name], name);
2705
+ // Issue #587: an entry under an id pi renamed that lacks what a provider of its own needs (an api, a baseUrl
2706
+ // and models). It was an override of the builtin provider; under the new pi it is a custom provider with no
2707
+ // models, so it overrides nothing, and the builtin models it meant run with their own (empty) baseUrl.
2708
+ const entry = providers[name];
2709
+ if (Object.hasOwn(RENAMED_PROVIDERS, name) && entry !== null && typeof entry === "object" && !Array.isArray(entry) && (entry.api === undefined || entry.baseUrl === undefined || !Array.isArray(entry.models))) {
2710
+ const renamedTo = RENAMED_PROVIDERS[name];
2711
+ checks.push({
2712
+ ok: false,
2713
+ warn: true,
2714
+ label: `Overlay models.json entry ${JSON.stringify(name)} no longer overrides anything: pi 1.0.3 renamed that provider to ${JSON.stringify(renamedTo)}, so this entry is now a provider of its own, holding only what it declares itself, and the ${renamedTo} models do not get its settings`,
2715
+ fix: `rename the entry to ${JSON.stringify(renamedTo)} in ${modelsPath} (and every ${name}/ model reference in the triggers to ${renamedTo}/), then re-run doctor`,
2716
+ });
2717
+ }
2718
+ const problem = overlayProviderProblem(entry, name);
2696
2719
  if (problem === null) continue;
2697
2720
  checks.push({
2698
2721
  ok: false,
@@ -4241,10 +4264,14 @@ function noKeyVariableCheck(provider, oracle, keyless = null) {
4241
4264
  const owner = oracle.piProviders().find((id) => oracle.providerKeyCandidates(id).includes(wanted));
4242
4265
  // Why a custom provider the overlay defines is not keyless, when it is one: the one fact the operator has to change.
4243
4266
  const why = verdict.why ? `; it is not keyless because ${verdict.why}` : "";
4267
+ // Issue #587: an id pi renamed (azure-openai-responses is `azure` since pi 1.0.3), named, unless the overlay declares it.
4268
+ const renamed = providerRenameHint(provider, { models: keyless?.hintModels ?? keyless?.models ?? null, overlayUnread: keyless?.hintUnread === true, piProviders: oracle.piProviders() });
4244
4269
  return {
4245
4270
  ok: false,
4246
- label: `PI_PROVIDER is ${JSON.stringify(provider)}, which is not a provider pi has${why}`,
4247
- fix: owner
4271
+ label: `PI_PROVIDER is ${JSON.stringify(provider)}, which is not a provider pi has${why}${renamed ? `;${renamed}` : ""}`,
4272
+ fix: renamed
4273
+ ? `set PI_PROVIDER=${RENAMED_PROVIDERS[provider]}, and rename the provider in every trigger's model and allowed-models entries and in models.json the same way (docs/triggers.md)`
4274
+ : owner
4248
4275
  ? `set PI_PROVIDER=${owner}, the provider pi reads ${wanted} from -- or unset it for the default \`anthropic\`; or, ${KEYLESS_HOW}`
4249
4276
  : `set PI_PROVIDER to a provider id pi has, or unset it for the default \`anthropic\`: ${oracle.piProviders().join(", ")}; or, ${KEYLESS_HOW}`,
4250
4277
  };
@@ -33,7 +33,7 @@ import { egressEnv } from "./egress.mjs";
33
33
  import { forgeSpec } from "./forges.mjs";
34
34
  import { apiKeyVariable } from "./provider-key.mjs";
35
35
  import { isDeterminateFsCode } from "./transient.mjs";
36
- import { KEYLESS_HOW, keylessVerdict } from "./model-endpoints.mjs";
36
+ import { KEYLESS_HOW, keylessVerdict, providerRenameHint, readOverlayModels, RENAMED_PROVIDERS } from "./model-endpoints.mjs";
37
37
  import { KEYLESS_ENV_NAME } from "./reserved-env.mjs";
38
38
 
39
39
  function configError(message) {
@@ -161,7 +161,7 @@ export function resolveProviderCredential({ provider, hostEnv, authFromPi = fals
161
161
  error.code = unreadable.code;
162
162
  throw error;
163
163
  }
164
- throw configError(unknownProviderMessage(provider));
164
+ throw configError(unknownProviderMessage(provider, hintOverlay(provider, modelEndpoints, hostEnv)));
165
165
  }
166
166
  const held = candidates
167
167
  .map((name) => [name, hostEnv[name]])
@@ -209,8 +209,24 @@ export function keylessEndpointsFor(provider, modelEndpoints) {
209
209
  return verdict.keyless ? verdict.endpoints : null;
210
210
  }
211
211
 
212
- function unknownProviderMessage(provider) {
213
- return `pi has no provider "${provider}", so there is no key variable to give it a key. Use one of pi's provider ids with its key in the worker environment or in pi's auth.json, or, ${KEYLESS_HOW}.`;
212
+ function unknownProviderMessage(provider, overlay = {}) {
213
+ return `pi has no provider "${provider}", so there is no key variable to give it a key.${providerRenameHint(provider, { ...overlay, piProviders: piProviders() })} Use one of pi's provider ids with its key in the worker environment or in pi's auth.json, or, ${KEYLESS_HOW}.`;
214
+ }
215
+
216
+ /**
217
+ * The overlay models.json the rename hint is judged on (issue #587's review): the pickup's snapshot when it read one, else
218
+ * the overlay itself, read here, only for an id pi renamed. The snapshot reads the overlay only when an endpoint is
219
+ * declared, so a full custom provider under the old id, with no endpoint, was told "did you mean". A read that fails
220
+ * is `overlayUnread`: no guess.
221
+ */
222
+ function hintOverlay(provider, modelEndpoints, hostEnv) {
223
+ if (modelEndpoints?.models) return { models: modelEndpoints.models };
224
+ if (!Object.hasOwn(RENAMED_PROVIDERS, provider)) return {};
225
+ try {
226
+ return { models: readOverlayModels(hostEnv?.PI_GLOBAL_PI_DIR) };
227
+ } catch {
228
+ return { overlayUnread: true };
229
+ }
214
230
  }
215
231
 
216
232
  function defaultAgentDir(hostEnv) {
@@ -524,13 +540,15 @@ export function buildContainerEnv({ provider, model, maxTurns, maxTokens, maxCos
524
540
  if (exitAuth === true) env.PI_EXIT_AUTH = "stdin";
525
541
  else delete env.PI_EXIT_AUTH;
526
542
 
527
- // Issue #500: the runner sets these two in its own environment, for its child processes, and no value from outside
543
+ // Issue #500: the runner sets these in its own environment, for its child processes, and no value from outside
528
544
  // may arrive first. Both lists refuse them upstream (config.mjs for PI_FORWARD_ENV, the triggers loader for
529
545
  // run.secrets, through RUNNER_ENV_NAMES); this is the line that holds if either refusal is ever bypassed.
530
546
  // env-internal PI_DISPATCH_CHILD_LEDGER: set by the runner inside the container, never by the worker, so removed here.
531
547
  delete env.PI_DISPATCH_CHILD_LEDGER;
532
548
  // env-internal PI_DISPATCH_RUNNER_PID: set by the runner inside the container, never by the worker, so removed here.
533
549
  delete env.PI_DISPATCH_RUNNER_PID;
550
+ // env-internal PI_DISPATCH_PRICE_TABLE: set by the runner inside the container, never by the worker, so removed here.
551
+ delete env.PI_DISPATCH_PRICE_TABLE;
534
552
 
535
553
  // Forge-backed jobs, and local cron jobs that opted in via run.github. Other local-folder jobs have
536
554
  // no token (CONST-TOKEN-SCOPED-PER-JOB). The mint goes into BOTH of its forge's variables because
package/src/host-pi.mjs CHANGED
@@ -15,7 +15,7 @@
15
15
  * What it must never become. Nothing on the worker's BOOT path may import this file. It reads host paths and
16
16
  * may spawn a package manager, and neither belongs anywhere near `start.mjs`.
17
17
  *
18
- * Everything here MIRRORS a private detail of the pinned pi (0.99.1) rather than calling it: pi exports no
18
+ * Everything here MIRRORS a private detail of the pinned pi (1.0.3) rather than calling it: pi exports no
19
19
  * public answer to "where is this package installed" or "is this resource enabled", and importing the whole
20
20
  * coding-agent SDK to read two well-known paths is not worth the weight. That mirroring is a real risk --
21
21
  * pi could change the grammar and we would silently start staging something the operator turned off -- so it
@@ -91,7 +91,7 @@ function overlayDeclares(overlay, provider, id) {
91
91
 
92
92
  /**
93
93
  * Would pi COMPOSE this overlay provider (PR #536's review)? A file that passes pi's schema can still lose a provider
94
- * at the next step: pi 0.99.1's `applyModelsJson` and `modelFromJson` (`pi-coding-agent/dist/core/provider-composer.js`)
94
+ * at the next step: the pinned pi's `applyModelsJson` and `modelFromJson` (`pi-coding-agent/dist/core/provider-composer.js`)
95
95
  * throw for `oauth` with no `baseUrl`, and per model no resolvable `api` or
96
96
  * `baseUrl`, or a `contextWindow` or `maxTokens` at or below zero. pi then keeps the provider's BUILTIN models
97
97
  * (`ModelRuntime.composeProvider` falls back to the base) and drops every model the overlay added, so such a model
@@ -518,6 +518,28 @@ export function unreportedUsageModels(models, { builtinModel = () => null, built
518
518
  /** The exact `apiKey` a keyless provider's models.json entry carries: pi resolves `$NAME` from the job's environment. */
519
519
  export const KEYLESS_API_KEY = `$${KEYLESS_ENV_NAME}`;
520
520
 
521
+ /**
522
+ * Provider ids pi renamed, old to new (issue #587: pi 1.0.3 renamed `azure-openai-responses` to `azure`, and its catalog
523
+ * file with it; the api id `azure-openai-responses` is unchanged). Pinned against the catalog by
524
+ * worker/test/env-allowlist.test.mjs: every old id is gone from pi, every new id is there. A rename is not a refusal of
525
+ * its own: the old id is already refused before spend as a provider pi does not have, and this only lets that refusal
526
+ * and doctor say which id was meant.
527
+ */
528
+ export const RENAMED_PROVIDERS = Object.freeze({ "azure-openai-responses": "azure" });
529
+
530
+ /**
531
+ * The "did you mean" for a provider pi renamed, or "" (issue #587). Only for an old id pi no longer has, whose new id pi
532
+ * does have, and that the overlay's models.json (`models`, parsed, or null when there is none) does not declare: a
533
+ * provider the file declares under the old id is the operator's own custom provider, and the hint would be wrong there.
534
+ * `overlayUnread`: the overlay exists but could not be read, so whether it declares the id is not known: no guess.
535
+ */
536
+ export function providerRenameHint(provider, { models = null, piProviders = [], overlayUnread = false } = {}) {
537
+ if (typeof provider !== "string" || !Object.hasOwn(RENAMED_PROVIDERS, provider)) return "";
538
+ const renamed = RENAMED_PROVIDERS[provider];
539
+ if (overlayUnread || piProviders.includes(provider) || !piProviders.includes(renamed) || providerOf(models, provider) !== null) return "";
540
+ return ` pi renamed the provider "${provider}" to "${renamed}" in pi 1.0.3: did you mean "${renamed}"?`;
541
+ }
542
+
521
543
  /**
522
544
  * The second way into the credential gate, said once for every refusal and doctor line that names it (issue #503).
523
545
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The overlay `models.json`, read the way pi reads it (issue #502, PR #536's review): pi 0.99.1's
2
+ * The overlay `models.json`, read the way pi reads it (issue #502, PR #536's review): pi's
3
3
  * `ModelConfig.load` (`pi-coding-agent/dist/core/model-config.js`) strips a leading BOM, strips `//` comments and
4
4
  * trailing commas outside strings, parses, and then checks the whole document against its TypeBox schema. ANY
5
5
  * schema error drops the WHOLE file: pi then knows none of its providers or models.
@@ -121,7 +121,8 @@ const T = {
121
121
  const opt = (check) => [check, true];
122
122
  const req = (check) => [check, false];
123
123
 
124
- // The schema, transcribed from model-config.js at the 0.99.1 pin, in its order.
124
+ // The schema, transcribed from model-config.js at the pin (1.0.3; worker/test/models-json.test.mjs holds the file's
125
+ // content hash), in its order.
125
126
  const PercentileCutoffs = T.obj({ p50: opt(T.num()), p75: opt(T.num()), p90: opt(T.num()), p99: opt(T.num()) });
126
127
  const numOrStr = T.union(T.num(), T.str());
127
128
  const OpenRouterRouting = T.obj({
@@ -142,6 +143,9 @@ const OpenRouterRouting = T.obj({
142
143
  const VercelGatewayRouting = T.obj({ only: opt(T.arr(T.str())), order: opt(T.arr(T.str())) });
143
144
  const ThinkingValue = T.union(T.str(), T.nul());
144
145
  const ThinkingLevelMap = T.obj({ off: opt(ThinkingValue), minimal: opt(ThinkingValue), low: opt(ThinkingValue), medium: opt(ThinkingValue), high: opt(ThinkingValue), xhigh: opt(ThinkingValue), max: opt(ThinkingValue) });
146
+ // pi 1.0.2 (issue #587): a model's sampling parameters, and the same per thinking level, merged per call over them.
147
+ const SamplingParams = T.rec(T.unknown());
148
+ const SamplingParamsByThinkingLevel = T.obj({ off: opt(SamplingParams), minimal: opt(SamplingParams), low: opt(SamplingParams), medium: opt(SamplingParams), high: opt(SamplingParams), xhigh: opt(SamplingParams), max: opt(SamplingParams) });
145
149
  const KwargScalar = T.union(T.str(), T.num(), T.bool(), T.nul());
146
150
  const KwargVariable = T.obj({ $var: req(T.union(T.lit("thinking.enabled"), T.lit("thinking.effort"))), omitWhenOff: opt(T.bool()) });
147
151
  const Kwarg = T.union(KwargScalar, KwargVariable);
@@ -214,7 +218,8 @@ const ModelDefinition = T.obj({
214
218
  promptCache: opt(ModelPromptCache),
215
219
  contextWindow: opt(T.num()),
216
220
  maxTokens: opt(T.num()),
217
- samplingParams: opt(T.rec(T.unknown())),
221
+ samplingParams: opt(SamplingParams),
222
+ samplingParamsByThinkingLevel: opt(SamplingParamsByThinkingLevel),
218
223
  headers: opt(headers),
219
224
  compat: opt(ProviderCompat),
220
225
  });
@@ -228,7 +233,8 @@ const ModelOverride = T.obj({
228
233
  promptCache: opt(ModelPromptCache),
229
234
  contextWindow: opt(T.num()),
230
235
  maxTokens: opt(T.num()),
231
- samplingParams: opt(T.rec(T.unknown())),
236
+ samplingParams: opt(SamplingParams),
237
+ samplingParamsByThinkingLevel: opt(SamplingParamsByThinkingLevel),
232
238
  headers: opt(headers),
233
239
  compat: opt(ProviderCompat),
234
240
  });
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { composedCost, endpointsForModel, isZeroCost, modelEntryOf } from "./model-endpoints.mjs";
15
15
 
16
- /** The hosts pi's catalog serves openai-completions on: the runner's `COMPLETIONS_CATALOG_HOSTS`, copied. */
16
+ /** The hosts pi's catalog serves openai-completions on: the runner's `COMPLETIONS_CATALOG_HOSTS`, generated the same way. */
17
17
  export const COMPLETIONS_CATALOG_HOSTS = Object.freeze([
18
18
  "api.ant-ling.com",
19
19
  "api.cerebras.ai",
@@ -56,7 +56,7 @@ export function completionsOwnServer(model) {
56
56
  }
57
57
 
58
58
  /**
59
- * The parts of a model the rule reads, composed as pi 0.99.1's provider-composer.js composes them from the overlay
59
+ * The parts of a model the rule reads, composed as the pinned pi's provider-composer.js composes them from the overlay
60
60
  * `models.json` (`models`, parsed, or null) and the builtin catalog (`builtinModel(provider, id)`, injected):
61
61
  * - a model the overlay DEFINES: its own `api` and `baseUrl`, else its provider's, else those of pi's DEFAULTS
62
62
  * model (`findModelDefaults`, mirrored in `definedDefaults`); its compat field from its `modelOverrides` entry,
@@ -94,7 +94,7 @@ export function outputCapView({ models, provider, modelId, builtinModel = () =>
94
94
  const str = (v) => (typeof v === "string" ? v : undefined);
95
95
 
96
96
  /**
97
- * The `{ api, baseUrl }` pi 0.99.1 composes for the overlay-defined model `modelId` (provider-composer.js
97
+ * The `{ api, baseUrl }` the pinned pi composes for the overlay-defined model `modelId` (provider-composer.js
98
98
  * `applyModelsJson`): the provider's chat models start as the catalog's (each on the provider's `baseUrl` when it sets
99
99
  * one), and each definition in the file's order is composed and then replaces the model of its id or joins the list.
100
100
  * A definition's `api` is its own, else the provider's, else its DEFAULTS model's, and its `baseUrl` likewise, where the
@@ -64,11 +64,12 @@ export const CONTAINER_ENV_NAMES = new Set([
64
64
 
65
65
  /**
66
66
  * Issue #500: the names the RUNNER sets in its own environment inside the container, for its descendants: the child
67
- * ledger directory and the runner's pid (image/runner/src/usage-meter.mjs, openChildLedger). The worker never writes
67
+ * ledger directory and the runner's pid (image/runner/src/usage-meter.mjs, openChildLedger), and the hash of the price
68
+ * table the runner hands its children (publishPriceTable, issue #587's review). The worker never writes
68
69
  * them, so they are not in the set above (which a test pins to what buildContainerEnv emits). They are reserved all the
69
70
  * same: a trigger binding one through `run.secrets`, or a host value forwarded through `PI_FORWARD_ENV`, would arrive in
70
71
  * the runner's environment before the runner sets its own, and a value the runner then failed to replace would point
71
72
  * every pi child's ledger at a directory the agent chose. Refused at load in both lists, and deleted by
72
73
  * buildContainerEnv after both loops as the backstop.
73
74
  */
74
- export const RUNNER_ENV_NAMES = new Set(["PI_DISPATCH_CHILD_LEDGER", "PI_DISPATCH_RUNNER_PID"]);
75
+ export const RUNNER_ENV_NAMES = new Set(["PI_DISPATCH_CHILD_LEDGER", "PI_DISPATCH_PRICE_TABLE", "PI_DISPATCH_RUNNER_PID"]);
package/src/triggers.mjs CHANGED
@@ -173,12 +173,14 @@ export const REVIEW_STATES = new Set(["approved", "changes_requested", "commente
173
173
  const REVIEW_ACTION = "review_submitted";
174
174
 
175
175
  /**
176
- * The pi tool names a trigger may exclude (issue #291) -- the built-in set of the PINNED pi, 0.99.1, in pi's
177
- * own `allToolNames` order. `powershell` joined it at the 0.99.1 bump (issue #509): pi registers it as a
176
+ * The pi tool names a trigger may exclude (issue #291) -- the built-in set of the PINNED pi (the version
177
+ * image/runner/package.json pins), in pi's own `allToolNames` order. `powershell` joined it at the 0.99.1 bump (issue #509): pi registers it as a
178
178
  * built-in but does not activate it by default (`DEFAULT_TOOL_NAMES` is read, bash, edit, write), so a
179
179
  * trigger that means "no shell" names it beside `bash`.
180
- * Hand-written because this validator is pure and pi-free (the worker does not depend on the agent
181
- * package), and therefore BOLTED twice to the artifact it restates: `worker/test/exclude-tools.pinned.test.mjs`
180
+ * A literal because this validator is pure and pi-free (the worker does not depend on the agent
181
+ * package). The line is GENERATED from the pinned pi by .github/scripts/pi-derived.mjs (issue #587; a pi bump
182
+ * reruns it), held to that generation by worker/test/pi-derived.test.mjs, and BOLTED twice to the artifact it
183
+ * restates: `worker/test/exclude-tools.pinned.test.mjs`
182
184
  * and `image/runner/test/pinned-api.test.mjs` both derive the set from the pinned package and fail with a
183
185
  * grow-these-together message when a pin bump moves it. Extension and custom tool names are deliberately
184
186
  * NOT here: they register at container start (a staged package, a serviced repo's own `.pi/extensions`),