Skip to content

Commit 68d7abb

Browse files
committed
Fix: verify against the pinned artifact, not HEAD -- ModelRuntime does not exist in 0.80.7
CI caught this on the first container run and it is the most important mistake of the project so far, because it is methodological rather than local. Every upstream claim in this repo was verified by reading source at earendil-works/pi@5e336cf. That is HEAD. The image pins npm 0.80.7. Those are different artifacts and nothing here ever checked they agreed. ModelRuntime is a value export at that sha and does not exist in 0.80.7 at all: no model-runtime module in dist/, not exported from dist/index.js. pi's changelog files the migration under [Unreleased] -- which was exactly correct -- and OQ-005 had "corrected" the changelog for being out of date, concluding "the changelog is not a reliable signal". The changelog was right. The methodology was wrong. The runner imported the phantom API, the image built cleanly, and every job would have died on a missing export. The real 0.80.7 wiring, read from the tarball: AuthStorage.create(authPath) // sync ModelRegistry.create(authStorage, modelsPath) // sync modelRegistry.find(provider, modelId) // NOT getModel modelRegistry.hasConfiguredAuth(model) createAgentSession({ authStorage, modelRegistry, model, ... }) Everything else survives the pin, which is why the loader assertions passed: noContextFiles, noSkills, noExtensions, additionalSkillPaths, additionalExtensionPaths, appendSystemPromptOverride and SettingsManager.inMemory are all present at 0.80.7. The instruction model was never affected -- only the model/auth wiring. Fixes: - run-job.mjs uses AuthStorage + ModelRegistry, and checks hasConfiguredAuth before the container spends anything. - constitution.md's evidence convention now says a sha is not a version. A sha citation establishes where behaviour lives and nothing about whether the pinned release contains it: necessary, not sufficient. Claims the code depends on must hold in the published artifact -- npm pack it, or assert it in a test that imports what the lockfile resolves. - interfaces.md separates "Evidence (pinned artifact -- authoritative)" from "Evidence (HEAD -- explains behaviour, does NOT establish the pin contains it)". - OQ-005 retracted and re-corrected, with the wrong entry kept struck through rather than deleted: a spec that hides having been wrong teaches the next reader to trust it more than it deserves. - pinned-api.test.mjs asserts every symbol run-job.mjs imports exists in the resolved package, and asserts ModelRuntime is ABSENT -- so the day the migration ships, a test fails with a message instead of a container failing with a stack trace. Also fixed: the CI in-image assertions all ran `docker run img <cmd>` while ENTRYPOINT is the runner and ignores CMD, so they silently tested the runner instead of what they named. Every one now overrides --entrypoint.
1 parent 7b1ff27 commit 68d7abb

6 files changed

Lines changed: 214 additions & 58 deletions

File tree

.github/workflows/pi-upgrade-check.yml

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,22 +96,24 @@ jobs:
9696
# `pi --mode print` does not exist: --mode accepts text|json|rpc, and --print/-p is a separate
9797
# boolean. --help rather than a real prompt, because a real prompt needs a paid API key and a
9898
# contract test that costs money is a contract test that gets disabled.
99+
# --entrypoint is required: ENTRYPOINT is the runner and ignores CMD, so `docker run img pi ...`
100+
# would silently run the runner instead of pi and the assertion would test nothing.
99101
- name: pi -p is still a flag
100-
run: docker run --rm pi-job:ci pi -p --help >/dev/null
102+
run: docker run --rm --entrypoint pi pi-job:ci -p --help >/dev/null
101103

