@supernovae-st/nika 0.120.0 → 0.120.3

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/README.md CHANGED
@@ -10,8 +10,9 @@
10
10
  <h1 align="center">@supernovae-st/nika</h1>
11
11
 
12
12
  <p align="center">
13
- <strong>The TypeScript door to Nika: audit a workflow, run it, watch it, prove it.</strong><br>
14
- Locally through the released engine, or against an authenticated <code>nika serve</code>.
13
+ <strong>Run AI workflows from TypeScript.</strong><br>
14
+ Audit a <code>.nika</code> file, run it, read the result, verify the receipt — locally
15
+ or against authenticated <code>nika serve</code>.
15
16
  </p>
16
17
 
17
18
  <p align="center">
@@ -29,22 +30,17 @@
29
30
  <a href="https://archive.softwareheritage.org/browse/origin/?origin_url=https://github.com/supernovae-st/nika-client"><img src="https://archive.softwareheritage.org/badge/origin/https://github.com/supernovae-st/nika-client/" alt="Archived by Software Heritage"></a>
30
31
  </p>
31
32
 
32
- ## Thirty seconds, no API key
33
+ ## Run a workflow from your app
33
34
 
34
- One package installs the client, the `nika` command and the engine payload for
35
- your platform (macOS and Linux, arm64 and x64):
35
+ Put repeatable AI work in a `.nika` file. From Node, run it and read the
36
+ result. No server and no API key for the first run: `mock/echo` is a
37
+ **local simulation** (output is prefixed `mock(echo) ·`, not a model answer).
36
38
 
37
39
  ```sh
38
- npm install @supernovae-st/nika@0.118.7
39
- ./node_modules/.bin/nika --version
40
- ```
41
-
42
- ```
43
- nika 0.118.7 (f3a31a6ee)
40
+ npm install @supernovae-st/nika
44
41
  ```
45
42
 
46
- Write `hello.nika`. The `mock/echo` model rehearses with no key and no
47
- network:
43
+ Save as `hello.nika`:
48
44
 
49
45
  ```yaml
50
46
  nika: hello
@@ -59,7 +55,35 @@ outputs:
59
55
  greeting: ${{ tasks.greeting.output }}
60
56
  ```
61
57
 
