diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f3266032..b3d942ebd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,13 +81,14 @@ jobs: ]}' else # main, release/**, workflow_call: full cross-platform matrix. - # All targets cross-compiled from ubuntu-latest via fossilize. + # macOS targets build natively for V8 code cache (~15% faster startup). + # Other targets cross-compile from ubuntu-latest via fossilize. echo '{"include":[ - {"target":"darwin-arm64", "os":"ubuntu-latest", "can-test":false}, + {"target":"darwin-arm64", "os":"macos-latest", "can-test":true}, + {"target":"darwin-x64", "os":"macos-13", "can-test":false}, {"target":"linux-x64", "os":"ubuntu-latest", "can-test":true}, - {"target":"windows-x64", "os":"ubuntu-latest", "can-test":false}, - {"target":"darwin-x64", "os":"ubuntu-latest", "can-test":false}, - {"target":"linux-arm64", "os":"ubuntu-latest", "can-test":false} + {"target":"linux-arm64", "os":"ubuntu-latest", "can-test":false}, + {"target":"windows-x64", "os":"ubuntu-latest", "can-test":false} ]}' fi echo 'MATRIX_EOF' @@ -261,12 +262,25 @@ jobs: env: APPLE_CERT_DATA: ${{ secrets.APPLE_CERT_DATA }} APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + shell: bash run: | - curl -L 'https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.29.0/apple-codesign-0.29.0-x86_64-unknown-linux-musl.tar.gz' -o 'rcodesign.tar.gz' - echo 'dbe85cedd8ee4217b64e9a0e4c2aef92ab8bcaaa41f20bde99781ff02e600002 rcodesign.tar.gz' | sha256sum -c + # Install rcodesign (platform-aware binary + checksum tool) + if [[ "$RUNNER_OS" == "macOS" && "$RUNNER_ARCH" == "ARM64" ]]; then + RCODESIGN_ARCHIVE="apple-codesign-0.29.0-aarch64-apple-darwin.tar.gz" + RCODESIGN_SHA256="d1a532150adaf90048260d76359261aa716abafc45c53c5dc18845029184334a" + elif [[ "$RUNNER_OS" == "macOS" ]]; then + RCODESIGN_ARCHIVE="apple-codesign-0.29.0-x86_64-apple-darwin.tar.gz" + RCODESIGN_SHA256="14ef11bedd51a8d95eafd767939ae96d5900e5a61511bef75bb21db6e7c74140" + else + RCODESIGN_ARCHIVE="apple-codesign-0.29.0-x86_64-unknown-linux-musl.tar.gz" + RCODESIGN_SHA256="dbe85cedd8ee4217b64e9a0e4c2aef92ab8bcaaa41f20bde99781ff02e600002" + fi + curl -L "https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.29.0/${RCODESIGN_ARCHIVE}" -o 'rcodesign.tar.gz' + echo "${RCODESIGN_SHA256} rcodesign.tar.gz" | shasum -a 256 -c tar -xzf rcodesign.tar.gz --strip-components=1 mv rcodesign /usr/local/bin/rcodesign rm rcodesign.tar.gz + # Decode Apple signing credentials if [ -n "$APPLE_CERT_DATA" ]; then echo "$APPLE_CERT_DATA" | base64 --decode > /tmp/certs.p12 echo 'APPLE_CERT_PATH=/tmp/certs.p12' >> $GITHUB_ENV diff --git a/.lore.md b/.lore.md index a160a005b..e3f2bc576 100644 --- a/.lore.md +++ b/.lore.md @@ -22,9 +22,15 @@ * **isSentrySaasUrl vs isSaaSTrustOrigin: two intentional SaaS checks**: \`src/lib/sentry-urls.ts\` exports two SaaS-detection helpers with intentional split: (1) \`isSentrySaasUrl(url)\` — hostname-only check (\`sentry.io\` or \`\*.sentry.io\`), accepts any protocol/port. Used for routing/UX: custom-headers warning, \`getSentryBaseUrl\`/\`isSelfHosted\`, region resolution skip, telemetry \`is\_self\_hosted\` tag. (2) \`isSaaSTrustOrigin(url)\` — stricter: additionally requires \`https:\` and default port. Used for security decisions: token-host trust comparison, sentryclirc URL trust check, URL-arg trust, login refusal. Rule: hostname-only for routing/UX (don't break users behind TLS-terminating proxies with \`http://sentry.io\`); strict for credential scoping. JSDoc on \`isSentrySaasUrl\` points callers to \`isSaaSTrustOrigin\` for security contexts. Keep both implementations in sync re: hostname matching. + +* **Issue resolve --in grammar: release + @next + @commit sentinels**: \*\*Issue resolve --in grammar + repo\_cache SQLite table\*\*: \`sentry issue resolve --in\` grammar: (a) omitted→immediate, (b) \`\\`→\`inRelease\`, (c) \`@next\`→\`inNextRelease\`, (d) \`@commit\`→auto-detect git HEAD via \`src/lib/git.ts\`, (e) \`@commit:\@\\`→explicit. \`parseResolveSpec\` splits on LAST \`@\` for scoped names. API requires \`statusDetails.inCommit: {commit, repository}\` — not bare SHA. Repo matching uses \`listRepositoriesCached(org)\` (7-day SQLite cache in \`repo\_cache\` table, schema v14). Always use \`listAllRepositories\` (paginated via \`API\_MAX\_PER\_PAGE\`) — never \`listRepositories\` (silently caps ~25). \`setCachedRepos\` wrapped in try/catch so read-only DBs (macOS \`sudo brew install\`) don't crash commands. + * **Node SEA ink sidecar: node:sea.getAsset() replaces Bun /$bunfs/ virtual FS**: (architecture) Node SEA ink sidecar: \`node:sea.getAsset()\` replaces Bun \`/$bunfs/\` virtual FS. Ink UI sidecar embedded via \`fossilize --assets dist-build/ink-app.js\`; asset key = raw CLI arg. At runtime: \`sea.getRawAsset('dist-build/ink-app.js')\`. Main bundle never calls \`import('ink')\` — sidecar pre-bundled by text-import-plugin. Dual-mode: detect SEA via \`createRequire(import.meta.url)('node:sea')\` with try/catch fallback. \`useSnapshot: true\` BROKEN. \`useCodeCache: true\` ~15% startup improvement but platform-specific V8 blob. Suppress \`ExperimentalWarning: SQLite\`: \`process.on('warning', ...)\` at very top of \`src/bin.ts\` BEFORE any imports. fossilize asset manifest key = \`basename(manifestPath)\`; entry keys = \`entry.file\`. \`new Worker(new URL(...))\` HANGS in SEA — use Blob+URL.createObjectURL. + +* **Response cache hit invisibility — synthetic Response carries no marker**: Response cache hit invisibility — synthetic Response from \`getCachedResponse()\` in \`src/lib/response-cache.ts\` is indistinguishable from network. Solved via module-level \`lastCacheHitAgeMs\`: set on hit, cleared at top of \`authenticatedFetch()\` per-call (single-process CLI = race-free). \`src/lib/cache-hint.ts\` provides \`formatCacheHint()\` (\`"cached · 3m ago · use -f to refresh"\`) and \`appendCacheHint(existingHint)\` (joins with \` | \`). Wired in \`buildCommand\` (\`src/lib/command.ts\`): \`appendCacheHint(returned?.hint)\` runs only when generator returns a \`CommandReturn\` — bare \`return;\` paths (e.g. \`--web\`) skip the hint. Same chokepoint can host future cross-cutting hint decorators. Test-only \`\_setLastCacheHitAgeForTesting(ms)\` exposes state. + * **safe-read.ts wraps isRegularFile + Bun.file().text() for FIFO-safe user-path reads**: \`src/lib/safe-read.ts\` \`safeReadFile(path, operation)\` combines \`isRegularFile()\` + file read + broad error swallow (FIFO/ENOENT/EACCES/EPERM/EISDIR/ENOTDIR). Do NOT use for committed config loads — swallows EPERM/EISDIR, making \`chmod 000 .sentryclirc\` manifest as confusing 'no auth token'. For loud permission surfacing, call \`fs.promises.stat\` directly, gate on \`isFile()\`, catch only ENOENT/EACCES. General rule: bare \`catch {}\` swallows \`EACCES\`/\`EPERM\`/\`EIO\` — always check \`(err as NodeJS.ErrnoException).code === 'ENOENT'\` and re-throw anything else. \`read-files.ts\`/\`workflow-inputs.ts\` use direct stat to reuse one stat for size-gating. Test with real \`mkfifo\` + short timeout as hang detector. @@ -56,6 +62,27 @@ ### Gotcha + +* **--json schema stability: collapse=organization drops nested org fields**: --json schema + response cache gotchas: (1) \`?collapse=organization\` shrinks \`organization\` to \`{id, slug}\` — silent --json regression. \`jsonTransform\` re-hydrates \`organization.name\` via \`resolveOrgDisplayName\` against \`org\_regions\` cache. (2) \`buildCacheKey()\` normalizes URL with sorted query params, so \`invalidateCachedResponse(baseUrl)\` misses entries with query suffixes. Use \`invalidateCachedResponsesMatching(prefix)\` (raw \`startsWith()\`); \`buildApiUrl()\` always emits trailing slash → safe prefix. (3) When \`jsonTransform\` is set, \`jsonExclude\` and \`filterFields\` are NOT applied — transform must call \`filterFields(result, fields)\` and omit excluded keys itself. + + +* **API tests must use useTestConfigDir to isolate disk response cache**: \*\*API tests must use useTestConfigDir to isolate disk response cache\*\*: Tests mocking \`globalThis.fetch\` MUST call \`useTestConfigDir()\` + \`setAuthToken()\`. \`authenticatedFetch\` checks a filesystem response cache (\`~/.sentry/cache/responses/\`) BEFORE calling fetch — without per-test dirs, test N's response is served to test N+1. TTL tiers in \`classifyUrl()\`: stable=5min, volatile=60s (issues/logs), immutable=24h (events/traces by ID). Also: \`@sentry/api\` SDK calls \`\_fetch(request)\` with no init — fall back to \`input.headers\` when \`init\` is undefined (prevents HTTP 415). SDK returns \`data={}\` for empty/204 responses — always guard with \`Array.isArray(data)\` before \`.map()\`. Use \`unwrapPaginatedResult\` (not \`unwrapResult\`) for Link header pagination. + + +* **Bun --isolate coverage inflates LF count for files with verbose comments/JSDoc**: Bun --isolate coverage inflates LF count: under \`bun test --isolate --parallel\` (CI's \`test:unit\`), Bun's coverage instrumentation counts comments, blank lines, type annotations, and closing braces as 'executable'. E.g. \`zstd-transport.ts\` LF=165 locally → 210 under --isolate, dropping coverage 99%→78%. Workaround: trim verbose inline comments inside function bodies; move rationale to JSDoc above the function. Statement coverage stays 100% — 'missing' lines are non-executable. + + +* **Bun /$bunfs/ virtual FS uses JS parser — embedded .tsx files fail on TS syntax**: \*\*Bun \`/$bunfs/\` virtual FS + Ink TUI sidecar embedding\*\*: Files embedded via \`with { type: "file" }\` run from \`/$bunfs/root/\` using a JS parser (not TypeScript) — raw \`.tsx\` crashes on \`import { type Foo }\`. Fix: pre-bundle \`.tsx\` → \`.js\` via esbuild before embedding (\`script/text-import-plugin.ts\`). \`/$bunfs/\` has no \`node\_modules\` — inline all deps; use \`createRequire\` banner for CJS deps. Only \`node:\*\` builtins external. Query strings in \`/$bunfs/\` paths cause ENOENT. Related: Ink TUI sidecar (\`ink-app.tsx\`) must be fully self-contained — main bundle must NOT import \`ink\`/\`react\` separately; call \`app.mountApp()\` from the sidecar only to avoid dual-React "Invalid hook call" errors. + + +* **Bun 1.3.11 tty.ReadStream leaks libuv handle — process.stdin.unref is undefined**: Bun 1.3.11 macOS TTY bug: \`process.stdin\` via kqueue \`EVFILT\_READ\` fails to deliver keystrokes when fd 0 is inherited via \`exec bin \ +* **MastraClient has no dispose API — use AbortController for cleanup**: MastraClient has no \`close()\`/\`dispose()\` API — cleanup via \`ClientOptions.abortSignal\` (constructor) or per-prompt \`signal\`. Without explicit abort, Bun's fetch dispatcher keep-alive sockets hold the event loop alive past natural exit. Pattern in \`src/lib/init/wizard-runner.ts\`: create \`AbortController\` per \`runWizard\`, pass \`abortSignal: controller.signal\` to \`new MastraClient(...)\`, abort via \`using \_ = { \[Symbol.dispose]: () => controller.abort() }\`. Custom \`fetch\` wrapper must preserve \`init.signal\` via spread. Tests capture \`ClientOptions\` via \`spyOn(MastraClient.prototype, 'getWorkflow').mockImplementation(function() { capturedOpts.push(this.options); ... })\`. + + +* **Multi-region fan-out: distinguish all-403 from empty orgs with hasSuccessfulRegion flag**: In \`listOrganizationsUncached\` (\`src/lib/api/organizations.ts\`), \`Promise.allSettled\` collects multi-region results. Don't use \`flatResults.length === 0\` to detect all-regions-failed — a region returning 200 OK with zero orgs pushes nothing into \`flatResults\`. Track a \`hasSuccessfulRegion\` boolean on any \`"fulfilled"\` settlement. Only re-throw 403 \`ApiError\` when \`!hasSuccessfulRegion && lastScopeError\`. + * **process.stdin.isTTY unreliable in Bun — use isatty(0) and backfill for clack**: \`process.stdin.isTTY\` unreliable — use \`isatty(0)\` from \`node:tty\`. Bun's single-file binary can leave \`process.stdin.isTTY === undefined\` on TTY fds. \`@clack/core\` gates \`setRawMode(true)\` on \`input.isTTY\`, silently disabling raw mode. Fix: backfill \`process.stdin.isTTY = true\` when \`isatty(0)\` confirms. Debugging: \`src/lib/init/tty-diagnostics.ts\` \`dumpTtyDiagnostics(label)\` — no-op unless \`SENTRY\_INIT\_DIAGNOSTICS=1\`. @@ -70,16 +97,49 @@ ### Pattern + +* **CLI-1D3 Windows download visibility race: poll statSync with exponential backoff**: Windows upgrade download visibility race: \`waitForBinaryVisible\` in \`src/lib/upgrade.ts\` polls \`statSync\` with exponential backoff (6 attempts, 5 sleeps: 100+200+400+800+1600ms). Loop breaks BEFORE final sleep — \`VERIFY\_MAX\_ATTEMPTS=N\` yields N-1 sleeps (off-by-one trap). Covers Bun 1.3.9 race where \`Bun.file().writer().end()\` returns before OS surfaces file by path. \`isEnoentSpawnError()\` in \`src/commands/cli/upgrade.ts\` catches both \`code==='ENOENT'\` and Bun's path-string error → \`UpgradeError('execution\_failed')\`. Race-free tests: writer must poll until bad state exists, then overwrite. + + +* **Cross-compile sentry-cli with patched Bun: drop compile.target to use selfExePath**: Cross-compile sentry-cli with patched Bun: \`Bun.build({compile})\` downloads stock Bun from npm when \`compile.target\` is set. Workaround in \`script/build.ts\`: omit \`target\` entirely so Bun uses \`selfExePath()\` as embed runtime. Only works when host OS/arch matches desired output. Escape hatch: place \`bun-\-\-v\\` in \`$CWD\`. Build requires \`SENTRY\_CLIENT\_ID\` env var. + + +* **Dedupe resolved entity IDs in batch operations before API call**: Batch issue merge (\`src/commands/issue/merge.ts\`): (1) Dedupe by resolved numeric ID after \`Promise.all(args.map(resolveIssue))\` — users may pass same entity as \`CLI-K9\`, \`my-org/CLI-K9\`, or \`123\`. Throw \`ValidationError\` if \`new Set(ids).size < 2\`. (2) Reject \`undefined\` orgs in cross-org check — bare numeric IDs without DSN/config resolve with \`org: undefined\`. (3) Pass \`--into\` through \`resolveIssue()\`; compare by numeric \`id\`, not \`shortId\`. (4) Sentry bulk merge API picks canonical parent by event count — \`--into\` is preference only; warn when API's \`parent\` differs. + + +* **findProjectsByPattern as fuzzy fallback for exact slug misses**: When \`findProjectsBySlug\` returns empty (no exact match), use \`findProjectsByPattern\` as a fallback to suggest similar projects. \`findProjectsByPattern\` does bidirectional word-boundary matching (\`matchesWordBoundary\`) against all projects in all orgs — the same logic used for directory name inference. In the \`project-search\` handler, call it after the exact miss, format matches as \`\/\\` suggestions in the \`ResolutionError\`. This avoids a dead-end error for typos like 'patagonai' when 'patagon-ai' exists. Note: \`findProjectsByPattern\` makes additional API calls (lists all projects per org), so only call it on the failure path. + * **Grouped widget --limit auto-default via applyGroupLimitAutoDefault helper**: Dashboard widget flag normalization: (1) Dataset aliases (errors→error-events) normalize ONCE at top of \`func()\` via \`normalizeDataset()\` in \`src/commands/dashboard/resolve.ts\`. In \`edit.ts\`, pass \`normalizedFlags\` to \`buildReplacement\` — \`validateAggregateNames\` reads \`flags.dataset\` and rejects valid aggregates like \`failure\_rate\` if it sees raw alias. (2) Grouped widgets need \`limit\` (API rejects). \`applyGroupLimitAutoDefault\` defaults to \`DEFAULT\_GROUP\_BY\_LIMIT=5\` only when user passed \`--group-by\` without \`--limit\`; skip for auto-defaulted columns like \`\["issue"]\`. (3) Tests asserting \`--limit\` >10 survives into PUT body must use \`display: "line"\` — \`prepareWidgetQueries\` clamps bar/table to max=10. + +* **Hidden --org/--project compat flags via mergeGlobalFlags**: Hidden global \`--org\`/\`--project\` flags accept old \`sentry-cli\` syntax. Defined in \`GLOBAL\_FLAGS\` (global-flags.ts) so argv-hoist relocates them. \`mergeGlobalFlags()\` in command.ts injects hidden flag shapes (skip if command owns the flag — e.g. \`release create --project -p\`) and returns \`stripKeys\` set used by \`cleanRawFlags\`. \`applyOrgProjectFlags()\` writes values to \`SENTRY\_ORG\`/\`SENTRY\_PROJECT\` via \`getEnv()\` before auth guard, overwriting existing env vars (explicit CLI > env var). Resolution chain in resolve-target.ts picks them up at priority #2. No short aliases (\`-p\` conflicts). The helper extraction was needed to keep \`buildCommand\` under Biome's cognitive complexity limit of 15. + + +* **Preserve ApiError type so classifySilenced can silence 4xx errors**: Preserve ApiError type for classifySilenced: \`classifySilenced\` (src/lib/error-reporting.ts) only silences \`ApiError\` with status 401-499 — wrapping in generic \`CliError\` loses \`status\` and causes 403s to be captured. Re-throw via \`new ApiError(msg, error.status, error.detail, error.endpoint)\` with terse message (\`ApiError.format()\` appends detail/endpoint). \`ValidationError\` without \`field\` collapses unfielded errors into one fingerprint; always pass \`field\`. Fingerprint rule changes don't retroactively re-fingerprint — manually merge new groups into canonical old parents. \`ApiError\` rule keys by \`api\_status + command\`. + + +* **Sentry SDK tree-shaking patches must be regenerated via bun patch workflow**: Sentry SDK tree-shaking via bun patch: \`patchedDependencies\` in \`package.json\` strips unused exports from \`@sentry/core\` and \`@sentry/node-core\`. Non-light root of \`@sentry/node-core\` pulls uninstalled \`@opentelemetry/instrumentation\` — \*\*always import from \`@sentry/node-core/light\`\*\* (subpaths: \`.\`, \`./light\`, \`./light/otlp\`, \`./init\`, \`./loader\`, \`./import\`). No supported import for \`HttpsProxyAgent\`. Bumping SDK: remove old patches, \`rm -rf ~/.bun/install/cache/@sentry\`, \`bun install\`, \`bun patch @sentry/core\`, edit, \`bun patch --commit\`; repeat for node-core. Preserved: \`\_INTERNAL\_safeUnref\`, \`\_INTERNAL\_safeDateNow\`, \`nodeRuntimeMetricsIntegration\`. Before stripping any core export, grep \`node-core/build/{cjs,esm}/light/sdk.js\` for runtime usage (e.g. \`spanStreamingIntegration\` when \`traceLifecycle === 'stream'\`). Remove \`.bun-tag-\*\` hunks from generated patches. Manual \`git diff\` patches fail. + -* **Shared pagination infrastructure: buildPaginationContextKey and parseCursorFlag**: Pagination infrastructure + org flag injection: Bidirectional pagination via cursor stack in \`src/lib/db/pagination.ts\`. \`resolveCursor(flag, key, contextKey)\` maps keywords (next/prev/first/last) to \`{cursor, direction}\`. \`advancePaginationState\` manages stack — back-then-forward truncates stale entries. Critical: \`resolveCursor()\` must be called INSIDE \`org-all\` override closures, not before \`dispatchOrgScopedList\`. \`issue list --limit\` is global total: \`fetchWithBudget\` Phase 1 divides evenly, Phase 2 redistributes surplus. \`trimWithProjectGuarantee\` ensures ≥1 issue per project. Compound cursor (pipe-separated) enables \`-c last\` for multi-target pagination. JSON output wraps in \`{ data, hasMore }\` with optional \`errors\` array. +* **Shared pagination infrastructure: buildPaginationContextKey and parseCursorFlag**: Bidirectional pagination via cursor stack in \`src/lib/db/pagination.ts\`. \`resolveCursor(flag, key, contextKey)\` maps keywords (next/prev/first/last) to \`{cursor, direction}\`. \`advancePaginationState\` manages stack — back-then-forward truncates stale entries. \`hasPreviousPage\` checks \`page\_index > 0\`. \`paginationHint()\` builds nav strings. All list commands use this. Critical: \`resolveCursor()\` must be called inside \`org-all\` override closures, not before \`dispatchOrgScopedList\`. + + +* **Telemetry instrumentation pattern: withTracingSpan + captureException for handled errors**: For graceful-fallback operations, use \`withTracingSpan\` from \`src/lib/telemetry.ts\` for child spans and \`captureException\` from \`@sentry/bun\` (named import — Biome forbids namespace imports) with \`level: 'warning'\` for non-fatal errors. \`withTracingSpan\` uses \`onlyIfParent: true\` — no-op without active transaction. User-visible fallbacks use \`log.warn()\` not \`log.debug()\`. Several commands bypass telemetry by importing \`buildCommand\` from \`@stricli/core\` directly instead of \`../../lib/command.js\` (trace/list, trace/view, log/view, api.ts, help.ts). + + +* **Testing Stricli command func() bodies via spyOn mocking**: Testing Stricli command func() bodies: (1) \`const func = await cmd.loader(); func.call(mockContext, flags, ...args)\` with mock \`stdout\`, \`stderr\`, \`cwd\`, \`setContext\`. \`loader()\` return type union causes \`.call()\` LSP false-positives that pass \`tsc --noEmit\`. (2) When API functions are renamed, update both spy target AND mock return shape. (3) \`normalizeSlug\` replaces \`\_\`→\`-\` but does NOT lowercase. (4) Bun \`mockFetch()\` replaces \`globalThis.fetch\` — use one unified mock dispatching by URL. (5) \`mock.module()\` pollutes module registry for ALL subsequent files — put in \`test/isolated/\` and run via \`test:isolated\`. (6) For \`Bun.spawn\`, use direct property assignment in \`beforeEach\`/\`afterEach\`. ### Preference * **Always honor Retry-After header when present in LLM adapter**: (architecture) LLM adapter backoff in \`packages/gateway/src/llm-adapter.ts\`: Always honor Retry-After — \`backoffMs()\` returns \`Math.min(retryAfterMs, cap)\` where cap is \`RETRY\_AFTER\_CAP\_URGENT\_MS=8\_000\` or \`RETRY\_AFTER\_CAP\_BACKGROUND\_MS=120\_000\`. TRANSIENT\_CODES={429,500,502,503,529}; MAX\_RETRIES: rate-limit=3, server=3, urgent=2. Backoff (no Retry-After): 429 background=60s/120s/180s; urgent=min(1000×2^n,4000); 5xx background=min(1000×2^n,8000). Bearer tokens inject \`billingBlock\` as first system block; \`signBody()\` replaces \`cch=00000\` with xxHash64. System prompt caching uses \`cache\_control:{type:'ephemeral',ttl:'1h'}\`. \`opts.thinking\` NOT forwarded to bare API calls. Circuit breaker tripped on non-urgent 429s via \`tripCircuitBreaker()\`. Gateway auth (\`packages/gateway/src/auth.ts\`): \`AuthCredential\` (api-key|bearer). Two-level lookup: \`sessionAuth\` Map → \`lastSeenAuth\` global fallback via \`resolveAuth(sessionID?)\`. \`authFingerprint()\` = SHA-256 truncated to 16 hex chars. + +* **Always migrate Bun-specific APIs and tooling to Node.js equivalents**: The user is actively migrating a CLI project from Bun to Node.js/pnpm. When encountering Bun-specific code, always replace it with Node.js equivalents: \`Bun.spawn\` → \`node:child\_process\` spawn, \`Bun.sleep\` → \`node:timers/promises\` setTimeout, \`bun:sqlite\` → \`node:sqlite\`, \`bun run\` → \`pnpm run\`/\`tsx\`, \`bun.lock\` → pnpm lockfile. All packages must be in \`devDependencies\` (never \`dependencies\`). Exception: \`script/build.ts\` uses \`Bun.build()\` and stays on Bun; the \`build-binary\` CI job retains \`setup-bun\`. After each migration phase, ensure lint and tests pass before committing. + + +* **Bot review triage: distinguish real bugs from SDK-mirroring false positives**: When Sentry Seer or Cursor Bugbot flags 'unusual' code that intentionally mirrors upstream SDK behavior (e.g., \`http\_proxy\` as last-resort fallback for HTTPS URLs — deliberate in \`@sentry/node-core\` \`applyNoProxyOption\`), decline with a written rationale referencing the SDK source rather than silently changing behavior. Removing the mirror creates a divergence where users get different proxy semantics from our transport vs. the SDK default. BYK's pattern: verify against \`node\_modules/@sentry/node-core/build/esm/transports/http.js\`, post a reply explaining the precedent, and resolve the thread. Real bugs (uppercase env var support, whitespace trimming, wildcard handling) get fixed; SDK-mirroring 'bugs' get explained and dismissed. + * **Prefers Bun-native APIs; use buildCommand from lib/command.js (never @stricli/core directly); use buildRouteMap from lib/route-map.js; silent catch blocks prohibited; every new src/lib/\*\*/\*.ts must start with module-level JSDoc; test isolation via useTestConfigDir(); prefer property-based and model-based tests over unit tests; DEFAULT\_NUM\_RUNS = 50; architecture tree documented; error exit code ranges: 1x=auth**: Project conventions (AGENTS.md): use \`pnpm run\`/\`pnpm install\`/\`pnpm add -D\` (NOT bun); use buildCommand from lib/command.js (never @stricli/core directly); use buildRouteMap from lib/route-map.js; silent catch blocks prohibited; every new src/lib/\*\*/\*.ts must start with module-level JSDoc; test isolation via useTestConfigDir(); prefer property-based and model-based tests over unit tests; DEFAULT\_NUM\_RUNS=50; error exit code ranges: 1x=auth, 2x=input/config, 3x=API/network, 4x=feature/billing, 5x=operations, 6x=command-specific. Testing: vitest + fast-check (migrated from bun:test). All packages in devDependencies (CI enforces via \`pnpm run check:deps\`). Always check package.json for latest scripts.