102104
# --- CONST-ISOLATION-CONTAINER-PER-JOB ---
103105
# --cap-drop=ALL is the enforcement surface. Read the effective capability set directly rather
104106
# than install libcap just to ask.
105107
- name: No capabilities under --cap-drop=ALL
106108
run: |
107-
caps=$(docker run --rm --cap-drop=ALL --security-opt no-new-privileges pi-job:ci \
108-
sh -c 'grep ^CapEff /proc/self/status' | awk '{print $2}')
109+
caps=$(docker run --rm --cap-drop=ALL --security-opt no-new-privileges \
110+
--entrypoint sh pi-job:ci -c 'grep ^CapEff /proc/self/status' | awk '{print $2}')
109111
[ "$caps" = "0000000000000000" ] || { echo "::error::Container retains capabilities: $caps"; exit 1; }
110112
echo "OK: CapEff=$caps"
111113
112114
- name: Runs as a non-root user
113115
run: |
114-
uid=$(docker run --rm pi-job:ci id -u)
116+
uid=$(docker run --rm --entrypoint id pi-job:ci -u)
115117
[ "$uid" != "0" ] || { echo "::error::Job container runs as root."; exit 1; }
116118
117119
# --- INT-CONTAINER-JOB-INPUTS ---
@@ -121,8 +123,8 @@ jobs:
121123
run: |
122124
mkdir -p fixture/pi
123125
echo "x" > fixture/pi/APPEND_SYSTEM.md
124-
if docker run --rm --cap-drop=ALL -v "$PWD/fixture:/job:ro" pi-job:ci \
125-
sh -c 'echo pwned > /job/pi/APPEND_SYSTEM.md' 2>/dev/null; then
126+
if docker run --rm --cap-drop=ALL -v "$PWD/fixture:/job:ro" --entrypoint sh pi-job:ci \
127+
-c 'echo pwned > /job/pi/APPEND_SYSTEM.md' 2>/dev/null; then
126128
echo "::error::/job is writable. The agent can rewrite its own instructions."
127129
exit 1
128130
fi
@@ -133,11 +135,11 @@ jobs:
133135
# dir is root-owned the job dies EACCES at runtime, on a path nothing in the Dockerfile hints
134136
# at. COPY --chown does not fix it (it skips auto-created parents), so assert the real thing.
135137
- name: The agent dir is writable by the runtime user
136-
run: docker run --rm pi-job:ci sh -c 'touch "$HOME/.pi/agent/auth.json" && rm "$HOME/.pi/agent/auth.json"'
138+
run: docker run --rm --entrypoint sh pi-job:ci -c 'touch "$HOME/.pi/agent/auth.json" && rm "$HOME/.pi/agent/auth.json"'
137139

138140
- name: The guardrails are baked where the runner reads them
139141
run: |
140-
docker run --rm pi-job:ci grep -q "pi-dispatch-guardrails-v1" /opt/pi-dispatch/HARD_RULES.md \
142+
docker run --rm --entrypoint grep pi-job:ci -q "pi-dispatch-guardrails-v1" /opt/pi-dispatch/HARD_RULES.md \
141143
|| { echo "::error::Guardrails sentinel missing from /opt/pi-dispatch/HARD_RULES.md"; exit 1; }
142144
143145
# --- DES-PLAYWRIGHT-CLI-NOT-CHROME-DEVTOOLS / REQ-FRONTEND-VISUAL-VERIFY ---
@@ -149,14 +151,14 @@ jobs:
149151
run: |
150152
echo '<html><body><h1>pi-dispatch</h1></body></html>' > fixture/page.html
151153
docker run --rm --init --cap-drop=ALL --security-opt no-new-privileges --shm-size=1g \
152-
-v "$PWD/fixture:/fixture:ro" pi-job:ci \
153-
sh -c 'playwright-cli screenshot --browser-arg=--no-sandbox file:///fixture/page.html /tmp/x.png && test -s /tmp/x.png' \
154+
-v "$PWD/fixture:/fixture:ro" --entrypoint sh pi-job:ci \
155+
-c 'playwright-cli screenshot --browser-arg=--no-sandbox file:///fixture/page.html /tmp/x.png && test -s /tmp/x.png' \
154156
|| { echo "::error::Chromium unusable as non-root — check PLAYWRIGHT_BROWSERS_PATH at build AND run"; exit 1; }
155157
156158
# Fonts absent => tofu boxes => screenshots that look fine and contain no legible text. That
157159
# silently guts the requirement full Chromium is in this image for.
158160
- name: Fonts are installed (or screenshots are tofu)
159161
run: |
160-
n=$(docker run --rm pi-job:ci sh -c 'fc-list | wc -l')
162+
n=$(docker run --rm --entrypoint sh pi-job:ci -c 'fc-list | wc -l')
161163
[ "$n" -gt 0 ] || { echo "::error::No fonts. Chromium will render boxes and REQ-FRONTEND-VISUAL-VERIFY is a lie."; exit 1; }
162164
echo "OK: $n fonts"

