- @sentry/symbolic 13.4.0 API surface: SourceBundleWriter for bundle-sources command: `@sentry/symbolic@13.4.0` exports 4 classes: `Archive`, `FileEntry`, `ObjectFile`, `SourceBundleWriter`, plus `SourceFileDescriptor`. Key for CLI source-tier commands: `SourceBundleWriter.writeObject(object: ObjectFile, object_name: string, filter: Function, provider: Function): Uint8Array | undefined` — callback-based; provider reads source content by path, filter selects files. `bundle-sources` is directly implementable (provider reads from disk). `print-sources` is BLOCKED — `ObjectFile` has no `sourceFiles()` enumeration method in 13.4.0 (only props: arch, codeId, debugId, fileFormat, hasDebugInfo, hasSources, hasSymbols, hasUnwindInfo, kind). `SourceFileDescriptor` has get/set props: contents, debugId, path, sourceMappingUrl, url, type. Confirmed by Dav1dde (Sebastian Zivota's colleague) on Jun 23 2026.
- Auth token env var override pattern: SENTRY_AUTH_TOKEN > SENTRY_TOKEN > SQLite: Auth token precedence in `src/lib/db/auth.ts`: `SENTRY_AUTH_TOKEN` > `SENTRY_TOKEN` > SQLite OAuth token. `getEnvToken()` trims env vars (empty/whitespace = unset). `AuthSource` tracks provenance. `ENV_SOURCE_PREFIX = "env:"` — use `.length` not hardcoded 4. Env tokens bypass refresh/expiry. `isEnvTokenActive()` guards auth commands. Logout must NOT clear stored auth when env token active. `runInteractiveLogin` catches OAuth flow errors internally and returns falsy on failure; login command sets `process.exitCode = 1` and returns normally (does NOT reject). Tests expecting `rejects.toThrow()` will fail — assert via fetch-call inspection instead. `requestDeviceCode` requires `SENTRY_CLIENT_ID` env var.
- Binary size breakdown: 94.5% is Node.js runtime — bundled code is ~6.3 MiB: Binary composition (linux-x64, Node 24 LTS): Node.js runtime=121 MiB (ships with debug symbols). `strip --strip-unneeded` → 99 MiB (-17 MiB raw, -4 MiB compressed). Strip built into fossilize 0.7.0 — happens on the copied binary BEFORE postject injection. After strip+SEA+binpunch: ~108 MiB raw, ~30 MiB gzip (vs 125 MiB / 34 MiB unstripped). .rodata=52.5 MB: V8 snapshot ~12 MB, ICU full-icu data ~28 MB. UPX compresses to 25 MiB but DESTROYS ELF notes — ruled out. `--with-intl=small-icu` saves ~26-28 MiB (biggest win from custom build); `--without-lief` BREAKS SEA; `--without-sqlite` BREAKS CLI; `--disable-single-executable-application` BREAKS EVERYTHING. Custom build deferred — poor cost/benefit (~3.5h build vs 5min fossilize). Final vs Bun: download 30 MiB (Bun: 32 MiB), `--version` ~1.0s (Bun: ~1.9s), completions ~150ms (Bun: ~180ms).
- binpatch progress UX contract: library emits, consumer renders: binpatch `src/events.ts` exports `ProgressEvent`, `ProgressHandler`, `ProgressPhase`, `safeProgress` — the library EMITS these events but NEVER renders them. Consumers handle all UI rendering (progress bars, spinners, etc.). This is the explicit contract: `safeProgress(handler, evt)` is the safe emit helper that catches handler exceptions. Any future PR that adds console/log output for progress inside the library violates this contract — push rendering to the consumer. Applies to all binpatch patches; library stays UI-agnostic.
- binpatch TRDIFF10 wire format + OCI tag scheme constants: binpatch wire format (TRDIFF10): 8-byte magic `TRDIFF10\x00`, then LE int64 `controlLen`/`diffLen`/`newSize` (sign-magnitude), 24-byte control tuples with `readDiffBy`/`readExtraBy`/`seekBy` fields, zstd-compressed control/diff/extra blocks. OCI tag scheme: `<repo>:nightly` (mutable pointer), `<repo>:nightly-<version>` (immutable), `<repo>:patch-<version>` (patches). Annotations: `from-version=<prev>` (pointer, NOT hash — trust model), `sha256-<binaryName>=<hex>` (final binary hash only), `org.opencontainers.image.title`. Artifact type: `application/vnd.<prefix>.patch`. Security limits: `MAX_OUTPUT_SIZE=2_147_483_648` (2 GiB), `MAX_NIGHTLY_CHAIN_DEPTH=30`, `MAX_STABLE_CHAIN_DEPTH=10`, `SIZE_THRESHOLD_RATIO=0.6`. Patches integrate via SHA-256 of FINAL output only — intermediate hops don't hash (perf).
- bspatch.ts in-memory chain refactor: transformPatch callback + three public APIs: Core patching logic extracted into `transformPatch(oldFile, patchData, onChunk)` callback-based function. Three public APIs: `applyPatchToFile(oldPath, patchData, destPath)→SHA-256` (disk sink, final hop); `applyPatchToMemory(oldFile, patchData)→Uint8Array` (in-memory, intermediate hops); `applyPatchChainInMemory(oldPath, patches[], destPath)→SHA-256` (full chain orchestrator). `applyPatch()` kept as thin wrapper for backward compat. Orchestration lives in bspatch.ts (not delta-upgrade.ts) to keep buffer handling encapsulated. `onChunk` callback checks `writeError` flag set by writer 'error' event — throws immediately rather than waiting for top-of-loop check. `applyPatchToMemory` preallocates Uint8Array of `newSize`; corrupt patch claiming huge size throws RangeError → triggers full download fallback.
- bspatch.ts: TRDIFF10 patch application — inline SHA-256, streaming zstd, CoW old-file copy: TRDIFF10 header: 32 bytes (magic + controlLen + diffLen + newSize, all i64 LE). Control block decompressed fully via `zstdDecompressSync` (needs random access). Diff + extra blocks streamed via `createZstdStreamReader` (Node Transform → Web ReadableStream → `BufferedStreamReader`). `applyPatch()` ALWAYS computes SHA-256 inline and returns it — no separate verification step. `loadOldBinary()` copies to temp via `COPYFILE_FICLONE` (CoW reflink, falls back to regular copy) then reads into memory. `cleanupPatchResources()` runs all cleanup steps regardless of prior failures. Write errors captured early via `writer.on('error')` to avoid ERR_UNHANDLED_ERROR on ENOSPC/EIO.
- check-fragments.ts: validates fragment files against actual route names: `script/check-fragments.ts`: validates fragment files against actual route names (Check 1-4) AND validates subcommand coverage within fragments (Check 5). Check 5: for each route with >1 command, verifies fragment mentions each subcommand via a heading (outside fenced code blocks) or `sentry <route> <subcommand>` code reference. Default commands handled: if fragment contains bare `sentry <route>`, the default command is covered. Default commands detected from route map (`defaultCommand` field in route index files). Fenced code block content stripped before heading scan to avoid false positives from bash comments. Warnings by default; `--strict` makes them errors. Run via `pnpm run check:fragments`. CI `check-generated` job triggers when `changes.outputs.skill == 'true'`.
- check:stale-refs: generic toolchain consistency scanner derived from package.json: `script/check-stale-references.ts`: reads `packageManager` from `package.json` (e.g., `pnpm@10.11.0`), derives stale PMs dynamically, and scans dev-facing docs/scripts for stale `<pm> run`, `<pm> remove`, `<pm> add -d` commands and `requires <pm>`/`<pm> installed` prerequisite prose. Excludes: user-facing install instructions (fenced code blocks with `install -g`/`add -g`), the check script itself, and `node_modules/`. Added to CI lint job. **Generic**: if project migrates from pnpm to yarn, changing `packageManager` in `package.json` auto-flags all `pnpm run` references in dev docs — no manual pattern updates needed. Trap: script must exclude itself from scanning or its own JSDoc examples trigger false positives.
- ci.yml generate-patches: same-series delta filter (PR #1329): `generate-patches` job in `.github/workflows/ci.yml` (lines 488-526) selects `PREV_TAG` by walking `git tags filtered by ^nightly-[0-9]` and breaking when the loop hits `nightly-${VERSION}` (about-to-be-pushed tag). Bug: `sort -V` places `0.41` nightly AFTER every `0.40` nightly, so `0.40` builds walked past all `0.40` tags — patch stamped `from-version: 0.41.x-dev.Y`, useless to `0.40` users. PR #1329 fix: `MAJOR_MINOR=$(echo "${VERSION}" | cut -d. -f1,2)` + `SAME_SERIES_TAGS=$(printf '%s\n' "$TAGS" | grep "^nightly-${MAJOR_MINOR}\.\" || true)` + loop walks `SAME_SERIES_TAGS`. PREV_TAG carried via job output to `publish-nightly` which strips `nightly-`/`v` prefix for `from-version` annotation. Verified end-to-end: 0.40.0-dev.1785526951→0.40.0-dev.1785546241 used 2 patches, 249.9 KB (vs ~31 MB full download).
- collapse=lifetime in issue list: LIFETIME_FIELDS, buildListApiOptions, and API gotcha: `src/commands/issue/list.ts` `LIFETIME_FIELDS = new Set(['count','userCount','firstSeen','lastSeen'])` — fields stripped by `collapse=lifetime` on the list endpoint. `buildListApiOptions(json, fields)`: `collapseLifetime` only true when `json && fields !== undefined && fields.length > 0 && !fields.some(f => LIFETIME_FIELDS.has(f))`. Human output NEVER collapses lifetime. `buildIssueListCollapse()` always starts with `['filtered','unhandled']`, conditionally adds `'lifetime'` then `'stats'`. `ISSUE_DETAIL_COLLAPSE` safely includes `'lifetime'` — detail endpoint preserves top-level fields regardless. `IssueViewOutputSchema` in `src/types/sentry.ts` extends `SentryIssueSchema` with enrichment fields (`event`, `org`, `replayIds`, `trace`) added by `jsonTransformIssueView`. Wired via `schema: IssueViewOutputSchema` on output config in `view.ts`. NOTE: `count`/`userCount`/`firstSeen`/`lastSeen` always present on `issue view` (detail endpoint) — only potentially absent on `issue list` when collapse=lifetime is active.
- Consola chosen as CLI logger with Sentry createConsolaReporter integration: Consola is the CLI logger with Sentry `createConsolaReporter` integration. Two reporters: FancyReporter (stderr) + Sentry structured logs. Level via `SENTRY_LOG_LEVEL`. `buildCommand` injects hidden `--log-level`/`--verbose` flags. `withTag()` creates independent instances; `setLogLevel()` propagates via registry. All user-facing output must use consola, not raw stderr. `HandlerContext` intentionally omits stderr. Telemetry opt-out priority: (1) `SENTRY_CLI_NO_TELEMETRY=1`, (2) `DO_NOT_TRACK=1`, (3) `metadata.defaults.telemetry`, (4) default on. Shell completions set `SENTRY_CLI_NO_TELEMETRY=1` in `bin.ts` before imports. Timing queued to `completion_telemetry_queue` SQLite table; normal runs drain via `DELETE ... RETURNING`. `ENV_VAR_REGISTRY` in `src/lib/env-registry.ts` is single source for all honored env vars; `topLevel: true` + `briefDescription` surfaces in `--help`. Add install-script-only vars with `installOnly: true`.
- Custom CA loading: priority, caching, TLS error detection, and SaaS warning: Custom CA in `src/lib/custom-ca.ts`: Priority: (1) `sentry cli defaults ca-cert` (SQLite), (2) `NODE_EXTRA_CA_CERTS`. Cached per-process via module-level vars (`hasResolved` flag). `resolve()` concatenates custom PEM with `rootCertificates` (additive — Bun replaces Mozilla bundle otherwise). `tryReadPem()` NEVER throws — missing CA file logs warn and returns `undefined`. `injectIntoNodeTls()` uses `tls.setDefaultCACertificates()` (Node 24+ only; no-op on Node 22). `TLS_ERROR_PATTERNS`: 5 patterns (local issuer, verify first cert, UNABLE_TO_VERIFY_LEAF_SIGNATURE, DEPTH_ZERO_SELF_SIGNED_CERT, SELF_SIGNED_CERT_IN_CHAIN) — explicitly excludes `CERT_HAS_EXPIRED` and `ERR_TLS_CERT_ALTNAME_INVALID`. `getTlsCertErrorMessage()` walks `error.cause` chain with cycle detection. SaaS target + env-sourced CA → one-time warning; stored default silences it. `__resetForTests()` resets all cached state.
- debug-files upload: per-file upload design and assemble body shape: The `sentry debug-files upload` command uses per-file upload (not per-slice). Assemble body shape: `{ [overallSha1]: { name, debug_id?, chunks: string[] } }`. Two modes: no-wait (stop once server holds chunks) and `--wait` (poll for `ok`/`error` up to `ASSEMBLE_MAX_WAIT_MS`). Filter rules mirror legacy `filter_features`. Auth deferred to `resolveOrgAndProject()` (standard cascade). Source bundles via `createSourceBundle` when `--include-sources`. Deduplication uses `debugId:sha1(content)` composite key. Early peek via `peekFormat()` in `prepareDifs` rejects non-DIF files before full read. Location: `src/lib/api/debug-files.ts`, `src/lib/dif/scan.ts`, `src/commands/debug-files/upload.ts`.
- delta-upgrade.ts: patch chain resolution and application architecture: Two channels: stable (GitHub Releases) and nightly (GHCR `patch-<version>` tags). Patch format: TRDIFF10 (zig-bsdiff + zstd). Constants: `MAX_STABLE_CHAIN_DEPTH=10`, `MAX_NIGHTLY_CHAIN_DEPTH=30`, `SIZE_THRESHOLD_RATIO=0.6`. Stable: single API call fetches releases with asset metadata, parallel `Promise.all` download. Nightly: list tags → filter semver range → fetch manifests → parallel blob download. `applyPatchesSequentially()` alternates between two intermediate files (`${destPath}.patching.a`/`.b`) — never read/write same path (mmap corruption). SHA-256 verified ONCE after all patches applied, not per-intermediate. Cache-first: `tryLoadCachedChain()` with key `patch-chain:{from}-{to}`. `canAttemptDelta()` blocks on dev version, cross-channel, or downgrade.
- embedded-ppdb: PE files are dropped, only extracted PPDB is uploaded: When scanning a managed PE (e.g. .NET assembly) with an embedded Portable PDB, `difFromCandidateBuffer` / `prepareFileDif` in `src/lib/dif/scan.ts` extracts the PPDB and returns it as a separate `PreparedDif` — the PE itself is dropped (featureless: no native debug info). Only the PPDB reaches the upload queue. This mirrors legacy `validate_dif` behavior which would reject featureless PEs anyway. The `--type portablepdb` filter is required to match; `--type pe` alone yields nothing for managed assemblies without native debug info.
- generate-docs-sections.ts: in-place marker injection into committed files: `script/generate-docs-sections.ts` (555+ lines): injects auto-generated content into committed files between named marker pairs. Marker styles: HTML `<!-- GENERATED:START name -->` (`.md`); MDX `{/* GENERATED:START name */}` (`.mdx`). `--check` flag: dry-run, exits 1 if stale. 13 sections across 5 files: `contributing.md` (project-structure, dev-prereq, build-commands), `DEVELOPMENT.md` (oauth-scopes, dev-env-vars, dev-prereq, build-toolchain), `self-hosted.md` (oauth-scopes, self-hosted-env-vars), `README.md` (dev-prereq, library-prereq, dev-scripts), `getting-started.mdx` (platform-support). Version extractors (`extractPnpmVersion`, `extractNodeVersion`) **throw on mismatch** — no silent fallbacks. No Bun references remain. CI `check-generated` job runs with `--check` flag.
- generate-docs-sections.ts: project-structure tree rendering invariant: In `generateProjectStructure()` (line 178 comment): groups (route directories) always use `├──` prefix regardless of position — because standalones always follow groups. Standalone entries include `help.ts` (added manually before sort); last standalone uses `└──`, others use `├──`. Both groups and standalones sorted alphabetically within their sections. Output is a fenced code block with `cli/` tree.
- generate:docs pipeline: 4-script sequence, prerequisites, and output ownership: Master orchestrator: `generate:docs` runs 4 scripts in sequence: (1) `generate:parser` → `script/generate-parser.ts`, (2) `generate:command-docs` → `script/generate-command-docs.ts`, (3) `generate:skill` → `script/generate-skill.ts`, (4) `generate:docs-sections` → `script/generate-docs-sections.ts`. Prerequisite for: `dev`, `build`, `build:all`, `bundle`, `typecheck`, `test:unit`, `test:changed`, `test:e2e`. Output ownership: `docs/src/content/docs/commands/` and `docs/src/content/docs/configuration.md` are gitignored (fully generated). `docs/src/fragments/` files are committed source of truth (hand-written custom content). `DEVELOPMENT.md`, `README.md`, `contributing.md`, `self-hosted.md`, `getting-started.mdx` are committed but have in-place injected sections between named markers.
- getsentry/cli skill system: generate-skill.ts outputs 4 artifacts, SKILL.md is auto-generated: `script/generate-skill.ts` (927 lines) generates skill files from Stricli CLI route tree introspection. Outputs: `plugins/sentry-cli/skills/sentry-cli/SKILL.md`, `plugins/sentry-cli/skills/sentry-cli/references/*.md` (26 files), `docs/public/.well-known/skills/index.json`, `src/generated/skill-content.ts`. The `skill-content.ts` embeds all skill files into the binary at build time so `agent-skills.ts` installs without network fetching. SKILL.md is auto-generated — never edit manually; regenerate with `pnpm run generate:docs`. `.cursor/skills/sentry-cli/` contains symlinks to `plugins/` location. Claude Code uses `.claude-plugin/marketplace.json` at repo root.
- getsentry/symbolic WASM architecture: zstd via C zstd-sys wasm-shim, self_cell ownership: WASM build uses C zstd via `zstd-sys` for ALL targets including `wasm32-unknown-unknown`. `zstd-sys` ships `wasm-shim/` with C headers; `build.rs` auto-enables shim for wasm32. CI `wasm-build` job installs `clang lld llvm`. ruzstd dropped (significantly slower per crate author + Sebastian Zivota). Ownership model: `self_cell`-based (`SelfCell<ByteView<'static>, di::Archive<'static>>`). `ObjectFile` rename fix: `#[wasm_bindgen(js_name = "ObjectFile")]`. Canonical field names use `.name()` method (lowercase: `elf`, `x86_64`) not `{:?}` Debug formatting. Smoke tests in `symbolic-wasm/npm/` so `npm test` works from npm dir. PR sequencing: symbolic PRs (#988, #992) must merge + republish before CLI `bundle-sources` PR.
- InkUI teardown order — 6 steps, all try/catch, torndown guard prevents double-unmount: `InkUI.tearDown()` must follow this order: (1) stop tip-rotation interval; (2) detach SIGINT listener + `store.setRequestCancel(undefined)`; (3) `instance.clear()`; (4) `instance.unmount()`; (5) restore alternate screen `\x1b[?1049l`; (6) `freshStdin.setRawMode(false)` + `.pause()` + `.destroy()`. `torndown: boolean` guard prevents double-unmount (throws on some platforms). `cancelRequested` guard: second Ctrl+C → `process.exit(130)`. Every step wrapped in try/catch.
- isSaaS() vs isSaaSTrustOrigin: different purposes, same URL source: In `src/lib/sentry-urls.ts`: `isSaaS()` (now exported) checks hostname only via `isSentrySaasUrl(getSentryBaseUrl())` — used for routing/UX decisions like `defaultIssueSort()`. `isSaaSTrustOrigin` is separate and requires https + default port — used for credential-trust decisions. JSDoc on `isSaaS()` explicitly points to `isSaaSTrustOrigin` for credential decisions. Trap: using `isSaaS()` for auth/credential gating looks correct but is wrong — it ignores scheme and port. `getConfiguredSentryUrl()` reads only env vars; `cli.ts` bootstrap (`preloadProjectContext`) injects stored SQLite default URL into `env.SENTRY_URL` before commands run, so `isSaaS()` sees self-hosted URLs correctly.
- 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 list sort values: SortValue type, VALID_SORT_VALUES, getComparator, and SDK IssueSort: In `src/commands/issue/list.ts`: `SortValue` type (line 141, @internal) and `VALID_SORT_VALUES` array (line 143) are the CLI's local sort constraints. `IssueSort` in `src/lib/api/issues.ts` (lines 40–42) is derived from `@sentry/api` SDK via `NonNullable<NonNullable<ListAnOrganizationSissuesData['query']>['sort']>` — no API layer change needed when adding new sort values. SDK already includes `'date' | 'freq' | 'inbox' | 'new' | 'recommended' | 'trends' | 'user'`. To add a new sort value: (1) add to `SortValue` union and `VALID_SORT_VALUES`, (2) add case to `getComparator()` switch (default falls back to date), (3) update flag `brief` string, (4) change `default` if needed. `appendIssueFlags()` guards `--sort` omission only when `flags.sort !== 'date'` — update guard if default changes.
- maxZipTotalSize: 2GiB memory-safety budget separate from server maxFileSize policy: `PrepareDifsOptions.maxZipTotalSize` (default `DEFAULT_MAX_ZIP_TOTAL_SIZE` = 2GiB) is a cumulative uncompressed-extraction budget per `.zip` archive and a container size cap — it bounds peak decompression memory. It is distinct from `maxFileSize` (per-entry server upload policy). Commands do not need to pass `maxZipTotalSize` explicitly; `prepareDifs` applies the 2GiB default automatically. `0` disables the budget. Passed through `prepareZipDifs` → `readZipDifEntries` as `maxTotalSize`.
- preprod API has no list endpoint — only 4 paths exist in api-schema.json: The Sentry preprod/build API has no list operation. Only 4 paths exist in `api-schema.json`: `organizations/{org}/preprodartifacts/{artifact_id}/install-details/`, `organizations/{org}/preprodartifacts/{artifact_id}/size-analysis/`, `projects/{org}/{project}/preprod/size-analysis/status-check-rules/`, `projects/{org}/{project}/preprodartifacts/build-distribution/latest/`. `@sentry/api` SDK exports confirm no list operation. A `build list` command cannot be implemented without a new server-side endpoint.
- preprod-artifacts.ts: auth token only sent to region-origin URLs, artifacts streamed to disk: Two hard invariants in `src/lib/api/preprod-artifacts.ts`: (1) Auth token is NEVER sent to third-party/signed storage URLs — `isRegionOrigin()` gates token attachment so it only goes to region-origin requests. (2) Large artifacts are NEVER buffered in memory — always streamed to disk via `pipeline()` + `createWriteStream()`. Constants: `SNAPSHOT_ARCHIVE_POLL_MS=2000`, `SNAPSHOT_ARCHIVE_TIMEOUT_MS=300_000`. `apiRequestToRegion` params type: `Record<string, string|number|boolean|string[]|undefined>`.
- Sentry CLI authenticated fetch architecture with response caching: Authenticated fetch + response cache: `createAuthenticatedFetch`: auth headers, 30s timeout, max 2 retries, 401 refresh, span tracing. `buildAttemptFactory` clones `Request`; do NOT materialize FormData (strips boundary). Per-endpoint timeout overrides (e.g. `/autofix/` 120s). Response cache RFC 7234 at `~/.sentry/cache/responses/`, GET 2xx only. TTL tiers: stable=5min, volatile=60s, immutable=24h. `@sentry/api` SDK passes Request with no init — undefined init → empty headers stripping Content-Type (HTTP 415); fall back to `input.headers` when init undefined. Guard `Array.isArray(data)` before `.map()` (SDK returns `{}` for 204/empty). Tests mocking fetch MUST call `useTestConfigDir()` + `setAuthToken()` + `resetCacheState()` + `disableResponseCache()` + `resetAuthenticatedFetch()` in beforeEach — GET response cache checked BEFORE fetch, so prior test cache hits produce 0 calls.
- Sentry CLI resolve-target cascade has 5 priority levels with env var support: Resolve-target cascade: (1) CLI flags, (2) SENTRY_ORG/SENTRY_PROJECT env vars, (3) SQLite defaults, (4) DSN auto-detection, (5) directory name inference. SENTRY_PROJECT supports `org/project` combo — SENTRY_ORG ignored if set. Schema v13 merged `defaults` table into `metadata` KV with keys `defaults.{org,project,telemetry,url}`; getters/setters in `src/lib/db/defaults.ts`. Prefer dedicated SQLite tables + migrations over `metadata` KV for non-trivial caches. Hidden global `--org`/`--project` flags: `mergeGlobalFlags()` in command.ts injects hidden flag shapes, `applyOrgProjectFlags()` writes to `SENTRY_ORG`/`SENTRY_PROJECT` before auth guard. No short aliases (`-p` conflicts). `@sentry/api` SDK: wrap types at `src/lib/api/*.ts` with `as unknown as SentryX` casts; never leak to commands. `unwrapResult`/`unwrapPaginatedResult` must stay CLI-owned. `apiRequestToRegion` auto-sets JSON Content-Type; `rawApiRequest` preserves strings.
- sentry-cli skill install paths: ~/.claude/ and ~/.agents/ only — OpenCode is never a target: Skill source-of-truth: `plugins/sentry-cli/skills/sentry-cli/SKILL.md` (602 lines) + `references/` (28 per-command .md files). `installAgentSkills()` in `src/lib/agent-skills.ts` installs to `
/.agents/skills/sentry-cli/` and `/.claude/skills/sentry-cli/` only — no OpenCode path. OpenCode IS detected via `OPENCODE_CLIENT` env in `src/lib/detect-agent.ts` but only for telemetry, not skill installation. `.opencode/` and `opencode.json*` are gitignored (lines 72–74). Cursor symlinks live at `.cursor/skills/sentry-cli/` pointing into `plugins/`. OpenCode scans `/.claude/skills/**/SKILL.md` and `/.agents/**/SKILL.md` — `.cursor/` is NOT scanned. `installAgentSkills()` never creates top-level agent roots — their presence is the detection signal that the user already has a compatible agent installed. Skills are updated on every version bump (write-if-changed optimization is unnecessary). Writes use atomic rename: temp file `.<name>.<pid>.<rand>.tmp` in same dir → `rename()` into place, guaranteeing readers never observe a partial file.
- SQLite dual-driver architecture and WASM runtime gotchas: Chose dual-driver SQLite approach over single-driver to support Node 18+. Uses node:sqlite (built-in) on Node 22.15+ and node-sqlite3-wasm fallback on Node 18.0-22.14. Required because node:sqlite is unavailable before Node 22.15. node-sqlite3-wasm has incompatible param passing (array vs spread) requiring adapter layer in sqlite.ts. WASM driver always uses spread for bind parameters, never passes undefined (maps to null defensively), and uses manual transaction wrapper. Standalone SEA binary must NOT contain the WASM driver.
- src/cli.ts: middleware chain, completion optimization, sensitive argv redaction: `src/cli.ts` exports `startCli()`, `runCli()`, `runCompletion()`. Middleware chain (innermost-first): `[seerTrialMiddleware, autoAuthMiddleware]` — auth is outermost. `autoAuthMiddleware` uses `isatty(0)` not `process.stdin.isTTY` (Bun returns undefined). `runCompletion()` sets `SENTRY_CLI_NO_TELEMETRY=1` to skip `@sentry/node-core` lazy-require (~280ms). `redactArgv()` handles `--flag=value` and `--flag <value>` forms; `SENSITIVE_ARGV_FLAGS` includes `token` and `auth-token`. `reportUnknownCommand()` wrapped in try/catch — telemetry must never crash CLI. `preloadProjectContext()` calls `captureEnvTokenHost()` BEFORE any env mutation.
- stdin-reopen.ts: forwardFreshTtyToStdin() idempotency and isTTY backfill pattern: `src/lib/init/stdin-reopen.ts` exports `forwardFreshTtyToStdin(deps?)` returning a `Disposable` (`TtyForwardingHandle`) — always non-null so callers use `using tty = forwardFreshTtyToStdin()` without null-checking. Idempotency: repeated calls return `NOOP_HANDLE` (secondary callers don't tear down primary's install). isTTY backfill: captures `previousIsTty` before touching; if `undefined`, uses `Object.defineProperty` to set `isTTY: true, writable: true, configurable: true` — required because Ink/clack gates `setRawMode(true)` on `input.isTTY`, so without backfill the fresh fd stays in canonical mode. `pause`/`resume` replaced with noops to prevent Bun kqueue EINVAL on fd-0 transitions. `TtyDeps` allows injection of `openTty` and `isTty` for test isolation.
- symbolic-wasm class-based API: Rc<Vec<u8>> ownership + on-demand re-parse pattern: symbolic-wasm ownership model: Dav1dde's PR #992 uses `SelfCell<ByteView<'static>, di::Archive<'static>>` — `self_cell`-based ownership, no re-parse. `derived_from_cell!` macro uses `std::mem::transmute` + `SelfCell::from_raw` to clone owner, letting `objects()` return owned `Object` cells sharing the same `ByteView`. PR #991 (Rc<Vec<u8>> + re-parse) closed in favor of this. `Object` getters: `debugId`, `codeId`, `arch`, `fileFormat`, `kind`, `hasSymbols`, `hasDebugInfo`, `hasUnwindInfo`, `hasSources`. `Archive` methods: `new(data)`, `peek(data)->Option<String>`, `fileFormat`, `objectCount`, `objects()->Result<Vec<ObjectFile>>`. CRITICAL: Rust struct named `Object` but exported as `ObjectFile` via `#[wasm_bindgen(js_name = "ObjectFile")]` — see gotcha entry for why.
- symbolic-wasm il2cpp WASM binding: free function + provider callback pattern: `il2cppLineMapping(object, provider)` is a free WASM function (not a method on `ObjectFile`) per Sebastian Zivota's preference. `provider` is a JS `Function` called with a path string; must return `Uint8Array` or `null`/`undefined`. `provider_bytes()` in `utils.rs` validates via `dyn_ref::<js_sys::Uint8Array>()` and throws a descriptive JS error for non-Uint8Array non-null values — `js_sys::Uint8Array::new` would silently zero-fill numbers or empty-fill plain objects. Returns `None` (JS `undefined`) when mapping is empty. `as_debuginfo()` is a `pub(crate)` non-wasm helper on `Object` so sibling modules can access the parsed object without re-exposing through the WASM API.
- Adopted workspace deps, cfg-zstd, required wasm-opt conventions from #989; kept #988 additions (serde_bytes: PR #988 (`feat/source-bundle-provider`, MERGED): `write_object_with_source_provider` + `write_object_with_filter` delegation. PR #989 (`fix/symbolic-followups`, MERGED, @sentry/symbolic@13.3.1): workspace deps, cfg-zstd, required wasm-opt conventions. PR #990 (C zstd on wasm, drop ruzstd, MERGED). PR #991 (`feat/wasm-api-classes`): CLOSED — superseded by Dav1dde's PR #992. PR #992 (`feat(wasm): Expose lower level API for debuginfo, add tests`, OPEN): Dav1dde's `self_cell`-based foundational API; +392/-101 across 11 files. Branch `prototype/wasm-artifact-smoke` (off #992 head `fd94b6fe`): adds artifact smoke test + `ObjectFile` rename fix; 5 files, +122/-6. SourceBundleWriter not yet in #992 — planned EOD Jun 23 2026. BYK will migrate `debug-files check` onto new API when republished.
- Banner art: direct bitmap grid read over area-averaging downsampling: Reference image (`sentry-ref.webp`, 2048×805px) is a SENTRY wordmark rendered from digit characters ('1'=on, '0'=off) in a monospace grid. Chosen approach: detect cell width via autocorrelation (~17.5px), read each cell on/off directly → 97×13 ASCII grid. Rejected: area-averaging downsampling (blended E's horizontal arms and R's counter/hole into solid fills, making letters unreadable); striped `▀` half-block rendering (50/50 duty-cycle dissolved E arms); solid `█` rendering (loses scanline texture). Post-processing: remove isolated cells (zero orthogonal neighbors) and small connected components to eliminate stray marks near Y's right arm.
- Chose Cloudflare for scalable website deployment over Vercel: User prefers using Cloudflare for scalable website deployment over Vercel.
- Chose version bump over force flag: chose version bump over force flag.
- CLI source-tier: bundle-sources first, print-sources deferred pending source enumeration API: Chose to implement `bundle-sources` first over `print-sources` because `SourceBundleWriter.writeObject()` in `@sentry/symbolic@13.4.0` provides exactly the right shape (callback provider reads from disk). `print-sources` deferred because `ObjectFile` has no `sourceFiles()` enumeration method in 13.4.0 — blocked on Dav1dde shipping source enumeration in a future `@sentry/symbolic` release. WASM debug-files track is otherwise complete: symbolic PRs #988–#993 merged, `@sentry/symbolic@13.4.0` published, CLI PR #1124 merged.
- Decided to use job ID instead: decided to use job ID instead.
- Merge CLI and MCP repositories: Chose to merge CLI and MCP repositories into a new repository named `toolkit`.
- Migrated to node: migrated from Bun to Node.
- Migrated to pnpm+node+vitest): migrated to pnpm+node+vitest).
- Node.js slim build flags for SEA binary size reduction: Node.js configure.py size-reduction flags for SEA builds: `--with-intl=small-icu` (English-only ICU, saves ~26-28 MiB — biggest win; CLI uses hardcoded en-US/sv-SE locales, safe); `--with-intl=none` (saves ~28-30 MiB but breaks `Intl.NumberFormat`/`String.normalize()` — NOT safe for this CLI); `--without-inspector` saves ~2-4 MiB; `--without-amaro` saves ~0.5 MiB; `--v8-disable-maglev` saves ~1-2 MiB; `--enable-lto` saves ~3-5 MiB. AVOID: `--without-ssl` (breaks HTTPS), `--without-lief` (BREAKS SEA), `--without-sqlite` (BREAKS CLI — uses node:sqlite), `--disable-single-executable-application` (BREAKS EVERYTHING), `--v8-lite-mode` (10x slower). Custom build deferred indefinitely — requires 5 native CI runners, ~3.5h cold build vs 5min fossilize. Cross-compilation from Linux to darwin NOT officially supported.
- Raw markdown output for non-interactive terminals, rendered for TTY: Markdown-first output pipeline: custom renderer in `src/lib/formatters/markdown.ts` walks `marked` tokens to produce ANSI-styled output. Commands build CommonMark using helpers (`mdKvTable()`, `mdRow()`, `colorTag()`, `escapeMarkdownCell()`, `safeCodeSpan()`) and pass through `renderMarkdown()`. `isPlainOutput()` precedence: `SENTRY_PLAIN_OUTPUT` > `NO_COLOR` > `FORCE_COLOR` > `!isTTY`. `--json` always outputs JSON. Colors defined in `COLORS` object in `colors.ts`. Tests run non-TTY so assertions match raw CommonMark; use `stripAnsi()` helper for rendered-mode assertions.
- Sixel banner approach deferred — block-art 56×8 wordmark chosen for now: User explicitly chose the 56×8 quadrant block-art wordmark over a sixel-based approach: 'Love the 56x8 let's roll with that. We can revisit sixel later.' Sixel is deferred as a future follow-up, not rejected permanently.
- Switched from Python to TypeScript for the project (no longer using Python): switched from Python to TypeScript for the project (no longer using Python)
- symbolic-wasm API scope: general-purpose base, not CLI-specific shortcuts: Agreed with Dav1dde (getsentry/symbolic maintainer): `symbolic-wasm` may live in the symbolic repo only if it exposes a full general-purpose API base (analogous to the Python package), not CLI-focused shortcuts. CLI-specific logic (e.g. `collect_il2cpp` orchestration, source bundle writing with CLI semantics) must NOT live in the symbolic repo — move to getsentry/cli. Rationale: symbolic is a general-purpose library; CLI concerns create unwanted coupling. PR C (`feat/wasm-api-classes`): class-based API — `Archive` owns `Rc<Vec<u8>>`, caches metadata; `Object` caches fields at construction, re-reads debug session on demand for `source_files()`/`create_source_bundle()`. Callback-based wasm API uses `js_sys::Function` (getSource(path) → Uint8Array | null). Free functions `list_source_files`/`create_source_bundle` removed; `parse_debug_file`/`peek_format` kept for back-compat.
- @stricli/core 1.2.7 patch: -H alias reserved-list removal: Trap: When Stricli throws on `-H` aliases used for `--header` or `--host`, removing the aliases from command files looks like the simple fix. But the project intentionally uses `-H` for curl-style API usage. Fix: the in-repo patch for @stricli/core (targeting 1.2.7) removes `-H` from the reserved list. Pin version to `1.2.7` (not `^1.2.8`) so the patch applies. Never remove `-H` alias usages from command files. Added in commit `78c9b04a5`. Cursor Bugbot and Seer both flag `-H` removal as blocking.
- `--require-all` false negatives for fat binaries — scan all matched objects: Trap: `missingRequestedIds` computed `foundIds` from only the primary object's `debugId` per file. Fat Mach-O with multiple slices causes non-primary slice IDs to be reported missing and exits 1 even though they were found. Fix: compute `foundIds` from all matched objects (`.objects` array), not just `selectBundledObject()?.debugId`. Applies to `src/commands/debug-files/upload.ts`.
- batch-queue.ts: 404 from upstream treated as transient — provider never disabled: Trap: `BatchProvider.submit()` returns `null` for any non-401/403 HTTP error, including 404. `submitBatch()` treats `null` as transient and falls back — no disable happens. For providers that don't implement `/v1/messages/batches` (e.g. MiniMax), this causes a wasted HTTP round-trip every 30s forever. Fix: add `"not-found"` return value for 404 in both Anthropic and OpenAI submit methods. In `submitBatch()`, handle `"not-found"` with provider-level disable: add `disabledBatchProviders: Set<string>` (keyed by provider name), persist to `kv_meta` via `setKV()`, restore on startup. Add fast-path bypass in both `flush()` and `prompt()`. Provider-level (not per-session) because the URL is baked in at construction — one provider per process. `groupKey()` = `authFingerprint(cred)|providerID`; per-credential disable was removed in favor of per-session historically.
- Biome noParameterProperties: never use TypeScript constructor parameter properties in class definitions: Trap: TypeScript parameter properties (`constructor(private readonly handle: FileHandle)`) look like idiomatic shorthand and compile fine. But Biome enforces `noParameterProperties` and will error. Fix: always declare explicit class fields and assign in constructor body. Applies to all new classes in `src/lib/**/*.ts`. Caught during `FileOldReader`/`MemoryOldReader` addition in `bspatch.ts` — 4 Biome errors at lines 281, 310, 311, 312.
- Biome stdin check reports 'contents aren't fixed' false positive — use file path instead: Trap: running `biome check --stdin-file-path=<file>` and piping content exits with code 1 and 'contents aren't fixed' even when `--write` produces no diff — looks like a real formatting violation. Fix: run Biome directly on the file path (`biome check <file>`) rather than via stdin mode. The stdin mode false positive was confirmed during the `agent-skills.ts` review: exit code 1 from stdin, but `--write` produced zero changes. Always use file-path invocation for authoritative Biome results.
- build-binary job in ci.yml uses setup-node — not Bun-only as commonly assumed: Trap: `build-binary` job in `ci.yml` is assumed to use only Bun (not `setup-node`) because the binary build pipeline uses fossilize/esbuild. But `ci.yml` lines 261–263 contain an `actions/setup-node@v6` step inside `build-binary` — PR #1145 changed it from `node-version: "22"` to `${{ env.NODE_VERSION_22 }}`. The PR description incorrectly stated `build-binary` is unaffected. Fix: always grep `ci.yml` for `setup-node` rather than assuming job intent from its name. The Node install in `build-binary` is likely for pnpm/tooling, not the compiled artifact.
- bundle-sources.ts exit code 1 for no-sources relies on cli.ts never resetting exitCode — fragile invariant: Trap: using `this.process.exitCode = 1` directly (at `bundle-sources.ts:145`) instead of throwing `OutputError`(=60) looks like a valid shortcut. It works today because `cli.ts:622-649` never resets `exitCode` on a clean return. But this is a fragile invariant — if `cli.ts` ever resets `exitCode`, the no-sources case silently exits 0. The `OutputError`(=60) idiom is the correct pattern. The `exitCode=1` approach was kept for consistency with `check.ts` pattern; a comment was added explaining the choice. Consider migrating to `OutputError` in a future cleanup.
- check.ts hasId() uses !== null but ObjectFile.codeId is string|undefined — null guard mismatch: Trap: `check.ts` `hasId()` guards `o.codeId !== null && o.codeId.length > 0`. Looks correct because old `DifObjectInfo.codeId` was `string | null`. But `ObjectFile.codeId` in `@sentry/symbolic` 13.4.0 is `string | undefined` — the getter returns `undefined` (not `null`) when Rust returns `None`. If `DifObjectInfo.codeId` is mapped as `string | null` (via `?? null`), the `!== null` guard works. If mapped as `string | undefined` (via `?? undefined`), the guard silently passes `undefined` through. Fix: ensure `parseDebugFile` maps `codeId` to `string | null` (using `obj.codeId ?? null`) so `check.ts`'s existing `!== null` guard remains correct without touching `check.ts`.
- ci.yml NODE_VERSION pins must be exact patch: Trap: using a floating major version like `22.x` in ci.yml looks safe and auto-updates. But GitHub Actions caches the resolved binary — a CVE fix (e.g. CVE-2026-48931 requiring 22.23.1/24.18.0) won't be picked up until the cache expires, silently running the vulnerable patch. Fix: always pin exact patch versions in ci.yml env vars (`NODE_VERSION_22: "22.23.1"`, `NODE_VERSION_24: "24.18.0"`). Update pins explicitly when a CVE fix requires a new patch. Pattern: add workflow-level `env:` block with both constants and rationale comment; reference via `${{ env.NODE_VERSION_22 }}`. Matrix jobs use ternary: `${{ matrix.node == '24' && env.NODE_VERSION_24 || env.NODE_VERSION_22 }}` [[019f03b9-97f4-7bb6-aeba-27fa5aeca79b]].
- ci.yml set-prev-release-tag has separate bug — chronological vs prior series: Trap: `set-prev-release-tag` (`.github/workflows/ci.yml:539-568`, runs on `release/**` branches) uses `gh api "repos/${REPO}/releases?per_page=5" | jq '[.[] | select(.prerelease==false and .draft==false)] | .[0].tag_name'` to pick most recent stable release PERIOD (chronologically). Looks equivalent to the nightly same-series fix but is NOT. If `0.40.1` patch lands AFTER `0.41.0` is released, `release/0.41` builds pick `0.40.1` as PREV_TAG (wrong for `0.40.0` users who need `0.40.0→0.41.0` delta). Fix requires different approach: derive from branch name (`release/0.41` → previous series `0.40`). Correctly OUT OF SCOPE for PR #1329 which only fixes nightly path. Release branch path also lacks access to `nightly-version` (changes job only outputs it for main).
- cli-docs: astro 7 requires @astrojs/starlight ^0.41.5 (rehype.js removed): Trap: a dependabot astro major bump in apps/cli-docs/package.json breaks Build Docs, preview, and CI Status with module-not-found './jsx/rehype.js' imported by @astrojs/mdx@5 — astro 7 removed that file. A docs-only failure looks like it shouldn't gate a CLI PR merge, but CI Status aggregates it, so the PR is blocked. Fix: bump @astrojs/starlight to the astro-7-compatible line (^0.39.2 → ^0.41.5) in the same change and regenerate pnpm-lock.yaml — every astro major must be paired with a starlight major to a compatible line. Verified on getsentry/cli PR #1322 (merged b1e2de3fa, 2026-07-31).
- Dependabot auto-closes reopened PR and opens duplicate — keep the original: Trap: when a dependabot PR needs intervention (rebase, fix of the bump it introduced), dependabot auto-closes the original and opens a duplicate with the same bump (getsentry/cli #1322 → #1325). The duplicate looks canonical because it has fresh CI runs. Fix: treat the original as canonical — reopen it and close the duplicate as redundant, so review history and the squash-merge land on the intended PR. Part of the duplicate-PR closure discipline in [[019fb86a-b8e5-771c-be26-75b90db6d88c]]. Don't switch targets mid-stream.
- dependency-review failure is org-managed transient — UNSTABLE from non-gate jobs is mergeable: When a PR's mergeStateStatus is UNSTABLE but mergeable=MERGEABLE, and the only failing/pending checks are non-gate jobs (e.g., Socket Security, dependency-review, nightly publish, skill eval, delta patches), the user treats the PR as safe to merge with squash. Action: verify the unstable source is a non-gate job, then proceed with squash merge rather than waiting indefinitely for those checks to complete. Transient org-managed dependency-review failures (like Socket Security stuck pending) should not block the merge.
- DEVELOPMENT.md hand-written prose is not covered by any staleness check: DEVELOPMENT.md hand-written prose is not covered by any staleness check
- docs-regen workflow force-advances getsentry/cli PR branches with bot commit: Trap: after force-pushing a rebased PR branch on getsentry/cli (TypeScript), treating your pushed head as the PR head makes final verification run on a stale commit. Fix: the docs-regen GitHub workflow auto-runs on every PR-branch push — it commits 'chore: regenerate docs' via github-actions[bot] on top of the pushed head and force-advances the branch (PR #1254: pushed 30ad8b075, remote auto-advanced to 605e8318d, touching 33 skill-doc .md files + packages/cli/script/bundle.ts). After any rebase+force-push, re-fetch and fast-forward local to the bot-advanced remote head, then re-run checks on the true head. The bot commit is generated content — do not revert or 'fix' it. Related to the gh PR head_sha desync trap [[019fb86a-b8e5-771c-be26-75b90db6d88c]].
- Error precedence inversion when refactoring stream to openSync/writeSync: Trap: refactoring `applyReaderToFile` from `createWriteStream` to `fs.openSync`/`writeSync`/`closeSync` looks like a drop-in replacement, but error precedence silently inverts. OLD code (writer.on('error') pattern): `finalErr = err ?? writeError` — close error wins. NEW code (openSync/closeSync pattern): `if (!writeError) writeError = closeErr` — write error wins. Fix: when claiming a refactor is a 'drop-in' fix, audit error precedence — which error wins matters for diagnostics. Either preserve precedence via `closeErr = writeError ?? closeErr` style, or call out the precedence change in CHANGELOG. Caught in binpatch review at bspatch.ts:677-688.
- fossilize 0.10.1 build:all 404 comma-joined platform download URL: Trap: `pnpm run build:all` failing right after a rebase looks like the rebase/merge broke something. Fix: fossilize 0.10.1 builds multiple platforms in one invocation by constructing a single download URL with comma-joined platform names — `https://nodejs.org/dist/v24.18.1/node-v24.18.1-darwin-arm64,darwin-x64,linux-arm64,linux-x64,win-x64.tar.xz\` → 404. Not a rebase problem and not CI-relevant (CI builds only linux-x64). For local verification after rebases run the single-platform build that matches CI (succeeded: 'Build complete: 1 succeeded, 0 failed'); expect build:all to fail until fossilize is fixed. build:all = generate:schema && generate:docs && generate:sdk && pnpm tsx script/build.ts.
- Frontify brand portal cannot be accessed programmatically — requires auth: Trap: `https://brand.getsentry.com/share/wLssCFiQ5ZzmQmKCWym4\` returns HTTP 200 and looks like a static asset server. It's a JS-rendered SPA — no static asset URLs are extractable from HTML. API probes (`/api/share/…`, `/api/shares/…`) return 404 or HTML. CDN URLs found embedded in the SPA shell (`media.ffycdn.net`) are portal chrome assets (OG illustration, favicon), not the actual brand files. Fix: use the pre-signed download endpoint `https://brand.getsentry.com/api/screen/download/\` — token must be extracted from the authenticated session or provided by the user. Always ask the user to provide Frontify asset URLs or download tokens directly.
- getCurlInstallPaths trusts stale stored install path — must guard with existsSync(dirname): Trap: `getCurlInstallPaths()` in `src/lib/upgrade.ts` reads the stored install path from SQLite and uses it directly — looks correct because the path was valid at install time. But macOS cleans `/tmp` on reboot, and users may delete test install dirs, leaving a stale DB entry. Fix: guard the stored-path branch with `existsSync(dirname(stored.path))` before trusting it; fall back to `process.execPath` startsWith-match against `KNOWN_CURL_DIRS` (`['.local/bin','bin','.sentry/bin']`), then `~/.sentry/bin` default. Conservative: only add the guard — do NOT prefer `execPath` over stored path (breaks npm→nightly migration flow). NFS edge case is self-resolving: if binary runs from NFS mount, mount must be active so `existsSync` passes.
- git add -A during rebase sweeps in stray untracked files and .lore.md conflicts: Trap: `git add -A` looks like a safe 'stage everything' shortcut during rebase conflict resolution. But it stages untracked stray files (e.g. `sentry-lightning-talk.md`) and auto-stages `.lore.md` conflict markers, polluting the commit. Fix: always stage specific file paths explicitly (e.g. `git add src/commands/debug-files/read-file.ts`) during rebase resolution. If the rebase is complex, abort with `git rebase --abort`, reset to `origin/main`, and re-apply edits manually.
- grep dot in MAJOR_MINOR: unescaped regex metachar — harmless here, defensive fix available: Trap: in `.github/workflows/ci.yml:507`, `grep "^nightly-${MAJOR_MINOR}\."` uses the unescaped dot in MAJOR_MINOR as a regex metachar. `^nightly-0.40\.` matches `nightly-0X40.<anything>` because `.` matches any char. Harmless in practice: MAJOR_MINOR always digits (from `cut -d. -f1,2` of `X.Y.Z-dev.TS`), and upstream `^nightly-[0-9]` filter on line 501 sanitizes. Defensive fixes: `grep -F "^nightly-${MAJOR_MINOR}."` (literal match), or `${MAJOR_MINOR//./\.}` (escape dots in shell), or use `[[ "$tag" == nightly-${MAJOR_MINOR}.* ]]` in bash directly.
- issue list --sort recommended: client-side re-sort must be skipped for single-project results: Trap: applying `getComparator('recommended')` to single-project results looks like consistent behavior. Fix: the `isMultiProject` guard at list.ts:1144-1148 is the ONLY client-side issue sort — it's intentionally skipped for single-project results because re-sorting would silently replace the server's recommended ranking with a `lastSeen` fallback. `getComparator('recommended')` returns `compareDates(a.lastSeen, b.lastSeen)` — same as `date` sort — so applying it to single-project results would corrupt the server's relevance ranking without any visible error.
- issue list sort default test coverage gap: tests always pass sort explicitly: Trap: `list.test.ts` passes `sort` explicitly in every `func.call(...)` invocation (`sort: 'date'` or `sort: 'recommended'`). No end-to-end test omits `sort` and asserts the API received the correct default (`recommended` on SaaS, `date` on self-hosted). This means `defaultIssueSort()` logic is not covered by integration tests — only unit-tested via `__testing` exports. When adding host-dependent flag defaults, always add a test that omits the flag and verifies the resolved default reaches the API call.
- listSources: never copy descriptor.contents across wasm boundary per-file — use type field instead: Trap: `descriptor.contents` looks like the right way to check if a source is embedded — it's the actual content. But reading it copies the full Rust String across the wasm↔JS boundary for every file, triggering the encoding mismatch dav1d flagged (Rust String vs JS String encoding). Fix: use `descriptor.type` (a cheap string tag: `'url'`, `'resolved'`, etc.) to classify the source without copying contents. `DifSourceFile` shape uses `type` field, not `embeddedBytes`. Only fetch `contents` if the caller explicitly needs the source text.
- loadOldBinary: new Uint8Array(readFile()) double-allocates — return Buffer directly: Trap: `new Uint8Array(await readFile(tempCopy))` looks like a safe type conversion. But `readFile()` already returns a `Buffer` which IS a `Uint8Array` — wrapping it in `new Uint8Array()` copies the entire ~100MB binary, causing a transient double-allocation peak. Fix: return the `Buffer` directly from `loadOldBinary` with no wrapper. Zero tradeoff — Buffer is already a Uint8Array subclass and works everywhere Uint8Array is expected.
- Local tarball paths in package.json break CI with pnpm install --frozen-lockfile: Trap: `file:/tmp/opencode/sentry-symbolic-new.tgz` in dependencies looks harmless locally — `pnpm install` works fine. But CI runs `pnpm install --frozen-lockfile`, which exits 254 when the tarball path doesn't exist. The lockfile also changes the specifier from published version to path, causing diffs on every install. Fix: always keep published version specifiers (e.g. `13.4.0`) in package.json. Use `npm pack` + separate install for local testing, or pnpm overrides.
- login.ts blind catch: bare catch around getUserRegions() mislabels network/server failures as invalid token: Trap: a bare `catch {}` around `getUserRegions()` in `src/commands/auth/login.ts` that always calls `clearAuth()` + throws `AuthError('invalid')` looks like correct token-validation error handling. But it conflates genuine 401/403 (bad token) with network errors, 5xx server errors, and parse failures — clearing a possibly-valid token and showing a misleading 'Invalid API token' message. Fix (PR #1153): extract `handleTokenValidationError()` helper — only clears auth and throws `AuthError('invalid')` for `ApiError` with status 401 or 403; re-throws original error for all other failures. `AuthError('invalid')` is now safe to silence in `classifySilenced` because it only fires on genuine auth rejections.
- 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`.
- OpenCode memoizes skill discovery at session start — no hot-reload of skill files or SKILL.md changes: Trap: modifying `~/.claude/skills/sentry-cli/SKILL.md` and seeing count=6 (sentry-cli absent) in the current session looks like a parse/load failure. Fix: OpenCode's `InstanceState.make` caches skill discovery once per instance at session start — `opencode debug skill` from a fresh invocation shows the true live count (7 including sentry-cli). Stale session snapshots always show the count from when the session started. To verify skill loading, always run `opencode debug skill` from a new shell rather than checking `available_skills` in an already-running session.
- OpenCode not detected for skill installation — only .claude and .agents roots are supported: Trap: OpenCode is detected in `src/lib/detect-agent.ts` via `OPENCODE_CLIENT` env var and `PROCESS_NAME_AGENTS` map — looks like it should drive skill installation. But detection is for telemetry only. `installAgentSkills()` and `src/commands/cli/uninstall.ts` hardcode `agentRoots = ['.claude', '.agents']` — OpenCode is never a skill install target. Fix: to add OpenCode skill support, add its root dir to `agentRoots` in both `agent-skills.ts` and `uninstall.ts`, and add a `detectOpenCode()` function parallel to `detectClaudeCode()`.
- OutputError must not be preceded by a yield — causes double-render: Trap: `OutputError` looks like a normal error you can throw after yielding a partial result, since other error types allow prior yields. Fix: `OutputError` (src/lib/errors.ts:292) is handled in `src/lib/command.ts:723` by re-rendering `err.data` via `handleYieldedValue()` then re-throwing — so any prior `yield` of the same data causes double-render. For FAILED/NOT_RAN terminal states in `build size`, throw `OutputError(result)` directly without yielding first.
- parseWithHash short-circuits before the main validateResourceId guard — must self-validate (CLI-1G1): GitHub-style `org/project#SHORTID` issue identifiers handled by `parseWithHash()` in `src/lib/arg-parsing.ts`, inserted in `parseIssueArg` AFTER the `@`-selector block and BEFORE the `validateResourceId(input.replace(/\//g,''))` guard (line ~1115, which rejects `#`). Because it runs before that guard, `parseWithHash` MUST validate BOTH the project prefix AND the fragment itself. `validateResourceId` permits `:`, so `:` mixed with `#` is rejected explicitly. Semantics: `org/project#ID` → delegates to `parseWithSlash('org/project/ID')`; `project#ID` → `project-search` via `parseProjectIdentifier`; `#ID` → bare identifier via `parseBareIssueIdentifier`. `parseProjectIdentifier` is shared with `parseWithColon`. BEHAVIORAL CHANGE: `CLI-G#anchor` went from `ValidationError` → `project-search{projectSlug:'cli-g', suffix:'ANCHOR'}`. Test at `arg-parsing.test.ts` injection-hardening block updated accordingly.
- pnpm nested script invocation loses TTY — inline tsx to fix: Trap: pnpm nested script invocation loses TTY — inline tsx to fix — Trap: `"cli": "pnpm tsx src/bin.ts"` creates nested pnpm invocations (pnpm → /bin/sh → pnpm → /bin/sh → tsx → node). Each inner pnpm layer pipes stdio, so `process.stdin.isTTY` and `process.stdout.isTTY` are `undefined` in the final Node process. Fix: inline tsx directly — `"cli": "tsx --import ./script/require-shim.mjs src/bin.ts"` and same for `dev`.
- pnpm test runs generate:docs + generate:sdk pre-steps — causes multi-minute runs/timeouts: Trap: `pnpm test` looks like the standard way to run tests. But `test:unit` = `pnpm run generate:docs && pnpm run generate:sdk && vitest run test/lib test/commands test/types --coverage` — the doc/SDK generation pre-steps cause 120s+ timeouts. Fix: run vitest directly on specific test files: `npx vitest run test/lib/dif test/commands/debug-files` (or similar scoped paths). Use `pnpm test` only for final pre-commit validation. `test:init-eval` is the only script without the preamble (uses `--testTimeout 600000` instead).
- PR auto-merge completes before manual squash-merge step: Trap: after confirming CI is green, issuing an explicit "squash merge PR #N" command looks like the required next step — but if auto-merge was enabled on the PR (e.g. by the bot that opened it), GitHub merges it automatically the instant CI turns green, with no explicit merge action from the assistant. Confirmed on getsentry/cli PR #1287: `gh pr view` returned mergeStateStatus/mergeable=UNKNOWN and state=MERGED before any merge command was issued. Fix: after CI goes green, re-fetch PR state (`gh pr view --json state,mergeStateStatus,mergeCommit`) before attempting to merge — if already MERGED, just verify the merge commit has exactly 1 parent (confirms squash, not a regular merge) and move on.
- prepareZipDifs null vs empty array semantics — fully-handled vs fall-through: `prepareZipDifs` null/empty contract: non-null result (even EMPTY array) means the path was a `.zip` and is fully handled — caller continues past it. `null` means fall through to normal file handling — either not-a-zip OR container skipped wholesale (too large / malformed). The `continue` in `prepareDifs` fires on any non-null result, so an empty array correctly skips the file without re-entering non-zip logic. Document this dual-null semantics in JSDoc. Also: `PrepareDifsOptions.maxZipTotalSize` (default 2GiB) is a cumulative uncompressed-extraction budget per archive — distinct from `maxFileSize` (per-entry upload policy). Commands do not need to pass it; default applied automatically. `0` disables.
- prepareZipDifs oversizedCount must NOT feed exit-driving counter — format unknown pre-decompression: Trap: zip entry oversized warnings look like they should increment `oversizedCount` in `prepareDifs`, just like on-disk file oversize does — both are 'too large' signals. But a compressed entry's format is unknown until inflated, so it can't be attributed to the requested `--type`. Counting it would turn an unrelated oversized asset inside a `.zip` into a false 'all matched files too large' failure and wrong exit code. Fix: `prepareZipDifs` returns `PreparedDif[]` (not `{ prepared, oversizedCount }`); oversized zip entries warn per-entry inside `readZipDifEntries` only. The `oversizedCount` counter in `prepareDifs` is exclusively for format-accurate on-disk files.
- preprod build-distribution/latest requires appId + platform query params: Trap: `projects/<org>/<project>/preprodartifacts/build-distribution/latest/` looks like it should return the latest build without params. Fix: it requires `?appId=<bundle-id>&platform=apple|android` — omitting either returns 400 `{"appId":["This field is required."],"platform":["This field is required."]}`. Platform accepts ONLY `"apple"` or `"android"` — Electron and other platforms are unsupported. Without a real appId that matches an uploaded build, `latestArtifact` will be `null` (valid 200, no error).
- ruzstd partial decompression: must validate output size explicitly: Trap: `ruzstd::StreamingDecoder` (unlike `zstd::bulk::decompress`) silently returns a partial result when passed a too-small `size` — it does NOT error. Fix: read `size + 1` bytes into the output buffer, then assert `decompressed.len() == size`; return `None` on mismatch. This matches `zstd::bulk::decompress` error-on-mismatch semantics. Confirmed via test: exact(560)→Some(560)✓, toosmall(550)→None✓, toolarge(570)→None✓.
- scan.ts prepareDifs: peek header (4096) before full parse — oversized gating must not skip embedded PPDB nor OOM: Gotcha: scan.ts: oversized PE early-return must not skip embedded PPDB extraction — Trap: in `prepareFileDif` (src/lib/dif/scan.ts), gating on a PE's on-disk size vs `maxFileSize` looks like a safe early-exit optimization — but it isn't PPDB-aware. A large assembly can contain a small embedded .pdb well within the limit, yet the pre-fix code returned before `embeddedPpdbDif` ever ran, silently dropping the extractable PDB. Fix: `embeddedPpdbDif` cheap-peeks only the first `PEEK_HEADER_BYTES` (4096) via `peekFormat()` to confirm PE format before doing a second full `Archive` parse, so oversized-container gating never blocks embedded-content extraction.
- sentry-cli SKILL.md has non-standard frontmatter fields (version, requires) — safe for OpenCode but worth knowing: SKILL.md frontmatter includes `version` and `requires: {bins: ["sentry"], auth: true}` — fields not in the OpenCode skill spec. OpenCode's `isSkillFrontmatter()` only validates `name: string` and optional `description: string`; extra fields are silently ignored. gray-matter (js-yaml) parses the nested `requires` object without error. Trap: nested objects in frontmatter look like they'd cause a YAML parse failure or schema rejection. They don't — confirmed via gray-matter test and zero "failed to load skill" log entries. The skill loads correctly; absence from a session is always a stale-snapshot issue, not a parse issue.
- sentry-cli v3→v4 codemod: authToken shorthand and execute→run edge cases: Trap 1: `{ authToken }` shorthand — renaming key `authToken`→`token` while leaving shorthand=true produces `{ token }` referencing a nonexistent binding. Fix: when `shorthand=true` and key is `authToken`, disable shorthand and emit `token: authToken`. Trap 2: `execute([...])` → `run(...)` passes raw v3 argv tokens (e.g. `releases`, `new`) verbatim; v4 command paths differ (`releases→release`, `new→create`). Fix: insert `// TODO(sentry-v4)` comment before the rewritten call reminding the user to remap command names. Trap 3: `--header` shim overwrites `SENTRY_CUSTOM_HEADERS` on each iteration. Fix: accumulate into semicolon-separated string; assign once after loop. v4 format: `"Name: Value; Name2: Value2"`.
- Silent nonexistent path in scan — throw ValidationError instead of skip: Trap: `scanPaths` silently skipped nonexistent paths (ENOENT from `stat` → `log.debug` + continue). Users got empty results with no indication the path was missing. Fix: for explicitly provided paths (not directory children), throw `ValidationError` with the path name. Directory children that don't exist are still silently skipped.
- skill-eval E2E tests fail on Anthropic API network errors — not a code regression: Trap: `test/e2e/skill-eval.test.ts` failures (`claude-sonnet-4-6 meets threshold`, `claude-opus-4-6 meets threshold`) look like regressions introduced by the current PR. Root cause: these tests call `api.anthropic.com` directly — `[planner] API error: Invalid response body ... Premature close` is an external Anthropic API outage, not a code bug. All 126 non-LLM E2E tests pass. Fix: confirm by checking logs for `Premature close` pattern; if present, stop re-running (wastes CI resources) and post a PR comment documenting the outage. Do not merge while CI is red — wait for API recovery.
- SQLite transaction() ROLLBACK can throw, discarding original error: (gotcha) SQLite transaction ROLLBACK error-swallowing trap: In `src/lib/db/sqlite.ts`, `transaction()` catches errors and runs `this.db.exec('ROLLBACK')`. If ROLLBACK itself throws, the original error is lost. Fix: `const origErr = e; try { this.db.exec('ROLLBACK'); } catch (rbErr) { log.debug(...); } throw origErr;`
- streamDecompressToFile: openSync/writeSync/closeSync, never drain — fd-release race vs spawn: streamDecompressToFile: never emit 'drain' on ENOSPC — race drain against error to avoid hang
- strip fails on Node SEA binaries — must strip BEFORE fossilize injection; UPX destroys ELF notes: Strip debug symbols must happen BEFORE fossilize SEA injection. Trap: `strip --strip-unneeded` on a plain Node binary saves ~17 MiB and still runs — looks like it should work on the final SEA binary too. But after postject injects the SEA blob, `strip` fails: 'section .text can't be allocated in segment 2'. Fix: as of fossilize 0.7.0, stripping is built into fossilize itself — it strips the copied binary (already unsigned for macOS/Windows) BEFORE calling postject. Cross-strip from Linux to macOS silently fails (caught); native macOS runners strip correctly with `strip -x`. Windows skipped (no debug symbols). `stripCachedNodeBinaries()` was removed from `script/build.ts` in fossilize 0.7.0 update — fossilize handles it natively.
- symbolic-wasm: JS callback errors silently swallowed via .ok()? — must propagate as JsError: Trap: using `.ok()?` on `call1` (JS function invocation) and `dyn_into::<Uint8Array>()` looks like idiomatic Rust error-to-Option conversion. Fix: both failures must propagate as `JsError` to the caller — thrown JS exceptions yield partial bundles with no feedback; non-`Uint8Array` returns (ArrayBuffer, plain arrays) silently skip files. Pattern: capture callback error in `Option<JsValue>` outside closure, set it on failure, check after closure returns. Make `with_object` generic over `E: From<JsError>` so both error paths unify. Flagged by Cursor Bugbot (Medium) and Sentry Seer (Medium) on PR #991.
- Symlink cycle hang in recursive file collection — use lstat + visited-realpath set: Trap: `collectFiles` uses `stat` (follows symlinks) with no cycle detection. Directory symlinks pointing to ancestors cause unbounded recursion — never returns. macOS `.framework`/dSYM trees routinely contain cyclic symlinks. Fix: use `lstat`, skip symlinked directories, and track visited realpaths in a `Set` to break cycles. File symlinks are safe to follow.
- Upload assembly `not_found` after deadline is a real failure — must set exit code 1: Trap: upload assembly only treated `"error"` state as failure; `"not_found"` was treated as incomplete (exit 0 with debug log). But after deadline, `not_found` means chunks were never delivered — a genuine failure. Fix: treat `not_found` as failure with `log.warn` + exit code 1. Also upgrade deadline-break log from `debug` to `warn`. Discovered during self-review of `debug-files upload` PR.
- useTestConfigDir afterEach: never delete CONFIG_DIR_ENV_VAR — always restore previous value: Trap: deleting `process.env.SENTRY_CONFIG_DIR` in `afterEach` looks like proper cleanup. But `preload.ts` always sets `SENTRY_CONFIG_DIR`, so `savedConfigDir` is always defined — deleting it causes subsequent test files' module-level code or `beforeEach` hooks to read `undefined`. Fix: always restore the previous value, never delete. The `else { delete process.env[CONFIG_DIR_ENV_VAR] }` branch is intentionally omitted in `test/helpers.ts` `useTestConfigDir`. Same principle applies in `test/fixture.ts` `setAuthToken()` finally block — the delete there is acceptable only because it's a scoped try/finally restore, not a test lifecycle hook.
- Vitest fd-lingering regression test: race condition defeats proc/self/fd check: Trap: a regression test that writes a file via `applyReaderToFile` and then checks `/proc/self/fd` for lingering fds looks like it should catch a fd-leak bug. It does NOT — vitest's extra awaits in `applyPatchChainInMemory` (loadOldBinary → copyFileSync → open → stat → transformPatch → Promise.all(cancel both readers) → writer.end + test body's `await import("node:fs")`) drain pending closes before `readdirSync("/proc/self/fd")` runs. Confirmed in binpatch: NEW code test passes 100%, OLD `createWriteStream` code test passes 0/50 in vitest, but standalone repro catches 11/200 (5.5%). Fix: deterministic structural assertion via helper `applyReaderToFileOpenHandleCount(destPath)` that re-opens path and asserts returned fd number > original openSync result — Linux never recycles lowest unused fd while higher-numbered one is open, so a higher returned fd proves closeSync ran.
- wasm-pack test never tests the published package — builds its own glue instead: Trap: `wasm-pack test --node` looks like a complete test of the WASM package — it runs Rust tests compiled to WASM. But it builds its own JS glue and never loads the `--target web` artifact. So `export class Object` shadowing the JS global `Object` passes all wasm-pack tests. Fix: use the two-layer approach — (1) `wasm_bindgen_test` + `wasm-pack test` for bulk behavior, (2) artifact smoke test that does `npm pack` → install into temp dir → `import "@sentry/symbolic"` → assert API loads. The smoke test catches packaging regressions that wasm-pack misses. Fix for Object shadowing: `#[wasm_bindgen(js_name = "ObjectFile")]` + `#[wasm_bindgen(js_class = "ObjectFile")]`; Rust struct name `Object` unchanged.
- Whole-buffer matchAll slower than split+test when aggregated over many files: Grep/scan traps in `src/lib/scan/`: (1) Whole-buffer `regex.exec` 12× faster per-file but ~1.6× SLOWER over 10k files — early-exit at `maxResults` via `mapFilesConcurrent.onResult` wins. (2) Literal prefilter is FILE-LEVEL gate (`indexOf`→skip); per-line verify breaks cross-newline patterns and Unicode length-changing `toLowerCase`. (3) Extractor `hasTopLevelAlternation`+`skipGroup` must call `skipCharacterClass`. (4) Wake-latch race: use latched `pendingWake` flag, not `let notify=null; await new Promise(r=>notify=r)`. (5) `mapFilesConcurrent` filters `null` but NOT `[]` — return `null` for no-op files. (6) `collectGlob`/`collectGrep` must NOT forward `maxResults` to iterator; drain uncapped, set `truncated=true`. Worker pool: lazy singleton, size `min(8, max(2, availableParallelism()))`. Matches encoded as `Uint32Array` quads transferred via `postMessage` (~40% faster). `new Worker(new URL(...))` HANGS in SEA binaries — use Blob+URL.createObjectURL. FIFO `pending` queue per worker. `ref()`/`unref()` idempotent — only unref when `inflight` drops to 0. Disable via `SENTRY_SCAN_DISABLE_WORKERS=1`.
- Windows rename() raises EPERM/EBUSY on open destination — atomic write is POSIX-only: Trap: `rename()` for atomic file swap looks cross-platform because Node.js exposes it on all OSes. But on Windows, if the destination file is open by a concurrent reader, `rename()` raises EPERM or EBUSY — the swap fails rather than being atomic. The JSDoc claim 'eliminates the truncation race' is unqualified but only holds on POSIX. `win32-x64` is a shipped sentry-cli target. Impact is graceful (write returns null, captureException fires) but the atomicity guarantee must be documented as POSIX-only. Fix: qualify JSDoc with 'on POSIX systems'; on Windows the fallback is non-atomic `writeFile` (same as before the patch).
- worktree node_modules symlink causes esbuild host/binary version mismatch: Trap: when running multiple agent worktrees in parallel, `node_modules` may be a symlink to a sibling worktree's install — looks fine until esbuild runs. Error: 'Host version X does not match binary version Y' crashes all generate scripts and typecheck. Fix: remove the symlink and run `pnpm install` in the worktree root to get an independent `node_modules`. The symlink is created by mutation-test workflows that intentionally share `node_modules` for speed — but only safe when both worktrees are on the same branch/lockfile.
- zstd-sys supports wasm32-unknown-unknown via wasm-shim — C zstd usable in WASM without libc: Trap: `zstd-sys` (C zstd) looks incompatible with `wasm32-unknown-unknown` because it compiles C code and the target has no libc/sysroot. Fix: `zstd-sys v2.0.16+` ships `wasm-shim/` headers that `#define` `malloc`/`free`/`memcpy` etc. to `rust_zstd_wasm_shim_*` Rust functions backed by Rust's allocator. `build.rs` auto-enables the shim for `wasm32-unknown-unknown` (controlled by `no_wasm_shim` feature). Requires clang/lld/llvm installed (`sudo apt-get install -y clang lld llvm` on Ubuntu). `ruzstd` was evaluated as pure-Rust alternative but rejected by maintainers (Dav1dde) as significantly slower. CI `wasm-build` job AND `build.yml` `npm-package` job (which runs `make npm`) BOTH must install clang/lld/llvm before building — `npm-package` job previously only had `binaryen` and `rustup wasm32-unknown-unknown`, causing silent C-zstd compile failures.
- @sentry/symbolic 13.4.0 API: Archive/ObjectFile class contract and WASM memory management: Exported classes: `Archive` (`objects()`, `peek()`, `fileFormat`, `objectCount`), `ObjectFile` (`debugId`, `codeId`, `arch`, `fileFormat`, `hasDebugInfo`, `hasSources`, `hasSymbols`, `hasUnwindInfo`, `kind`, `debugSession()`), `DebugSession` (`files()`, `sourceByPath()`), `SourceBundleWriter` (`writeObject()`, `collectIl2cppSources` setter, `isEmpty`), `SourceFileDescriptor` (`type`, `contents`, `url`, `path`, `sourceMappingUrl`, `debugId`). NOT exposed: `ObjectLineMapping::from_object` (il2cpp line-mapping DIF), BCSymbolMap parsing, UuidMapping plist parsing, embedded Portable PDB extraction. These gaps make BCSymbolMap/il2cpp DIF creation impossible without native tooling.
- @sentry/symbolic release flow: build from master → local tarball → npm publish via craft: Steps: 1. Checkout `origin/master` in `~/Code/getsentry/symbolic` — ensures all merged PRs included. 2. Run `cd symbolic-wasm && bash build-npm.sh` — compiles wasm32, runs wasm-opt, packs tarball, runs smoke test (3 assertions). 3. Pin CLI `package.json` to `file:` tarball for local validation — run tests, typecheck, lint. 4. Revert `file:` pin before committing — wait for npm publish (craft flow via getsentry/publish). 5. Once new version (e.g. 13.5.0) appears on npm, update `package.json` to semver pin. Gotchas: - Merge ≠ publish: PR #997 merged Jun 24 but npm still showed 13.4.0 — always verify `npm dist-tags` before committing semver pin. - `file:` tarball pin must never be committed to the CLI repo. Verify: - [ ] `npm view @sentry/symbolic dist-tags.latest` shows expected version. - [ ] Smoke test 3/3 pass including 'debug session enumerates referenced source files'.
- @sentry/symbolic: object.has_sources() only reports embedded sources — use debug_session.files() to detect any sources: In `@sentry/symbolic` (mirroring `print_sources.rs` in legacy Rust sentry-cli): `object.has_sources()` only reports *embedded* sources, NOT referenced files. To detect whether an object has any sources at all, use `debug_session.files().next().is_none()`. Core enumeration pattern: `Archive::parse(&data)` → `archive.objects()` → `object.debug_session()?.files()` → `FileEntry.abs_path_str()` → `debug_session.source_by_path(abs_path)` → `SourceFileDescriptor` (fields: `contents()`, `url()`, `debug_id()`, `source_mapping_url()`). PE with embedded PDB: also handle via `pe.embedded_ppdb()`.
- atomicWriteFile in agent-skills.ts: same-dir temp + rename guarantees no partial reads: `atomicWriteFile(destPath, content)` at `src/lib/agent-skills.ts:72`: writes to `.<name>.<pid>.<rand>.tmp` in the same directory as `destPath`, then calls `rename()` into place. Same-directory placement guarantees same filesystem → POSIX atomic rename. Concurrent readers never observe a truncated or partially-written file. Temp file is cleaned up on error. Used by `writeSkillFiles()` (replacing in-place `writeFile`). Skills are written on every version — write-if-changed optimization was explicitly rejected as unnecessary.
- CI Node version pinning: centralized env block per workflow file, ternary for matrix jobs: Node CVE-2026-48931 fix: `NODE_VERSION_22="22.23.1"`, `NODE_VERSION_24="24.18.0"` (22.23.0 had the vulnerability; fix landed in 22.23.1 via nodejs/node#64004). Pattern: add top-level `env:` block to each workflow file (ci.yml, release.yml, sentry-release.yml, docs-preview.yml) with both constants + rationale comment. Reference via `${{ env.NODE_VERSION_22 }}`. Matrix jobs (build-npm) use ternary: `${{ matrix.node == '24' && env.NODE_VERSION_24 || env.NODE_VERSION_22 }}` — matrix labels stay as bare majors (`["22","24"]`) for job naming. Gotcha: `eval-skill-fork.yml` has no `setup-node` step at all — must add one explicitly [[019f03bb-f9cf-7208-a183-d4f0074480f9]].
- clack-utils.ts rename deferred (still pre-monorepo path): `src/lib/init/clack-utils.ts` filename kept (not renamed to `wizard-utils.ts`) to keep PR 4 diff focused on clack removal. No clack references remain in the file. `WizardCancelledError` lives here. `abortIfCancelled<T>()` return type uses `Exclude<T, symbol>` to narrow union types. `FEATURE_DISPLAY_ORDER` and `CANONICAL_STEP_ORDER` (12 steps) also defined here. Post-monorepo: full path is `packages/cli/src/lib/init/clack-utils.ts`. Rename still intentionally deferred — pre-shape merge does NOT depend on it.
- createSourceBundle: object selection, sync provider contract, writer lifecycle: `createSourceBundle(data, objectName, readSource)` in `src/lib/dif/index.ts`: selects `objects.find(o => o.hasDebugInfo) ?? objects[0]`; returns `{bundle:null, debugId:null, fileCount:0}` if no objects. `SourceBundleWriter.writeObject` is synchronous — provider/filter callbacks must be sync (`readFileSync` in bundle-sources.ts). Writer is single-use: `writeObject` calls `__destroy_into_raw()` (zeroes ptr, unregisters FinalizationRegistry). Provider returning `null` signals skip (WASM glue checks `arg0 == null`). `bundle === null || fileCount === 0` correctly catches manifest-only ZIPs with zero source files.
- debug-files upload: DIF assemble wire format and chunk-upload pipeline: Native DIF assemble body: `{ [overallSha1]: { name, debug_id?, chunks: string[] } }` — identical shape to `proguard.ts`/`dart-symbols.ts`. `debug_id` is advisory (server re-parses). Per-file upload: each file chunked as raw bytes via `hashBuffer`; primary object selected via `selectBundledObject` (first with debug info, fallback to first). Assemble endpoint: `projects/${org}/${project}/files/difs/assemble/`. Constants: `DEFAULT_MAX_DIF_SIZE=2GB`, `DEFAULT_MAX_WAIT=300s`. `--wait` flag controls whether to poll until assembly completes. Deferred: ZIP scanning, BCSymbolMap/dsymutil, Xcode derived-data, il2cpp mapping (require native tools not available in WASM).
- debug-files/bundle-sources: SourceBundleWriter is one-shot/consuming — create fresh per call: `SourceBundleWriter.writeObject` in `@sentry/symbolic` consumes the writer via `__destroy_into_raw()` — a single instance cannot be reused. `createSourceBundle(data: Uint8Array)` in `src/lib/dif/index.ts` creates a fresh writer per call. Returns `SourceBundleResult | null` (null = empty bundle, i.e. `writeObject` returned `undefined`). Wasm boundary masks original JS error messages thrown inside provider callbacks — errors do propagate but message is not preserved; tests should use `.toThrow()` without message assertion for provider-error cases.
- 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.
- error-reporting.ts silencing rules: which error types are silenced and why: Current `classifySilenced()` branches (post-PRs #1148–#1153): - `OutputError` → `'output_error'` (piped output closed early) - `ContextError` → `'context_missing'` (user omitted required value; never a CLI bug) - `AuthError` (any reason: `not_authenticated`, `expired`, `invalid`) → `'auth_expected'` (all auth reasons are user/env state after CLI-19 fix) - `ApiError` status >400 && <500 → `'api_user_error'` - `ApiError` + `isSearchQueryParseError()` → `'api_query_error'` (status 400 with 'Error parsing search query' detail) - `TypeError` + `isNetworkError()` → `'network_error'` (raw 'fetch failed' only) - else → `null` (captured) Metric emission must never block error handling — all metric/logger calls in `recordSilencedError()` are wrapped in try/catch.
- getsentry/symbolic CHANGELOG.md: use bold **Features** style, not ### headings: Danger bot on getsentry/symbolic PRs requires a CHANGELOG.md entry before merge. The bot's example uses `### Features` heading style, but the actual repo convention (established in PR #997 and #1004) uses `**Features**` bold style under the `## Unreleased` section. Always match the bold style. Entry format: `- WASM: expose \`Method()\` for <description>. ([#NNNN](url))`. Danger re-runs after the changelog commit and reports 'All green' when satisfied.
- getsentry/symbolic: follow-up PR pattern for merged PR review comments: getsentry/symbolic PR #988 (feat/source-bundle-provider) follow-up to Dav1dde review: **Implemented:** (1) `write_object_with_filter_and_provider` private inner method takes both filter `F` and provider `P: Fn(&str) -> Option<impl Read>`; public methods delegate to it. (2) `SharedCursor` (`Rc<RefCell<Cursor<Vec<u8>>>>`) removed — replaced with plain `Cursor::new(Vec::new())` + `into_inner()`. (3) Provider changed from destructive `contents.remove(path)` to non-destructive `contents.get(path).map(|v| v.as_slice())`. (4) Tests use `unwrap()` not `-> Result`. (5) `smoke-test.mjs` + `build-npm.sh` wiring + `ci.yml` `wasm-smoke` job added — closes gap where wasm bindings were only compile-checked on PRs. Byte-identity verified: sha256 `4d29224558f27174fef90e81d5c7e80fd388249f25cb48cfc4c5eb316e3b067b` identical BASE vs HEAD.
- getsentry/symbolic: Rust test conventions — unwrap() not -> Result: Per Dav1dde review on PR #988: test functions in `symbolic-debuginfo/tests/` must NOT return `Result` — use `unwrap()` so stack traces point to the assert location. If a shared error type is needed, define a module-level type alias: `type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;` but still prefer `unwrap()` in test bodies.
- getsentry/symbolic: wasm smoke test pattern — smoke-test.mjs + build-npm.sh + ci.yml wasm-smoke job: symbolic-wasm smoke test pattern: Two-file approach in `symbolic-wasm/npm/`: `smoke-test.mjs` (orchestrator: packs tgz, installs to temp dir, resolves wasm via exports map, spawns `node --test` on `package-smoke.test.mjs`) + `package-smoke.test.mjs` (node:test assertions against installed package via `initSync`). Test files excluded from `files[]` in `package.json` — nothing extra ships to consumers. Wired into `build-npm.sh` replacing bare `npm pack`. CI: `wasm-smoke` job in `ci.yml`. `cd symbolic-wasm/npm && npm test` runs the suite. Pack+install approach catches exports-map/resolution breakage, not just runtime errors. CRITICAL: `wasm-pack test --node` does NOT exercise the shipped artifact — it compiles tests with its own generated glue, never loads `--target web` `symbolic.js` + `symbolic_bg.wasm` via `initSync`. Confirmed by Burak Yigit Kaya: 'wasm-pack test sails past it because it builds its own glue and never loads what we ship'. PR #993 adds smoke tests.
- 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.
- idle.ts eviction: upstream uses per-function cleanup in idle.ts, not centralized evictSession in pipeline.ts: Upstream (main branch) puts session eviction logic directly in `idle.ts` rather than a centralized `evictSession()` in `pipeline.ts`. `idle.ts` imports cleanup functions individually: `evictSession as evictGradientSession` from `@loreai/core`; also `deleteSessionAuth`, `clearAuthStale` from `./auth`; `deleteSessionCosts` from `./cost-tracker`; `deleteBillingPrefix` from `./cch`; `clearWarmupAuthDisabled` from `./cache-warmer`. The `startIdleScheduler` signature uses `onEvict?: (sessionID: string) => void` (upstream) vs `onEvictSession?: (sessionID: string) => boolean` (branch). Upstream inline `onEvict` in `pipeline.ts` cleans 5 Maps: `headerSessionIndex`, `ltmSessionCache`, `ltmPinnedText`, `stableLtmCache`, `cwdWarned`. When merging, adopt upstream's per-function approach and add any missing cleanup calls.
- Monorepo split: packages/cli scripts must preserve root script semantics + paths: Post-monorepo-split `packages/cli/package.json` scripts must mirror root scripts verbatim — including: (1) `generate:docs` chains `generate:banner` + `generate:parser` BEFORE other generators (parser outputs under `src/generated/` are gitignored); (2) `typecheck`, `build`, `bundle` MUST run `generate:docs` + `generate:sdk` as prerequisites (`src/sdk.generated.ts` is gitignored and imported by `src/index.ts` — bare `tsc --noEmit` fails on clean checkout); (3) `test:e2e` and `test:changed` use direct `vitest` invocation; (4) `check:*` scripts point at the real script filenames (`generate-api-schema.ts`, `check-no-deps.ts`, `check-error-patterns.ts`, `check-stale-references.ts`, `generate-banner-sixel.ts`, `generate-docs-sections.ts --check`). Bugbot B1 + B2 caught PR #1254 dropping these chain prerequisites in the rebased base. Fix: take main's `scripts` block verbatim into `packages/cli/package.json` rather than re-deriving. ADDITIONAL gotcha: ci.yml `code` paths-filter must include root workspace config files (`package.json`, `.npmrc`, `pnpm-workspace.yaml`) — `pnpm.patchedDependencies`/`pnpm.overrides`/`node-linker` moved there post-split.
- Node version pinning convention: workflow-level env vars NODE_VERSION_22 / NODE_VERSION_24: As of PR #1145, all GitHub Actions workflows in sentry-cli (TypeScript) centralize Node version pins as workflow-level `env` vars: `NODE_VERSION_22: "22.23.1"` and `NODE_VERSION_24: "24.18.0"`. All `actions/setup-node` steps reference `${{ env.NODE_VERSION_22 }}` or `${{ env.NODE_VERSION_24 }}` — no bare `"22"`/`"24"` strings. Matrix jobs use ternary: `${{ matrix.node == '24' && env.NODE_VERSION_24 || env.NODE_VERSION_22 }}`. Motivation: Node 24.17.0/22.23.0 shipped `ERR_STREAM_PREMATURE_CLOSE` regression (CVE-2026-48931 http.Agent fix); fixed in 24.18.0/22.23.1 (nodejs/node#64004). When bumping Node, update the `env` block in each workflow file.
- Node.js version matrix testing pattern for npm package: Build npm package on development Node floor (22.15+), smoke-test on matrix Node versions. CI env vars: NODE_VERSION_20: "20.20.2" (WASM-SQLite floor), NODE_VERSION_22: "22.23.1", NODE_VERSION_24: "24.18.0". Build job: setup-node uses env.NODE_VERSION_22. Smoke-test job: setup-node uses matrix.node with fallback to env vars. Ensures fallback path is exercised in CI.
- 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`.
- scan.ts: per-object extraction errors always swallowed — never abort surrounding upload: In `src/lib/dif/scan.ts` and `src/lib/dif/index.ts`, extraction errors for embedded PPDBs (`extractEmbeddedPpdb`), IL2CPP mappings (`createIl2cppLineMapping`), and source bundles are caught per-object, logged at debug level, and swallowed — they never abort the surrounding upload or scan. This mirrors legacy Rust sentry-cli behavior. Similarly, `PeekResult.format` is never `'unknown'` — unrecognized formats return `null` from `peekHeader`. Nested ZIP archives are never recursed regardless of `scanZips` setting.
- Security dep bump pattern: pnpm.overrides for transitive vulns + direct version bumps: Security dep bump pattern for getsentry/cli: (1) Bump direct deps in `package.json`. (2) Add `pnpm.overrides` for transitive-only vulns (e.g. `shell-quote`, `qs`). (3) Add `pnpm.overrides` to `docs/package.json` for docs-workspace transitive vulns. (4) Run `pnpm install` in root, then in `docs/`. (5) After lockfile regen, verify no vulnerable versions remain and no previously-patched versions were downgraded — run `pnpm update <pkg>` to fix regressions. (6) Run `check:deps` to confirm no runtime deps added. Pre-existing WARNs about `@sentry/core`/`@sentry/node-core` patches on `@sentry/node`'s nested copies are expected. Trap: `pnpm install` can downgrade already-patched transitive deps (e.g. `hono` downgraded reintroducing GHSA vuln) — always verify versions post-regen.
- selectBundledObject: shared generic helper for first-debug-info-else-first selection: `selectBundledObject<T>(items: T[], hasDI: (t: T) => boolean): T | undefined` in `src/lib/dif/index.ts` is the single source of truth for 'first object with debug info, fallback to first object' selection. Used by both `createSourceBundle` (WASM `Object[]`) and `print-sources` (multi-object warning). Chosen over duplicating the heuristic in each consumer — divergence between bundler and inspector is structurally impossible. Generic predicate parameter lets it work with both WASM `ObjectFile` and `DifObjectSources` arrays.
- sensitive argv flags must never reach telemetry — redactArgv() in cli.ts: `SENSITIVE_ARGV_FLAGS = new Set(['token', 'auth-token'])` in `src/cli.ts`. `redactArgv()` replaces values of these flags with `[REDACTED]` before any telemetry call. This is an absolute invariant — never pass raw `process.argv` to telemetry without running through `redactArgv()` first.
- sentry-cli banner architecture and test commands: Banner only renders in interactive terminals (`if (process.stdout.isTTY)`). Test commands: `pnpm cli help` or `pnpm cli` (fastest, tsx via src/bin.ts, no build); `pnpm cli init` (Ink wizard banner path). Quick sanity check without CLI: `FORCE_COLOR=3 pnpm exec tsx -e "import('./src/lib/banner.ts').then(m => console.log(m.formatBanner()))"`. Env vars: `FORCE_COLOR=3` forces color output; `NO_COLOR` or `SENTRY_PLAIN_OUTPUT` strips gradient.
- setup.ts bestEffort() wrapper: post-install steps must never crash setup: `src/commands/cli/setup.ts` `bestEffort(stepName, fn)` wraps non-essential post-install steps (recording install info, shell completions, agent skills) in try/catch. On failure: calls `warn(stepName, error)` + `captureException(error, { level: 'warning', tags: { 'setup.step': stepName } })`. These steps must NEVER crash setup — enforced by `bestEffort()`. `runConfigurationSteps()` applies `bestEffort()` independently to all 4 steps. Install dir priority: (1) `$SENTRY_INSTALL_DIR`, (2) `
/.local/bin` if exists+in PATH, (3) `/bin` if exists+in PATH, (4) `~/.sentry/bin` fallback. Welcome message only on fresh install (not upgrades).
- 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. `sort` flag is resolved once in `func()` before dispatch — never re-derived by infra. `handleOrgAllIssues` returns server order (no client-side sort). `isMultiProject` guard gates client-side sort at list.ts:1144-1148.
- symbolic-il2cpp integration tests: use symbolic-testutils dev-dependency with Object::parse pattern: Integration tests for `symbolic-il2cpp` live in `symbolic-il2cpp/tests/` (separate from unit tests in `src/`). Add `symbolic-testutils = { path = "../symbolic-testutils" }` as dev-dependency (path-only, safe for publishing — matches `symbolic-debuginfo` pattern). Use `ByteView::open(fixture("..."))` → `Object::parse(&view)?` to get a real `ObjectLike`. Fixture files live in `symbolic-testutils/fixtures/`. Native unit test with mock `ObjectLike` rejected as too heavyweight (many methods to implement). PR #1005 added `from_object_with_provider_empty_without_sources` and `from_object_with_provider_parses_source_info` tests.
- 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`.
- WASM module bundling pattern: node-sqlite3-wasm requires special bundling: externalize in build scripts, include .wasm file in package.json files list, use locateFile override for wasm path resolution.
- wizard-runner.ts: large shared context via initialState, not inputData — D1 row size limit: In `wizard-runner.ts`, large shared context (`dirListing`, `fileCache`, `existingSentry`) travels via `initialState` (not `inputData`) to avoid D1 per-row size overflow (see getsentry/cli-init-api#98). `MAX_RESUME_RETRIES = 3`, `RETRY_BACKOFF_MS = [2000, 4000, 8000]`. `resumeWithRetry()` handles stale-step recovery via `tryRecoverCurrentRunState()` when `isStepAlreadyAdvancedError()` detects 'was not suspended' 500.
- Always call plan_exit at end of plan-mode turns and run parallel exploration agents per defined phases: The user consistently enforces a strict 5-phase plan workflow when in plan mode: Phase 1 Initial Understanding (run explore agents in parallel, max 3, prefer minimum), Phase 2 Design (up to 1 general/design agent), Phase 3 Review, Phase 4 write the final plan to the plan file, and Phase 5 call `plan_exit`. The user explicitly directs that every plan-mode turn must end with either a question or `plan_exit` — never stop without one of these. The assistant must always call `plan_exit` to signal completion of planning. Additionally, when investigating GitHub security issues, the user repeats the same two `gh api` commands (with headers `Accept: application/vnd.github+json` and `X-GitHub-Api-Version: 2022-11-28`) against `/repos/{owner}/{repo}/security-advisories` and `/repos/{owner}/{repo}/dependabot/alerts` to gather data before planning fixes.
- Always emit explicit user-stated intent as red-highlighted facts and demand binary isolation when classifying fetch vs. content failures: Across sessions, the user (1) states permanent design assertions verbatim with 🔴 HIGH priority (e.g., "fetch failures are always transient, never poison", "never target the same path", "intermediates NEVER hit disk", "always clean up intermediates on failure") and expects the assistant to treat these as immutable contracts, not refutable hypotheses; (2) insists on narrow, binary classifications — network/transient vs. malformed_chain, cache hit vs. miss, in-memory vs. disk — and rejects fuzzy or hybrid categorizations; (3) drives investigation toward isolation of the specific failure surface (e.g., isolating the bug to delta-patch vs. spawn/replace by spawning the .download directly), not toward broad refactors. AI should: (a) preserve verbatim user directives as non-negotiable, (b) use the exact enum/string values the user defines for classification, (c) narrow the search space to one layer at a time before proposing fixes.
- Always fetchable from the base repo with github: User stated always fetchable from the base repo with github.
- Always follow the regular PR workflow with minimal, clean diffs for dependency updates: When the user asks to fix an issue or bump a dependency (e.g., Starlight, Astro), they expect the standard PR flow: create a properly named branch, commit (including .lore.md), open a PR, review, and merge. Before pushing, verify the change is minimal — inspect package.json and lockfile diffs, confirm peer dependency ranges resolve, and ensure no unrelated churn. Double-check the branch tracks the correct remote and push to the right PR branch. Close any redundant/duplicate PRs (e.g., dependabot replacements). Ensure all CI checks are green; rerun flaky jobs when needed. Only merge after everything is clean.
- Always guard Node.js API availability at module top-level: User consistently identifies and fixes module-level calls to Node.js APIs that may not exist in certain versions. They require wrapping such calls in runtime availability checks (e.g., `typeof api === 'function'`) to prevent import-time crashes. This applies particularly to newer APIs like `zlib.zstdCompress` that aren't available in older Node.js versions.
- Always inject debug IDs (even without auth); never ship sourcemaps to users: Always inject debug IDs into JS bundles during local builds, even when no auth token is present — upload is gated inside the plugin. This ensures debug info is always available for analysis regardless of auth status. Also: never ship sourcemaps to end users in server-side deployments, while preserving debuggability during development.
- Always investigate root cause of failures before fixing: When a test or CI job fails, the user consistently expects a fix grounded in verified root cause analysis, not superficial patches. This means pulling and inspecting full logs to confirm the exact failure path, checking whether the issue is pre-existing or introduced by the change, and searching the codebase for established conventions (e.g., sleep utilities, test helpers) before writing new code. The user prefers minimal-scope fixes that don't affect unrelated tests, and always adds a regression test for the specific bug. Before adding a test, verify the edited file is correct and review existing test structure. Also clean up any dead code encountered (e.g., unused constants).
- Always keep Dependabot PR branches clean by resetting to latest main and cherry-picking the update commit: When updating a Dependabot PR, the user prefers a linear history: reset the local branch to origin/main, then cherry-pick the Dependabot commit (e.g., 76793bbe6) on top rather than merging main in. They verify the correct remote and tracking branch before pushing, push to the existing PR branch (dependabot/...) to update the same PR, and afterward confirm the PR head SHA matches the pushed commit. If a cherry-pick or rebase conflicts, they pause, diagnose any mixed changes, and reset/re-apply only the relevant commit to exclude unrelated upstream modifications. Always preserve clean history, avoid extra merge commits, and verify remote/PR state after each operation.
- Always migrate Bun-specific APIs and tooling to Node.js equivalents: Always migrate Bun-specific APIs and tooling to Node.js equivalents: 🔴 Directive (repeated 25+ sessions): ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. NEVER proactively create documentation files (*.md) or README files — only create documentation files if explicitly requested by the user.
- Always request read-only adversarial code reviews with explicit no-modification constraints: When requesting code reviews (especially of Dependabot/security-fix PRs or restructuring PRs), the user explicitly demands a READ-ONLY, ADVERSARIAL review with strict prohibitions: "DO NOT modify files," "findings only," "verify git working tree stays clean." The user provides detailed context upfront — PR number, branch, base branch, commit SHA, exact files in scope, and often specific items/alert numbers to inspect. The user also pre-specifies the exact verification commands the reviewer should run (gh api/audit/why/ls-files/grep/cat). For Dependabot alert reviews, the user specifies each alert number to investigate and expected override constraints to validate. Reviews must be rigorous and skeptical, hunting for path correctness, stale artifacts, broken overrides, and missed transitive dependencies.
- Always request read-only adversarial code reviews with structured findings, not code changes: When reviewing a PR or branch, the user consistently demands a READ-ONLY adversarial review with explicit constraints: do NOT write code, do NOT make commits, deliver findings as a categorized severity list (BLOCKING / SHOULD-FIX / NIT or Critical/High/Medium/Low/Nit). They pre-specify the files to review, the verification commands to run (install/test/specific test names), and the specific concerns to validate (correctness, error paths, regression test validity, doc accuracy, perf regressions, API surface). They want empirical verification — e.g., run tests, revert a file to confirm a regression test catches the bug, build repro scripts. They expect file:line references with concrete fix suggestions. This applies to all PR/branch reviews in any repo.
- Always resolve a string (or change the type definition): User stated always resolve a string (or change the type definition).
- Always restores the prior tty state: Always restores the prior tty state. This is a critical behavior for handling terminal interactions.
- Always return a report even when empty or partial: Directive (observed across 16+ prior sessions): whenever a report/result is expected, always deliver one — if there are no findings, return an empty report rather than omitting output. Partial findings must be included as-is; never withhold a response waiting for completeness.
- Always return to automatic detection after tests: Always return to automatic detection after tests so we don't leak forced state. Ensures test isolation and prevents state leakage between test cases.
- Always run full/targeted tests and lint before committing: The user follows a consistent verification workflow before committing: after applying a fix, they (or the assistant on their behalf) run the relevant tests to confirm the fix works and no regressions exist. Once tests pass, they proceed to lint check on touched files, review the git diff, and then commit — often amending the commit if additional lint/typecheck fixes are needed. Apply this pattern by: (1) running targeted tests first to confirm the fix, optionally reverting-and-re-checking to confirm the test catches the bug, (2) running the full test suite to verify no regressions, (3) running typecheck and lint on touched files, (4) reviewing the full git diff before committing.
- Always separate intermediate writes from source reads when applying patch chains: When applying multi-step patch chains, never read from and write to the same path. Multi-hop chains alternate between two distinct intermediate files (e.g., `${destPath}.patching.a` and `${destPath}.patching.b`) so the reader is never aliased to the writer — writing to the source would truncate the mmap'd read and corrupt the output. When in-memory chains are feasible, load the base once, keep intermediates in memory, and only write the final binary to disk (then SHA-256 verify). Always clean up intermediate files in a `finally` block, even on failure.
- Always update .lore.md before committing; always take HEAD on conflicts: Always verify and update .lore.md before committing; on rebase/cherry-pick conflicts, always take HEAD (main's version) for `.lore.md` since it is auto-regenerated. Resolved by stashing with key prefix `lore-md-pre-rebase-` before rebase, then restoring and re-committing after.
- Always validate CI configuration changes with real job execution: The user consistently demonstrates a pattern of making CI configuration changes (especially to GitHub Actions workflows) and then immediately validating those changes by triggering the actual CI jobs. They monitor the job execution in real-time, check logs when failures occur, and iterate on fixes until the jobs pass. This applies to any CI changes including matrix updates, job restructuring, path adjustments, or dependency changes.
- Always verify and address stale review findings against outdated base commits: When automated reviewers (Bugbot, Seer) flag issues, the user/agent verifies whether findings are against stale base commits or still apply. Across PR #1254 cycles, Bugbot repeatedly posted findings referencing `6e72f45b4` while current HEAD was `89f8c2c59`. The consistent pattern: (1) identify the base commit the review ran against, (2) diff against current HEAD, (3) mark stale findings as already-resolved rather than re-fixing, (4) only action findings that are genuinely current. Also: distinguish transient infra failures (CodeQL, dependency-review — not in CI Status gate) from real substantive failures, and proceed with merge when only infra-only checks fail.
- Always verify CI on the latest commit and separate known flakes from regressions: The user consistently expects CI status verification after every push to a PR branch, ensuring results reflect the newest commit (not stale). They schedule follow-up polls for critical jobs (Build Docs, E2E, preview, CI Status) and provide contingency directives: if docs/preview pass but E2E flakes due to a pre-existing race, treat it as non-blocking and investigate separately. Before merging, the user requires a full PR review, all security/required checks green (or only known transient infra failures), and confirmation the CI results are from the current head. Any real regression must be fixed (e.g., dependency bump) and CI re-run on the new commit before landing.
- Always verify Sentry CLI auth and org access before querying issues: Before running any `sentry issue list` or similar query, the user expects verification of: (1) authenticated identity and token expiry, (2) which orgs are accessible, and (3) which org/project the relevant telemetry flows into. If access is insufficient (e.g., 403 on internal org), the user switches accounts to gain access to the correct org (e.g., from `ben@byk.im` to `byk@sentry.io`). The user also wants proactive identification of which org/project maps to the code being debugged — never assume; always confirm before pulling issue data.
- Always verify SHA mismatches by byte-level diff and patch-header forensics before proposing fixes: When a delta-patch produces a SHA mismatch (patches claimed `newSize` = 116,460,352 but produced 111,136,512 bytes, or `patch newSize` disagrees with annotation SHA), the user does not accept surface-level hypotheses. They drill down to: (1) byte-level diff counts between patched output and real published binary (e.g., 9,374,818 differing bytes, first diff at offset 42), (2) parse raw patch headers manually (`controlLen`, `diffLen`, `newSize` from TRDIFF10 magic) via `bspatch.ts:80` `offtin` i64 LE decoding, (3) regenerate bsdiff locally with zig-bsdiff and compare patch sizes (fresh 128,297B vs official 343,157B = 2.67x), (4) cross-check CI workflow YAML (`ci.yml:430-749`) for the publish/generate pipeline, (5) verify source/target SHAs match user's local binary before drawing conclusions. Treat any `expectedSha256` mismatch as a forensic investigation: confirm the annotation SHA corresponds to the FULL binary (not delta output), prove patch application is reproducible with isolated test scripts against GHCR data, and only then propose a root cause.
- Always write tests alongside implementation: Behavioral pattern detected across 9 sessions (action: requested-tests). The user consistently demonstrates this behavior.
- bun add -d <package> for dev dependencies (never omit -d): Directive (repeated across ~20 sessions): when adding a dev dependency in Bun-based repos (e.g. opencode, loreai CLI), always run `bun add -d <package>` — the `-d` flag installs into devDependencies, which is the default expectation. Do not omit the flag; do not use plain `bun add <package>` or `bun install <package>` for a dev dependency.
- Call plan_exit to indicate planning done: Always call plan_exit to indicate that planning is done.
- Defer to Craft's auto-computed version without override: When Craft auto-computes a release version (e.g., 0.4.0 from a 'feat:' commit, even if the bump level seems unexpected or the baseline seems off), the user instructs the assistant to accept it as-is. The user's standing directive: 'Don't specify a target version unless explicitly being told.' This applies even when the assistant suspects Craft picked the wrong baseline (e.g., v0.3.1 instead of v0.3.2) or chose an unconventional bump. Do not second-guess, override, or manually pin a version. Apply the 'accepted' label to the release issue and let Craft proceed.
- Demand rigorous read-only adversarial PR reviews with categorized findings: When reviewing PRs, the user requires a strict, read-only adversarial review (no code changes, no commits) with findings categorized as BLOCKING / SHOULD-FIX / NIT. The review must verify correctness by checking upstream artifacts (e.g., `gh api` against the action repo, `git ls-remote`, tag existence, ref resolution), run the full test suite, and trace exact file:line locations for every issue. Findings must distinguish latent vs active bugs, call out dead code/outputs by tracing consumers, flag false claims in PR descriptions against actual code state, and document non-obvious assumptions (e.g., branch mutual exclusivity) inline. The user expects pin/reference correctness (e.g., no `v` prefix when repo doesn't tag with one) and defense-in-depth vs prevention distinctions to be made explicit.
- Diagnose CI failures by reading context thoroughly before applying fixes: When CI fails on a rebased branch, the user expects a methodical root-cause investigation: read the failing job logs, identify the exact import/module path mismatch (e.g., file moved during rebase but import path not updated, git treating rename as delete+add causing files to land at old paths), classify conflicts by category (UU/AU/DU/D), and propose a targeted fix — not a speculative change. Pattern shows the user values understanding WHY a failure occurred (e.g., git rename detection behavior, monorepo path conventions) before committing to a fix, and wants conflicts categorized into resolvable groups before resolution begins.
- Diagnose Craft release failures by inspecting workflow chain end-to-end: When Craft-based releases fail (e.g., 'no commits since last release', missing GitHub releases), the user methodically inspects the entire pipeline: release.yml → publish.yml → ci.yml → .craft.yml, then cross-references CHANGELOG entries, git log commits, and version tags to reconstruct what Craft should have done. They list concrete corrective steps (e.g., 'add github target', 'tag commit as v0.3.1', 'rerun flow') rather than guessing. Follow this pattern: trace from workflow dispatch → craft prepare → artifact provider → targets → publish trigger; verify OIDC/publish_repo wiring; map commits to versions via CHANGELOG before proposing fixes.
- Evidence-based code review and verification: Always verify code/PR claims against actual file contents before accepting them as true: read the real source files at specific line numbers, quote exact code/comments, and flag discrepancies. Mark confirmed findings with 🟡 and actionable directives with 🔴. Cross-check automated tooling (scripts, CI configs) against what they actually produce. When a PR introduces a fix, confirm the underlying problem genuinely existed. Never accept a PR description or assertion at face value — ground-truth it against the codebase with precise line references.
- Flaky test diagnosis workflow: For intermittent test failures, ground the fix in evidence, not guesses. Get the full CI job log and failing test source; reconstruct timings/interleavings from log lines. Before patching, verify the flake pre-exists on unmodified code (clean leftover state like dist/, run from the correct package dir, e.g. packages/cli in pnpm monorepo). Run the suite repeatedly (4-6+ runs) to measure flakiness and capture a failing run. Instrument first — add diagnostic logging/lock traces to prove concurrency behavior at runtime before changing design. Run full vitest and tsc --noEmit after changes, and investigate Vitest deprecation warnings. Add a regression test for the specific bug and keep the fix minimal.
- Follow consistent code style conventions: Behavioral pattern detected across 4 sessions (action: corrected-style). The user consistently demonstrates this behavior.
- Follow the established git workflow (branch, PR, review): Behavioral pattern detected across 12 sessions (action: enforced-workflow). The user consistently demonstrates this behavior.
- Ink import behavior: User stated code should 'never calls `import("ink")` at runtime —', indicating a preference against runtime imports of Ink library.
- Investigate production bugs via root-cause methodology with cross-repo pattern detection: When debugging a reported production failure, the user drives a systematic root-cause investigation that (1) reproduces the failure manually with concrete SHA evidence, (2) traces the code path across all affected repos (in this case both `getsentry/cli` and `binpatch`), (3) classifies bugs by scope (CLI-only vs shared) and ranks fixes by whether they sidestep or eliminate the issue, and (4) proposes fixes that propagate the resolved state through artifact files rather than recomputing in independent jobs. The user expects concrete SHA comparisons, byte-size smoking guns, and explicit git/CI workflow line citations — not vague speculation. When proposing fixes, surface both implementation options (e.g., 'close' event listener vs sync fd release) and call out latent issues that a primary fix doesn't address.
- Investigate tooling/build failures by tracing symptoms to recent dependency or infrastructure changes: When the user encounters a CLI/build/tooling error, they consistently pivot to identifying the most recent infrastructure change as the likely culprit and trace the failure toward its source repository or runtime artifact. Across sessions, error reports (ETXTBSY, ENOENT, ESM resolution failures, skill loader drops, dev/SEA/host binary mismatches) are followed by hypotheses pointing at a specific recent change — the `binpatch` switchover, a vendored-crate replacement with `@sentry/symbolic`, a custom opencode build replacing a downloaded release, or the `acquireLock` path on `sentry cli upgrade`. The user expects the assistant to (1) confirm provenance of the running binary vs source, (2) read the suspected module directly rather than assume cache/version drift, and (3) reproduce the failing code path against the published/upstream artifact before proposing fixes.
- Investigate upgrade flow root causes before proposing fixes: When the user reports an upgrade-related symptom (wrong binary downloaded, delta upgrade not used, stale comments, etc.), they expect the assistant to investigate the full upgrade flow end-to-end: identify which code path is responsible, why fallback logic triggered, and trace the chain of decisions (gz vs raw, delta vs full). They want hypotheses tied to specific files and functions (e.g., `downloadStableToPath`, `downloadNightlyToPath`, `delta-upgrade.ts`) before suggesting code changes. Prior sessions show the user values reading existing test coverage (`upgrade.test.ts`, `delta-upgrade.test.ts`) and CI workflow steps (e.g., `Generate delta patches`) to understand intended behavior before changing it. Avoid jumping to patches; first map the call sites and failure modes.
- Lock management safety guarantees and gotchas: The user consistently implements and refines database lock management with strong safety guarantees. They ensure locks are never re-touched after release, never deleted while potentially in use by another process, and never leave empty directories behind. The user methodically tests edge cases like PID reuse scenarios and stale lock recovery, always adding comprehensive tests for each lock management behavior. They prioritize preventing cross-process database corruption through careful lock cleanup strategies.
- Never throws - errors are caught and reported to Sentry: Never throws - errors are caught and reported to Sentry. This ensures that the application remains stable and provides useful error information.
- Never use node_modules/: User stated never to use 'node_modules/.'.
- Node version floors, matrix, and dependency policy: The user consistently provides precise version requirements and technical constraints when discussing dependencies, platform support, and compatibility. This includes specifying exact Node.js version floors (e.g., '>=18.0'), defining version ranges for conditional logic (e.g., 'Node 18–22.14'), and setting strict version pins in CI configurations. The user expects these constraints to be accurately implemented and documented across all relevant files.
- Prefer deterministic structural assertions over non-deterministic runtime checks for regression tests: When the user reviews or writes regression tests, they reject flaky runtime-race detectors and demand deterministic structural assertions that prove the fix mechanically. Pattern: prefer an assertion that proves the contract holds by construction (e.g., after a close call returns, a sibling helper re-opens the path and verifies the returned fd number is strictly higher than the original — Linux fd allocation never recycles a lower unused number while a higher one is open, so the test cannot pass unless closeSync actually ran). The user explicitly overrides weaker approaches: in this session they replaced a /proc/self/fd readdir race check (0% bug-detection rate in vitest, 5.5% in standalone) with a structural fd-number comparison that is guaranteed-deterministic. Apply this: when proposing a regression test, design it so passing the test is logically equivalent to the fix being present, with no timing/scheduling dependence.
- Preference for deep SQLite understanding: User repeatedly asks to understand HOW SQLite is used across multiple sessions, indicating a strong preference for deep technical understanding of database usage patterns.
- Prefers Bun-native APIs over Node: prefer Bun-native APIs over Node.
- Push branch and open PR immediately after committing fixes, then hand off to CI/follow-ups: After making a fix on a fix/* branch, the user pushes the branch to origin and opens a PR (often via gh CLI) without manually merging or waiting locally. The assistant is expected to then shift focus to: (1) monitoring/waiting for CI on the opened PR, (2) identifying follow-up work in dependent repos (e.g., filing issues in BYK/binpatch when an action diverges), and (3) flagging unintended changes in the branch (such as auto-generated files like .lore.md) before proceeding. The user does not request local verification gates beyond running tests; pushing and opening the PR is the natural completion step for a fix branch.
- Run CI status checks with patience using polling loops rather than long single waits: When polling CI status on PRs, the assistant repeatedly hits shell command timeouts (120000ms / 600000ms) while waiting for checks to complete. The user/assistant pattern is to retry with timeout rather than wait synchronously. To follow this pattern: when CI is polled and the command times out, retry the check with the same or extended timeout; break long CI waits into shorter polling intervals rather than a single long-running command. This avoids losing progress and lets the assistant respond incrementally as each check (e.g., docs, E2E, warden) completes.
- Run full test suite plus lint after every code change: After implementing or modifying code, the user consistently expects a verification cycle consisting of: (1) running the test suite to confirm all tests pass and noting any expected error messages, (2) running the lint check to confirm no issues, and (3) often a typecheck on modified files. Test output is reviewed carefully — expected warnings (skipped files, unsupported formats, Sentry CLI auth warnings) are explicitly called out as non-blocking, while actual failures are investigated. The assistant should proactively run lint and tests after changes, report pass/fail counts with timing, distinguish expected vs unexpected warnings, and suggest logical next steps (typecheck, commit, push) once verification is green.
- Strict PR merge-readiness workflow: Drive every PR to a clean merge-ready state: branch, commit with .lore.md, push, then wait for CI and bot reviews. Rebase onto latest main and resolve conflicts; take HEAD for auto-generated .lore.md. Investigate CI failures by reading logs and conflict categories before fixing. Resolve every inline review thread (Bugbot, Seer, Warden): verify against current code, reply to stale/false positives, then resolve. Run full local verification (install, typecheck, lint, build, tests) and confirm required CI checks pass. After CI turns green, re-fetch PR state before merging — auto-merge may already have merged; verify merge commit has one parent. Final sweep for new comments, then squash-merge only when mergeStateStatus=CLEAN.
- Update configuration files systematically and verify paths after restructuring: Always update configuration files systematically: update specific sections/blocks methodically (e.g., paths-filter globs, version-read), with detailed examination before edits. Applies to CI/CD configs (ci.yml, docs-preview.yml, eval-skill-fork.yml), with paths verified after repository restructuring (e.g., packages/cli/, apps/cli-docs/) and absolute paths used where necessary.
- Verify all review comments are addressed and CI checks pass before merging PRs: User consistently requires thorough pre-merge verification: (1) all inline review comments from Bugbot, Seer, and Warden must be addressed or explicitly tracked, not just CI checks passing; (2) wait for all CI checks (including warden and E2E tests, not just Bugbot/Seer) to complete before merging; (3) review bot comment threads across merged PRs to confirm no unaddressed findings remain. Before approving any merge, the user expects: a complete check of all review threads, resolution status of every comment (B/S/N severity), and full CI green status. Apply this rigor — don't merge based on partial CI pass or assume bot comments are resolved without explicit verification.
- Verify all review findings and end-to-end behavior before declaring work complete: Before considering a merge or release done, the user insists on two checks: (1) all inline review threads, Bugbot findings, and Seer/Warden comments across every PR in the batch must be verified as addressed or explicitly accepted, not assumed resolved because the PR is merged; (2) the actual end-to-end flow (e.g., running `sentry --verbose cli upgrade`) must be exercised to confirm the upgrade works as intended. The user does not accept 'PR merged' as proof of completion. Pattern: when finalizing a batch of PRs/releases, enumerate unaddressed review items per PR first, address or defer each explicitly, then run the real upgrade/usage command to validate behavior before reporting done.
- Verify PR descriptions against actual implementation before adoption: When reviewing or adopting a dependency, PR, or action that claims to fix a specific bug or provide a specific behavior, the user always cross-references the claim against the actual code/config. Pattern observed: agent inspecting PR #1330 found its description falsely claimed a cross-major fix existed in the binpatch action, but verification of action.yml showed the fix was only in the workflow being deleted. Also confirmed by tracing wire contracts, threat model limits, and SHA-256 annotation semantics directly from source/docs rather than trusting summary claims. Action: read the source, not the PR body.
- Verify pre-existing test failures by reproducing on main before treating as regressions: When E2E test failures occur (especially in telemetry-exit.test.ts, bundle.test.ts, library.test.ts, or any bundle/race-condition related test), the user expects the assistant to first verify whether the failure reproduces on the main branch (without the user's PR change) before concluding it is a regression. Pattern observed across instances: 1. Run tests, observe failures (e.g., telemetry-exit timing, bundle 'Bundle not built' errors). 2. Check if change touches relevant code — if not, reproduce on main. 3. If failures reproduce on main, classify as pre-existing/flaky/infrastructure issue, not caused by the PR. 4. Proceed with merge once failure is confirmed unrelated. This avoids spurious investigations into unrelated flaky tests and unblocks the PR. Apply this verification step whenever a test failure appears and the PR's diff clearly doesn't touch the failing code path (e.g., workflow-only changes vs. test setup failures).