diff --git a/.lore.md b/.lore.md index 38c524ec6..4810871bd 100644 --- a/.lore.md +++ b/.lore.md @@ -4,268 +4,65 @@ ### Architecture - -* **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. + +* **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. - -* **Binary build pipeline: esbuild → fossilize → Node SEA (replacing Bun.build compile)**: Binary build pipeline: \`src/bin.ts → \[esbuild CJS, node24 target] → dist-build/bin.js → \[fossilize --no-bundle] → Node SEA binary → \[binpunch ICU hole-punch] → gzip\`. Strip debug symbols handled INSIDE fossilize (as of fossilize #16) — fossilize strips the copied binary BEFORE postject injection. Strip MUST happen before injection — after SEA injection, \`strip\` fails ('section .text can't be allocated in segment 2'). macOS: \`strip -x\` on unsigned copy (fossilize unsigns before copy); cross-strip from Linux silently fails (caught). Windows: skipped (no debug symbols). NODE\_VERSION='lts'. ALL\_TARGETS: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64. Post-process: rename \`sentry-win-x64.exe\`→\`sentry-windows-x64.exe\`. UPX RULED OUT — destroys ELF notes. \`FOSSILIZE\_SIGN=y\` on push to main/release. Gzip only when \`RELEASE\_BUILD=1\`. Sourcemap uploaded to Sentry, never shipped. + +* **InkUI vs OpenTUI decision — pure JS wins over native binary cost**: Chose Ink over OpenTUI because OpenTUI added ~10.7 MB to the binary (libopentui.so + ~12k-line FFI bindings) and required alternate-screen buffer + post-dispose stderr replay. Ink is pure JS, writes incrementally to stdout so log lines land in scrollback. UI evolution: ClackUI → OpenTuiUI (PR 4) → InkUI (current). \`exitOnCtrlC: false\` routes Ctrl+C through prompt cancellation; \`patchConsole: false\` keeps \`console.\*\` flowing to real stdout (Sentry SDK breadcrumbs not swallowed). - -* **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\` → 101 MiB (-17 MiB raw, -4 MiB compressed). Strip built into fossilize #16 — 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. Slim Node build flags: \`--with-intl=small-icu\`, \`--without-inspector\`, \`--without-sqlite\`, \`--without-npm\`, \`--enable-lto\` (high CI cost, deferred). Cross-compilation from Linux to darwin not officially supported. Final vs Bun: download 30 MiB (Bun: 32 MiB), \`--version\` ~1.0s (Bun: ~1.9s), completions ~0.16s (Bun: ~0.42s). - - -* **CI build-binary matrix: PR=2 targets, main/release=7 targets**: \`ci.yml\` \`build-binary\` job: PRs build only \`linux-x64\` (can-test:true) + \`linux-x64-musl\` (can-test:false). Main/release/workflow\_call builds all 7: darwin-arm64, linux-x64, linux-x64-musl, windows-x64, darwin-x64, linux-arm64, linux-arm64-musl. Build command: \`bun run build --target ${{ matrix.target }}\`. \`test-e2e\` downloads \`sentry-linux-x64\` artifact and sets \`SENTRY\_CLI\_BINARY\`. \`build-npm\` matrix: Node 22+24; smoke test is only \`node dist/bin.cjs --help\`. \`SENTRY\_CLIENT\_ID\` defaults to \`ci-fork-pr-dummy\` for fork PRs (can't read repo vars). Gzip artifacts only on non-PR runs. \`FOSSILIZE\_SIGN=y\` on push to main/release. - - -* **CLI mode never calls setEnv() — getEnv() returns process.env directly**: CLI mode NEVER calls \`setEnv()\` — \`getEnv()\` returns \`process.env\` directly. Only library mode calls \`setEnv()\` with a merged env copy to avoid mutating the consumer's \`process.env\`. This prevents unexpected side effects when the CLI is embedded as a library. - - -* **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. - - -* **DSN cache invalidation uses two-level mtime tracking (sourceMtimes + dirMtimes)**: DSN cache invalidation — two-level mtime tracking: \`sourceMtimes\` (DSN-bearing files, catches in-place edits) + \`dirMtimes\` (every walked dir, catches new files) + root mtime fast-path + 24h TTL. Dropping either map is a correctness regression. Walker emits mtimes via \`onDirectoryVisit\` hook + \`recordMtimes\` option; DSN scanner uses \`grepFiles({pattern: DSN\_PATTERN, recordMtimes: true, onDirectoryVisit})\`. \`scanCodeForFirstDsn\` stays on direct walker loop (worker init ~20ms dominates). Invariants: \`processMatch\` must record mtime for EVERY file with host-validated DSN via \`fileHadValidDsn\` flag independent of \`seen.has(raw)\`. \`scanDirectory\` catch MUST return empty \`dirMtimes: {}\`, NOT partial map; \`ConfigError\` re-throws. - - -* **E2E test infrastructure: fixture.ts, helpers.ts, mocks/, and test:e2e script**: \`test/fixture.ts\`: \`getCliCommand()\` returns \`\[SENTRY\_CLI\_BINARY]\` or \`\[process.execPath, 'run', 'src/bin.ts']\`. \`createE2EContext(configDir, serverUrl)\` sets env \`SENTRY\_AUTH\_TOKEN: ''\`, \`SENTRY\_TOKEN: ''\`, \`SENTRY\_CLI\_NO\_TELEMETRY: '1'\`, \`SENTRY\_URL: serverUrl\`. \`setAuthToken(token)\` calls \`dbSetAuthToken(token, undefined, undefined, { host: serverUrl })\` then \`closeDatabase()\`, scoped to mock server URL for host-scoping fetch-layer guard. \`test/mocks/server.ts\`: \`createMockServer(routes, options?)\` uses Node \`http.createServer\` (migrated from \`Bun.serve\`). \`test/mocks/multiregion.ts\`: \`createMultiRegionMockServer()\` — US+EU regions + control silo; \`selfHostedMode\`, \`singleRegionMode\`. \`test:e2e\` runs WITHOUT \`--isolate --parallel\`. \`test:unit\` runs WITH \`--isolate --parallel\`. \`telemetry-exit.test.ts\` verifies \`@sentry/core\` patch adds \`.unref()\` to flush timers. Project uses vitest (migrated from bun:test); \`vitest.config.ts\` at repo root. - - -* **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. - - -* **Issue resolve --in grammar: release + @next + @commit sentinels**: \*\*Issue resolve --in grammar + repo\_cache SQLite table\*\*: \`sentry issue resolve --in\` grammar: (a) omitted→immediate, (b) \`\\`→\`inRelease\`, (c) \`@next\`→\`inNextRelease\`, (d) \`@commit\`→auto-detect git HEAD via \`src/lib/git.ts\`, (e) \`@commit:\@\\`→explicit. \`parseResolveSpec\` splits on LAST \`@\` for scoped names. API requires \`statusDetails.inCommit: {commit, repository}\` — not bare SHA. Repo matching uses \`listRepositoriesCached(org)\` (7-day SQLite cache in \`repo\_cache\` table, schema v14). Always use \`listAllRepositories\` (paginated via \`API\_MAX\_PER\_PAGE\`) — never \`listRepositories\` (silently caps ~25). \`setCachedRepos\` wrapped in try/catch so read-only DBs (macOS \`sudo brew install\`) don't crash commands. - - -* **Node SEA ink sidecar: node:sea.getAsset() replaces Bun /$bunfs/ virtual FS**: (architecture) Node SEA ink sidecar: \`node:sea.getAsset()\` replaces Bun \`/$bunfs/\` virtual FS. Ink UI sidecar embedded via \`fossilize --assets dist-build/ink-app.js\`; asset key = raw CLI arg. At runtime: \`sea.getRawAsset('dist-build/ink-app.js')\`. Main bundle never calls \`import('ink')\` — sidecar pre-bundled by text-import-plugin. Dual-mode: detect SEA via \`createRequire(import.meta.url)('node:sea')\` with try/catch fallback. \`useSnapshot: true\` BROKEN. \`useCodeCache: true\` ~15% startup improvement but platform-specific V8 blob. Suppress \`ExperimentalWarning: SQLite\`: \`process.on('warning', ...)\` at very top of \`src/bin.ts\` BEFORE any imports. fossilize asset manifest key = \`basename(manifestPath)\`; entry keys = \`entry.file\`. \`new Worker(new URL(...))\` HANGS in SEA — use Blob+URL.createObjectURL. - - -* **npm bundle (script/bundle.ts): esbuild CJS with bun:sqlite polyfill, external Ink, debug ID injection**: Entry: \`src/index.ts\` → \`dist/index.cjs\` (CJS, minified, Node 22). External: \`node:\*\`, \`ink\`, \`react\`, \`react-reconciler\`, \`yoga-layout\` (use top-level await, can't emit CJS). \`bunSqlitePlugin\` redirects \`bun:sqlite\` → \`globalThis.\_\_bun\_sqlite\_polyfill\` (injected by \`node-polyfills.ts\`). \`sentrySourcemapPlugin\` (onEnd): injects debug IDs, replaces \`PLACEHOLDER\_DEBUG\_ID\` with real UUID (same length → sourcemap positions valid), uploads only if \`SENTRY\_AUTH\_TOKEN\` set. \`textImportPlugin\` handles \`with { type: 'file' }\`. \`dist/bin.cjs\`: shebang, Node 22.12 floor check, warning suppression, calls \`require('./index.cjs').\_cli()\`. Build script: \`pnpm run bundle\`. Exits with code 1 if \`SENTRY\_CLIENT\_ID\` missing. - - -* **Response cache hit invisibility — synthetic Response carries no marker**: Response cache hit invisibility — synthetic Response from \`getCachedResponse()\` in \`src/lib/response-cache.ts\` is indistinguishable from network. Solved via module-level \`lastCacheHitAgeMs\`: set on hit, cleared at top of \`authenticatedFetch()\` per-call (single-process CLI = race-free). \`src/lib/cache-hint.ts\` provides \`formatCacheHint()\` (\`"cached · 3m ago · use -f to refresh"\`) and \`appendCacheHint(existingHint)\` (joins with \` | \`). Wired in \`buildCommand\` (\`src/lib/command.ts\`): \`appendCacheHint(returned?.hint)\` runs only when generator returns a \`CommandReturn\` — bare \`return;\` paths (e.g. \`--web\`) skip the hint. Same chokepoint can host future cross-cutting hint decorators. Test-only \`\_setLastCacheHitAgeForTesting(ms)\` exposes state. - - -* **safe-read.ts wraps isRegularFile + Bun.file().text() for FIFO-safe user-path reads**: \`src/lib/safe-read.ts\` \`safeReadFile(path, operation)\` combines \`isRegularFile()\` + file read + broad error swallow (FIFO/ENOENT/EACCES/EPERM/EISDIR/ENOTDIR). Do NOT use for committed config loads — swallows EPERM/EISDIR, making \`chmod 000 .sentryclirc\` manifest as confusing 'no auth token'. For loud permission surfacing, call \`fs.promises.stat\` directly, gate on \`isFile()\`, catch only ENOENT/EACCES. General rule: bare \`catch {}\` swallows \`EACCES\`/\`EPERM\`/\`EIO\` — always check \`(err as NodeJS.ErrnoException).code === 'ENOENT'\` and re-throw anything else. \`read-files.ts\`/\`workflow-inputs.ts\` use direct stat to reuse one stat for size-gating. Test with real \`mkfifo\` + short timeout as hang detector. - - -* **Seer trial prompt uses middleware layering in bin.ts error handling chain**: Seer trial prompt via error middleware layering: \`bin.ts\` chain is \`main() → executeWithAutoAuth() → executeWithSeerTrialPrompt() → runCommand()\`. Seer trial prompts (\`no\_budget\`/\`not\_enabled\`) caught by inner wrapper; auth errors bubble to outer. Trial API: \`GET /api/0/customers/{org}/\` → \`productTrials\[]\` (prefer \`seerUsers\`, fallback \`seerAutofix\`). Start: \`PUT /api/0/customers/{org}/product-trial/\`. SaaS-only; self-hosted 404s gracefully. \`ai\_disabled\` excluded. \`startSeerTrial\` accepts \`category\` from trial object — don't hardcode. - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: (architecture) Sentry API quirks: (1) Events need org+project (\`/projects/{org}/{project}/events/{id}/\`); issues use legacy global \`/api/0/issues/{id}/\`; traces need org only. (2) \`/users/me/\` returns 403 for OAuth — use \`/auth/\` via \`getControlSiloUrl()\`. (3) Chunk upload endpoint returns camelCase (\`chunkSize\`) — exception to snake\_case. (4) 204/205 responses throw \`ApiError\` not \`TypeError\` from \`rawApiRequest\`. (5) Magic \`@\` selectors: \`@latest\`, \`@most\_frequent\` in \`parseIssueArg\` detected before \`validateResourceId\`; \`SELECTOR\_MAP\` case-insensitive; \`resolveSelector\` calls \`listIssuesPaginated\` with \`perPage: 1\`; supports org-prefixed \`sentry/@latest\`. (6) \`issue resolve --in\` grammar: omitted→immediate, \`\\`→\`inRelease\`, \`@next\`→\`inNextRelease\`, \`@commit\`→auto-detect git HEAD. \`parseResolveSpec\` splits on LAST \`@\`. API requires \`statusDetails.inCommit: {commit, repository}\` — not bare SHA. - - -* **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 local command: Hono+Spotlight SDK server with SSE tail output**: \`sentry local\` (default: \`serve\`) and \`sentry local run\` — both \`auth: false\`. Default port 8969. Uses \`@spotlightjs/spotlight/sdk\` (\`createSpotlightBuffer\`/\`pushToSpotlightBuffer\`) for envelope buffering; custom Hono HTTP server for ingest. Endpoints: \`POST /stream\`, \`POST /api/:projectId/envelope\[/]\`, \`GET /stream\` (SSE), \`GET /health\`. CORS restricted to localhost origins only. Browser SDK \`sendBeacon\` workaround: overrides \`text/plain\` → \`application/x-sentry-envelope\` when \`sentry\_client\` query param starts with \`sentry.javascript.browser\`. \`sentry local run\` injects \`SENTRY\_SPOTLIGHT\`, \`NEXT\_PUBLIC\_SENTRY\_SPOTLIGHT\`, \`SENTRY\_TRACES\_SAMPLE\_RATE=1\` into child env. Attach mode: if server already running, connects as SSE consumer (manual SSE parser — no \`EventSource\`). Formatters in \`src/lib/formatters/local.ts\`: \`sanitize()\` strips ANSI/control/bidi chars; source inferred from \`sdk.name\` → \`\[SERVER]\`/\`\[BROWSER]\`/\`\[MOBILE]\`. - - -* **Sentry log IDs are UUIDv7 — enables deterministic retention checks**: Sentry log IDs are UUIDv7 — enables deterministic retention checks. \`decodeUuidV7Timestamp()\` and \`ageInDaysFromUuidV7()\` in \`src/lib/hex-id.ts\` return null for non-v7, safe to call unconditionally. \`RETENTION\_DAYS.log = 90\` in \`src/lib/retention.ts\`; traces/events are \`null\` (plan-dependent). \`LOG\_RETENTION\_PERIOD\` is DERIVED as \`\` \`${RETENTION\_DAYS.log}d\` \`\` — never hardcode \`'90d'\`. Shared hex primitives (\`HEX\_ID\_RE\`, \`SPAN\_ID\_RE\`, \`UUID\_DASH\_RE\`) live in \`hex-id.ts\`. Three Sentry span APIs: (1) \`/trace/{traceId}/\` — hierarchical tree with \`additional\_attributes\`. (2) \`/projects/{org}/{project}/trace-items/{itemId}/\` — single span with ALL attributes. (3) \`/events/?dataset=spans\` — list/search. \`meta.fields\` order is non-deterministic — derive column order from user's \`--field\` list via \`orderFieldNames()\` in \`explore.ts\`. - - -* **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 \\` 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. - - -* **Zod schema on OutputConfig enables self-documenting JSON fields in help and SKILL.md**: Zod schema on OutputConfig enables self-documenting JSON fields: List commands register \`schema?: ZodType\` on \`OutputConfig\\`. \`extractSchemaFields()\` produces \`SchemaFieldInfo\[]\` from Zod shapes. \`buildFieldsFlag()\` enriches \`--fields\` brief; \`enrichDocsWithSchema()\` appends fields to \`fullDescription\`. Schema exposed as \`\_\_jsonSchema\` on built commands — \`introspect.ts\` reads it into \`CommandInfo.jsonFields\`, \`help.ts\` and \`generate-skill.ts\` render it. For \`buildOrgListCommand\`/\`dispatchOrgScopedList\`, pass \`schema\` via \`OrgListConfig\`. - -### Decision - - -* **All view subcommands should use \ \ positional pattern**: All \`\* view\` subcommands use \`\ \\` positional pattern (Intent-First Correction UX): target is optional \`org/project\`. Use opportunistic arg swapping with \`log.warn()\` when args are wrong order — when intent is unambiguous, do what they meant. Normalize at command level, keep parsers pure. Model after \`gh\` CLI. Exception: \`auth\` uses \`defaultCommand: "status"\` (no viewable entity). Routes without defaults: \`cli\`, \`sourcemap\`, \`repo\`, \`team\`, \`trial\`, \`release\`, \`dashboard/widget\`. - - -* **Node.js slim build flags for SEA binary size reduction**: Node.js configure.py size-reduction flags relevant to SEA builds: \`--with-intl=small-icu\` (English-only ICU, no download needed; saves ~20 MB .rodata); \`--with-intl=none\` (disables Intl/String.normalize entirely); \`--without-inspector\` (removes V8 inspector protocol); \`--without-sqlite\` (removes SQLite + Web Storage API); \`--without-npm\`/\`--without-corepack\` (already excluded by default); \`--enable-lto\` (LTO, GCC 5.4.1+ or Clang 3.9.1+); \`--without-node-snapshot\`/\`--without-node-code-cache\` (removes V8 snapshot/code cache). AVOID: \`--disable-single-executable-application\` (removes SEA support), \`--v8-lite-mode\` (no JIT, much slower), \`--without-ssl\` (breaks crypto/https). Cross-compilation from Linux to darwin NOT officially supported — darwin builds require macOS runners with Xcode >= 16.4. - - -* **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. + +* **SDK invoke path bypasses Stricli parsing — no defaults, no parsePeriod**: (architecture) SDK invoke path bypasses Stricli parsing — no defaults, no parsePeriod: \`src/lib/sdk-invoke.ts\` \`buildInvoker()\` calls command \`func()\` directly with pre-built flags, skipping Stricli's \`parseInputsForFlag\`. Stricli's default application (including \`parsePeriod('90d')\` for \`kind:'parsed'\` flags) never runs. Systemic fix: refactor \`resolveCommand()\` to cache BOTH the loader AND \`target.parameters.flags\` (a \`Record\\`). In \`buildInvoker\`, before each \`func.call\` (streaming lines 413-425, non-streaming lines 428-435), iterate flags: for each with \`kind:'parsed'\` and a \`default\`, if SDK caller passed \`undefined\`, call \`flag.parse(flag.default)\`. This fixes all 14+ affected commands. \`cleanRawFlags()\` strips only injected globals (\`log-level\`, \`verbose\`, \`org\`, \`project\`); command-defined flags like \`period\`, \`limit\`, \`query\`, \`json\`, \`fields\` pass through intact. \`issue list\` defines its own \`period\` flag inline (\`default: '90d'\`), not via shared \`LIST\_PERIOD\_FLAG\` (\`default: '7d'\`). ### Gotcha - -* **--json schema stability: collapse=organization drops nested org fields**: --json schema + response cache gotchas: (1) \`?collapse=organization\` shrinks \`organization\` to \`{id, slug}\` — silent --json regression. \`jsonTransform\` re-hydrates \`organization.name\` via \`resolveOrgDisplayName\` against \`org\_regions\` cache. (2) \`buildCacheKey()\` normalizes URL with sorted query params, so \`invalidateCachedResponse(baseUrl)\` misses entries with query suffixes. Use \`invalidateCachedResponsesMatching(prefix)\` (raw \`startsWith()\`); \`buildApiUrl()\` always emits trailing slash → safe prefix. (3) When \`jsonTransform\` is set, \`jsonExclude\` and \`filterFields\` are NOT applied — transform must call \`filterFields(result, fields)\` and omit excluded keys itself. - - -* **API tests must use useTestConfigDir to isolate disk response cache**: \*\*API tests must use useTestConfigDir to isolate disk response cache\*\*: Tests mocking \`globalThis.fetch\` MUST call \`useTestConfigDir()\` + \`setAuthToken()\`. \`authenticatedFetch\` checks a filesystem response cache (\`~/.sentry/cache/responses/\`) BEFORE calling fetch — without per-test dirs, test N's response is served to test N+1. TTL tiers in \`classifyUrl()\`: stable=5min, volatile=60s (issues/logs), immutable=24h (events/traces by ID). Also: \`@sentry/api\` SDK calls \`\_fetch(request)\` with no init — fall back to \`input.headers\` when \`init\` is undefined (prevents HTTP 415). SDK returns \`data={}\` for empty/204 responses — always guard with \`Array.isArray(data)\` before \`.map()\`. Use \`unwrapPaginatedResult\` (not \`unwrapResult\`) for Link header pagination. - - -* **Bun --isolate coverage inflates LF count for files with verbose comments/JSDoc**: Bun --isolate coverage inflates LF count: under \`bun test --isolate --parallel\` (CI's \`test:unit\`), Bun's coverage instrumentation counts comments, blank lines, type annotations, and closing braces as 'executable'. E.g. \`zstd-transport.ts\` LF=165 locally → 210 under --isolate, dropping coverage 99%→78%. Workaround: trim verbose inline comments inside function bodies; move rationale to JSDoc above the function. Statement coverage stays 100% — 'missing' lines are non-executable. - - -* **Bun /$bunfs/ virtual FS uses JS parser — embedded .tsx files fail on TS syntax**: \*\*Bun \`/$bunfs/\` virtual FS + Ink TUI sidecar embedding\*\*: Files embedded via \`with { type: "file" }\` run from \`/$bunfs/root/\` using a JS parser (not TypeScript) — raw \`.tsx\` crashes on \`import { type Foo }\`. Fix: pre-bundle \`.tsx\` → \`.js\` via esbuild before embedding (\`script/text-import-plugin.ts\`). \`/$bunfs/\` has no \`node\_modules\` — inline all deps; use \`createRequire\` banner for CJS deps. Only \`node:\*\` builtins external. Query strings in \`/$bunfs/\` paths cause ENOENT. Related: Ink TUI sidecar (\`ink-app.tsx\`) must be fully self-contained — main bundle must NOT import \`ink\`/\`react\` separately; call \`app.mountApp()\` from the sidecar only to avoid dual-React "Invalid hook call" errors. - - -* **Bun 1.3.11 tty.ReadStream leaks libuv handle — process.stdin.unref is undefined**: Bun 1.3.11 macOS TTY bug: \`process.stdin\` via kqueue \`EVFILT\_READ\` fails to deliver keystrokes when fd 0 is inherited via \`exec bin \ -* **MastraClient has no dispose API — use AbortController for cleanup**: MastraClient has no \`close()\`/\`dispose()\` API — cleanup via \`ClientOptions.abortSignal\` (constructor) or per-prompt \`signal\`. Without explicit abort, Bun's fetch dispatcher keep-alive sockets hold the event loop alive past natural exit. Pattern in \`src/lib/init/wizard-runner.ts\`: create \`AbortController\` per \`runWizard\`, pass \`abortSignal: controller.signal\` to \`new MastraClient(...)\`, abort via \`using \_ = { \[Symbol.dispose]: () => controller.abort() }\`. Custom \`fetch\` wrapper must preserve \`init.signal\` via spread. Tests capture \`ClientOptions\` via \`spyOn(MastraClient.prototype, 'getWorkflow').mockImplementation(function() { capturedOpts.push(this.options); ... })\`. - - -* **Multi-region fan-out: distinguish all-403 from empty orgs with hasSuccessfulRegion flag**: In \`listOrganizationsUncached\` (\`src/lib/api/organizations.ts\`), \`Promise.allSettled\` collects multi-region results. Don't use \`flatResults.length === 0\` to detect all-regions-failed — a region returning 200 OK with zero orgs pushes nothing into \`flatResults\`. Track a \`hasSuccessfulRegion\` boolean on any \`"fulfilled"\` settlement. Only re-throw 403 \`ApiError\` when \`!hasSuccessfulRegion && lastScopeError\`. - - -* **process.stdin.isTTY unreliable in Bun — use isatty(0) and backfill for clack**: \`process.stdin.isTTY\` unreliable — use \`isatty(0)\` from \`node:tty\`. Bun's single-file binary can leave \`process.stdin.isTTY === undefined\` on TTY fds. \`@clack/core\` gates \`setRawMode(true)\` on \`input.isTTY\`, silently disabling raw mode. Fix: backfill \`process.stdin.isTTY = true\` when \`isatty(0)\` confirms. Debugging: \`src/lib/init/tty-diagnostics.ts\` \`dumpTtyDiagnostics(label)\` — no-op unless \`SENTRY\_INIT\_DIAGNOSTICS=1\`. - - -* **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;\` - -* **strip fails on Node SEA binaries — must strip BEFORE fossilize injection**: 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 #16, 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). No need for \`stripCachedNodeBinaries\` in the CLI build script — fossilize handles it. - - -* **UPX destroys ELF notes — incompatible with Node SEA binaries**: Trap: UPX compresses Node binaries from 99 MiB to 25 MiB and the compressed binary still runs — looks like a huge win. But UPX rewrites the entire ELF structure: original binary has 2 ELF notes (NT\_GNU\_BUILD\_ID + NT\_GNU\_ABI\_TAG), UPX'd binary has 0 notes and 0 sections. NODE\_SEA\_BLOB is stored as an ELF note — UPX destroys it. Fix: use \`strip --strip-unneeded\` instead, BUT only on the plain Node binary BEFORE fossilize SEA injection. After injection, \`strip\` fails with 'section .text can't be allocated in segment 2' — the SEA blob corrupts the ELF section-to-segment mapping. Strip the \`.node-cache/\` binaries before calling fossilize. Saves ~17 MB raw / ~4 MB compressed. Strip is idempotent — already-stripped binaries are unchanged. Recommended order: strip cached Node → fossilize (inject) → binpunch → gzip. - - -* **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 worker pool requires pool:forks + UV\_USE\_IO\_URING=0 on GitHub Actions**: Vitest/CI gotchas: (1) GitHub Actions io\_uring crashes Node.js workers (exit 134/SIGABRT) — fix: \`pool: 'forks'\` in \`vitest.config.ts\` AND \`UV\_USE\_IO\_URING=0\` in CI. (2) Vitest 4: options must be second arg: \`test(name, { timeout }, fn)\`. (3) \`http.createServer(async ...)\` — unhandled rejections crash test server; wrap body in try/catch. (4) \`dorny/paths-filter\` diffs against base — empty commits produce all-false outputs. (5) \`node:sqlite\` requires \`--experimental-sqlite\` on Node 22. (6) Lazy \`require()\` in test fixtures bypasses Vite's \`.js→.ts\` resolver — use top-level \`import\`. (7) \`spawn(process.execPath, \[workerScript.ts])\` fails under vitest/Node — use \`spawn('tsx', \[workerScript.ts])\`. Project uses vitest (migrated from bun:test); imports from 'vitest', mocks via \`vi.mock()\`. \`vitest.config.ts\` at repo root. - - -* **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\`. +* **strip fails on Node SEA binaries — must strip BEFORE fossilize injection**: Node SEA binary build gotchas: (1) Strip BEFORE fossilize injection — after postject injects SEA blob, \`strip\` fails ('section .text can't be allocated in segment 2'). fossilize handles stripping internally since v0.7.0. (2) UPX destroys ELF notes — NODE\_SEA\_BLOB stored as ELF note; UPX'd binary has 0 notes. Use \`strip --strip-unneeded\` instead. (3) \`useSnapshot: true\` BROKEN. \`useCodeCache: true\` gives ~15% startup improvement but is platform-specific — requires native runners per target. (4) Suppress \`ExperimentalWarning: SQLite\` at very top of \`src/bin.ts\` BEFORE any imports. (5) Cross-strip from Linux to macOS silently fails; macOS strip requires re-codesigning. (6) Size: linux-x64 Node 24: raw ~108 MiB, gzip ~30 MiB after strip+SEA+binpunch. Slim build flags: \`--with-intl=small-icu\`, \`--without-inspector\`, \`--without-sqlite\`, \`--enable-lto\`. AVOID: \`--disable-single-executable-application\`, \`--v8-lite-mode\`, \`--without-ssl\`. ### Pattern - -* **CLI-1D3 Windows download visibility race: poll statSync with exponential backoff**: Windows upgrade download visibility race: \`waitForBinaryVisible\` in \`src/lib/upgrade.ts\` polls \`statSync\` with exponential backoff (6 attempts, 5 sleeps: 100+200+400+800+1600ms). Loop breaks BEFORE final sleep — \`VERIFY\_MAX\_ATTEMPTS=N\` yields N-1 sleeps (off-by-one trap). Covers Bun 1.3.9 race where \`Bun.file().writer().end()\` returns before OS surfaces file by path. \`isEnoentSpawnError()\` in \`src/commands/cli/upgrade.ts\` catches both \`code==='ENOENT'\` and Bun's path-string error → \`UpgradeError('execution\_failed')\`. Race-free tests: writer must poll until bad state exists, then overwrite. - - -* **Cross-compile sentry-cli with patched Bun: drop compile.target to use selfExePath**: Cross-compile sentry-cli with patched Bun: \`Bun.build({compile})\` downloads stock Bun from npm when \`compile.target\` is set. Workaround in \`script/build.ts\`: omit \`target\` entirely so Bun uses \`selfExePath()\` as embed runtime. Only works when host OS/arch matches desired output. Escape hatch: place \`bun-\-\-v\\` in \`$CWD\`. Build requires \`SENTRY\_CLIENT\_ID\` env var. - - -* **Dedupe resolved entity IDs in batch operations before API call**: Batch issue merge (\`src/commands/issue/merge.ts\`): (1) Dedupe by resolved numeric ID after \`Promise.all(args.map(resolveIssue))\` — users may pass same entity as \`CLI-K9\`, \`my-org/CLI-K9\`, or \`123\`. Throw \`ValidationError\` if \`new Set(ids).size < 2\`. (2) Reject \`undefined\` orgs in cross-org check — bare numeric IDs without DSN/config resolve with \`org: undefined\`. (3) Pass \`--into\` through \`resolveIssue()\`; compare by numeric \`id\`, not \`shortId\`. (4) Sentry bulk merge API picks canonical parent by event count — \`--into\` is preference only; warn when API's \`parent\` differs. - - -* **findProjectsByPattern as fuzzy fallback for exact slug misses**: When \`findProjectsBySlug\` returns empty (no exact match), use \`findProjectsByPattern\` as a fallback to suggest similar projects. \`findProjectsByPattern\` does bidirectional word-boundary matching (\`matchesWordBoundary\`) against all projects in all orgs — the same logic used for directory name inference. In the \`project-search\` handler, call it after the exact miss, format matches as \`\/\\` suggestions in the \`ResolutionError\`. This avoids a dead-end error for typos like 'patagonai' when 'patagon-ai' exists. Note: \`findProjectsByPattern\` makes additional API calls (lists all projects per org), so only call it on the failure path. - - -* **Grouped widget --limit auto-default via applyGroupLimitAutoDefault helper**: Dashboard widget flag normalization: (1) Dataset aliases (errors→error-events) normalize ONCE at top of \`func()\` via \`normalizeDataset()\` in \`src/commands/dashboard/resolve.ts\`. In \`edit.ts\`, pass \`normalizedFlags\` to \`buildReplacement\` — \`validateAggregateNames\` reads \`flags.dataset\` and rejects valid aggregates like \`failure\_rate\` if it sees raw alias. (2) Grouped widgets need \`limit\` (API rejects). \`applyGroupLimitAutoDefault\` defaults to \`DEFAULT\_GROUP\_BY\_LIMIT=5\` only when user passed \`--group-by\` without \`--limit\`; skip for auto-defaulted columns like \`\["issue"]\`. (3) Tests asserting \`--limit\` >10 survives into PUT body must use \`display: "line"\` — \`prepareWidgetQueries\` clamps bar/table to max=10. - - -* **Hidden --org/--project compat flags via mergeGlobalFlags**: Hidden global \`--org\`/\`--project\` flags accept old \`sentry-cli\` syntax. Defined in \`GLOBAL\_FLAGS\` (global-flags.ts) so argv-hoist relocates them. \`mergeGlobalFlags()\` in command.ts injects hidden flag shapes (skip if command owns the flag — e.g. \`release create --project -p\`) and returns \`stripKeys\` set used by \`cleanRawFlags\`. \`applyOrgProjectFlags()\` writes values to \`SENTRY\_ORG\`/\`SENTRY\_PROJECT\` via \`getEnv()\` before auth guard, overwriting existing env vars (explicit CLI > env var). Resolution chain in resolve-target.ts picks them up at priority #2. No short aliases (\`-p\` conflicts). The helper extraction was needed to keep \`buildCommand\` under Biome's cognitive complexity limit of 15. - - -* **Preserve ApiError type so classifySilenced can silence 4xx errors**: Preserve ApiError type for classifySilenced: \`classifySilenced\` (src/lib/error-reporting.ts) only silences \`ApiError\` with status 401-499 — wrapping in generic \`CliError\` loses \`status\` and causes 403s to be captured. Re-throw via \`new ApiError(msg, error.status, error.detail, error.endpoint)\` with terse message (\`ApiError.format()\` appends detail/endpoint). \`ValidationError\` without \`field\` collapses unfielded errors into one fingerprint; always pass \`field\`. Fingerprint rule changes don't retroactively re-fingerprint — manually merge new groups into canonical old parents. \`ApiError\` rule keys by \`api\_status + command\`. + +* **clack-utils.ts filename preserved intentionally — rename deferred to next cleanup PR**: \`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\()\` return type uses \`Exclude\\` to narrow union types. \`FEATURE\_DISPLAY\_ORDER\` and \`CANONICAL\_STEP\_ORDER\` (12 steps) also defined here. Rename is intentionally deferred. - -* **Sentry SDK tree-shaking patches must be regenerated via bun patch workflow**: Sentry SDK tree-shaking via bun patch: \`patchedDependencies\` in \`package.json\` strips unused exports from \`@sentry/core\` and \`@sentry/node-core\`. Non-light root of \`@sentry/node-core\` pulls uninstalled \`@opentelemetry/instrumentation\` — \*\*always import from \`@sentry/node-core/light\`\*\* (subpaths: \`.\`, \`./light\`, \`./light/otlp\`, \`./init\`, \`./loader\`, \`./import\`). No supported import for \`HttpsProxyAgent\`. Bumping SDK: remove old patches, \`rm -rf ~/.bun/install/cache/@sentry\`, \`bun install\`, \`bun patch @sentry/core\`, edit, \`bun patch --commit\`; repeat for node-core. Preserved: \`\_INTERNAL\_safeUnref\`, \`\_INTERNAL\_safeDateNow\`, \`nodeRuntimeMetricsIntegration\`. Before stripping any core export, grep \`node-core/build/{cjs,esm}/light/sdk.js\` for runtime usage (e.g. \`spanStreamingIntegration\` when \`traceLifecycle === 'stream'\`). Remove \`.bun-tag-\*\` hunks from generated patches. Manual \`git diff\` patches fail. + +* **InkUI ink-app.js sidecar loading — three runtime contexts**: \`createInkUI()\` resolves \`inkAppPath\` differently per runtime: (1) Node SEA binary — \`sea.getAsset('dist-build/ink-app.js', 'utf-8')\`, write to \`mkdtempSync\`, import via \`pathToFileURL\`, then \`rmSync\` temp dir (best-effort); (2) Node/npm bundle — \`inkAppPath\` starts with \`'./'\`, resolve via \`new URL(inkAppPath, import.meta.url).href\`; (3) Dev mode — absolute filesystem path. Imported via \`with { type: 'file' }\` from \`./ink-app.tsx\`. See also \[\[019e4fe7-dbf1-7ed6-8b39-473e2e4ea29e]] for SEA temp file cleanup pattern. - -* **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. + +* **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. - -* **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\`. + +* **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. ### Preference -* **Always check with user before taking irreversible or external actions**: When the user asks the assistant to perform actions that affect external systems (sending messages, merging PRs, deploying, etc.), they explicitly require confirmation before proceeding. The user states 'check with user before sending any messages' or similar directives. The assistant should always pause and present a plan or draft to the user for approval before executing any action that cannot be easily undone — such as sending communications, merging code, or triggering external workflows. This applies even when the user has asked the assistant to handle the task end-to-end. - - -* **Always commit and push after tests pass locally**: When a local test run completes successfully, the user consistently moves immediately to committing the changes and pushing to the remote branch. This pattern applies regardless of test suite size or duration. The assistant should proactively plan or execute a commit+push step as the natural next action after a passing test run, without waiting to be asked. Commit messages should follow conventional commit format (e.g., 'fix:', 'refactor:'). If warnings are present in the test output, they are noted but do not block the commit/push flow. - - -* **Always explore e2e test infrastructure thoroughly before debugging or modifying tests**: When approaching e2e test work, always explore the full infrastructure before making changes: \`test/e2e/\` (14 files: api, auth, bundle, completion, delta-upgrade, event, issue, library, log, multiregion, project, skill-eval, telemetry-exit, trace), \`test/fixture.ts\` (getCliCommand, runCli, createE2EContext), \`test/helpers.ts\` (useTestConfigDir, useEnvSandbox, resetHostScopingState, mintSntrysToken, extractFetchUrl), \`test/mocks/\` (server.ts, routes.js, multiregion.ts), \`src/bin.ts\`, \`src/cli.ts\`. Key: \`getCliCommand()\` returns \`\[SENTRY\_CLI\_BINARY]\` if set, else \`\[process.execPath, 'run', 'src/bin.ts']\`. \`createE2EContext.run()\` sets \`SENTRY\_AUTH\_TOKEN: ''\`, \`SENTRY\_TOKEN: ''\`, \`SENTRY\_CLI\_NO\_TELEMETRY: '1'\`. \`test:e2e\` runs without \`--isolate --parallel\`. Map full infrastructure before proposing fixes. - - -* **Always fix CI lint failures immediately before proceeding with other work**: When CI reports lint or typecheck failures, the user consistently treats them as blocking issues that must be resolved before continuing with feature work or merging. The user runs \`biome check --write\` (safe fixes first), then \`biome check --write --unsafe\` for remaining issues, and manually fixes any residual errors (e.g., hoisting regex to module level, reformatting long strings). Only after lint is clean does the user proceed with the next task. This applies across projects (getsentry/cli, loreai) and includes both formatter and linter rule violations. - - -* **Always get a PR up and monitor CI until all checks pass**: After completing implementation work, the user consistently requests that the assistant create a PR, then actively monitor CI and ensure all checks pass before considering the task done. This applies across different repos and task types. The user expects the assistant to: create the PR on the correct branch (not a wrong base), watch CI results, fix any failing checks, and only declare completion once all checks are green. The user sometimes also instructs the assistant to merge the PR once CI passes. In plan mode, the assistant should note it cannot execute these steps and call plan\_exit to signal readiness to proceed in build mode. +* **Always check with user before taking irreversible or external actions**: Always confirm with the user before taking irreversible or external actions: sending messages, merging PRs, deploying, triggering external workflows. Present a plan or draft for approval first. Also: when sending messages on the user's behalf (e.g., via chat tools), read prior conversation history to understand relationships, match the user's short/casual/stream-of-consciousness style — never formal or long. Never reintroduce people who already know each other. - -* **Always honor Retry-After header when present in LLM adapter**: (architecture) LLM adapter backoff in \`packages/gateway/src/llm-adapter.ts\`: Always honor Retry-After — \`backoffMs()\` returns \`Math.min(retryAfterMs, cap)\` where cap is \`RETRY\_AFTER\_CAP\_URGENT\_MS=8\_000\` or \`RETRY\_AFTER\_CAP\_BACKGROUND\_MS=120\_000\`. TRANSIENT\_CODES={429,500,502,503,529}; MAX\_RETRIES: rate-limit=3, server=3, urgent=2. Backoff (no Retry-After): 429 background=60s/120s/180s; urgent=min(1000×2^n,4000); 5xx background=min(1000×2^n,8000). Bearer tokens inject \`billingBlock\` as first system block; \`signBody()\` replaces \`cch=00000\` with xxHash64. System prompt caching uses \`cache\_control:{type:'ephemeral',ttl:'1h'}\`. \`opts.thinking\` NOT forwarded to bare API calls. Circuit breaker tripped on non-urgent 429s via \`tripCircuitBreaker()\`. Gateway auth (\`packages/gateway/src/auth.ts\`): \`AuthCredential\` (api-key|bearer). Two-level lookup: \`sessionAuth\` Map → \`lastSeenAuth\` global fallback via \`resolveAuth(sessionID?)\`. \`authFingerprint()\` = SHA-256 truncated to 16 hex chars. - - -* **Always investigate and distinguish pre-existing issues from PR-specific failures before fixing**: When CI failures occur, the user consistently checks whether the failure exists on main (e.g., checking last successful main runs) before treating it as something to fix. If the failure is pre-existing or unrelated to current changes, the user explicitly notes it as such and moves on rather than fixing it. Apply this pattern: when a test failure or lint warning appears, first determine if it's on main or PR-specific. Only fix PR-specific regressions; document pre-existing issues as out of scope for the current change. + +* **Always conduct thorough PR reviews with severity-classified findings**: PR review standards: (1) Compare branch vs main first (\`git log main..origin/\\`, \`git diff --stat\`). (2) Verify every PR description claim against actual source files at specific line numbers — never trust PR metadata. (3) Classify findings as BLOCKING vs NON-BLOCKING with file paths and line numbers. (4) Flag LLM-generated planning artifacts (e.g., DOCS-AUDIT.md) as blocking violations of repo conventions. (5) Investigate root causes — check bundle output, trace esbuild variable renaming, identify silent regressions. (6) Run relevant check scripts and grep codebase directly rather than reasoning from PR metadata. * **Always investigate bundle resolution issues by inspecting minified variable names and esbuild's static analysis limitations**: When debugging 'Cannot find module' errors in bundled output, the user consistently digs into the root cause at the esbuild level: checking whether require calls use bare \`require()\` vs renamed aliases like \`\_require\`, inspecting minified bundle output for renamed variable patterns, and verifying that esbuild only statically resolves bare \`require()\` calls. The user expects the assistant to check the actual bundle contents (grep for minified names, count createRequire occurrences, verify no relative requires remain unresolved) rather than assuming the fix worked. The fix pattern is always: use bare \`require\` (not \`\_require\` or other aliases) for local relative imports so esbuild can inline them at bundle time. - -* **Always investigate root causes before accepting PR fixes at face value**: When reviewing PRs, the user consistently digs past the stated fix to verify whether the implementation actually solves the root cause. They examine bundle output, run smoke tests, check CI logs, and trace failure modes (e.g., esbuild variable renaming, wrong \`createRequire\` anchor, silent runtime failures). They expect the reviewer/assistant to identify not just surface bugs but also latent/silent failures introduced by the fix. Reviews should include: confirming the fix works end-to-end, identifying any new failure modes introduced, and flagging silent regressions (e.g., features that appear to work but silently fall back or skip logic). - - -* **Always match user's casual, short, stream-of-consciousness messaging style**: When sending messages on the user's behalf (e.g., via Beeper or other chat tools), always read further back in the conversation history to understand existing relationships and context before composing. Messages must be short, casual, and match the user's natural style — not formal, long, or structured. Never reintroduce people who already know each other. If unsure about the relationship between recipients, check prior chat history first. The user will explicitly correct style mismatches and flag social context errors as serious mistakes. - - -* **Always migrate Bun-specific APIs and tooling to Node.js equivalents**: Migrating from Bun to Node.js/pnpm. Replace Bun-specific APIs: \`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\` (never \`dependencies\`). Exception: \`script/build.ts\` uses \`Bun.build()\` and stays on Bun; \`build-binary\` CI job retains \`setup-bun\`. \`script/bundle.ts\` (npm bundle) uses esbuild via tsx and is Node-native. After each migration phase, ensure lint and tests pass before committing. Migration is largely complete as of main branch (bun.lock deleted, vitest.config.ts added, all test files migrated to vitest). + +* **Always investigate root cause by tracing through multiple specific code layers before accepting a fix**: When facing a runtime bug (especially undefined values from framework internals), the user consistently demands thorough investigation across multiple layers — framework source code (node\_modules), wrapper utilities, bundler config, and call sites — before accepting any fix. The user explicitly rejects surface-level explanations and pushes for tracing the exact code path that produces the unexpected value. Only after exhausting the investigation does the user accept a defensive fix strategy. When directing investigation, the user specifies concrete areas to search (e.g., 4 specific code locations). Always read and analyze the relevant framework internals, not just application code. - -* **Always monitor CI after push and fix all failures before considering work done**: After pushing code or merging PRs, the user expects the assistant to actively monitor CI results, wait for all checks (including bots like Sentry Seer and Cursor BugBot) to complete, fix any failing jobs, and address all unresolved comments (from both bots and humans). The cycle repeats until CI is fully green and no unresolved comments remain. Use \`gh run view --log-failed\` and \`gh pr checks\` to identify failures. Do not consider a task complete until this full cycle is done. + +* **Always investigate root causes before accepting PR fixes at face value**: When reviewing PRs: always investigate root causes rather than accepting stated fixes at face value. Examine bundle output, run smoke tests, check CI logs, trace failure modes (e.g., esbuild variable renaming, wrong \`createRequire\` anchor, silent runtime failures). Identify latent/silent failures introduced by the fix — e.g., features that appear to work but silently fall back. Also: when CI failures occur, first check if the failure exists on main before treating it as PR-specific. Only fix PR-specific regressions; document pre-existing issues as out of scope. -* **Always pause current tasks to resolve architectural blockers before implementation**: When the user discovers that a foundational architectural assumption is wrong or a planned approach has a critical flaw (e.g., a security boundary that doesn't hold, a rejected integration pattern), they immediately reprioritize: they pause the current implementation task and require the architecture to be redesigned first before any further implementation proceeds. This applies even mid-task. The user explicitly calls out the rejected approach and states the new design direction, expecting the assistant to treat the architectural decision as a prerequisite gate before resuming the original work. - - -* **Always provide documentation/context dumps before requesting technical analysis**: The user consistently pastes large reference documents, source files, or full code listings into the conversation before asking for analysis or implementation work. This applies when exploring new APIs (e.g., Node.js SEA docs), auditing codebases, or planning migrations. The user expects the assistant to extract key insights, identify problems, and propose solutions directly from the pasted material — not from general knowledge alone. When responding, prioritize findings grounded in the specific pasted content, cite line numbers or section names where possible, and proactively surface implications the user may not have explicitly asked about. - - -* **Always pull from origin/main before starting any exploration or work in getsentry/cli**: Before beginning any exploration, investigation, or implementation work in the getsentry/cli repository, always run \`git pull origin/main\` first. If there are local changes (e.g., in \`.lore.md\`) that block the pull, stash them, complete the pull, resolve any conflicts by checking out the index version, then drop the stash. This is an explicit user directive that applies at the very start of every session involving this repo, regardless of what work is planned. - - -* **Always pursue native runner builds to enable platform-specific optimizations**: When the user discovers that cross-compilation from a non-native runner is blocking optimizations (e.g., code cache, codesigning, strip+resign), they consistently push to move builds to native runners for each target platform. macOS targets require macOS runners (Xcode >= 16.4 for Node.js builds; \`strip -x\` on Mach-O requires re-codesigning). Linux cross-compilation to darwin is NOT officially supported by Node.js. The user will switch to per-platform native builds if bytecode (\`useCodeCache\`) yields meaningful startup improvement. Always check whether current CI runners match the target platform and propose native runner alternatives when they don't. - - -* **Always reference external tools and prior art when exploring build/size optimization approaches**: When investigating build pipeline improvements or binary size reduction, the user consistently references specific external tools, repos, and contacts (e.g., Vercel's build-binary.mjs, binpunch, fossilize, Melkey's work) as starting points for evaluation. They expect the assistant to analyze whether each referenced approach actually applies to their specific setup before recommending it. The user wants a clear breakdown of what's relevant vs. irrelevant given their actual architecture (e.g., 'we already use esbuild full bundling, so node\_modules stripping doesn't apply'), followed by concrete alternative opportunities ranked by impact. +* **Always pause current tasks to resolve architectural blockers before implementation**: When a foundational architectural assumption is wrong or a planned approach has a critical flaw (e.g., security boundary that doesn't hold, rejected integration pattern), immediately pause the current implementation task and redesign the architecture first. The architectural decision is a prerequisite gate before resuming original work — even mid-task. The user explicitly calls out the rejected approach and states the new design direction. Rejected approaches must be respected and not re-proposed. - -* **Always request a critical pre-merge review before merging PRs to production**: Before merging any PR, the user consistently requests a thorough, final pre-merge review — often using a subagent for objectivity. The review follows a structured checklist covering: file corruption (especially from biome formatter), import consistency (.js extensions, unused/missing imports), type safety, PR description accuracy, dead code, security issues (e.g., shell injection), error handling correctness, test coverage, and lint/CI status. The user expects the reviewer to surface BLOCKING vs NON-BLOCKING findings explicitly, and only approves merge when zero blocking issues are confirmed. This pattern applies to all PRs regardless of size or prior review rounds. + +* **Always prefer systemic/automated solutions over one-off fixes**: Always prefer systemic/automated solutions over one-off fixes. When identifying errors, gaps, or problems, create or fix systems that prevent the entire class of errors — automated checks (CI steps, lint rules, scripts) that scale. Also: always plan systemic fixes with structured multi-problem breakdowns (numbered problems, file locations, line numbers, root causes) before implementation. Consolidate related problems rather than treating each in isolation. Implementation only follows after the plan is agreed upon. -* **Always research technical approaches thoroughly before implementation**: When facing a significant technical decision or migration, the user consistently requests deep research into multiple approaches before writing any code. This includes: fetching specific upstream documentation/source files (e.g., BUILDING.md, configure.py), identifying concrete flags/options, estimating build times, and evaluating cross-compilation feasibility. The user wants tradeoffs between paths laid out explicitly. Only after research is complete does implementation begin. When presenting research, include specific flags, URLs, estimated costs (time/size), and platform constraints. - - -* **Always track migration progress with explicit completion criteria and remaining blockers**: The Bun→Node migration is complete only when \`Bun.build({ compile: true })\` is replaced by fossilize in \`script/build.ts\`. As of the current session, \`script/build.ts\` already uses fossilize (\`--no-bundle\`, \`--out-dir dist-bin\`, \`--node-version lts\`) with esbuild for bundling — the migration is complete. NODE\_VERSION='lts' in build.ts. The user expects the assistant to track this state across sessions and confirm the migration is done. When resuming sessions, verify \`script/build.ts\` does not contain \`Bun.build({ compile: true })\` before declaring migration complete. - - -* **Always update dependencies promptly after releasing new versions**: When the user releases a new version of a tool they own (e.g., fossilize), they immediately update dependent projects to use that new version. This includes bumping the version in package files, creating a dedicated branch with a descriptive name (e.g., \`chore/tool-x.y.z\`), and opening a pull request. The commit message follows conventional commit format: \`chore: update \ to \ (\)\`. The assistant should proactively handle the full update workflow: fetch latest main, create the branch, update the dependency, commit, push, and open a PR. - - -* **Always work around the worktree conflict error when merging to main**: When merging PRs locally, the user consistently encounters \`fatal: 'main' is already used by worktree at ...\` and expects a workaround to be applied automatically rather than treating it as a blocking error. The merge is always completed successfully despite this error (e.g., using \`gh pr merge\` via CLI or other workaround). Never stop or report failure when this specific worktree conflict appears — proceed with the merge using an alternative method and confirm the PR was merged successfully. - - -* **Always work from a plan file before making code changes**: When starting work on a GitHub issue or significant feature, the user expects a plan file to be created first (at \`.opencode/plans/\-\.md\`) before any edits to other files. The user enforces a 'plan mode' where only the plan file may be written. Only after the plan is established does implementation proceed. This applies to new features, bug fixes, and test additions alike. The assistant should create the plan file first, document the problem, proposed approach, and estimated impact, then await confirmation before touching source or CI files. +* **Always research technical approaches thoroughly before implementation**: Before significant technical decisions or migrations: research multiple approaches thoroughly first — fetch upstream docs/source (e.g., BUILDING.md, configure.py), identify concrete flags/options, estimate build times, evaluate cross-compilation feasibility. Present explicit tradeoffs with specific flags, URLs, estimated costs (time/size), and platform constraints. Only begin implementation after research is complete and approach is chosen. Reference specific external tools and prior art (e.g., Vercel's build-binary.mjs, binpunch, fossilize) and evaluate whether each actually applies to the current architecture before recommending. - -* **Always work from a structured plan file before executing multi-step tasks**: When tackling multi-step or multi-file changes, the user consistently creates a formal plan file (e.g., \`.opencode/plans/\-\.md\`) during a planning phase before any edits are made. The plan enumerates discrete numbered tasks with priorities and target files. Execution only begins after the user explicitly approves the plan. During execution, tasks are marked in\_progress and completed sequentially. The user expects this plan-then-execute workflow to be followed strictly — no file edits during planning, and tasks tracked against the approved plan. + +* **Always stage all modified files before committing, not just already-staged ones**: Git commit discipline: (1) Stage ALL modified files before committing — not just already-staged ones. Review full \`git status\` as a checklist against completed tasks. (2) Commit with conventional commit messages (e.g., \`docs: fix stale Bun references\`) summarizing the session's primary theme. (3) Always pull from \`origin/main\` before starting work in getsentry/cli — stash local changes if needed, resolve conflicts with \`git checkout --theirs\`, then drop stash. (4) When \`fatal: 'main' is already used by worktree\` appears, use \`gh pr merge\` or alternative — never treat as blocking failure. - -* **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. - - -* **Never merge a PR if CI is failing**: NEVER merge a PR if CI is failing unless the user explicitly says to ignore specific failures in that session. This is an absolute directive repeated across 20+ sessions. + +* **Always store plans as markdown files in the \`.opencode/plans\` directory with timestamp-prefixed filenames**: When working in plan mode, the user expects plans to be written to \`.opencode/plans/\` as markdown files. Filenames follow the pattern \`{timestamp}-{slug}.md\` (e.g., \`1779289703678-sentryclirc-migration.md\`). Some plans use descriptive slugs without timestamps (e.g., \`require-conventional-pr-title.md\`). Plans are created before implementation begins, and the assistant should call \`plan\_exit\` when done planning. Plans may be edited iteratively during the planning phase before switching to build mode. -* **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 \\` — never add to \`dependencies\`. - - -* **Respect explicitly rejected approaches**: Behavioral pattern detected across 3 sessions (action: rejected-approach). The user consistently demonstrates this behavior. - - -* **Review code before committing**: Behavioral pattern detected across 5 sessions (action: requested-review). The user consistently demonstrates this behavior. - - -* **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). - - -* **Spend time on robust esbuild config — cover sourcemaps too**: When working on the build pipeline, spend time figuring out a robust esbuild config and stick to it. The config must also cover sourcemaps (debug ID injection, upload gating on SENTRY\_AUTH\_TOKEN). Don't iterate ad-hoc — design the full config once correctly. - - -* **Telemetry implementation invariants: handler cleanup, uid check, non-blocking, redaction**: Four absolute telemetry directives in \`src/lib/telemetry.ts\`: (1) \`initSentry()\` ALWAYS removes \`currentBeforeExitHandler\` via \`process.removeListener\` before registering new one — prevents duplicate handlers on re-init. (2) \`isOwnedByRoot()\` ALWAYS returns \`false\` immediately on \`process.platform === 'win32'\` — Windows \`fs.stat().uid\` always returns 0 regardless of actual ownership. (3) NEVER block CLI execution for telemetry emission — all telemetry drains are best-effort, wrapped in try/catch. (4) \`SENSITIVE\_ARGV\_FLAGS\` (includes \`token\`, \`auth-token\`) NEVER sent to telemetry — \`redactArgv()\` handles both \`--flag=value\` and \`--flag \\` forms. - - -* **tryReadPem must never throw — missing CA file is non-fatal**: Absolute directive: \`tryReadPem()\` in \`src/lib/custom-ca.ts\` NEVER throws. A missing or unreadable CA file must not crash the CLI — log a warning and return \`undefined\`. This is a design invariant for CA loading: failures are best-effort, not fatal. +* **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**: (preference) Project conventions (AGENTS.md): Use \`pnpm run\`/\`pnpm install\`/\`pnpm add -D\`. Use \`buildCommand\` from \`lib/command.js\` (never \`@stricli/core\` directly); \`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/model-based tests (fast-check); \`DEFAULT\_NUM\_RUNS=50\`. Error exit codes: 1x=auth, 2x=input/config, 3x=API/network, 4x=feature/billing, 5x=operations, 6x=command-specific. All packages in \`devDependencies\` (CI enforces via \`check:deps\`). NEVER merge if CI failing. Vitest + \`pool: 'forks'\` + \`UV\_USE\_IO\_URING=0\` in CI. \`node:sqlite\` requires \`--experimental-sqlite\` on Node 22. \`new Worker(new URL(...))\` HANGS in SEA — use Blob+URL.createObjectURL. Bun-native APIs forbidden; use Node equivalents. When creating a new check script, add to BOTH \`package.json\` AND \`.github/workflows/ci.yml\`. diff --git a/package.json b/package.json index 39e4b239d..92083b8a3 100644 --- a/package.json +++ b/package.json @@ -91,8 +91,8 @@ }, "scripts": { "tsx": "tsx --import ./script/require-shim.mjs", - "cli": "pnpm tsx src/bin.ts", - "dev": "pnpm run generate:schema && pnpm run generate:docs && pnpm run generate:sdk && pnpm tsx src/bin.ts", + "cli": "tsx --import ./script/require-shim.mjs src/bin.ts", + "dev": "pnpm run generate:schema && pnpm run generate:docs && pnpm run generate:sdk && tsx --import ./script/require-shim.mjs src/bin.ts", "build": "pnpm run generate:schema && pnpm run generate:docs && pnpm run generate:sdk && pnpm tsx script/build.ts --single", "build:all": "pnpm run generate:schema && pnpm run generate:docs && pnpm run generate:sdk && pnpm tsx script/build.ts", "bundle": "pnpm run generate:schema && pnpm run generate:docs && pnpm run generate:sdk && pnpm tsx script/bundle.ts",