62
- Audit it before anything runs:
58
+ Save as `demo.mjs`:
59
+
60
+ ```js
61
+ import { Nika, isNikaRunSucceeded } from '@supernovae-st/nika';
62
+
63
+ const run = await new Nika({ cwd: process.cwd() }).run('hello.nika', { maxCostUsd: 0 });
64
+ const result = await run.result();
65
+ if (!isNikaRunSucceeded(result)) {
66
+ console.error(result.status, result.error?.code, result.error?.message);
67
+ process.exitCode = 1;
68
+ } else {
69
+ console.log(result.outputs);
70
+ // { greeting: "mock(echo) · Say hello from the Nika SDK." }
71
+ }
72
+ ```
73
+
74
+ ```sh
75
+ node demo.mjs
76
+ ```
77
+
78
+ `run()` already admits: a red file throws `NikaOperationError` and never
79
+ returns a handle. The `.nika` file is the contract; the SDK does not parse
80
+ YAML. Pin the version you tested — see [Install](#install): the SDK and the
81
+ standalone engine CLI release on independent clocks.
82
+
83
+ `check()`, `run.events()`, and `traceVerify()` are the next steps. They stay
84
+ taught and tested; they are not required to see the first result.
85
+
86
+ ### Next: audit without running (`check`)
63
87
 
64
88
  ```sh
65
89
  ./node_modules/.bin/nika check hello.nika
@@ -74,45 +98,31 @@ Audit it before anything runs:
74
98
  layers · valid ✔ · access ready ✔ · capacity fit ✔ · run ready ✔
75
99
  ```
76
100
 
77
- Now drive the same engine from TypeScript:
101
+ The same report is `await nika.check('hello.nika', { nativeStrict: true })`.
102
+ A red check never becomes a run.
78
103
 
79
- ```ts
80
- import { Nika } from '@supernovae-st/nika';
81
-
82
- const nika = new Nika({
83
- cwd: process.cwd(),
84
- // bin: '/absolute/path/to/nika', // or set NIKA_BIN
85
- });
104
+ ### Next: watch the run (`run.events()`)
86
105
 
87
- const report = await nika.check('hello.nika', {
88
- nativeStrict: true,
89
- });
90
- if (!report.clean) throw new Error('workflow did not pass nika check');
91
-
92
- const run = await nika.run('hello.nika', { maxCostUsd: 0 });
106
+ ```ts
93
107
  for await (const event of run.events()) {
94
- // The same lifecycle words on both transports; the engine's own frame,
95
- // in its protocol vocabulary, stays on event.raw.
96
108
  console.log([event.kind, event.task, event.status].filter(Boolean).join(' '));
97
109
  }
98
-
99
- const result = await run.result();
100
- console.log(result.status, result.outputs, result.receipt);
101
110
  ```
102
111
 
103
- Expected output: `run.started`, `task.scheduled greeting`,
104
- `task.started greeting`, `task.completed greeting`, `engine.event`, then
105
- `run.settled succeeded`, then the terminal `succeeded` line with the outputs
106
- and the receipt. The `engine.event` is the native journal's own
107
- `workflow_completed` frame: the SDK gives it no lifecycle name and no state,
108
- drops nothing, and shows it on `event.raw.kind`.
109
-
110
- That is six frames for one task; the same shape with 90 tasks was measured at
111
- 273 (`3N + 3`). A session retains the most recent 4096 frames by default, so
112
- `run.events()` opened after `run.result()` replays them all, and is refused,
113
- never shortened, past that bound; see
112
+ Expected native events for this one-task file: `run.started`,
113
+ `task.scheduled greeting`, `task.started greeting`,
114
+ `task.completed greeting`, `engine.event` (`workflow_completed` on
115
+ `event.raw.kind`), then `run.settled succeeded`. Six `run.events()` frames,
116
+ sealed or not (probed on 0.120.0). A session retains the most recent 4096
117
+ frames by default; see
114
118
  [Observing a run after the fact](#observing-a-run-after-the-fact).
115
119
 
120
+ ### Next: verify a seal (`traceVerify`)
121
+
122
+ `traceVerify` **verifies** an existing seal. It does not create one. A keyless
123
+ machine still **succeeds**; the receipt is unsealed — see
124
+ [Admission, execution, seal](#admission-execution-seal).
125
+
116
126
  <p align="center">
117
127
  <img src="https://raw.githubusercontent.com/supernovae-st/nika-client/main/media/local-driver.gif" alt="The typed driver over the released binary: check the workflow, gate on the report, run it to the end under a cost ceiling, count the events" width="960">
118
128
  </p>
@@ -183,8 +193,11 @@ An engine without the capability rejects with `NikaCompatibilityError`
183
193
  (`capability: 'inputsLiteral'` or `'jobInputs'`) and nothing runs. The SDK
184
194
  never falls back to `--var`, and a resident that merely answers 202 has
185
195
  negotiated nothing: one from before the envelope accepts the field and ignores
186
- its values. The engine payload pinned by this package version advertises
187
- neither capability yet, so `inputs` is refused on it until the pin moves.
196
+ its values.
197
+
198
+ The **published 0.120.0** payload advertises `inputsLiteral` (`nika --sdk-identity`).
199
+ Native `run({ inputs })` works on it. It does **not** advertise `jobInputs`;
200
+ HTTP literal inputs stay a compatibility refusal until a resident that does.
188
201
 
189
202
  `vars` is deprecated. It remains the native `--var KEY=VALUE` operator channel,
190
203
  unchanged: the engine reads `@env:NAME` from its environment and coerces text
@@ -197,10 +210,9 @@ A `try { await run.result() } catch {}` alone therefore never catches a failed
197
210
  workflow: a CI job or an application must read `result.status` and treat
198
211
  anything but `succeeded` as its own failure, or a red run passes silently.
199
212
 
200
- On the development branch, `isNikaRunSucceeded` provides that TypeScript
201
- narrowing. It is **unreleased** and is not exported by the published
202
- `0.118.7` package; that package uses `result.status === 'succeeded'` directly.
203
- For CI built from this branch:
213
+ `isNikaRunSucceeded` is exported by published **0.120.0**. Use it (or
214
+ `result.status === 'succeeded'`) before treating a result as success.
215
+ Paused, failed, cancelled, and interrupted all return false:
204
216
 
205
217
  ```ts
206
218
  import { Nika, isNikaRunSucceeded } from '@supernovae-st/nika';
@@ -229,17 +241,40 @@ into a fabricated output map.
229
241
  secret and the cost floor. A red check never becomes a run.
230
242
  - **Sovereign by default.** The same file runs on local models (Ollama,
231
243
  llama.cpp, vLLM), on Mistral, Hugging Face, OpenAI, xAI, Anthropic and the
232
- rest of the engine's catalog; `mock/echo` rehearses with no key and no
233
- network.
234
- - **Traced after.** Every native run leaves a hash-chained journal and hands
235
- back a receipt; `traceVerify()` asks the engine to verify it. The SDK never
236
- re-implements the proof.
244
+ rest of the engine's catalog; `mock/echo` is a local simulation with no key
245
+ and no network.
246
+ - **Traced after.** A native run writes a hash-chained journal and a receipt.
247
+ `traceVerify()` asks the engine to verify that evidence. A succeeded run can
248
+ still be unsealed. The SDK never re-implements the proof.
237
249
  - **One vocabulary, two transports.** `check`, `run`, `events`, `cancel`,
238
250
  `traceVerify` and `schedule` read the same against a local process and an
239
251
  authenticated `nika serve`; only the constructor changes. Run events carry
240
252
  the same lifecycle words on both (`run.started`, `run.waiting`,
241
253
  `run.settled`), with the engine's own frame kept on `event.raw`.
242
254
 
255
+ ## Admission, execution, seal
256
+
257
+ Three different facts. Do not collapse them.
258
+
259
+ | Fact | How you see it | What it is not |
260
+ |---|---|---|
261
+ | **Admission** | `check()` returns `clean: false` and findings. `run()` **throws** `NikaOperationError` (`NIKA-PARSE-005`, `NIKA-1708`, …) and yields no handle. | Not a successful execution. |
262
+ | **Execution** | `run.result()` / `isNikaRunSucceeded(result)`. Admitted failure is **data** (`status: "failed"`). A human gate is `paused` (`run.waiting`), not success. | Not proof the journal is sealed. |
263
+ | **Seal** | `result.receipt.sealed` and `traceVerify(receipt)`. Tamper-evident, not tamper-proof; not replayed unless you pass `--replay`; not anchored without a sidecar. | Not “the workflow was correct” and not “a human read the output”. |
264
+
265
+ Probed on published 0.120.0 (2026-09-19): `run.events()` still yields the
266
+ same six lifecycle frames with or without a signing key. With
267
+ `~/.nika/keys/run-signing.*`, the hello fixture sealed and `traceVerify`
268
+ returned `{ verified: true, verdict: "verified" }`. With an isolated `HOME`
269
+ and no key files (keychain skipped: stderr not a TTY), the same file
270
+ **succeeded**, `sealed: false`, and `traceVerify` returned
271
+ `{ verified: false, verdict: "invalid", reason: "receipt_mismatch" }` while
272
+ the engine still said the journal chain was OK and **UNSEALED**. In this
273
+ keyless case that reason means no signed binding, not a failed workflow.
274
+ `receipt_mismatch` is also the engine's word for a tampered or
275
+ field-mismatched receipt — do not treat every occurrence as merely unsigned.
276
+ The CLI verify line that counts journal events is not `run.events()`.
277
+
243
278
  ## One vocabulary
244
279
 
245
280
  `Nika` exposes `check`, `run`, `attachRun`, `traceVerify`, `listWorkflows`,
@@ -287,19 +322,34 @@ not parse YAML or reconstruct proof in TypeScript.
287
322
  Socratic risk matrix
288
323
  - [Migrating to 0.116](docs/migrating-to-0.116.md) · the intentional breaking
289
324
  migration to the smaller durable client surface
290
- - [docs.nika.sh](https://docs.nika.sh) · the language, the engine and the
291
- other doors
325
+ - [docs.nika.sh](https://docs.nika.sh) · language and engine
326
+ - [SDK quickstart](https://docs.nika.sh/sdk/start/quickstart) · app walkthrough
327
+ (install **npm `@supernovae-st/nika`**, matching this README)
292
328
 
293
329
  ## Install
294
330
 
295
- Pin the version you tested, then verify the package the project actually
296
- resolved:
331
+ ```sh
332
+ npm install @supernovae-st/nika
333
+ ```
334
+
335
+ Every SDK package bundles its own matching engine: the `nika` binary under
336
+ `node_modules/.bin` is the exact engine that package was qualified against.
337
+ The standalone engine CLI (GitHub releases, brew, install script) releases on
338
+ an independent clock, so its newest tag can be ahead of or behind the engine
339
+ bundled in the latest npm package. The
340
+ [npm registry](https://www.npmjs.com/package/@supernovae-st/nika?activeTab=versions)
341
+ lists current published versions. Pin the version you tested, then verify the
342
+ package the project actually resolved:
297
343
 
298
344
  ```sh
299
- npm install @supernovae-st/nika@0.118.7
300
345
  node -p "require('@supernovae-st/nika/package.json').version"
346
+ ./node_modules/.bin/nika --version
301
347
  ```
302
348
 
349
+ This repository's source train is **0.120.3** (`package.json`), lockstep with
350
+ public engine tag `v0.120.3` (`578352a31`). A source train is not a published
351
+ npm version until the release workflow publishes it.
352
+
303
353
  This package metadata subpath is exported for CommonJS, ESM build tools and CI
304
354
  pin checks. It reports the installed dependency, not a moving registry tag.
305
355
  The native payloads `@supernovae-st/nika-<os>-<arch>` are optional
@@ -318,19 +368,66 @@ deprecated`. The repository keeps its name (`supernovae-st/nika-client`).
318
368
 
319
369
  ## Scaffold with the engine
320
370
 
321
- The lowest-friction creation door is the engine-owned scaffold:
371
+ On the published 0.120.0 binary, `nika new` is **retired** (`unrecognized
372
+ subcommand`). The creation door is `compile`. Exact skeleton name or `hello`;
373
+ free-text intent stays incomplete and writes nothing:
322
374
 
323
375
  ```sh
324
376
  ./node_modules/.bin/nika init --project-file
325
- ./node_modules/.bin/nika new 01-hello hello.nika
377
+ ./node_modules/.bin/nika compile hello hello.nika
378
+ ```
379
+
380
+ No destination means preview only. `nika.yaml` is the project control plane
381
+ (needed for `nika serve`). `hello.nika` is the executable contract passed to
382
+ `check()` and `run()`. The engine's `hello` skeleton names its task `greet`
383
+ and asks for French; the hand-written file above is enough if it keeps
384
+ `outputs.greeting` and `model: mock/echo`.
385
+
386
+ ## Compile a candidate without running it
387
+
388
+ `compile()` requires an engine that advertises the `compile` capability. The
389
+ released 0.120.3 engine supports native compilation and its Serve advertises
390
+ HTTP compilation (the route landed in engine commit `4334e58b`, first published
391
+ in v0.120.3); a 0.120.2 or older Serve predates that route and is refused
392
+ without fallback. This SDK method first publishes with 0.120.3. This foundation resolves
393
+ exact embedded skeleton names (including `hello`) and edits existing constants;
394
+ unsupported intent remains `incomplete`.
395
+
396
+ ```ts
397
+ const candidate = await nika.compile({
398
+ intent: 'classify-and-route',
399
+ answers: { 'const.request': 'An outage affects support customers.' },
400
+ }, { timeoutMs: 15_000 });
401
+
402
+ // The outcome is source and review data. Incomplete/refused are also outcomes.
403
+ console.log(candidate.status, candidate.questions, candidate.diagnostics);
404
+ if (candidate.ready && candidate.candidate !== null) {
405
+ const edited = await nika.compile({
406
+ workflow: candidate.candidate,
407
+ change: { set_constant: { name: 'request', value: 'One customer is affected.' } },
408
+ });
409
+ console.log(edited.candidate);
410
+ }
326
411
  ```
327
412
 
328
- `nika.yaml` is the project control plane. `hello.nika` is executable
329
- workflow intent and is the file passed to `check()` and `run()`. The scaffold
330
- writes the engine's own annotated `01-hello` example (its task is named
331
- `greet` and its prompt asks for French); the contract this README relies on is
332
- the `outputs.greeting` key and the `mock/echo` model, which the hand-written
333
- file above satisfies too.
413
+ Edits also accept a string such as `Set const.request to "One customer"`.
414
+ Answers and structured values are strict JSON values, preserving numbers,
415
+ booleans, null, strings, arrays and objects. The native adapter uses the engine's
416
+ CLI; the HTTP adapter sends authenticated `POST /v1/compile`. An unavailable
417
+ remote capability raises `NikaCompatibilityError` without local fallback.
418
+
419
+ `candidate` is `.nika` source, distinct from the path accepted by `run()`.
420
+ The caller reviews and materializes it before calling `run(path)`, which performs
421
+ normal admission. `requested_boundary` and the source-only `check_preview` grant
422
+ no execution authority. Compile creates no Run, job, approval or Proof, and the
423
+ SDK writes no persistent candidate file. `signal`/`timeoutMs` stop only the
424
+ compile request. The common outcome has no `exitCode` or `written` field.
425
+
426
+ Compile accepts standard `AbortSignal`s, including `AbortSignal.any` composites.
427
+ It rejects direct signal interface overrides and proxies before starting work.
428
+ Composite sources must retain their standard interfaces: Node may read their
429
+ public fields while inspecting or subscribing to a composite, so the SDK cannot
430
+ validate a modified hidden source graph without invoking those fields.
334
431
 
335
432
  ## Verify a local trace
336
433
 
@@ -340,7 +437,12 @@ Pass that receipt back unchanged:
340
437
  ```ts
341
438
  if (!result.receipt) throw new Error('run did not issue a receipt');
342
439
  const proof = await nika.traceVerify(result.receipt);
343
- if (!proof.verified) throw new Error(proof.output ?? 'trace verification failed');
440
+ // proof.verified is the seal/binding, not the workflow outcome.
441
+ if (isNikaRunSucceeded(result) && proof.verified) {
442
+ // run succeeded and the journal is sealed
443
+ } else if (isNikaRunSucceeded(result) && !proof.verified) {
444
+ // succeeded, unsigned or otherwise unverified — read proof.reason
445
+ }
344
446
  ```
345
447
 
346
448
  The SDK does not implement cryptography or inspect the trace itself. It asks the
@@ -388,9 +490,8 @@ settles on the terminal the resident records, `cancelled`, `succeeded`,
388
490
  `failed`, or `interrupted` once its grace expired. A job that already ended
389
491
  replays its result with `accepted: false` and `status: 'already_settled'`. The
390
492
  native transport signals its process the same way; the result is whatever the
391
- engine then wrote, `cancelled` when it settled the request (the released
392
- 0.118.7 engine does), or `interrupted` when the process ended with no
393
- settlement frame.
493
+ engine then wrote, `cancelled` when it settled the request, or `interrupted`
494
+ when the process ended with no settlement frame.
394
495
 
395
496
  ## Connect to `nika serve`
396
497
 
@@ -646,8 +747,9 @@ A view opened late is seeded with every frame the session observed, or it is
646
747
  refused. It is never handed a shortened replay. How many frames a session
647
748
  retains is `eventBufferSize`, **4096 by default**.
648
749
 
649
- **The measured frame count: `3N + 3`, for one shape.** On the released 0.118.7
650
- engine, a clean run of N independent `mock/echo` `infer` tasks wrote
750
+ **The measured frame count: `3N + 3`, for one sealed shape.** On the released
751
+ 0.118.7 engine (and still the native 0.120.0 hello fixture when sealed), a
752
+ clean run of N independent `mock/echo` `infer` tasks wrote
651
753
  `workflow_started`, then `task_scheduled`, `task_started` and `task_completed`
652
754
  per task, then `workflow_completed` and `run_settled`. Two points were
653
755
  measured: 1 task is 6 frames, 90 tasks are 273. That is this fixture, not a
@@ -957,7 +1059,7 @@ The repository also carries 100 distinct use-case workflows and provider proof
957
1059
  under `gauntlet/`.
958
1060
 
959
1061
  <!-- engine hero pinned to the release tag it demonstrates · re-pin on lockstep bumps -->
960
- ![nika check audits the workflow, then runs and seals its trace](https://raw.githubusercontent.com/supernovae-st/nika/v0.118.7/media/nika-hero.gif)
1062
+ ![nika check audits the workflow, then runs and seals its trace](https://raw.githubusercontent.com/supernovae-st/nika/v0.120.1/media/nika-hero.gif)
961
1063
 
962
1064
  ## Keeping it fresh
963
1065
 
package/dist/bin/nika.js CHANGED
@@ -150,29 +150,41 @@ function captureEngine(bin, args, options) {
150
150
  shell: false,
151
151
  stdio: ["ignore", "pipe", "pipe"]
152
152
  });
153
- let stdout = "";
154
- let stderr = "";
153
+ const stdout = [];
154
+ const stderr = [];
155
+ const bytes = { stdout: 0, stderr: 0 };
155
156
  let overflow = false;
156
157
  let spawnError;
158
+ let closed = false;
159
+ let killTimer;
160
+ const stop = () => {
161
+ child.kill("SIGTERM");
162
+ if (options.killGraceMs === void 0) return;
163
+ killTimer ??= setTimeout(() => {
164
+ if (!closed) child.kill("SIGKILL");
165
+ }, options.killGraceMs);
166
+ killTimer.unref();
167
+ };
157
168
  const append = (stream, chunk) => {
158
169
  if (overflow) return;
159
- if (stream === "stdout") stdout += chunk;
160
- else stderr += chunk;
161
- if (Buffer.byteLength(stdout) > options.bufferBytes || Buffer.byteLength(stderr) > options.bufferBytes) {
170
+ bytes[stream] += chunk.byteLength;
171
+ if (bytes[stream] > options.bufferBytes) {
162
172
  overflow = true;
163
- child.kill("SIGTERM");
173
+ stop();
174
+ return;
164
175
  }
176
+ (stream === "stdout" ? stdout : stderr).push(chunk);
165
177
  };
166
- child.stdout.setEncoding("utf8");
167
- child.stderr.setEncoding("utf8");
168
178
  child.stdout.on("data", (chunk) => append("stdout", chunk));
169
179
  child.stderr.on("data", (chunk) => append("stderr", chunk));
170
- const abort = () => child.kill("SIGTERM");
180
+ const abort = () => stop();
171
181
  options.signal?.addEventListener("abort", abort, { once: true });
172
182
  child.once("error", (cause) => {
173
183
  spawnError = cause;
174
184
  });
175
- child.once("close", (code) => {
185
+ child.once("close", (code, exitSignal) => {
186
+ closed = true;
187
+ if (killTimer) clearTimeout(killTimer);
176
188
  options.signal?.removeEventListener("abort", abort);
177
189
  if (options.signal?.aborted) {
178
190
  reject(new NikaTransportError(options.transport, `${options.label} aborted by caller`));
@@ -188,7 +200,19 @@ function captureEngine(bin, args, options) {
188
200
  `${options.label} exceeded ${options.bufferBytes} bytes`
189
201
  ));
190
202
  } else {
191
- resolve({ exitCode: code ?? 3, stdout, stderr });
203
+ let decoded;
204
+ try {
205
+ decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Buffer.concat(stdout, bytes.stdout));
206
+ } catch {
207
+ reject(new NikaProtocolError(options.transport, `${options.label} stdout was not valid UTF-8`));
208
+ return;
209
+ }
210
+ resolve({
211
+ exitCode: code ?? 3,
212
+ stdout: decoded,
213
+ stderr: Buffer.concat(stderr, bytes.stderr).toString("utf8"),
214
+ exitSignal
215
+ });
192
216
  }
193
217
  });
194
218
  });
@@ -252,9 +276,9 @@ function incompatible(transport, message) {
252
276
 
253
277
  // src/lib/binary/verify.ts
254
278
  var IDENTITY_BUFFER_BYTES = 16 * 1024;
255
- async function verifyNikaEngine(engine) {
279
+ async function verifyNikaEngine(engine, options = {}) {
256
280
  const expectedVersion = engine.packageRoot ? await verifyManagedPayload(engine) : void 0;
257
- const identity = await probeIdentity(engine.bin);
281
+ const identity = await probeIdentity(engine.bin, options);
258
282
  if (expectedVersion !== void 0 && identity.engineVersion !== expectedVersion) {
259
283
  throw incompatible2(
260
284
  `Packaged engine version ${identity.engineVersion} does not match payload version ${expectedVersion}`
@@ -293,13 +317,15 @@ async function verifyManagedPayload(engine) {
293
317
  }
294
318
  return manifest.version;
295
319
  }
296
- async function probeIdentity(bin) {
320
+ async function probeIdentity(bin, options) {
297
321
  const notAnEngine = `is ${bin} a nika engine? (run "${bin} --sdk-identity" by hand)`;
298
322
  const captured = await captureEngine(bin, ["--sdk-identity"], {
323
+ ...options,
299
324
  bufferBytes: IDENTITY_BUFFER_BYTES,
300
325
  transport: "native-process",
301
326
  label: "Engine identity probe"
302
327
  }).catch((cause) => {
328
+ if (options.signal?.aborted) throw cause;
303
329
  throw incompatible2(
304
330
  `Engine identity probe of ${bin} failed`,
305
331
  cause