@code-yeongyu/senpi-codemode 2026.10.31-1 → 2026.11.2-1
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/CHANGELOG.md +38 -0
- package/README.md +1 -1
- package/package.json +4 -4
- package/src/extension/host-facts.ts +81 -3
- package/src/extension/skill-contribution.ts +6 -6
- package/src/kernels/js/cell-ownership.js +12 -3
- package/src/kernels/py/path_identity_windows.py +3 -1
- package/src/kernels/shared/subprocess-kernel.ts +4 -1
- package/src/kernels/shared/subprocess-process.ts +12 -0
- package/src/prompt/eval-prompt-template.ts +1 -1
- package/src/prompt/eval-prompt.ts +4 -2
- package/src/skill/bun/SKILL.md +53 -0
- package/src/skill/{bun-1-4 → bun}/references/breaking-changes.md +26 -4
- package/src/skill/{bun-1-4 → bun}/references/build.md +32 -6
- package/src/skill/bun/references/dev-tooling.md +67 -0
- package/src/skill/bun/references/http-networking.md +157 -0
- package/src/skill/bun/references/install.md +137 -0
- package/src/skill/{bun-1-4 → bun}/references/node-compat-platforms.md +16 -2
- package/src/skill/{bun-1-4 → bun}/references/performance.md +9 -3
- package/src/skill/{bun-1-4 → bun}/references/runtime-apis.md +38 -13
- package/src/skill/bun/references/security.md +74 -0
- package/src/skill/bun/references/session-recipes.md +71 -0
- package/src/skill/bun/references/test.md +132 -0
- package/src/tool/detached-cell-manager.ts +50 -3
- package/src/tool/eval-argument-validator.ts +89 -0
- package/src/tool/eval-language-inference.ts +116 -0
- package/src/tool/eval-request.ts +34 -13
- package/src/tool/eval-tool-options.ts +1 -1
- package/src/tool/eval-tool.ts +17 -0
- package/src/tool/types.ts +2 -0
- package/src/skill/bun-1-4/SKILL.md +0 -45
- package/src/skill/bun-1-4/references/dev-tooling.md +0 -34
- package/src/skill/bun-1-4/references/http-networking.md +0 -113
- package/src/skill/bun-1-4/references/install.md +0 -93
- package/src/skill/bun-1-4/references/security.md +0 -52
- package/src/skill/bun-1-4/references/test.md +0 -103
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# bun test in 1.4.x
|
|
2
|
+
|
|
3
|
+
Composable CI recipe:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun test --changed=main # only what your branch touches
|
|
7
|
+
bun test --parallel --timings=timings.json --update-timings
|
|
8
|
+
bun test --parallel --shard=1/3 --timings=timings.json # in CI, per machine
|
|
9
|
+
bun test --check # type-check test files + imports first; no tests run on a type error [v1.4.3]
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Traps — each one cost a past session hours
|
|
13
|
+
|
|
14
|
+
| Trap | Fact | Fix |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Timeout reaches only the first file | `setDefaultTimeout()` in a preload is file-scoped (first file only); bunfig `[test] timeout` is silently ignored (bun#7789). Only slow hosts push tests past 5 s, so it looks like a Windows flake (2026-10-20) | `bun test --timeout 30000` on the command line, or a per-test third argument (`0` = no limit) |
|
|
17
|
+
| Zombie killer takes your DB with it | When a test times out, Bun SIGTERMs every child the test process spawned, including an embedded Postgres started in `beforeAll`; every later test in the file fails with "shutting down" and the failing set changes with host load (2026-10-23) | raise the budget for DB suites, or start the server outside the test process |
|
|
18
|
+
| Path without `./` is a name filter | `bun test path/file.test.ts` matches files whose path CONTAINS that string; `bun test ./path/file.test.ts` runs exactly that file | start exact paths with `./` or `/`; `-t <regex>` filters by test name |
|
|
19
|
+
| `expect(p).rejects` hangs after a Worker reply | `await expect(p).rejects…` reached from the continuation of an awaited Worker message deadlocks (oven-sh/bun#37189, still on 1.4.3) | `const err = await p.then(() => undefined, (e) => e)` then assert on `err` |
|
|
20
|
+
| `mock.module()` reaches every importer | mocking a re-exporting entry replaces the function for modules importing it by relative path too; one file's `mock.module` of a shared logger breaks another file's `spyOn` in the same run (2026-10-26/27) | copy the real namespace first (`{ ...(await import(x)) }`), keep such tests in their own process (`bun test ./x.isolated.test.ts`) or run `--isolate` |
|
|
21
|
+
| Preload patch of a builtin is invisible | a preload's `require("node:os").x = …` reaches product `import * as os` only if nothing ESM-imported that builtin before the patch; Bun snapshots the namespace at first ESM import and `module.syncBuiltinESMExports()` does not help (2026-10-29) | patch before any ESM import of the builtin, or use the product's env seam |
|
|
22
|
+
| Respawned `"bun"` is not this Bun | PATH can resolve to a mise shim or a stray older `bun` in a global `.bin`; the test's `calls.log` ENOENT hid a `--version` exit 1 (2026-09-28, 10-29) | spawn `process.execPath`; surface the child's stderr when a stub log is missing |
|
|
23
|
+
| Env-less child misses runtime env | `Bun.spawn` without `env` gives the child the STARTUP environment | `env: process.env` — see [session-recipes](session-recipes.md) |
|
|
24
|
+
| Timed-out test keeps running | the per-test timeout marks the test failed but its body keeps executing; a body awaiting a never-settling promise is not ended at all, and `onTestFinished` runs while it is still awaiting (2026-10-12/16) | own async work in a hook registered before fixture removal; wrap a never-settles RED in an outer `timeout -k 5 <s>` |
|
|
25
|
+
| Shared-helper hooks bind once | an `afterEach`/`afterAll` at module scope of a helper several files import binds only to the FIRST importer; 16 files skipped cleanup and left 106 temp dirs (2026-10-10) | register per call with `onTestFinished(() => …)` inside the helper |
|
|
26
|
+
| `bun run --cwd pkg test file` runs everything | bun appends your positional AFTER the script's own (`vitest run test/ file`), so the whole suite runs (2026-09-21); `bun test` on a vitest-based package hung 12 h (2026-10-27) | invoke the package's runner directly for one file; check which runner a package uses before `bun test` |
|
|
27
|
+
| Root `pathIgnorePatterns` hides a package | a root bunfig pattern silently excluded a workspace's suite; the run was green with 0 of its files (2026-09-27) | run the package's own script or explicit paths and verify the discovered file count |
|
|
28
|
+
| Duplicate package.json key | every child prints `Duplicate key` on stderr, failing stderr-asserting tests (2026-10-24) | fix the JSON |
|
|
29
|
+
|
|
30
|
+
Flake tools: `bun test --rerun-each 20 ./x.test.ts` runs a file N times in one process (order/leak bugs); `--randomize --seed N` reproduces order-dependent failures; `--only-failures` re-runs what failed last time; `test.failing("name", fn)` inverts a known-red test so it alerts when it starts passing; `--bail` stops at the first failure (also stops every `--parallel` worker).
|
|
31
|
+
|
|
32
|
+
## --parallel: worker processes [v1.3.13, improved v1.4.0]
|
|
33
|
+
|
|
34
|
+
Blog: [#bun-test-parallel](https://bun.com/blog/bun-v1.4#bun-test-parallel)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bun test --parallel # N = CPU count
|
|
38
|
+
bun test --parallel=4
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- Files go to whichever worker frees up next. Coverage and JUnit output merged across workers; `--bail` stops every worker.
|
|
42
|
+
- `--parallel` implies `--isolate`; `--no-isolate` opts out.
|
|
43
|
+
- Workers expose 1-indexed `JEST_WORKER_ID` / `BUN_TEST_WORKER_ID`, so Jest setups keying DBs/ports off worker id work unchanged.
|
|
44
|
+
- Preload scripts with top-level `await` complete before any worker runs tests.
|
|
45
|
+
|
|
46
|
+
## --isolate: fresh global per file [v1.3.13, hardened v1.4.0]
|
|
47
|
+
|
|
48
|
+
Blog: [#bun-test-isolate](https://bun.com/blog/bun-v1.4#bun-test-isolate)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bun test --isolate
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Between files, Bun creates a new `globalThis`, clears ESM/CJS module registries, closes leaked servers/sockets/watchers/subprocesses, cancels timers, restores fake timers, and re-runs `--preload`. Transpiled source/bytecode stay cached process-wide, so only top-level code re-runs. This is how Jest/Vitest behave by default — use it to kill "passes alone, fails in the full suite" bugs. v1.4.0 fixed the 1.3.13 stability issues (fake-timer leaks, module-scope subprocess leaks, `process.chdir` bleed, N-API across files, debugger breakpoints); v1.4.1 stopped files that call `mock()`, `spyOn()`, `mock.module()` or `Bun.plugin()` from keeping their module graph alive (80 mocked files: 1591 MB → 215 MB) and stopped `TZ`, proxy and TLS settings from leaking into later files.
|
|
55
|
+
|
|
56
|
+
## --shard: split across CI machines [v1.3.13]
|
|
57
|
+
|
|
58
|
+
Blog: [#bun-test-shard](https://bun.com/blog/bun-v1.4#bun-test-shard)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bun test --shard=1/3 # deterministic, round-robin, 1-based (matches Jest/Vitest/Playwright)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Works with `--changed` and `--randomize`; an empty shard exits 0.
|
|
65
|
+
|
|
66
|
+
## --timings: balance by wall time [v1.4.0]
|
|
67
|
+
|
|
68
|
+
Blog: [#bun-test-timings](https://bun.com/blog/bun-v1.4#bun-test-timings)
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bun test --timings=timings.json --update-timings # record per-file durations
|
|
72
|
+
bun test --shard=1/3 --timings=timings.json # shards cut by equal time, not file count
|
|
73
|
+
bun test --parallel --timings=timings.json # workers start slowest file first (LPT scheduling)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The timings file is written slowest-first, so it doubles as a slow-test report. Files sharing imports stay together (warm module cache).
|
|
77
|
+
|
|
78
|
+
## --changed: only affected tests [v1.3.13]
|
|
79
|
+
|
|
80
|
+
Blog: [#bun-test-changed](https://bun.com/blog/bun-v1.4#bun-test-changed)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bun test --changed # uncommitted (unstaged + staged + untracked)
|
|
84
|
+
bun test --changed=main # diff against branch/commit/tag
|
|
85
|
+
bun test --changed --watch # re-filters on every restart
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Bun scans test imports, asks git what changed, walks the import graph backwards. tsconfig `paths` aliases (`@/*`) work. Vitest-compatible flag.
|
|
89
|
+
|
|
90
|
+
## --retry and repeats [v1.3.3]
|
|
91
|
+
|
|
92
|
+
Blog: [#bun-test-retry](https://bun.com/blog/bun-v1.4#bun-test-retry)
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
test("flaky network call", async () => { await fetch("https://example.com"); }, { retry: 5 });
|
|
96
|
+
test("stress", () => { if (Math.random() < 0.1) throw new Error("uh oh!"); }, { repeats: 20 });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`bun test --retry <N>` sets a suite-wide default. NOTE: prefer fixing the flake (`--rerun-each`, `--randomize --seed`, `--isolate` find it); retry is a containment tool.
|
|
100
|
+
|
|
101
|
+
## jest.useFakeTimers() [v1.3.4, improved v1.4.0]
|
|
102
|
+
|
|
103
|
+
Blog: [#jest-usefaketimers](https://bun.com/blog/bun-v1.4#jest-usefaketimers)
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { jest, test, expect } from "bun:test";
|
|
107
|
+
test("debounce", () => {
|
|
108
|
+
jest.useFakeTimers();
|
|
109
|
+
let called = 0;
|
|
110
|
+
setTimeout(() => called++, 1000);
|
|
111
|
+
jest.advanceTimersByTime(1000);
|
|
112
|
+
expect(called).toBe(1);
|
|
113
|
+
jest.useRealTimers();
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- Controls `setTimeout`, `setInterval`, `Date`. `jest.setSystemTime()` works with `advanceTimersByTime()`.
|
|
118
|
+
- `@testing-library/react`'s `waitFor` detects fake timers and advances instead of sleeping. `Bun.cron` schedules can be driven by the fake clock.
|
|
119
|
+
|
|
120
|
+
## Also in bun:test
|
|
121
|
+
|
|
122
|
+
- `expectTypeOf<T>()` type assertions (Vitest-compatible); `test.if/skipIf/todoIf(cond)`, `test.each([...])`, `describe.each`; `onTestFinished(fn)` runs after all `afterEach` hooks (serial tests only).
|
|
123
|
+
- `bun test --inspect` / `--inspect-brk` attaches the debugger to the runner; `--reporter=junit --reporter-outfile=x.xml`; inside GitHub Actions, failures are emitted as annotations automatically.
|
|
124
|
+
- `--coverage-reporter=lcov` implies `--coverage` [v1.4.3]. v1.4.4 stopped counting skipped branches (`||`/`??`/`?.` right sides, code after an `await` that never resumed) and implicit constructors as covered — `% Lines`/`% Funcs` can drop after upgrading, so re-check `coverageThreshold`.
|
|
125
|
+
- `--update-snapshots` keeps the old file when a run ends early (`--bail`) [v1.4.4].
|
|
126
|
+
|
|
127
|
+
## Behavior changes in 1.4.x (test-related)
|
|
128
|
+
|
|
129
|
+
- `jest.resetAllMocks()` now drops implementations too (matches Jest); use `clearAllMocks()` for history-only.
|
|
130
|
+
- `expect().toContain()` compares with `===` not `Object.is` (`[NaN]` no longer contains `NaN`).
|
|
131
|
+
- `toEqual()` compares `Temporal` objects by value.
|
|
132
|
+
- Coverage percentages are stricter in v1.4.4 (above).
|
|
@@ -38,12 +38,52 @@ export type {
|
|
|
38
38
|
export class EvalCellNotFoundError extends Error {
|
|
39
39
|
readonly code = "eval_cell_not_found";
|
|
40
40
|
|
|
41
|
-
constructor(cellId: string) {
|
|
42
|
-
super(
|
|
41
|
+
constructor(cellId: string, suggestion?: string) {
|
|
42
|
+
super(
|
|
43
|
+
suggestion === undefined
|
|
44
|
+
? `Unknown detached eval cell "${cellId}"`
|
|
45
|
+
: `Unknown detached eval cell "${cellId}"; did you mean "${suggestion}"?`,
|
|
46
|
+
);
|
|
43
47
|
this.name = "EvalCellNotFoundError";
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
function editDistance(a: string, b: string): number {
|
|
52
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
53
|
+
for (let i = 1; i <= a.length; i++) {
|
|
54
|
+
const current = [i];
|
|
55
|
+
for (let j = 1; j <= b.length; j++) {
|
|
56
|
+
const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
57
|
+
current.push(Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, substitution));
|
|
58
|
+
}
|
|
59
|
+
previous = current;
|
|
60
|
+
}
|
|
61
|
+
return previous[b.length] ?? 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The one known id within a quarter of the mistyped id's length (at least 1 edit), or undefined when
|
|
66
|
+
* none is that close or two or more tie for closest, so a suggestion never picks between candidates.
|
|
67
|
+
*/
|
|
68
|
+
function closestCellId(cellId: string, known: Iterable<string>): string | undefined {
|
|
69
|
+
const limit = Math.max(1, Math.floor(cellId.length / 4));
|
|
70
|
+
let best: string | undefined;
|
|
71
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
72
|
+
let tied = false;
|
|
73
|
+
for (const candidate of known) {
|
|
74
|
+
const distance = editDistance(cellId, candidate);
|
|
75
|
+
if (distance > limit) continue;
|
|
76
|
+
if (distance < bestDistance) {
|
|
77
|
+
best = candidate;
|
|
78
|
+
bestDistance = distance;
|
|
79
|
+
tied = false;
|
|
80
|
+
} else if (distance === bestDistance) {
|
|
81
|
+
tied = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return tied ? undefined : best;
|
|
85
|
+
}
|
|
86
|
+
|
|
47
87
|
export class EvalDetachedCellManager {
|
|
48
88
|
readonly #onStatusChange: ((entries: readonly EvalDetachedCellStatusEntry[]) => void) | undefined;
|
|
49
89
|
readonly #onWakeSourceState: ((state: WakeSourceState) => void) | undefined;
|
|
@@ -317,7 +357,14 @@ export class EvalDetachedCellManager {
|
|
|
317
357
|
|
|
318
358
|
#terminal(cellId: string): EvalDetachedCellSnapshot {
|
|
319
359
|
const snapshot = this.#terminalSnapshots.get(cellId);
|
|
320
|
-
if (snapshot === undefined)
|
|
360
|
+
if (snapshot === undefined) {
|
|
361
|
+
const known = new Set([
|
|
362
|
+
...this.#cells.keys(),
|
|
363
|
+
...this.#interrupting.keys(),
|
|
364
|
+
...this.#terminalSnapshots.list().map((entry) => entry.cellId),
|
|
365
|
+
]);
|
|
366
|
+
throw new EvalCellNotFoundError(cellId, closestCellId(cellId, known));
|
|
367
|
+
}
|
|
321
368
|
return snapshot;
|
|
322
369
|
}
|
|
323
370
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type JsonObject, registerToolArgumentValidator, validateToolArguments } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { TSchema } from "typebox";
|
|
3
|
+
import { EVAL_LANGUAGE_INFERRED_KEY, inferEvalLanguage, markEvalLanguageFilled } from "./eval-language-inference.ts";
|
|
4
|
+
import { type EvalRunField, evalRunFieldRequiredMessage, normalizeEvalSummary } from "./eval-request.ts";
|
|
5
|
+
import type { EvalLanguage } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
type EvalArgumentValidator = Parameters<typeof registerToolArgumentValidator>[1];
|
|
8
|
+
type EvalArgumentError = NonNullable<EvalArgumentValidator["errors"]>[number];
|
|
9
|
+
|
|
10
|
+
const RUN_FIELDS: readonly EvalRunField[] = ["language", "code", "summary"];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Owns eval argument validation (senpi#5138). A run call (action omitted or "run") missing a run field fails with
|
|
14
|
+
* one teaching error per missing field, before the generic anyOf validation whose per-branch dump a model cannot act
|
|
15
|
+
* on. Every other call goes through the generic validation and coercion unchanged: a failure there rethrows the generic
|
|
16
|
+
* path's own error, so its message (and the `Received arguments` block) is byte-for-byte what an unregistered schema gets.
|
|
17
|
+
*/
|
|
18
|
+
export function registerEvalArgumentValidator(schema: TSchema, languages: readonly EvalLanguage[]): void {
|
|
19
|
+
// The same schema under another identity, so the generic path validates it instead of calling back here.
|
|
20
|
+
const generic: TSchema = Object.create(Object.getPrototypeOf(schema), Object.getOwnPropertyDescriptors(schema));
|
|
21
|
+
const genericValidate = (args: unknown): unknown =>
|
|
22
|
+
validateToolArguments(
|
|
23
|
+
{ name: "eval", description: "", parameters: generic },
|
|
24
|
+
{ type: "toolCall", id: "", name: "eval", arguments: args as JsonObject },
|
|
25
|
+
);
|
|
26
|
+
const validate: EvalArgumentValidator = (value) => {
|
|
27
|
+
const callerArguments = structuredClone(value);
|
|
28
|
+
const inferred = fillInferredLanguage(value, languages);
|
|
29
|
+
const missing = missingRunFieldErrors(value, languages);
|
|
30
|
+
if (missing.length > 0) {
|
|
31
|
+
validate.errors = missing;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
let coerced: unknown;
|
|
35
|
+
try {
|
|
36
|
+
// Throws the generic validation error unchanged when the call is invalid for any other reason.
|
|
37
|
+
coerced = genericValidate(value);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// A filled language stands only for a call that validates; otherwise report what the caller sent.
|
|
40
|
+
if (inferred !== undefined) genericValidate(callerArguments);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
// A registered validator's input is what the caller returns, so the generic coercion lands in place.
|
|
44
|
+
if (isRecord(value) && isRecord(coerced)) {
|
|
45
|
+
for (const key of Object.keys(value)) delete value[key];
|
|
46
|
+
Object.assign(value, coerced);
|
|
47
|
+
if (inferred !== undefined) {
|
|
48
|
+
value[EVAL_LANGUAGE_INFERRED_KEY] = inferred;
|
|
49
|
+
markEvalLanguageFilled(value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
validate.errors = null;
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
registerToolArgumentValidator(schema, validate);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fills an omitted run language when it is unambiguous (senpi#5154), before the missing-field check, and drops any
|
|
60
|
+
* inference marker the caller sent so only this fill can set it. An explicit language is never changed.
|
|
61
|
+
*/
|
|
62
|
+
function fillInferredLanguage(value: unknown, languages: readonly EvalLanguage[]): string | undefined {
|
|
63
|
+
if (!isRecord(value) || (value.action !== undefined && value.action !== "run")) return undefined;
|
|
64
|
+
delete value[EVAL_LANGUAGE_INFERRED_KEY];
|
|
65
|
+
if (!isMissing("language", value.language)) return undefined;
|
|
66
|
+
const inference = inferEvalLanguage(value.code, languages);
|
|
67
|
+
if (inference === undefined) return undefined;
|
|
68
|
+
value.language = inference.language;
|
|
69
|
+
return inference.basis;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function missingRunFieldErrors(value: unknown, languages: readonly EvalLanguage[]): EvalArgumentError[] {
|
|
73
|
+
if (!isRecord(value) || (value.action !== undefined && value.action !== "run")) return [];
|
|
74
|
+
return RUN_FIELDS.filter((field) => isMissing(field, value[field])).map((field) => ({
|
|
75
|
+
instancePath: "",
|
|
76
|
+
keyword: "required",
|
|
77
|
+
message: evalRunFieldRequiredMessage(field, languages),
|
|
78
|
+
params: { missingProperty: field },
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isMissing(field: EvalRunField, value: unknown): boolean {
|
|
83
|
+
if (value === undefined || value === null) return true;
|
|
84
|
+
return field === "summary" && typeof value === "string" && normalizeEvalSummary(value) === undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
88
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
89
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { EvalLanguage } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/** Why an omitted run language was filled: the only enabled kernel, or unambiguous code markers (senpi#5154). */
|
|
4
|
+
export type EvalLanguageInferenceBasis = "only-enabled-language" | "code-markers";
|
|
5
|
+
|
|
6
|
+
export interface EvalLanguageInference {
|
|
7
|
+
readonly language: EvalLanguage;
|
|
8
|
+
readonly basis: EvalLanguageInferenceBasis;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The argument key validation sets when it filled the language, so execution can tell the model. */
|
|
12
|
+
export const EVAL_LANGUAGE_INFERRED_KEY = "language_inferred";
|
|
13
|
+
|
|
14
|
+
// Argument objects whose language this module's validator filled; only these may carry a trusted inference marker.
|
|
15
|
+
const filledArguments = new WeakSet<object>();
|
|
16
|
+
|
|
17
|
+
export function markEvalLanguageFilled(args: object): void {
|
|
18
|
+
filledArguments.add(args);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** True only when argument validation filled this object's language; a caller-sent marker is never trusted. */
|
|
22
|
+
export function wasEvalLanguageFilled(args: object): boolean {
|
|
23
|
+
return filledArguments.has(args);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface LanguageSignals {
|
|
27
|
+
/** Any one of these fills the language when the other language shows no signal at all. */
|
|
28
|
+
readonly strong: readonly RegExp[];
|
|
29
|
+
/** These never fill; they only veto a fill for the other language. */
|
|
30
|
+
readonly weak: readonly RegExp[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PYTHON_IDENTIFIER = "[A-Za-z_]\\w*";
|
|
34
|
+
const PYTHON_MODULE = `${PYTHON_IDENTIFIER}(?:\\.${PYTHON_IDENTIFIER})*`;
|
|
35
|
+
const PYTHON_IMPORT_ITEM = `${PYTHON_MODULE}(?:[ \\t]+as[ \\t]+${PYTHON_IDENTIFIER})?`;
|
|
36
|
+
|
|
37
|
+
const SIGNALS: Readonly<Record<"js" | "py", LanguageSignals>> = {
|
|
38
|
+
py: {
|
|
39
|
+
strong: [
|
|
40
|
+
/^[ \t]*def[ \t]+[A-Za-z_]\w*[ \t]*\(/mu,
|
|
41
|
+
new RegExp(
|
|
42
|
+
`^[ \\t]*import[ \\t]+${PYTHON_IMPORT_ITEM}(?:[ \\t]*,[ \\t]*${PYTHON_IMPORT_ITEM})*[ \\t]*$`,
|
|
43
|
+
"mu",
|
|
44
|
+
),
|
|
45
|
+
new RegExp(`^[ \\t]*from[ \\t]+\\.*${PYTHON_MODULE}[ \\t]+import[ \\t]+[\\w(*]`, "mu"),
|
|
46
|
+
],
|
|
47
|
+
weak: [
|
|
48
|
+
/\b(?:None|True|False|elif|lambda|self)\b/u,
|
|
49
|
+
/^[ \t]*(?:if|elif|else|for|while|with|try|except|finally|class|async[ \t]+def)\b.*:[ \t]*(?:#.*)?$/mu,
|
|
50
|
+
/^[ \t]*#/mu,
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
js: {
|
|
54
|
+
strong: [
|
|
55
|
+
// `print(` is a prelude helper in both runtimes, so it is no signal at all; these forms are JS-only.
|
|
56
|
+
/\bawait[ \t]+[A-Za-z_$][\w$.]*[ \t]*\(/u,
|
|
57
|
+
/`/u,
|
|
58
|
+
/(?<![\w.$])(?:JSON|Object|Array)\.[A-Za-z_$]/u,
|
|
59
|
+
/^[ \t]*(?:const|let)[ \t]+[A-Za-z_$[{]/mu,
|
|
60
|
+
/=>/u,
|
|
61
|
+
/\bconsole\.log\(/u,
|
|
62
|
+
/^[ \t]*import[ \t].*\bfrom[ \t]*["']/mu,
|
|
63
|
+
/^[ \t]*import[ \t]*["']/mu,
|
|
64
|
+
],
|
|
65
|
+
weak: [
|
|
66
|
+
/\b(?:null|undefined|function|var|typeof|new)\b/u,
|
|
67
|
+
/===|!==|&&|\|\||\+\+/u,
|
|
68
|
+
/;[ \t]*$/mu,
|
|
69
|
+
/^[ \t]*\/\//mu,
|
|
70
|
+
/[{}][ \t]*$/mu,
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function signals(code: string, language: "js" | "py"): { readonly strong: boolean; readonly any: boolean } {
|
|
76
|
+
const { strong, weak } = SIGNALS[language];
|
|
77
|
+
const hasStrong = strong.some((pattern) => pattern.test(code));
|
|
78
|
+
return { strong: hasStrong, any: hasStrong || weak.some((pattern) => pattern.test(code)) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Classifies a cell body as js or py only on a strong marker of one language with no signal of the other;
|
|
83
|
+
* anything else, including any conflicting signal, is unclassified.
|
|
84
|
+
*/
|
|
85
|
+
export function classifyEvalCode(code: string): "js" | "py" | undefined {
|
|
86
|
+
const py = signals(code, "py");
|
|
87
|
+
const js = signals(code, "js");
|
|
88
|
+
if (py.strong && !js.any) return "py";
|
|
89
|
+
if (js.strong && !py.any) return "js";
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The language an omitted run language may be filled with: the only enabled kernel, or, when exactly js and py are
|
|
95
|
+
* enabled, the classified code. Any other case returns undefined and the caller keeps its teaching error.
|
|
96
|
+
*/
|
|
97
|
+
export function inferEvalLanguage(
|
|
98
|
+
code: unknown,
|
|
99
|
+
languages: readonly EvalLanguage[],
|
|
100
|
+
): EvalLanguageInference | undefined {
|
|
101
|
+
const [only] = languages;
|
|
102
|
+
if (languages.length === 1 && only !== undefined) return { language: only, basis: "only-enabled-language" };
|
|
103
|
+
if (languages.length !== 2 || !languages.includes("js") || !languages.includes("py")) return undefined;
|
|
104
|
+
if (typeof code !== "string") return undefined;
|
|
105
|
+
const language = classifyEvalCode(code);
|
|
106
|
+
return language === undefined ? undefined : { language, basis: "code-markers" };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isEvalLanguageInferenceBasis(value: unknown): value is EvalLanguageInferenceBasis {
|
|
110
|
+
return value === "only-enabled-language" || value === "code-markers";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The line an inferred-language result leads with, so the model sees the fill and passes language next time. */
|
|
114
|
+
export function evalLanguageInferredNote(language: EvalLanguage, basis: EvalLanguageInferenceBasis): string {
|
|
115
|
+
return `${EVAL_LANGUAGE_INFERRED_KEY}: ${language} (${basis}). This eval run omitted language; pass language explicitly.`;
|
|
116
|
+
}
|
package/src/tool/eval-request.ts
CHANGED
|
@@ -6,6 +6,13 @@ import {
|
|
|
6
6
|
type EvalToolRequest,
|
|
7
7
|
evalLanguageOrder,
|
|
8
8
|
} from "./types.ts";
|
|
9
|
+
import {
|
|
10
|
+
EVAL_LANGUAGE_INFERRED_KEY,
|
|
11
|
+
type EvalLanguageInferenceBasis,
|
|
12
|
+
inferEvalLanguage,
|
|
13
|
+
isEvalLanguageInferenceBasis,
|
|
14
|
+
wasEvalLanguageFilled,
|
|
15
|
+
} from "./eval-language-inference.ts";
|
|
9
16
|
|
|
10
17
|
const NON_INTERACTIVE_MODES = new Set(["print", "json"]);
|
|
11
18
|
|
|
@@ -40,16 +47,20 @@ export function parseEvalRequest(
|
|
|
40
47
|
}
|
|
41
48
|
if (params.action !== undefined && params.action !== "run")
|
|
42
49
|
throw new TypeError(`Unknown eval action "${String(params.action)}"`);
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
// An omitted language is filled only when unambiguous (senpi#5154); argument validation may already have filled it.
|
|
51
|
+
const inference = params.language === undefined ? inferEvalLanguage(params.code, enabledLanguages) : undefined;
|
|
52
|
+
const language: unknown = inference?.language ?? params.language;
|
|
53
|
+
if (language === undefined) throw new TypeError(evalRunFieldRequiredMessage("language", enabledLanguages));
|
|
54
|
+
if (!isEvalLanguage(language))
|
|
45
55
|
throw new TypeError(`eval run language must be one of: ${enabledLanguages.join(", ")}`);
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
const inferredBy: EvalLanguageInferenceBasis | undefined =
|
|
57
|
+
inference?.basis ??
|
|
58
|
+
(wasEvalLanguageFilled(params) && isEvalLanguageInferenceBasis(params[EVAL_LANGUAGE_INFERRED_KEY])
|
|
59
|
+
? params[EVAL_LANGUAGE_INFERRED_KEY]
|
|
60
|
+
: undefined);
|
|
61
|
+
if (typeof params.code !== "string") throw new TypeError(evalRunFieldRequiredMessage("code", enabledLanguages));
|
|
48
62
|
const summary = normalizeEvalSummary(params.summary);
|
|
49
|
-
if (summary === undefined)
|
|
50
|
-
throw new TypeError(
|
|
51
|
-
"eval run requires summary (one line in the user's language): what you are working on and for what purpose",
|
|
52
|
-
);
|
|
63
|
+
if (summary === undefined) throw new TypeError(evalRunFieldRequiredMessage("summary", enabledLanguages));
|
|
53
64
|
if (params.on_timeout !== undefined && params.on_timeout !== "detach" && params.on_timeout !== "error")
|
|
54
65
|
throw new TypeError(`Unknown eval on_timeout value "${String(params.on_timeout)}"`);
|
|
55
66
|
if (params.isolate !== undefined && params.isolate !== false) {
|
|
@@ -57,12 +68,11 @@ export function parseEvalRequest(
|
|
|
57
68
|
if (options.sandbox !== true) {
|
|
58
69
|
throw new EvalIsolateInvalidError("sandbox cells are turned off (set sandbox.enabled in codemode settings)");
|
|
59
70
|
}
|
|
60
|
-
if (
|
|
61
|
-
throw new EvalIsolateInvalidError('sandbox cells run JavaScript only (language: "js")');
|
|
71
|
+
if (language !== "js") throw new EvalIsolateInvalidError('sandbox cells run JavaScript only (language: "js")');
|
|
62
72
|
if (params.reset === true) throw new EvalIsolateInvalidError("a sandbox cell starts fresh; reset does not apply");
|
|
63
73
|
}
|
|
64
74
|
return {
|
|
65
|
-
language
|
|
75
|
+
language,
|
|
66
76
|
code: params.code,
|
|
67
77
|
summary,
|
|
68
78
|
...(params.action === "run" ? { action: "run" as const } : {}),
|
|
@@ -70,6 +80,7 @@ export function parseEvalRequest(
|
|
|
70
80
|
...(params.on_timeout === "detach" || params.on_timeout === "error" ? { on_timeout: params.on_timeout } : {}),
|
|
71
81
|
...(typeof params.reset === "boolean" ? { reset: params.reset } : {}),
|
|
72
82
|
...(params.isolate === true ? { isolate: true } : {}),
|
|
83
|
+
...(inferredBy === undefined ? {} : { language_inferred: inferredBy }),
|
|
73
84
|
};
|
|
74
85
|
}
|
|
75
86
|
|
|
@@ -90,6 +101,16 @@ function isEvalLanguage(value: unknown): value is EvalToolInput["language"] {
|
|
|
90
101
|
return value === "py" || value === "js" || value === "rb" || value === "jl";
|
|
91
102
|
}
|
|
92
103
|
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
export type EvalRunField = "language" | "code" | "summary";
|
|
105
|
+
|
|
106
|
+
/** The teaching error for a run call missing one of its fields, shared by argument validation and execution. */
|
|
107
|
+
export function evalRunFieldRequiredMessage(field: EvalRunField, languages: readonly EvalLanguage[]): string {
|
|
108
|
+
switch (field) {
|
|
109
|
+
case "language":
|
|
110
|
+
return `eval run requires language: one of ${languages.map((language) => `"${language}"`).join(", ")}`;
|
|
111
|
+
case "code":
|
|
112
|
+
return "eval run requires code: the cell body to execute, verbatim";
|
|
113
|
+
case "summary":
|
|
114
|
+
return "eval run requires summary (one line in the user's language): what you are working on and for what purpose";
|
|
115
|
+
}
|
|
95
116
|
}
|
|
@@ -67,7 +67,7 @@ export interface CreateEvalToolOptions {
|
|
|
67
67
|
readonly hostLine?: string;
|
|
68
68
|
/** Display identity of each language's runtime, shown in headers and details; `js` also selects the prompt's runtime line. */
|
|
69
69
|
readonly runtimes?: EvalRuntimes;
|
|
70
|
-
/** Absolute path of the active bun
|
|
70
|
+
/** Absolute path of the active bun skill; the prompt names it as MUST READ on a bun kernel. */
|
|
71
71
|
readonly bunSkillPath?: string;
|
|
72
72
|
/** Kernel globals of the tools active when a cell is submitted; read once per cell. */
|
|
73
73
|
readonly kernelPreludes?: () => readonly KernelPreludeContribution[];
|
package/src/tool/eval-tool.ts
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
} from "../config/settings.ts";
|
|
9
9
|
import { buildEvalPrompt } from "../prompt/eval-prompt.ts";
|
|
10
10
|
import { EvalDetachedCellManager } from "./detached-cell-manager.ts";
|
|
11
|
+
import { registerEvalArgumentValidator } from "./eval-argument-validator.ts";
|
|
12
|
+
import { evalLanguageInferredNote } from "./eval-language-inference.ts";
|
|
11
13
|
import { executeEvalControl } from "./detached-eval-result.ts";
|
|
12
14
|
import {
|
|
13
15
|
EvalIsolateInvalidError,
|
|
@@ -37,6 +39,7 @@ export type { EnabledEvalLanguages, EvalKernel, EvalKernelManager } from "./type
|
|
|
37
39
|
type EvalExecuteArgs<Request extends EvalToolRequest> = Parameters<
|
|
38
40
|
ToolDefinition<TUnsafe<Request | Partial<EvalToolInput>>, EvalToolDetails>["execute"]
|
|
39
41
|
>;
|
|
42
|
+
type RunArgs = EvalExecuteArgs<EvalToolInput>;
|
|
40
43
|
|
|
41
44
|
export function createEvalTool(options: CreateEvalToolOptions) {
|
|
42
45
|
const foregroundWindowSeconds = options.foregroundWindowSeconds ?? DEFAULT_FOREGROUND_WINDOW_SECONDS;
|
|
@@ -66,6 +69,7 @@ export function createEvalTool(options: CreateEvalToolOptions) {
|
|
|
66
69
|
...(options.promptKernelPreludes === undefined ? {} : { kernelPreludes: options.promptKernelPreludes }),
|
|
67
70
|
});
|
|
68
71
|
const languages = enabledLanguageList(options.enabledLanguages);
|
|
72
|
+
registerEvalArgumentValidator(parameters, languages);
|
|
69
73
|
const cellManager =
|
|
70
74
|
options.cellManager ??
|
|
71
75
|
new EvalDetachedCellManager({
|
|
@@ -85,6 +89,19 @@ export function createEvalTool(options: CreateEvalToolOptions) {
|
|
|
85
89
|
): Promise<AgentToolResult<EvalResultDetails>> {
|
|
86
90
|
const request = parseEvalRequest(params, languages, { sandbox });
|
|
87
91
|
if (isEvalControlRequest(request)) return await executeEvalControl(cellManager, request);
|
|
92
|
+
const output = await executeRun(request, toolCallId, signal, onUpdate, ctx);
|
|
93
|
+
if (request.language_inferred === undefined) return output;
|
|
94
|
+
// The model sees that its omitted language was filled, and how, so it passes language next time (senpi#5154).
|
|
95
|
+
const note = evalLanguageInferredNote(request.language, request.language_inferred);
|
|
96
|
+
return { ...output, content: [{ type: "text", text: note }, ...output.content] };
|
|
97
|
+
}
|
|
98
|
+
async function executeRun(
|
|
99
|
+
request: EvalToolInput,
|
|
100
|
+
toolCallId: RunArgs[0],
|
|
101
|
+
signal: RunArgs[2],
|
|
102
|
+
onUpdate: RunArgs[3],
|
|
103
|
+
ctx: RunArgs[4],
|
|
104
|
+
): Promise<AgentToolResult<EvalResultDetails>> {
|
|
88
105
|
if (options.proxyExecutor) {
|
|
89
106
|
// A proxy runs cells elsewhere and knows nothing of sandbox cells: an isolated cell must never run unisolated.
|
|
90
107
|
if ("isolate" in request && request.isolate === true)
|
package/src/tool/types.ts
CHANGED
|
@@ -59,6 +59,8 @@ export interface EvalToolInput {
|
|
|
59
59
|
readonly reset?: boolean;
|
|
60
60
|
/** Run in a fresh sandbox VM instead of the persistent kernel; accepted only while `sandbox.enabled` is on. */
|
|
61
61
|
readonly isolate?: boolean;
|
|
62
|
+
/** Set when the call omitted language and it was filled (senpi#5154); the result then says so. */
|
|
63
|
+
readonly language_inferred?: "only-enabled-language" | "code-markers";
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export interface EvalListInput {
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: bun-1-4
|
|
3
|
-
description: "Read before a js eval cell that installs a package, spawns a server or PTY, or starts a long run, and before any bun -e, script, server, CLI, test, bundle, or package-management work: this session's eval js kernel runs Bun 1.4+ (this skill is present only when it does). Bun 1.4 ships builtins that replace 15+ npm deps — check here BEFORE installing sharp, puppeteer/playwright (scraping), marked, node-cron, node-pty, concurrently, serve-static, tar, json5, fast-xml-parser, string-width. Triggers: eval js, bun, Bun.serve, bun test, bun build, bun install, bun run, image resize, headless browser, markdown render, cron, PTY."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Bun 1.4 — Use the Builtins First
|
|
7
|
-
|
|
8
|
-
Bun 1.4 (2026) ships builtins that replace most utility npm packages. **Before adding a dependency or writing a workaround, check the capability map below — if Bun ships it, use the builtin.** This skill is loaded because this session's `eval` js kernel runs Bun >= 1.4; on other machines or pinned projects, confirm with `bun --version` ([v1.3.x]/[v1.4.0] tags in the references give exact minimums). Release post: <https://bun.com/blog/bun-v1.4>
|
|
9
|
-
|
|
10
|
-
## Capability map — what you can do now
|
|
11
|
-
|
|
12
|
-
| You want to... | Use | Replaces | Ref = `references/<name>.md` |
|
|
13
|
-
|---|---|---|---|
|
|
14
|
-
| Resize/convert/rotate images | `Bun.file(p).image().resize().webp().write()` | sharp | runtime-apis |
|
|
15
|
-
| Headless browser: navigate, click, screenshot, evaluate, CDP | `new Bun.WebView()` | puppeteer, playwright (scraping) | runtime-apis |
|
|
16
|
-
| Markdown → HTML / React / ANSI | `Bun.markdown.html() / .react() / .render()` | marked, react-markdown | runtime-apis |
|
|
17
|
-
| Schedule cron jobs | `Bun.cron()` | node-cron | runtime-apis |
|
|
18
|
-
| Drive a PTY (bash, vim, TUIs) from JS | `Bun.spawn([...], { terminal })` | node-pty | runtime-apis |
|
|
19
|
-
| Run package scripts concurrently, glob-matched | `bun run --parallel "build:*"` | concurrently, npm-run-all | runtime-apis |
|
|
20
|
-
| Parse JSON5 / JSONL / JSONC / XML / TOML; tarballs | `Bun.JSON5/.JSONL/.JSONC/.XML/.TOML`, `Bun.Archive` | json5, ndjson, jsonc-parser, fast-xml-parser, @iarna/toml, tar | runtime-apis |
|
|
21
|
-
| ANSI-aware terminal text: width, slice, wrap | `Bun.stringWidth()`, `Bun.sliceAnsi()`, `Bun.wrapAnsi()` | string-width, slice-ansi, wrap-ansi | runtime-apis |
|
|
22
|
-
| Call C libraries 3x faster, plain strings back | `bun:ffi` (`buffer_length`, `cstring`) | — | runtime-apis |
|
|
23
|
-
| Serve a static dir (ETag/Range/304 handled) | `Bun.serve({ routes: { "/x/*": { dir } } })` | express.static, serve-static, sirv | http-networking |
|
|
24
|
-
| HTTP/3 server; `fetch` protocol/compress/proxy | `http3: true`, `fetch(url, { protocol, compress, proxy })` | — | http-networking |
|
|
25
|
-
| Tests across CPU workers / CI shards / changed files / flaky retry | `bun test --parallel --shard=1/3 --changed=main --retry` | jest/vitest infra | test |
|
|
26
|
-
| Kill "passes alone, fails in suite" bugs | `bun test --isolate` | vitest default behavior | test |
|
|
27
|
-
| React Compiler auto-memoization | `bun build --react-compiler` | babel-plugin-react-compiler | build |
|
|
28
|
-
| Embed assets/dirs into a single-file executable | `bun build --compile --asset ./public` | pkg hacks | build |
|
|
29
|
-
| Compile-time feature flags, in-memory bundling, metafile | `bun:bundle feature()`, `files:{}`, `metafile: true` | esbuild define/plugins | build |
|
|
30
|
-
| Diff package versions, fix vulns, dedupe/prune, license audit | `bun pm diff`, `bun audit fix`, `bun dedupe`, `bun prune`, `bun pm licenses` | npm-diff, npm audit fix | install |
|
|
31
|
-
| 7x faster CI installs (global virtual store) | `linker = "isolated"` in bunfig.toml | pnpm store | install |
|
|
32
|
-
| Profile CPU/heap/bundle as grep-able Markdown | `bun --cpu-prof-md`, `--heap-prof-md`, `bun build --metafile-md` | Chrome DevTools round-trip | dev-tooling |
|
|
33
|
-
| Native REPL; render Markdown in terminal | `bun repl`, `bun ./README.md` | node repl, glow | runtime-apis |
|
|
34
|
-
| Run Playwright, vitest, Next.js 16, OpenTelemetry, dd-trace | they now just work | Node.js | node-compat-platforms |
|
|
35
|
-
|
|
36
|
-
Free wins (no code change): 2x lower CPU, 13-48% less server memory, 2-2.5x faster startup — full numbers in [performance](references/performance.md).
|
|
37
|
-
|
|
38
|
-
## Operating rules
|
|
39
|
-
|
|
40
|
-
1. **Eval-first.** The `eval` js kernel runs on Bun (that is why this skill is present): call `Bun.*` APIs directly inside js cells. Reach for `bash` or a spawned `bun <file>` / `bun -e` only for CLI surfaces (`bun test`, `bun build`, `bun pm`) or work that needs a separate process.
|
|
41
|
-
2. **Builtin-first.** The Replaces column above is a ban list for new dependencies, in production code, test helpers, and one-off scripts alike.
|
|
42
|
-
3. **Read the matching reference before using an unfamiliar API** — signatures and caveats (e.g. `Bun.markdown` HTML is unsanitized, HTTP/3 is experimental) live there, not here.
|
|
43
|
-
4. **Upgrading a project to 1.4 or debugging behavior that changed?** Read [breaking-changes](references/breaking-changes.md) first — YAML/TOML strictness, `.xml`/`.css` import semantics, `Bun.$` globbing, fetch header combining, and WebSocket close timing all changed.
|
|
44
|
-
5. **TLS errors after upgrade are usually intentional** — 1.4 tightened certificate verification across `fetch`, `tls.connect`, `Bun.connect`, `RedisClient`. See [security](references/security.md) before loosening anything.
|
|
45
|
-
6. **A long job outlives a tool call only as `bash({ command, run_in_background: true })`** — plain `&` dies with the call's PTY hangup (native backend), `nohup` at best leaves an untracked orphan a timeout still group-kills, and a non-persistent `monitor` kills its watched command at `timeout_ms`; start the job in a background session and only watch it.
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# Bun 1.4 Dev Tooling & Observability
|
|
2
|
-
|
|
3
|
-
Blog: [#dev-tooling](https://bun.com/blog/bun-v1.4#dev-tooling), [#observability](https://bun.com/blog/bun-v1.4#observability)
|
|
4
|
-
|
|
5
|
-
## Markdown profilers — built for terminals and LLMs
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
bun --cpu-prof-md ./app.ts # CPU profile as Markdown: hot functions by self time,
|
|
9
|
-
# call tree, who-calls-whom. grep it, paste into a bug or an LLM.
|
|
10
|
-
bun --heap-prof-md ./app.ts # heap profile as Markdown: total size, types by retained size,
|
|
11
|
-
# largest objects, retention chains. Includes grep recipes in the header.
|
|
12
|
-
bun build ./src/index.ts --outdir ./dist --metafile-md=./dist/meta.md # bundle-size analysis as Markdown
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
- `bun --cpu-prof` / `--heap-prof` still write `.cpuprofile` / V8-compatible `.heapsnapshot` for Chrome DevTools / VS Code.
|
|
16
|
-
- `BUN_CPU_PROFILE=1` turns on the CPU profiler for a process you cannot pass flags to (e.g. a framework-spawned worker).
|
|
17
|
-
- **Agent workflow: when a Bun process is slow or leaking, run the `-md` variants and read the report directly — no DevTools round-trip.**
|
|
18
|
-
|
|
19
|
-
## Async stack traces [v1.4.0]
|
|
20
|
-
|
|
21
|
-
Errors from async native APIs (`fs.promises`, `Bun.file()`, S3, DNS, crypto, `fetch`) point at the `await` in your code, not native frames.
|
|
22
|
-
|
|
23
|
-
## Process lifetime & env flags
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
bun --no-orphans app.ts # exit when parent dies; SIGKILL every descendant on exit (Linux/macOS/Windows)
|
|
27
|
-
bun --no-env-file app.ts # skip automatic .env loading (or env = false in bunfig.toml) — use in prod/CI
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## APM / tracing works now
|
|
31
|
-
|
|
32
|
-
- **`node:inspector`**: a `Session` can start/stop CPU profiles while the app runs (`Profiler.start`/`Profiler.stop`).
|
|
33
|
-
- **Datadog**: `dd-trace` traces requests; `@datadog/pprof` profiles continuously (required V8 C++ APIs implemented).
|
|
34
|
-
- **OpenTelemetry**: `@opentelemetry/instrumentation-http` and `-fs` export spans; `shimmer` and `require-in-the-middle` can patch bundled code.
|