image/runner/run-job.mjs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { readFileSync } from "node:fs";
22
import {
3+
AuthStorage,
34
createAgentSession,
45
getAgentDir,
5-
ModelRuntime,
6+
ModelRegistry,
67
SessionManager,
78
SettingsManager,
89
} from "@earendil-works/pi-coding-agent";
@@ -29,20 +30,32 @@ async function main() {
2930
const maxTurns = Number.parseInt(requireEnv("PI_MAX_TURNS"), 10);
3031

3132
const agentDir = getAgentDir();
32-
const modelRuntime = await ModelRuntime.create({
33-
authPath: `${agentDir}/auth.json`,
34-
modelsPath: `${agentDir}/models.json`,
35-
});
3633

37-
// Pin the model explicitly. With `model` omitted, findInitialModel picks from settings
38-
// and provider defaults -- nondeterministic across images, and it silently changes cost
39-
// per job. A missing model yields modelFallbackMessage on the RESULT rather than a
40-
// throw, so validate here and fail loudly instead of discovering it in a bill.
41-
const model = modelRuntime.getModel(provider, modelId);
34+
// AuthStorage + ModelRegistry, NOT ModelRuntime.
35+
//
36+
// pi's [Unreleased] changelog says these two are replaced by an async `modelRuntime`.
37+
// That is true of its main branch and NOT of 0.80.7, which is what we pin -- the source
38+
// at HEAD and the artifact on npm are different things, and conflating them cost a build.
39+
// When the migration ships, REQ-UPSTREAM-CONTRACT-TESTS fires on the pin bump and this
40+
// is the code that changes. See OQ-005.
41+
const authStorage = AuthStorage.create(`${agentDir}/auth.json`);
42+
const modelRegistry = ModelRegistry.create(authStorage, `${agentDir}/models.json`);
43+
44+
// Pin the model explicitly. With `model` omitted, pi picks from settings and provider
45+
// defaults -- nondeterministic across images, and it silently changes cost per job. A
46+
// missing model surfaces as a fallback message on the RESULT rather than a throw, so
47+
// validate here and fail loudly instead of discovering it on a bill.
48+
const model = modelRegistry.find(provider, modelId);
4249
if (!model) {
4350
log("model_unknown", { provider, modelId });
4451
return EXIT_POLICY; // config error: retrying cannot fix it
4552
}
53+
if (!modelRegistry.hasConfiguredAuth(model)) {
54+
// Catch this before the container spends anything. Preflight would throw on it
55+
// anyway, but a clear signal beats parsing a message out of an exception.
56+
log("model_no_auth", { provider, modelId });
57+
return EXIT_POLICY;
58+
}
4659

4760
// Pin pi's own retry settings rather than inherit `maxRetries ?? 3`. An upstream default
4861
// change would silently move our spend -- CONST-PI-VERSION-PINNED's reasoning, applied to
@@ -62,7 +75,8 @@ async function main() {
6275
const { session } = await createAgentSession({
6376
cwd: WORKSPACE,
6477
agentDir,
65-
modelRuntime,
78+
authStorage,
79+
modelRegistry,
6680
model,
6781
settingsManager,
6882
sessionManager: SessionManager.inMemory(WORKSPACE),
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import assert from "node:assert/strict";
2+
import { readFileSync } from "node:fs";
3+
import { fileURLToPath } from "node:url";
4+
import { test } from "node:test";
5+
6+
/**
7+
* REQ-UPSTREAM-CONTRACT-TESTS -- assert against the PINNED ARTIFACT, not against HEAD.
8+
*
9+
* This exists because of a real and expensive mistake. Every claim about pi in this
10+
* project was verified by reading source at `earendil-works/pi @ 5e336cf` -- which is
11+
* HEAD, not the 0.80.7 release we pin. `ModelRuntime` is a value export in that source
12+
* and DOES NOT EXIST in 0.80.7 at all: pi's changelog files it under [Unreleased] and
13+
* the changelog was exactly right. The runner imported it, the image built cleanly, and
14+
* every job would have died on a missing export.
15+
*
16+
* Reading a moving branch to verify a fixed version is not verification. These tests
17+
* import the package the lockfile actually resolves and assert the symbols exist there,
18+
* so the next time HEAD and the pin disagree, a test says so instead of a container.
19+
*/
20+
const pkg = "@earendil-works/pi-coding-agent";
21+
22+
let mod;
23+
let importError;
24+
try {
25+
mod = await import(pkg);
26+
} catch (error) {
27+
importError = error;
28+
}
29+
30+
const required = process.env.PI_DISPATCH_REQUIRE_LOADER_TESTS === "1";
31+
if (!mod && required) {
32+
throw new Error(`${pkg} must be importable here; a skip would hide a pin/HEAD mismatch.\n${importError}`);
33+
}
34+
const skip = mod ? false : `pi not installed (node ${process.version} < 22.19.0); CI runs these`;
35+
36+
/** Every value the runner imports at runtime. If pi drops one, the job dies on module load. */
37+
const REQUIRED_VALUE_EXPORTS = [
38+
"createAgentSession",
39+
"getAgentDir",
40+
"AuthStorage",
41+
"ModelRegistry",
42+
"SessionManager",
43+
"SettingsManager",
44+
"DefaultResourceLoader",
45+
];
46+
47+
test("the pinned package exports everything the runner imports", { skip }, () => {
48+
const missing = REQUIRED_VALUE_EXPORTS.filter((name) => typeof mod[name] === "undefined");
49+
assert.deepEqual(missing, [], `pinned ${pkg} is missing value exports the runner needs: ${missing}`);
50+
});
51+
52+
test("model/auth wiring is the 0.80.7 shape, not HEAD's", { skip }, () => {
53+
// The [Unreleased] migration replaces these two with an async ModelRuntime. When the pin
54+
// moves past it, THIS fails -- which is the signal to rewrite run-job.mjs's wiring,
55+
// rather than discovering it when every queued job becomes a no-op.
56+
assert.equal(typeof mod.AuthStorage?.create, "function", "AuthStorage.create missing");
57+
assert.equal(typeof mod.ModelRegistry?.create, "function", "ModelRegistry.create missing");
58+
assert.equal(
59+
typeof mod.ModelRuntime,
60+
"undefined",
61+
"ModelRuntime now EXISTS at the pin -- the [Unreleased] migration shipped. " +
62+
"Rewrite run-job.mjs to modelRuntime and re-verify sdk.d.ts before bumping.",
63+
);
64+
});
65+
66+
test("the resource-loader options the instruction model depends on still exist", { skip }, () => {
67+
// These are asserted behaviourally in loader.test.mjs, but a rename would fail there with
68+
// a confusing symptom (an empty prompt) rather than a clear one. This names them.
69+
const loader = Object.getOwnPropertyNames(mod.DefaultResourceLoader?.prototype ?? {});
70+
for (const method of ["reload", "getAppendSystemPrompt", "getAgentsFiles", "getSkills"]) {
71+
assert.ok(loader.includes(method), `DefaultResourceLoader.${method} missing at the pin`);
72+
}
73+
});
74+
75+
test("the runner imports nothing the pinned package does not export", { skip }, () => {
76+
// Catches a new import added to run-job.mjs that only exists at HEAD -- the exact
77+
// mistake this file was written for, generalised so it cannot recur silently.
78+
const source = readFileSync(fileURLToPath(new URL("../run-job.mjs", import.meta.url)), "utf8");
79+
const block = source.match(/import\s*\{([^}]+)\}\s*from\s*["']@earendil-works\/pi-coding-agent["']/);
80+
assert.ok(block, "could not find the runner's pi import block");
81+
82+
const imported = block[1]
83+
.split(",")
84+
.map((s) => s.trim())
85+
.filter(Boolean);
86+
const missing = imported.filter((name) => typeof mod[name] === "undefined");
87+
assert.deepEqual(missing, [], `run-job.mjs imports symbols absent from the pinned package: ${missing}`);
88+
});

specs/constitution.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,29 @@ was verified against pi's docs across two adversarial passes, recorded 48/50 cla
1919
wrong on roughly seven points within twenty-four hours — every one of them found by reading source. For
2020
a dependency that moves this fast, docs are a hint.
2121

22+
**Verify against the PINNED ARTIFACT, not against HEAD. A sha is not a version.** This rule is written
23+
in blood: every upstream claim in this repository was originally verified by reading source at
24+
`earendil-works/pi @ 5e336cf`, while the image pins **npm `0.80.7`**. Those are different artifacts.
25+
`ModelRuntime` is a value export at that sha and **does not exist in 0.80.7 at all** — pi's changelog
26+
files it under `[Unreleased]`, which was exactly correct, and a spec entry here "corrected" the
27+
changelog for being out of date. The changelog was right; the methodology was wrong. The runner imported
28+
it, the image built cleanly, and every job would have died on a missing export.
29+
30+
Reading a moving branch to verify a fixed version is not verification — it is verification of something
31+
else. So:
32+
33+
- A sha citation establishes *where the behaviour lives and why*, and nothing about whether the pinned
34+
release contains it. It is necessary and **not sufficient**.
35+
- Any claim the code depends on must additionally hold in the **published artifact**: `npm pack` it and
36+
read `dist/`, or assert it in a test that imports what the lockfile resolves.
37+
- Prefer the release tag over `main`. Where a sha is cited, state its relationship to the pin.
38+
- `REQ-UPSTREAM-CONTRACT-TESTS` is the enforcement: `image/runner/test/pinned-api.test.mjs` asserts the
39+
runner's imports exist in the resolved package, so the next pin/HEAD divergence fails a test rather
40+
than a container.
41+
42+
The failure this guards is the familiar one: it is **silent**. HEAD and the pin agree often enough that
43+
the habit forms, and the day they disagree the build still passes.
44+
2245
**Two evidence classes, deliberately.** `Evidence (upstream)` and `Code evidence` have different drift
2346
semantics and must not share a field: upstream facts are pinned to *pi's* sha and must never be
2447
drift-checked against *our* HEAD. `detect_drift.py` keys only on the literal `- **Code evidence**:`, so

specs/interfaces.md

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@ Evidence convention as in `constitution.md`.
2121
*invisibly*.
2222

2323
- **Contract**:
24+
**Verified against the published `0.80.7` tarball, not against HEAD** — see the evidence convention
25+
in `constitution.md`. At this pin the model/auth wiring is `AuthStorage` + `ModelRegistry`. There is
26+
**no `ModelRuntime`**: that is HEAD-only, `[Unreleased]`, and importing it makes every job die on a
27+
missing export while the image builds cleanly.
28+
2429
```typescript
25-
const modelRuntime = await ModelRuntime.create({ authPath, modelsPath });
26-
const model = modelRuntime.getModel(process.env.PI_PROVIDER, process.env.PI_MODEL);
30+
const authStorage = AuthStorage.create(`${agentDir}/auth.json`);
31+
const modelRegistry = ModelRegistry.create(authStorage, `${agentDir}/models.json`);
32+
const model = modelRegistry.find(process.env.PI_PROVIDER, process.env.PI_MODEL); // NOT getModel
2733
if (!model) throw new InfraError(`unknown model`); // exit 2 — config, not retryable
34+
if (!modelRegistry.hasConfiguredAuth(model)) throw new InfraError(`no auth`); // exit 2
2835

2936
// Guardrails read EXPLICITLY from a path we own — never via discovery. See (e).
3037
const guardrails = readFileSync("/opt/pi-dispatch/HARD_RULES.md", "utf8");
@@ -46,7 +53,8 @@ Evidence convention as in `constitution.md`.
4653
const { session } = await createAgentSession({
4754
cwd: "/workspace",
4855
agentDir: getAgentDir(),
49-
modelRuntime,
56+
authStorage,
57+
modelRegistry,
5058
model,
5159
sessionManager: SessionManager.inMemory("/workspace"),
5260
settingsManager: SettingsManager.inMemory({ retry: { maxRetries, baseDelayMs } }),
@@ -108,24 +116,34 @@ Evidence convention as in `constitution.md`.
108116
`noSkills: true` + `additionalSkillPaths` loads *exactly* what we hand it and nothing from the tree.
109117
Explicit beats gated: the same principle as `noContextFiles` + an explicit read.
110118

111-
`modelRuntime.getModel(provider, modelId)` is a **method**, not a free function; there is no exported
112-
`getModel`. Pin the model explicitly: with `model` omitted, `findInitialModel` picks from settings and
113-
provider defaults, which is nondeterministic across images and silently changes cost per job. A missing
114-
model yields a `modelFallbackMessage` on the *result*, not a throw — validate and fail loudly.
119+
`modelRegistry.find(provider, modelId)` is a **method**, not a free function; there is no exported
120+
`getModel`. Pin the model explicitly: with `model` omitted, pi picks from settings and provider
121+
defaults, which is nondeterministic across images and silently changes cost per job. A missing model
122+
yields a fallback message on the *result*, not a throw — validate and fail loudly.
115123
`SessionManager.inMemory()` because the container is ephemeral: session storage would write to a
116124
filesystem that is about to cease existing.
117125
`SettingsManager.inMemory()` is load-bearing beyond the retry pin: it writes our settings to the
118126
**global** scope of a storage with **no project file**, so a serviced project's `.pi/settings.json` is
119127
never read and **cannot override our spend controls**. `SettingsManager.create(cwd, agentDir)` would
120128
read it and `deepMergeSettings(global, project)` lets project win. Use `inMemory`. Deliberately.
121129

122-
The complete option set is `cwd`, `agentDir`, `modelRuntime`, `model`, `thinkingLevel`, `scopedModels`,
123-
`noTools`, `tools`, `excludeTools`, `customTools`, `resourceLoader`, `sessionManager`,
124-
`settingsManager`, `sessionStartEvent`. Note `modelRuntime` is **already present at the pin** — part of
125-
`OQ-005`'s migration has landed.
126-
- **Evidence (upstream)**: `earendil-works/pi @ 5e336cf → packages/coding-agent/src/core/sdk.ts:33-80`
130+
The complete option set **at 0.80.7** is `cwd`, `agentDir`, `authStorage`, `modelRegistry`, `model`,
131+
`thinkingLevel`, `scopedModels`, `noTools`, `tools`, `excludeTools`, `customTools`, `resourceLoader`,
132+
`sessionManager`, `settingsManager`, `sessionStartEvent`. `OQ-005`'s migration replaces the first two
133+
with an async `modelRuntime` and **has not shipped** — it exists only on `main`.
134+
- **Evidence (pinned artifact — authoritative)**: `npm @earendil-works/pi-coding-agent@0.80.7 →
135+
dist/core/sdk.d.ts → CreateAgentSessionOptions``authStorage?: AuthStorage` ("Default:
136+
AuthStorage.create(agentDir/auth.json)"), `modelRegistry?: ModelRegistry` ("Default:
137+
ModelRegistry.create(authStorage, agentDir/models.json)"); **no `modelRuntime` field, and no
138+
`model-runtime` module in `dist/` at all** · `→ dist/core/model-registry.d.ts → find(provider, modelId)`,
139+
`hasConfiguredAuth(model)`, `getAvailable()`, `getAll()`, `static create(authStorage, modelsJsonPath?)`
140+
· `→ dist/index.js``AuthStorage` and `ModelRegistry` are value exports; `ModelRuntime` is absent ·
141+
`→ dist/core/resource-loader.d.ts``noContextFiles`, `noSkills`, `noExtensions`,
142+
`additionalSkillPaths`, `additionalExtensionPaths`, `appendSystemPromptOverride` all present at the pin
143+
- **Evidence (HEAD — explains behaviour, does NOT establish the pin contains it)**:
144+
`earendil-works/pi @ 5e336cf → packages/coding-agent/src/core/sdk.ts:33-80`
127145
(option set; no append fields, `resourceLoader?: ResourceLoader`) · `→ sdk.ts:164` (`createAgentSession`
128-
is async) · `→ sdk.ts:171` (`ModelRuntime.create`, async) · `→ sdk.ts:176-180` (default loader is built
146+
is async) · `→ sdk.ts:176-180` (default loader is built
129147
**and `reload()`ed** only when none is passed) · `→ sdk.ts:187-217` (`findInitialModel` fallback;
130148
`modelFallbackMessage` returned, not thrown) · `→ resource-loader.ts:122-157`
131149
(`DefaultResourceLoaderOptions`; `cwd`/`agentDir` **required**) · `→ resource-loader.ts:156`

0 commit comments

Comments
 (0)