Long-term Knowledge

Architecture

  • 403/401 enrichment pipeline in infrastructure.ts — centralized, no interactive auto-fix: `src/lib/api/infrastructure.ts` centralizes HTTP error enrichment. `enrichDetail()` dispatches: 403→`enrich403Detail`, 401→`enrich401Detail`, others pass raw. `enrich403Detail()` three branches: (1) `rawDetail.includes('disabled this feature')` → org-policy message; (2) `isEnvTokenActive()` → `extractRequiredScopes(rawDetail)`; scopes found → definite missing-scope message; no scopes → hedged message; (3) OAuth → re-auth suggestion. `throwApiError()` and `throwRawApiError()` both set `ApiError.enriched403=true`. No interactive auto-fix. `buildPermissionError()` in `project/delete.ts` NEVER suggests `sentry auth login` — re-auth via OAuth won't change permissions. OAuth `auth login` always grants required scopes, so scope hints only apply to env-var tokens. 401 errors: fix is always re-authenticate — scope hints do NOT apply.
  • 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 build pipeline: esbuild → fossilize → Node SEA (replacing Bun.build compile): Binary build pipeline: `src/bin.ts → [esbuild CJS, node22] → dist-build/bin.js → [fossilize --no-bundle] → Node SEA binary → [binpunch] → gzip`. Keep inputs in `dist-build/` (separate from fossilize's `--out-dir dist-bin/`). Single fossilize invocation for all platforms via `FOSSILIZE_PLATFORMS`. Post-process: rename `sentry-win-x64.exe`→`sentry-windows-x64.exe`. Platform matrix: darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64 (no musl). Ink sidecar: `--assets dist-build/ink-app.js`. `FOSSILIZE_SIGN=y` on push to main/release. esbuild: `format:'cjs'` + `target:'node22'` + inject `import-meta-url.js` shim. Gzip only when `RELEASE_BUILD=1`. Sourcemap uploaded to Sentry, never shipped. npm bundle (`dist/index.cjs`): `bunSqlitePlugin` redirects `bun:sqlite` → `globalThis.__bun_sqlite_polyfill`. External: `node:*`, `ink`, `react`, `react-reconciler`, `yoga-layout`. PRs build only `linux-x64` + `linux-x64-musl`; main/release builds all 7 targets. `SENTRY_CLIENT_ID` required.
  • 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.
  • Host-scoped token model: auth.host column + three-layer enforcement: Host-scoped token model (schema v16): every token bound to issuing host via `auth.host` column, lazy-migrated from boot-env. Trust established ONLY via `sentry auth login --url` or shell-exported `SENTRY_HOST`/`SENTRY_URL` at boot — `.sentryclirc` URL never a trust source. Three enforcement layers: (1) `applySentryUrlContext` throws on URL-arg mismatch; (2) `applySentryCliRcEnvShim` throws on rc-url mismatch (auth login/logout bypass via `skipUrlTrustCheck`); (3) fetch-layer `isRequestOriginTrusted`. Region trust: in-process Set in `db/regions.ts`, auto-synced by `setOrgRegion(s)`. `clearTrustedHostState` must NOT clear login anchor (breaks IAP re-auth). `HostScopeError` has overloads `(message)` and `(source, destinationUrl, tokenHost)`. Test helpers: `resetHostScopingState()` bundles `resetEnvTokenHostForTesting` + `resetLoginTrustAnchorForTesting` + `resetTrustedRegionUrlsForTesting`. E2E: pass `--url ${ctx.serverUrl}` to `auth login --token`; `SENTRY_URL` alone doesn't anchor. Multi-region tests need `registerTrustedRegionUrls`.
  • 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.
  • **Node SEA ink sidecar: node:sea.getAsset() replaces Bun /bunfs/` virtual FS. Ink UI sidecar embedded via `fossilize --assets dist-build/ink-app.js`; asset key = raw CLI arg. At runtime: `sea.getRawAsset('dist-build/ink-app.js')`. Main bundle never calls `import('ink')` — sidecar pre-bundled by text-import-plugin. Dual-mode: detect SEA via `createRequire(import.meta.url)('node:sea')` with try/catch fallback. `useSnapshot: true` BROKEN. `useCodeCache: true` ~15% startup improvement but platform-specific V8 blob. fossilize asset manifest key = `basename(manifestPath)`; entry keys = `entry.file`. `new Worker(new URL(...))` HANGS in SEA — use Blob+URL.createObjectURL. Files embedded via `with { type: 'file' }` run from `/bunfs/` has no `node_modules` — inline all deps. Query strings in `/$bunfs/` paths cause ENOENT.
  • Sentry CLI authenticated fetch architecture with response caching: Authenticated fetch + response cache: `createAuthenticatedFetch`: auth headers, 30s timeout, max 2 retries, 401 refresh, span tracing. 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. `buildCacheKey()` normalizes URL with sorted params; use `invalidateCachedResponsesMatching(prefix)` for prefix-based invalidation. Use `unwrapPaginatedResult` (not `unwrapResult`) for Link header pagination.
  • 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. Schema v13 merged `defaults` table into `metadata` KV with keys `defaults.{org,project,telemetry,url}`; getters/setters in `src/lib/db/defaults.ts`. Hidden global `--org`/`--project` flags: `mergeGlobalFlags()` injects hidden flag shapes, `applyOrgProjectFlags()` writes to `SENTRY_ORG`/`SENTRY_PROJECT` before auth guard. No short aliases (`-p` conflicts). Sentry API quirks: events need org+project (`/projects/{org}/{project}/events/{id}/`); issues use legacy global `/api/0/issues/{id}/`; `/users/me/` returns 403 for OAuth — use `/auth/` via `getControlSiloUrl()`. Chunk upload returns camelCase (`chunkSize`). Magic `@` selectors: `@latest`, `@most_frequent` in `parseIssueArg`; `SELECTOR_MAP` case-insensitive. `@sentry/api` SDK: wrap types at `src/lib/api/*.ts`; `unwrapResult`/`unwrapPaginatedResult` must stay CLI-owned.
  • src/cli.ts: middleware chain, completion optimization, sensitive argv redaction: `src/cli.ts` exports `startCli()`, `runCli()`, `runCompletion()`. Middleware chain (innermost-first): `[seerTrialMiddleware, rcImportMiddleware, autoAuthMiddleware]` — auth is outermost. `autoAuthMiddleware` uses `isatty(0)` not `process.stdin.isTTY` (Bun returns undefined). `rcImportMiddleware`: fires on `AuthError{reason:'not_authenticated'}` + `!skipAutoAuth` + `isatty(0)`. `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. Seer trial: `GET /api/0/customers/{org}/` → `productTrials[]`; `PUT` to start. SaaS-only; self-hosted 404s gracefully. Route map includes `debug-files` (visible, not hidden) with single subcommand `bundle-jvm`.

Decision

  • 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.

Gotcha

  • existsSync+realpathSync TOCTOU: catch ENOENT instead: Trap: `if (!existsSync(p)) return resolve(p); return realpathSync(p)` looks safe but has a TOCTOU race. Also: `realpathSync` inside async is inconsistent. Fix: call `await realpath(p)` (node:fs/promises) directly; catch `ENOENT` to fall back to `resolve(p)`; log non-ENOENT errors via `logger.debug(msg, error)` before falling back. When mocking in vitest, mock `node:fs/promises` not `node:fs`. RELATED: In cleanup/unlink catch blocks, only log non-ENOENT errors — `ENOENT` during cleanup is expected. Pattern: `if ((error as NodeJS.ErrnoException).code !== 'ENOENT') logger.debug(msg, error)`. Pre-existing silent `catch { // Ignore }` blocks must be fixed to log non-ENOENT errors. Confirmed fixed in PR #1046 (`fix/install-binary-symlink-self-copy`).
  • 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`.
  • 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 lockfile regeneration can downgrade already-patched transitive deps: Trap: Running `pnpm install` to regenerate a lockfile after bumping deps looks safe, but pnpm may resolve transitive deps to older versions than what was already pinned on `origin/main`. Example: `hono@4.12.25` on main was downgraded to `hono@4.12.23` (satisfying `^4.12.15`) after lockfile regen, reintroducing a GHSA vulnerability. Fix: After lockfile regen, run `pnpm update <pkg>` for any transitive dep that has a known vulnerability, then verify versions in lockfile. GitHub Dependency Review will flag regressions introduced by the PR even if the PR's intent was to fix vulnerabilities.
  • process.stdin.isTTY unreliable in Bun — use isatty(0) and backfill for clack: `process.stdin.isTTY` unreliable — use `isatty(0)` from `node:tty`. Bun's single-file binary can leave `process.stdin.isTTY === undefined` on TTY fds. `@clack/core` gates `setRawMode(true)` on `input.isTTY`, silently disabling raw mode. Fix: backfill `process.stdin.isTTY = true` when `isatty(0)` confirms. Debugging: `src/lib/init/tty-diagnostics.ts` `dumpTtyDiagnostics(label)` — no-op unless `SENTRY_INIT_DIAGNOSTICS=1`.
  • 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;`
  • 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.
  • 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; per-line verify breaks cross-newline patterns. (3) Extractor `hasTopLevelAlternation`+`skipGroup` must call `skipCharacterClass`. (4) Wake-latch race: use latched `pendingWake` flag. (5) `mapFilesConcurrent` filters `null` but NOT `[]` — return `null` for no-op files. (6) `collectGlob`/`collectGrep` must NOT forward `maxResults` to iterator. Worker pool: lazy singleton, size `min(8, max(2, availableParallelism()))`. Matches encoded as `Uint32Array` quads (~40% faster). `new Worker(new URL(...))` HANGS in SEA — 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`.

Pattern

  • 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.
  • 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.
  • 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`.
  • Security dep bump pattern: pnpm.overrides for transitive vulns + direct version bumps: To patch transitive dependency vulnerabilities in getsentry/cli: (1) Bump direct deps in `package.json` (e.g. `@mastra/client-js`, `esbuild`, `hono`). (2) Add `pnpm.overrides` block to root `package.json` 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 `pnpm install` in `docs/`. (5) After lockfile regen, verify no vulnerable versions remain and no previously-patched versions were downgraded. (6) Run `check:deps` to confirm no runtime deps added. Pre-existing WARNs about `@sentry/core` and `@sentry/node-core` patches failing on `@sentry/node`'s nested copies are expected and not a regression.
  • Sentry SDK tree-shaking patches must be regenerated via bun patch workflow: Sentry SDK tree-shaking via bun patch: `patchedDependencies` in `package.json` strips unused exports from `@sentry/core` and `@sentry/node-core`. Always import from `@sentry/node-core/light` (non-light root pulls uninstalled `@opentelemetry/instrumentation`). Bumping SDK: remove old patches, `rm -rf ~/.bun/install/cache/@sentry`, `bun install`, `bun patch @sentry/core`, edit, `bun patch --commit`; repeat for node-core. Before stripping any core export, grep `node-core/build/{cjs,esm}/light/sdk.js` for runtime usage. Remove `.bun-tag-*` hunks from generated patches. Manual `git diff` patches fail. Preserved: `_INTERNAL_safeUnref`, `_INTERNAL_safeDateNow`, `nodeRuntimeMetricsIntegration`.
  • Shared pagination infrastructure: buildPaginationContextKey and parseCursorFlag: Pagination infrastructure: Bidirectional 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.
  • 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`. `.call()` LSP false-positives 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) `mockFetch()` replaces `globalThis.fetch` — use one unified mock dispatching by URL. (5) `mock.module()` pollutes module registry for ALL subsequent files — put in `test/isolated/` and run via `test:isolated`. (6) For `Bun.spawn`, use direct property assignment in `beforeEach`/`afterEach`.

Preference

  • Always explore e2e test infrastructure thoroughly before debugging or modifying tests: E2E test infrastructure overview: `test/e2e/` (14 files), `test/fixture.ts` (`getCliCommand` returns `[SENTRY_CLI_BINARY]` or `[process.execPath, 'run', 'src/bin.ts']`; `createE2EContext.run()` sets `SENTRY_AUTH_TOKEN:''`, `SENTRY_TOKEN:''`, `SENTRY_CLI_NO_TELEMETRY:'1'`), `test/helpers.ts`, `test/mocks/` (server.ts, routes.js, multiregion.ts). `test:e2e` runs WITHOUT `--isolate --parallel`; `test:unit` WITH. `telemetry-exit.test.ts` verifies `@sentry/core` patch adds `.unref()`. Project uses vitest (migrated from bun:test). See also architecture entry for full detail.
  • Always honor Retry-After header when present in LLM adapter: LLM adapter backoff (`packages/gateway/src/llm-adapter.ts`): Honor Retry-After — `backoffMs()` returns `Math.min(retryAfterMs, cap)`; caps: urgent=8000ms, background=120000ms. TRANSIENT_CODES={429,500,502,503,529}; MAX_RETRIES: rate-limit=3, server=3, urgent=2. Backoff (no Retry-After): 429 background=60s/120s/180s; urgent=min(1000×2^n,4000); 5xx background=min(1000×2^n,8000). Bearer tokens inject `billingBlock` as first system block; `signBody()` replaces `cch=00000` with xxHash64. System prompt caching uses `cache_control:{type:'ephemeral',ttl:'1h'}`. `opts.thinking` NOT forwarded to bare API calls. Circuit breaker tripped on non-urgent 429s. Auth (`packages/gateway/src/auth.ts`): `AuthCredential` (api-key|bearer); two-level lookup: `sessionAuth` Map → `lastSeenAuth` global fallback. `authFingerprint()` = SHA-256 truncated to 16 hex chars.
  • Always isolate unrelated changes into separate branches before creating PRs: Always isolate unrelated changes into separate branches before creating PRs: Create a fresh branch off origin/main containing only the minimal focused changes for the PR. Stash or exclude unrelated WIP changes. Identify all modified files, categorize by concern, branch off origin/main, apply only relevant changes. Always include `.lore.md` changes in commits when it has been modified — never skip or defer it. Auto-regenerated doc noise (e.g. skill reference files with drifted date examples) should be excluded from unrelated PRs. Use squash merge strategy (matches repo's PR-merge style).
  • Always migrate Bun-specific APIs and tooling to Node.js equivalents: Bun→Node.js/pnpm migration (largely complete): Replace `Bun.spawn`→`node:child_process`, `Bun.sleep`→`node:timers/promises`, `bun:sqlite`→`node:sqlite`, `bun run`→`pnpm run`/`tsx`, `bun.lock`→pnpm lockfile. All packages in `devDependencies`. Exception: `script/build.ts` uses `Bun.build()` and stays on Bun; `build-binary` CI job retains `setup-bun`. `script/bundle.ts` uses esbuild via tsx (Node-native). Migration complete on main: bun.lock deleted, vitest.config.ts added, all test files migrated to vitest.
  • Always track task queue state with numbered items and completion status: Always track task queue state with numbered items and completion status: Maintain an explicit task queue with numbered tasks labeled completed/in_progress/pending and priority levels. Update the queue as work progresses. Treat it as the authoritative source of remaining work. Tasks are granular and updated at specific timestamps.
  • Always verify lint and typecheck pass after code changes before committing: Always verify lint and typecheck pass after code changes before committing: Run Biome lint and typecheck after implementing or modifying code. When lint errors appear, verify whether they are pre-existing (use git stash) vs. newly introduced — fix only newly introduced issues. Pre-existing errors on main are noted but not fixed in the current branch. Expect clean lint output (0 errors) and passing tests before committing or pushing.
  • Always work from a structured plan file before executing multi-step tasks: Always work from a plan file before executing multi-step tasks: Create `.opencode/plans/<id>-<name>.md` during planning phase before any edits. Plan enumerates discrete numbered tasks with priorities and target files. No file edits during planning. Execution only begins after explicit user approval. Tasks marked in_progress and completed sequentially. After implementation, create PR, monitor CI until all checks pass, fix failures, address bot/human comments. Only declare complete when CI is green and no unresolved comments remain. Use `gh run view --log-failed` and `gh pr checks` to identify failures. Use squash merge strategy. `gh pr view --json merged` field is unavailable — use `mergeCommit`, `mergedAt`, `mergedBy`, `state` instead.
  • Always write tests after implementing new modules or features: After implementing a new module or integrating a feature, the user consistently adds corresponding tests — both a dedicated test file for the new module (e.g., `semantic-display.test.ts`) and additional tests in existing test files for integration points (e.g., new describe blocks or test cases in `local.test.ts`). The user also reads existing test files first to understand patterns before writing new tests. Tests are added as a required step in the todo list, not as an afterthought, and are followed by typecheck/test runs to verify correctness.
  • Bot review triage: distinguish real bugs from SDK-mirroring false positives: When Sentry Seer or Cursor Bugbot flags 'unusual' code that intentionally mirrors upstream SDK behavior (e.g., `http_proxy` as last-resort fallback for HTTPS URLs — deliberate in `@sentry/node-core` `applyNoProxyOption`), decline with a written rationale referencing the SDK source rather than silently changing behavior. Removing the mirror creates a divergence where users get different proxy semantics from our transport vs. the SDK default. BYK's pattern: verify against `node_modules/@sentry/node-core/build/esm/transports/http.js`, post a reply explaining the precedent, and resolve the thread. Real bugs (uppercase env var support, whitespace trimming, wildcard handling) get fixed; SDK-mirroring 'bugs' get explained and dismissed.
  • Prefers Bun-native APIs; use buildCommand from lib/command.js (never @stricli/core directly); use buildRouteMap from lib/route-map.js; silent catch blocks prohibited; every new src/lib/**/*.ts must start with module-level JSDoc; test isolation via useTestConfigDir(); prefer property-based and model-based tests over unit tests; DEFAULT_NUM_RUNS = 50; architecture tree documented; error exit code ranges: 1x=auth: Project conventions (AGENTS.md): use `pnpm run`/`pnpm install`/`pnpm add -D` (NOT bun for package management); use buildCommand from lib/command.js (never @stricli/core directly); use buildRouteMap from lib/route-map.js; silent catch blocks prohibited; every new src/lib/**/*.ts must start with module-level JSDoc; test isolation via useTestConfigDir(); prefer property-based and model-based tests over unit tests; DEFAULT_NUM_RUNS=50; error exit code ranges: 1x=auth, 2x=input/config, 3x=API/network, 4x=feature/billing, 5x=operations, 6x=command-specific. Testing: vitest + fast-check. All packages in devDependencies (CI enforces via `pnpm run check:deps`). NEVER merge a PR if CI is failing unless explicitly told to ignore. Always use `pnpm add -D <package>` — never add to `dependencies`.
  • Smoke tests must cover critical lazy-loaded paths, not just --help/--version: Smoke tests that only run `--help` are insufficient — they never trigger lazy-loaded code paths. Critical paths: `auth status` (exits code 10, `auth: false`, exercises SQLite init/schema migrations/telemetry lazy import/CJS require chain, no network calls) and `cli defaults` (exits 0, `auth: false`, exercises `getAllDefaults()`/metadata KV). Both binary and npm bundle smoke tests must cover these paths. `init --dry-run` is NOT suitable as a smoke test — it lacks `auth: false`, so the auth guard runs first. CI currently only runs `--help` for all smoke tests (ci.yml lines 277-285, 683).

Managed by lore (https://github.com/BYK/loreai) — manual edits are imported on next session.

lore:019e8985-3dac-7712-aac5-b9adc1032d24

lore:019cbeba-e4d3-748c-ad50-fe3c3d5c0a0d

lore:019e4f9d-b0e0-72b0-ab0f-1740206c9d80

lore:019cbaa2-e4a2-76c0-8f64-917a97ae20c5

lore:019e5375-54f6-7411-a5a8-31bd2625a567

lore:019dc168-adb2-7bed-900e-cab5d3716099

lore:019dd0b5-eace-724b-88d9-66f7dd639e9f

lore:019e4fae-a4fc-7425-a545-e10c37ec810e

lore:019cb6ab-ab98-7a9c-a25f-e154a5adbbe1

lore:019c8b60-d21a-7d44-8a88-729f74ec7e02

lore:019e51e9-4acc-7f5e-aceb-c647d96a1bcc

lore:019c99d5-69f2-74eb-8c86-411f8512801d

lore:019e84e0-a08b-7eab-8da9-2da09019883c

lore:019db776-111b-73db-b4ad-b762dfd4808f

lore:019d0b04-ccec-7bd2-a5ca-732e7064cc1a

lore:019e8946-9b9b-7465-8030-9eb870e09ab2

lore:019ed0ed-baa9-789c-8fb4-2cad2cbc5690

lore:019d9b86-868d-7ff4-b2a6-74fdd1c9d56e

lore:019e471c-df3f-7e78-9df7-6b36d2d258db

lore:019e51e9-4a9c-7dde-814e-7c8904ee2833

lore:019db0c9-9cc7-7352-b1f2-61b34b87b252

lore:019da71e-6727-7ba1-9503-66d0ad80ade8

lore:019db03d-a728-72c0-ad83-0f9efbccabc2

lore:019da137-2058-7cab-8b2d-f1b78f110db3

lore:019ed0ed-bacc-7f8e-8f98-2b038c60fa8e

lore:019d49bf-65f3-7d79-bede-9f76e3e1ce1f

lore:019cb162-d3ad-7b05-ab4f-f87892d517a6

lore:019cbd5f-ec35-7e2d-8386-6d3a67adf0cf

lore:019cc43d-e651-7154-a88e-1309c4a2a2b6

lore:019e51da-a4fd-75f7-85e9-6627f83e74f4

lore:019e4cbd-d784-7468-a410-e34b8629df72

lore:019ed0dc-cb90-7d41-8eb4-7bf7cadfd2e5

lore:019e517a-f897-7dbd-8c96-a96ee5b9abaa

lore:019ed13d-8fed-7f77-a9e5-74ab3b45cc12

lore:019ed126-d1ce-7104-aec8-0ea9fcd9c2bb

lore:019e522e-e432-7aed-b2df-11241bc2bff4

lore:019e5258-8b4f-7c31-a445-8f11513f58c7

lore:019dc538-5549-73f8-b180-15068ba1cab3

lore:019e5026-1e77-7046-a0cc-9251e57550de

lore:019e51ac-82f5-7281-9e98-1d5964d8e931