diff --git a/.craft.yml b/.craft.yml index ffac3629a8..692ccd3bba 100644 --- a/.craft.yml +++ b/.craft.yml @@ -3,8 +3,8 @@ changelog: policy: auto versioning: policy: auto -preReleaseCommand: node --experimental-strip-types script/bump-version.ts --pre -postReleaseCommand: node --experimental-strip-types script/bump-version.ts --post +preReleaseCommand: bash -c 'cd packages/cli && node --experimental-strip-types script/bump-version.ts --pre' +postReleaseCommand: bash -c 'cd packages/cli && node --experimental-strip-types script/bump-version.ts --post' artifactProvider: name: github config: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8797d3fddf..32a2d3c8db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,24 +67,24 @@ jobs: with: filters: | skill: - - 'src/**' - - 'docs/**' - - 'package.json' - - 'README.md' - - 'DEVELOPMENT.md' - - 'script/generate-skill.ts' - - 'script/generate-command-docs.ts' - - 'script/generate-docs-sections.ts' - - 'script/eval-skill.ts' - - 'test/skill-eval/**' + - 'packages/cli/src/**' + - 'apps/cli-docs/**' + - 'packages/cli/package.json' + - 'packages/cli/README.md' + - 'packages/cli/DEVELOPMENT.md' + - 'packages/cli/script/generate-skill.ts' + - 'packages/cli/script/generate-command-docs.ts' + - 'packages/cli/script/generate-docs-sections.ts' + - 'packages/cli/script/eval-skill.ts' + - 'packages/cli/test/skill-eval/**' code: - - 'src/**' - - 'test/**' - - 'script/**' - - 'patches/**' - - 'docs/**' - - 'plugins/**' - - 'package.json' + - 'packages/cli/src/**' + - 'packages/cli/test/**' + - 'packages/cli/script/**' + - 'packages/cli/patches/**' + - 'apps/cli-docs/**' + - 'packages/cli/plugins/**' + - 'packages/cli/package.json' - 'pnpm-lock.yaml' - '.github/workflows/ci.yml' codemod: @@ -119,7 +119,7 @@ jobs: if: github.ref == 'refs/heads/main' && github.event_name == 'push' run: | TS=$(date -d "$COMMIT_TIMESTAMP" +%s) - CURRENT=$(jq -r .version package.json) + CURRENT=$(jq -r .version packages/cli/package.json) VERSION=$(echo "$CURRENT" | sed "s/-dev\.[0-9]*$/-dev.${TS}/") echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "Nightly version: ${VERSION}" @@ -169,8 +169,11 @@ jobs: - uses: actions/cache@v5 id: cache with: - path: node_modules - key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'patches/**') }} + path: | + node_modules + packages/*/node_modules + apps/*/node_modules + key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }} - if: steps.cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile - name: Generate API Schema @@ -182,7 +185,7 @@ jobs: - name: Check skill files id: check-skill run: | - if git diff --quiet plugins/sentry-cli/skills/sentry-cli/; then + if git diff --quiet packages/cli/plugins/sentry-cli/skills/sentry-cli/; then echo "Skill files are up to date" else echo "stale=true" >> "$GITHUB_OUTPUT" @@ -191,7 +194,7 @@ jobs: - name: Check docs sections id: check-sections run: | - if git diff --quiet README.md DEVELOPMENT.md docs/src/content/docs/contributing.md docs/src/content/docs/self-hosted.md docs/src/content/docs/getting-started.mdx; then + if git diff --quiet packages/cli/README.md packages/cli/DEVELOPMENT.md apps/cli-docs/src/content/docs/contributing.md apps/cli-docs/src/content/docs/self-hosted.md apps/cli-docs/src/content/docs/getting-started.mdx; then echo "Docs sections are up to date" else echo "stale=true" >> "$GITHUB_OUTPUT" @@ -202,7 +205,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add plugins/sentry-cli/skills/sentry-cli/ README.md DEVELOPMENT.md docs/src/content/docs/contributing.md docs/src/content/docs/self-hosted.md docs/src/content/docs/getting-started.mdx + git add packages/cli/plugins/sentry-cli/skills/sentry-cli/ packages/cli/README.md packages/cli/DEVELOPMENT.md apps/cli-docs/src/content/docs/contributing.md apps/cli-docs/src/content/docs/self-hosted.md apps/cli-docs/src/content/docs/getting-started.mdx git diff --cached --quiet || (git commit -m "chore: regenerate docs" && git push) - name: Fail for fork PRs with stale generated files if: (steps.check-skill.outputs.stale == 'true' || steps.check-sections.outputs.stale == 'true') && steps.token.outcome != 'success' @@ -224,8 +227,11 @@ jobs: - uses: actions/cache@v5 id: cache with: - path: node_modules - key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'patches/**') }} + path: | + node_modules + packages/*/node_modules + apps/*/node_modules + key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }} - if: steps.cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile - run: pnpm run generate:schema @@ -255,8 +261,11 @@ jobs: - uses: actions/cache@v5 id: cache with: - path: node_modules - key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'patches/**') }} + path: | + node_modules + packages/*/node_modules + apps/*/node_modules + key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }} - if: steps.cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile - name: Generate API Schema @@ -290,8 +299,11 @@ jobs: - uses: actions/cache@v5 id: cache with: - path: node_modules - key: node-modules-${{ matrix.os }}-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'patches/**') }} + path: | + node_modules + packages/*/node_modules + apps/*/node_modules + key: node-modules-${{ matrix.os }}-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }} - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' shell: bash @@ -337,8 +349,8 @@ jobs: if: needs.changes.outputs.nightly-version != '' shell: bash run: | - jq --arg v "${{ needs.changes.outputs.nightly-version }}" '.version = $v' package.json > package.json.tmp - mv package.json.tmp package.json + jq --arg v "${{ needs.changes.outputs.nightly-version }}" '.version = $v' packages/cli/package.json > packages/cli/package.json.tmp + mv packages/cli/package.json.tmp packages/cli/package.json - name: Build env: # Environment-scoped (production) — must be set at step level to @@ -357,9 +369,9 @@ jobs: shell: bash run: | if [[ "${{ matrix.target }}" == "windows-x64" ]]; then - ./dist-bin/sentry-windows-x64.exe --help + ./packages/cli/dist-bin/sentry-windows-x64.exe --help else - ./dist-bin/sentry-${{ matrix.target }} --help + ./packages/cli/dist-bin/sentry-${{ matrix.target }} --help fi - name: Smoke test (deep — SQLite, telemetry, auth DB) if: matrix.can-test @@ -369,9 +381,9 @@ jobs: SENTRY_TOKEN: "" run: | if [[ "${{ matrix.target }}" == "windows-x64" ]]; then - BIN=./dist-bin/sentry-windows-x64.exe + BIN=./packages/cli/dist-bin/sentry-windows-x64.exe else - BIN=./dist-bin/sentry-${{ matrix.target }} + BIN=./packages/cli/dist-bin/sentry-${{ matrix.target }} fi # auth status without a token exercises SQLite init, schema # migrations, telemetry lazy import, and the CJS require chain. @@ -397,7 +409,7 @@ jobs: (github.ref_name == 'main' || startsWith(github.ref_name, 'release/')) shell: bash run: | - BIN=./dist-bin/sentry-${{ matrix.target }} + BIN=./packages/cli/dist-bin/sentry-${{ matrix.target }} echo "Verifying code signature on $BIN..." rcodesign verify "$BIN" - name: Upload binary artifact @@ -405,15 +417,15 @@ jobs: with: name: sentry-${{ matrix.target }} path: | - dist-bin/sentry-* - !dist-bin/*.gz + packages/cli/dist-bin/sentry-* + !packages/cli/dist-bin/*.gz - name: Upload compressed artifact if: github.event_name != 'pull_request' uses: actions/upload-artifact@v7 with: name: sentry-${{ matrix.target }}-gz - path: dist-bin/*.gz + path: packages/cli/dist-bin/*.gz generate-patches: name: Generate Delta Patches @@ -754,8 +766,11 @@ jobs: - uses: actions/cache@v5 id: cache with: - path: node_modules - key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'patches/**') }} + path: | + node_modules + packages/*/node_modules + apps/*/node_modules + key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }} - if: steps.cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile - name: Download Linux binary @@ -801,8 +816,11 @@ jobs: - uses: actions/cache@v5 id: cache with: - path: node_modules - key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'patches/**') }} + path: | + node_modules + packages/*/node_modules + apps/*/node_modules + key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }} - if: steps.cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile - name: Bundle @@ -810,7 +828,6 @@ jobs: # Environment-scoped (production) — see note in build-binary. SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} run: pnpm run bundle - - run: npm pack # Switch to the matrix Node to run the produced artifact. This is the # consumer's perspective: Node 20 exercises the WASM SQLite fallback # (no node:sqlite before 22.15); 22/24 use the native driver. @@ -831,7 +848,7 @@ jobs: # create and open the database reliably. The runner's ~/.sentry # may not exist under the switched Node 20 runtime. SENTRY_CONFIG_DIR: ${{ runner.temp }}/.sentry-smoke - run: node dist/bin.cjs --help + run: node packages/cli/dist/bin.cjs --help - name: Smoke test (Node.js — deep) shell: bash env: @@ -843,7 +860,7 @@ jobs: # migrations, telemetry lazy import, and the CJS require chain. # On Node 20 this runs entirely through the bundled WASM SQLite # driver. Expected: exit 10 (AUTH_NOT_AUTHENTICATED), NOT a crash. - OUTPUT=$(node dist/bin.cjs auth status 2>&1) && EXIT_CODE=$? || EXIT_CODE=$? + OUTPUT=$(node packages/cli/dist/bin.cjs auth status 2>&1) && EXIT_CODE=$? || EXIT_CODE=$? if [[ $EXIT_CODE -ne 10 ]]; then echo "::error::Expected exit code 10 (not authenticated), got $EXIT_CODE" echo "$OUTPUT" @@ -854,12 +871,14 @@ jobs: echo "$OUTPUT" exit 1 fi + - run: npm pack + working-directory: packages/cli - name: Upload artifact if: matrix.node == '22' uses: actions/upload-artifact@v7 with: name: npm-package - path: "*.tgz" + path: "packages/cli/*.tgz" build-docs: name: Build Docs @@ -885,13 +904,16 @@ jobs: - uses: actions/cache@v5 id: cache with: - path: node_modules - key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'patches/**') }} + path: | + node_modules + packages/*/node_modules + apps/*/node_modules + key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }} - if: steps.cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile - name: Get CLI version id: version - run: echo "version=$(node -p 'require("./package.json").version')" >> "$GITHUB_OUTPUT" + run: echo "version=$(node -p 'require("./packages/cli/package.json").version')" >> "$GITHUB_OUTPUT" - name: Download compiled CLI binary uses: actions/download-artifact@v8 with: @@ -902,7 +924,7 @@ jobs: - name: Generate docs content run: pnpm run generate:schema && pnpm run generate:docs - name: Build Docs - working-directory: docs + working-directory: apps/cli-docs env: PUBLIC_SENTRY_ENVIRONMENT: production SENTRY_RELEASE: ${{ steps.version.outputs.version }} @@ -919,18 +941,18 @@ jobs: SENTRY_ORG: sentry SENTRY_PROJECT: cli-website run: | - ./dist-bin/sentry-linux-x64 sourcemap inject docs/dist/ - ./dist-bin/sentry-linux-x64 sourcemap upload docs/dist/ \ + ./dist-bin/sentry-linux-x64 sourcemap inject apps/cli-docs/dist/ + ./dist-bin/sentry-linux-x64 sourcemap upload apps/cli-docs/dist/ \ --release "${{ steps.version.outputs.version }}" \ --url-prefix "~/" # Remove .map files — they were uploaded to Sentry but shouldn't # be deployed to production. - name: Remove sourcemaps from output - run: find docs/dist -name '*.map' -delete + run: find apps/cli-docs/dist -name '*.map' -delete - name: Package Docs run: | - cp .nojekyll docs/dist/ - cd docs/dist && zip -r ../../gh-pages.zip . + cp .nojekyll apps/cli-docs/dist/ + cd apps/cli-docs/dist && zip -r "$GITHUB_WORKSPACE/gh-pages.zip" . - name: Upload docs artifact uses: actions/upload-artifact@v7 with: diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index 5df7f0cb36..cf8313474d 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -4,11 +4,11 @@ on: push: branches: [main] paths: - - 'docs/**' - - 'src/**' - - 'script/generate-command-docs.ts' - - 'script/generate-skill.ts' - - 'install' + - 'apps/cli-docs/**' + - 'packages/cli/src/**' + - 'packages/cli/script/generate-command-docs.ts' + - 'packages/cli/script/generate-skill.ts' + - 'packages/cli/install' - '.github/workflows/docs-preview.yml' pull_request: # No paths filter here: the 'closed' event must ALWAYS fire so @@ -48,11 +48,11 @@ jobs: with: filters: | docs: - - 'docs/**' - - 'src/**' - - 'script/generate-command-docs.ts' - - 'script/generate-skill.ts' - - 'install' + - 'apps/cli-docs/**' + - 'packages/cli/src/**' + - 'packages/cli/script/generate-command-docs.ts' + - 'packages/cli/script/generate-skill.ts' + - 'packages/cli/install' - '.github/workflows/docs-preview.yml' # Central gate. `build` = produce the site (push, or a docs PR being @@ -93,8 +93,11 @@ jobs: id: cache if: steps.gate.outputs.build == 'true' with: - path: node_modules - key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'patches/**') }} + path: | + node_modules + packages/*/node_modules + apps/*/node_modules + key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }} - if: steps.gate.outputs.build == 'true' && steps.cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile @@ -102,7 +105,7 @@ jobs: - name: Get CLI version id: version if: steps.gate.outputs.build == 'true' - run: echo "version=$(node -p 'require("./package.json").version')" >> "$GITHUB_OUTPUT" + run: echo "version=$(node -p 'require("./packages/cli/package.json").version')" >> "$GITHUB_OUTPUT" - name: Generate docs content if: steps.gate.outputs.build == 'true' @@ -110,7 +113,7 @@ jobs: - name: Build Docs for Preview if: steps.gate.outputs.build == 'true' - working-directory: docs + working-directory: apps/cli-docs env: DOCS_BASE_PATH: ${{ github.event_name == 'push' && '/_preview/pr-main' @@ -129,15 +132,15 @@ jobs: SENTRY_ORG: sentry SENTRY_PROJECT: cli-website run: | - pnpm run cli sourcemap inject docs/dist/ - pnpm run cli sourcemap upload docs/dist/ \ + pnpm run cli sourcemap inject "$GITHUB_WORKSPACE/apps/cli-docs/dist/" + pnpm run cli sourcemap upload "$GITHUB_WORKSPACE/apps/cli-docs/dist/" \ --release "${{ steps.version.outputs.version }}" \ --url-prefix "~/" # Remove .map files — uploaded to Sentry but shouldn't be deployed. - name: Remove sourcemaps from output if: steps.gate.outputs.build == 'true' - run: find docs/dist -name '*.map' -delete + run: find apps/cli-docs/dist -name '*.map' -delete - name: Ensure .nojekyll at gh-pages root # Runs whenever we deploy (build or a 'closed' cleanup), but never for @@ -180,7 +183,7 @@ jobs: if: steps.gate.outputs.deploy == 'true' uses: rossjrw/pr-preview-action@v1 with: - source-dir: docs/dist/ + source-dir: apps/cli-docs/dist/ preview-branch: gh-pages umbrella-dir: _preview pages-base-url: cli.sentry.dev diff --git a/.github/workflows/eval-skill-fork.yml b/.github/workflows/eval-skill-fork.yml index 71c0c381ac..a986023874 100644 --- a/.github/workflows/eval-skill-fork.yml +++ b/.github/workflows/eval-skill-fork.yml @@ -54,8 +54,11 @@ jobs: - uses: actions/cache@v5 id: cache with: - path: node_modules - key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'patches/**') }} + path: | + node_modules + packages/*/node_modules + apps/*/node_modules + key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }} - if: steps.cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile diff --git a/.gitignore b/.gitignore index 858ee3ec52..3f638f09d4 100644 --- a/.gitignore +++ b/.gitignore @@ -43,10 +43,10 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json .turbo .mastra -# docs -docs/dist -docs/node_modules -docs/.astro +# docs (now under apps/cli-docs) +apps/cli-docs/dist +apps/cli-docs/node_modules +apps/cli-docs/.astro # local planning notes (not for version control) .plans @@ -58,20 +58,20 @@ docs/.astro .DS_Store # Generated files (rebuilt at build time) -src/generated/ -src/sdk.generated.ts -src/sdk.generated.d.cts +packages/cli/src/generated/ +packages/cli/src/sdk.generated.ts +packages/cli/src/sdk.generated.d.cts # ...except the sixel banner: it's small, deterministic (baked from the committed # assets/banner-mask.png), and statically imported by src/lib/sixel.ts, so it must # exist on a fresh checkout before any build step regenerates it. -!src/generated/banner-sixel.ts +!packages/cli/src/generated/banner-sixel.ts # Generated docs pages (rebuilt from fragments + CLI introspection / env registry) -docs/src/content/docs/commands/ -docs/src/content/docs/configuration.md +apps/cli-docs/src/content/docs/commands/ +apps/cli-docs/src/content/docs/configuration.md # Generated discovery manifest (rebuilt by generate:skill, served via symlinked skill files) -docs/public/.well-known/skills/index.json +apps/cli-docs/public/.well-known/skills/index.json # OpenCode .opencode/ @@ -79,7 +79,7 @@ opencode.json* # Throwaway documentation audit reports (bot-generated, not real docs) DOCUMENTATION_GAPS.md -docs/DOCUMENTATION_GAPS.md +apps/cli-docs/DOCUMENTATION_GAPS.md DOCUMENTATION_GAP_REPORT.md # Throwaway planning / migration notes (agent- or human-authored scratch docs, diff --git a/.lore.md b/.lore.md index 961ce4b09b..9bee644c57 100644 --- a/.lore.md +++ b/.lore.md @@ -19,9 +19,15 @@ * **bspatch.ts: TRDIFF10 patch application — inline SHA-256, streaming zstd, CoW old-file copy**: TRDIFF10 header: 32 bytes (magic + controlLen + diffLen + newSize, all i64 LE). Control block decompressed fully via \`zstdDecompressSync\` (needs random access). Diff + extra blocks streamed via \`createZstdStreamReader\` (Node Transform → Web ReadableStream → \`BufferedStreamReader\`). \`applyPatch()\` ALWAYS computes SHA-256 inline and returns it — no separate verification step. \`loadOldBinary()\` copies to temp via \`COPYFILE\_FICLONE\` (CoW reflink, falls back to regular copy) then reads into memory. \`cleanupPatchResources()\` runs all cleanup steps regardless of prior failures. Write errors captured early via \`writer.on('error')\` to avoid ERR\_UNHANDLED\_ERROR on ENOSPC/EIO. + +* **check-fragments.ts: validates fragment files against actual route names**: \`script/check-fragments.ts\`: validates fragment files against actual route names (Check 1-4) AND validates subcommand coverage within fragments (Check 5). Check 5: for each route with >1 command, verifies fragment mentions each subcommand via a heading (outside fenced code blocks) or \`sentry \ \\` code reference. Default commands handled: if fragment contains bare \`sentry \\`, the default command is covered. Default commands detected from route map (\`defaultCommand\` field in route index files). Fenced code block content stripped before heading scan to avoid false positives from bash comments. Warnings by default; \`--strict\` makes them errors. Run via \`pnpm run check:fragments\`. CI \`check-generated\` job triggers when \`changes.outputs.skill == 'true'\`. + * **check:stale-refs: generic toolchain consistency scanner derived from package.json**: \`script/check-stale-references.ts\`: reads \`packageManager\` from \`package.json\` (e.g., \`pnpm@10.11.0\`), derives stale PMs dynamically, and scans dev-facing docs/scripts for stale \`\ run\`, \`\ remove\`, \`\ add -d\` commands and \`requires \\`/\`\ installed\` prerequisite prose. Excludes: user-facing install instructions (fenced code blocks with \`install -g\`/\`add -g\`), the check script itself, and \`node\_modules/\`. Added to CI lint job. \*\*Generic\*\*: if project migrates from pnpm to yarn, changing \`packageManager\` in \`package.json\` auto-flags all \`pnpm run\` references in dev docs — no manual pattern updates needed. Trap: script must exclude itself from scanning or its own JSDoc examples trigger false positives. + +* **collapse=lifetime in issue list: LIFETIME\_FIELDS, buildListApiOptions, and API gotcha**: \`src/commands/issue/list.ts\` \`LIFETIME\_FIELDS = new Set(\['count','userCount','firstSeen','lastSeen'])\` — fields stripped by \`collapse=lifetime\` on the list endpoint. \`buildListApiOptions(json, fields)\`: \`collapseLifetime\` only true when \`json && fields !== undefined && fields.length > 0 && !fields.some(f => LIFETIME\_FIELDS.has(f))\`. Human output NEVER collapses lifetime. \`buildIssueListCollapse()\` always starts with \`\['filtered','unhandled']\`, conditionally adds \`'lifetime'\` then \`'stats'\`. \`ISSUE\_DETAIL\_COLLAPSE\` safely includes \`'lifetime'\` — detail endpoint preserves top-level fields regardless. \`IssueViewOutputSchema\` in \`src/types/sentry.ts\` extends \`SentryIssueSchema\` with enrichment fields (\`event\`, \`org\`, \`replayIds\`, \`trace\`) added by \`jsonTransformIssueView\`. Wired via \`schema: IssueViewOutputSchema\` on output config in \`view.ts\`. NOTE: \`count\`/\`userCount\`/\`firstSeen\`/\`lastSeen\` always present on \`issue view\` (detail endpoint) — only potentially absent on \`issue list\` when collapse=lifetime is active. + * **Consola chosen as CLI logger with Sentry createConsolaReporter integration**: Consola is the CLI logger with Sentry \`createConsolaReporter\` integration. Two reporters: FancyReporter (stderr) + Sentry structured logs. Level via \`SENTRY\_LOG\_LEVEL\`. \`buildCommand\` injects hidden \`--log-level\`/\`--verbose\` flags. \`withTag()\` creates independent instances; \`setLogLevel()\` propagates via registry. All user-facing output must use consola, not raw stderr. \`HandlerContext\` intentionally omits stderr. Telemetry opt-out priority: (1) \`SENTRY\_CLI\_NO\_TELEMETRY=1\`, (2) \`DO\_NOT\_TRACK=1\`, (3) \`metadata.defaults.telemetry\`, (4) default on. Shell completions set \`SENTRY\_CLI\_NO\_TELEMETRY=1\` in \`bin.ts\` before imports. Timing queued to \`completion\_telemetry\_queue\` SQLite table; normal runs drain via \`DELETE ... RETURNING\`. \`ENV\_VAR\_REGISTRY\` in \`src/lib/env-registry.ts\` is single source for all honored env vars; \`topLevel: true\` + \`briefDescription\` surfaces in \`--help\`. Add install-script-only vars with \`installOnly: true\`. @@ -61,9 +67,6 @@ * **isSentrySaasUrl vs isSaaSTrustOrigin: two intentional SaaS checks**: \`src/lib/sentry-urls.ts\` exports two SaaS-detection helpers with intentional split: (1) \`isSentrySaasUrl(url)\` — hostname-only check (\`sentry.io\` or \`\*.sentry.io\`), accepts any protocol/port. Used for routing/UX: custom-headers warning, \`getSentryBaseUrl\`/\`isSelfHosted\`, region resolution skip, telemetry \`is\_self\_hosted\` tag. (2) \`isSaaSTrustOrigin(url)\` — stricter: additionally requires \`https:\` and default port. Used for security decisions: token-host trust comparison, sentryclirc URL trust check, URL-arg trust, login refusal. Rule: hostname-only for routing/UX (don't break users behind TLS-terminating proxies with \`http://sentry.io\`); strict for credential scoping. JSDoc on \`isSentrySaasUrl\` points callers to \`isSaaSTrustOrigin\` for security contexts. Keep both implementations in sync re: hostname matching. - -* **issue list collapse=stats/lifetime API gotcha: SEEN\_STATS\_FIELDS, LIFETIME\_FIELDS, buildListApiOptions**: \`src/commands/issue/list.ts\` \`LIFETIME\_FIELDS = new Set(\['count','userCount','firstSeen','lastSeen'])\` — fields stripped by \`collapse=lifetime\` on the list endpoint. \`buildListApiOptions(json, fields)\`: \`collapseLifetime\` only true when \`json && fields !== undefined && fields.length > 0 && !fields.some(f => LIFETIME\_FIELDS.has(f))\`. Human output NEVER collapses lifetime. \`buildIssueListCollapse()\` always starts with \`\['filtered','unhandled']\`, conditionally adds \`'lifetime'\` then \`'stats'\`. \`ISSUE\_DETAIL\_COLLAPSE\` safely includes \`'lifetime'\` — detail endpoint preserves top-level fields regardless. \`IssueViewOutputSchema\` in \`src/types/sentry.ts\` extends \`SentryIssueSchema\` with enrichment fields (\`event\`, \`org\`, \`replayIds\`, \`trace\`) added by \`jsonTransformIssueView\`. Wired via \`schema: IssueViewOutputSchema\` on output config in \`view.ts\`. NOTE: \`count\`/\`userCount\`/\`firstSeen\`/\`lastSeen\` always present on \`issue view\` (detail endpoint) — only potentially absent on \`issue list\` when collapse=lifetime is active. - * **Issue list sort values: SortValue type, VALID\_SORT\_VALUES, getComparator, and SDK IssueSort**: In \`src/commands/issue/list.ts\`: \`SortValue\` type (line 141, @internal) and \`VALID\_SORT\_VALUES\` array (line 143) are the CLI's local sort constraints. \`IssueSort\` in \`src/lib/api/issues.ts\` (lines 40–42) is derived from \`@sentry/api\` SDK via \`NonNullable\\['sort']>\` — no API layer change needed when adding new sort values. SDK already includes \`'date' | 'freq' | 'inbox' | 'new' | 'recommended' | 'trends' | 'user'\`. To add a new sort value: (1) add to \`SortValue\` union and \`VALID\_SORT\_VALUES\`, (2) add case to \`getComparator()\` switch (default falls back to date), (3) update flag \`brief\` string, (4) change \`default\` if needed. \`appendIssueFlags()\` guards \`--sort\` omission only when \`flags.sort !== 'date'\` — update guard if default changes. @@ -111,9 +114,6 @@ * **Banner art: direct bitmap grid read over area-averaging downsampling**: Reference image (\`sentry-ref.webp\`, 2048×805px) is a SENTRY wordmark rendered from digit characters ('1'=on, '0'=off) in a monospace grid. Chosen approach: detect cell width via autocorrelation (~17.5px), read each cell on/off directly → 97×13 ASCII grid. Rejected: area-averaging downsampling (blended E's horizontal arms and R's counter/hole into solid fills, making letters unreadable); striped \`▀\` half-block rendering (50/50 duty-cycle dissolved E arms); solid \`█\` rendering (loses scanline texture). Post-processing: remove isolated cells (zero orthogonal neighbors) and small connected components to eliminate stray marks near Y's right arm. - -* **bspatch.ts: no mmap — Node has no native mmap API and native addons break SEA bundling**: True mmap not viable: Node core has no \`fs.mmap\` (Buffer is always heap-backed); native addons (\`mmap-io\` etc.) break two hard project rules — no runtime dependencies and must bundle into Node SEA binary (esbuild can't bundle \`.node\` addons). \`Bun.mmap\` would have worked but project migrated off Bun. Alternative \`pread\` via \`fs.read(fd, buf, 0, len, pos)\` saves ~100MB JS heap for base binary (bytes stay in OS page cache) but cannot help intermediate hops — those buffers must stay in memory for in-memory chaining. Also: one big sequential \`readFile\` is faster than many small \`pread\`s. pread deferred as optional future optimization for single-patch case only. - * **Chose Cloudflare for scalable website deployment over Vercel**: User prefers using Cloudflare for scalable website deployment over Vercel. @@ -149,30 +149,15 @@ ### Gotcha - -* **@sentry/symbolic wasm-pack test never exercises the shipped artifact — use npm pack smoke test instead**: Trap: \`wasm-pack test\` looks like the right way to test \`@sentry/symbolic\` — it builds and runs tests. But it builds its own glue code and never loads the \`--target web\` \`symbolic.js\` + \`initSync\` + package \`exports\`/\`files\` that consumers actually import. Fix: use a packaged-artifact smoke test: \`npm pack\` → install tarball into tmp dir → \`import '@sentry/symbolic'\` → assert via \`node:test\`. This catches exports-map/resolution breakage and runtime errors (e.g., the \`Object\` naming bug) that \`wasm-pack test\` misses entirely. Test files live in the package dir but are excluded from \`files\[]\`; run via \`cd symbolic-wasm/npm && npm test\`. - - -* **@sentry/symbolic: naming wasm export class \`Object\` shadows JS global — breaks Object.create and Object.getPrototypeOf**: Trap: wasm-bindgen generates a JS class named after the Rust struct — naming it \`Object\` looks fine in Rust. But it shadows the JS global \`Object\` for the entire module: \`Object.create\` (used in \`objects()\`) and \`Object.getPrototypeOf\` (used in \`initSync\`) break with \`TypeError: Object.getPrototypeOf is not a function\`. Fix (PR #993): rename the export to \`ObjectFile\` via \`#\[wasm\_bindgen(js\_name = "ObjectFile")]\` and \`#\[wasm\_bindgen(js\_class = "ObjectFile")]\` — Rust struct name \`Object\` unchanged. Discovered via packaged-artifact smoke test, not \`wasm-pack test\`. Similarly, \`ObjectDebugSession\` needed \`AsSelf\` impl (PR #997) — check all new wasm-exposed types for missing \`AsSelf\` impls before using \`derived\_from\_cell!\`. - * **@stricli/core -H alias patch: pin version to 1.2.7, never remove aliases**: Trap: When Stricli throws on \`-H\` aliases used for \`--header\` or \`--host\`, removing the aliases from command files looks like the simple fix. But the project intentionally uses \`-H\` for curl-style API usage. Fix: the in-repo patch for @stricli/core (targeting 1.2.7) removes \`-H\` from the reserved list. Pin version to \`1.2.7\` (not \`^1.2.8\`) so the patch applies. Never remove \`-H\` alias usages from command files. Added in commit \`78c9b04a5\`. Cursor Bugbot and Seer both flag \`-H\` removal as blocking. * **\`--require-all\` false negatives for fat binaries — scan all matched objects**: Trap: \`missingRequestedIds\` computed \`foundIds\` from only the primary object's \`debugId\` per file. Fat Mach-O with multiple slices causes non-primary slice IDs to be reported missing and exits 1 even though they were found. Fix: compute \`foundIds\` from all matched objects (\`.objects\` array), not just \`selectBundledObject()?.debugId\`. Applies to \`src/commands/debug-files/upload.ts\`. - -* **acquireLock ENOENT: parent directory may not exist — must mkdir before writeFileSync**: acquireLock ENOENT: parent directory may not exist — must mkdir before writeFileSync - - -* **Atomic-write test tautology: 'no temp files' test passes even without atomic behavior**: Trap: a test that checks 'no \`.tmp\` files left behind after install' looks like it guards atomic write behavior. But if the non-atomic path (\`writeFile\` directly) never creates \`.tmp\` files in the first place, the \`leftovers\` filter is trivially empty and the test passes with the atomic code removed entirely — confirmed via mutation test on \`byk/fix/agent-skills-atomic-write\`. Fix: the test must exercise the overwrite/update path (where a concurrent reader could observe truncation) AND inject a fault (e.g., mock \`rename\` to throw) to verify cleanup. A test that only runs the happy-path fresh-install never touches the race condition the feature guards against. Per red-green directive: if the test passes with the guard deleted, it isn't testing the guard. - * **batch-queue.ts: 404 from upstream treated as transient — provider never disabled**: Trap: \`BatchProvider.submit()\` returns \`null\` for any non-401/403 HTTP error, including 404. \`submitBatch()\` treats \`null\` as transient and falls back — no disable happens. For providers that don't implement \`/v1/messages/batches\` (e.g. MiniMax), this causes a wasted HTTP round-trip every 30s forever. Fix: add \`"not-found"\` return value for 404 in both Anthropic and OpenAI submit methods. In \`submitBatch()\`, handle \`"not-found"\` with provider-level disable: add \`disabledBatchProviders: Set\\` (keyed by provider name), persist to \`kv\_meta\` via \`setKV()\`, restore on startup. Add fast-path bypass in both \`flush()\` and \`prompt()\`. Provider-level (not per-session) because the URL is baked in at construction — one provider per process. \`groupKey()\` = \`authFingerprint(cred)|providerID\`; per-credential disable was removed in favor of per-session historically. - -* **Biome cognitive complexity cap of 15 — ZIP branch in prepareDifs pushed it to 17**: Trap: adding a ZIP expansion branch to \`prepareDifs\` in \`scan.ts\` looks like a small addition. But Biome enforces a cognitive complexity cap of 15 — the ZIP branch pushed \`prepareDifs\` to 17. Fix: extract per-file processing into a private helper \`prepareFileDif(path, filters, maxFileSize)\` returning \`{ dif: PreparedDif | null; oversized: boolean }\`. Pattern: when adding conditional branches to existing complex functions, check Biome complexity score first and pre-extract helpers. - * **Biome noParameterProperties: never use TypeScript constructor parameter properties in class definitions**: Trap: TypeScript parameter properties (\`constructor(private readonly handle: FileHandle)\`) look like idiomatic shorthand and compile fine. But Biome enforces \`noParameterProperties\` and will error. Fix: always declare explicit class fields and assign in constructor body. Applies to all new classes in \`src/lib/\*\*/\*.ts\`. Caught during \`FileOldReader\`/\`MemoryOldReader\` addition in \`bspatch.ts\` — 4 Biome errors at lines 281, 310, 311, 312. @@ -185,72 +170,30 @@ * **bundle-sources.ts exit code 1 for no-sources relies on cli.ts never resetting exitCode — fragile invariant**: Trap: using \`this.process.exitCode = 1\` directly (at \`bundle-sources.ts:145\`) instead of throwing \`OutputError\`(=60) looks like a valid shortcut. It works today because \`cli.ts:622-649\` never resets \`exitCode\` on a clean return. But this is a fragile invariant — if \`cli.ts\` ever resets \`exitCode\`, the no-sources case silently exits 0. The \`OutputError\`(=60) idiom is the correct pattern. The \`exitCode=1\` approach was kept for consistency with \`check.ts\` pattern; a comment was added explaining the choice. Consider migrating to \`OutputError\` in a future cleanup. - -* **check:fragments only validates file existence — not subcommand coverage depth**: RESOLVED in PR #1024. \`script/check-fragments.ts\` now has Check 5: for each route with >1 command, verifies the fragment mentions each subcommand via a heading (outside fenced code blocks) or \`sentry \ \\` code reference. Default commands (e.g., \`local serve\`) are handled — if the fragment contains \`sentry \\` bare, the default command is considered covered. Default commands detected from route map \`defaultCommand\` field. Fenced code block content stripped before heading scan to avoid bash-comment false positives. Warnings (not errors) by default; \`--strict\` flag makes them errors. - * **check.ts hasId() uses !== null but ObjectFile.codeId is string|undefined — null guard mismatch**: Trap: \`check.ts\` \`hasId()\` guards \`o.codeId !== null && o.codeId.length > 0\`. Looks correct because old \`DifObjectInfo.codeId\` was \`string | null\`. But \`ObjectFile.codeId\` in \`@sentry/symbolic\` 13.4.0 is \`string | undefined\` — the getter returns \`undefined\` (not \`null\`) when Rust returns \`None\`. If \`DifObjectInfo.codeId\` is mapped as \`string | null\` (via \`?? null\`), the \`!== null\` guard works. If mapped as \`string | undefined\` (via \`?? undefined\`), the guard silently passes \`undefined\` through. Fix: ensure \`parseDebugFile\` maps \`codeId\` to \`string | null\` (using \`obj.codeId ?? null\`) so \`check.ts\`'s existing \`!== null\` guard remains correct without touching \`check.ts\`. - -* **chunk-upload gzip wire format: never emit Content-Encoding: gzip with file\_gzip field**: Trap: gzip-compressed chunks look like they should use \`Content-Encoding: gzip\` alongside the \`file\_gzip\` multipart field — standard HTTP compression convention. But zstd-aware Sentry servers reject that combination with 400 to avoid ambiguity. Fix: gzip path uses ONLY the \`file\_gzip\` multipart field with NO \`Content-Encoding\` header (legacy protocol). Zstd path uses \`Content-Encoding: zstd\` + \`file\` field (requires server opt-in via getsentry/sentry#113760+). This is a standing directive from code comments in \`src/lib/api/chunk-upload.ts\`. - * **ci.yml NODE\_VERSION pins must be exact patch — floating major silently reuses buggy cached patch**: Trap: using a floating major version like \`22.x\` in ci.yml looks safe and auto-updates. But GitHub Actions caches the resolved binary — a CVE fix (e.g. CVE-2026-48931 requiring 22.23.1/24.18.0) won't be picked up until the cache expires, silently running the vulnerable patch. Fix: always pin exact patch versions in ci.yml env vars (\`NODE\_VERSION\_22: "22.23.1"\`, \`NODE\_VERSION\_24: "24.18.0"\`). Update pins explicitly when a CVE fix requires a new patch. * **Ctrl+C mid-write on Node < 22.15 leaks lock blocking next run**: Trap: On Node < 22.15, default SIGINT (Ctrl+C) terminates the process without running exit handlers. If this happens mid-write, the WASM lock directory persists, blocking the next CLI invocation for up to 60 seconds. Fix: Add a global SIGINT handler that calls db.close() before process.exit(). - -* **dashboard revisions/restore and issue events subcommands are undocumented in fragment files**: RESOLVED in PR #1024. \`docs/src/fragments/commands/dashboard.md\` now documents \`revisions\` and \`restore\`. \`docs/src/fragments/commands/issue.md\` now documents \`events\` and \`@latest\`/\`@most\_frequent\` selectors. \`docs/src/fragments/commands/cli.md\` now documents \`defaults\` and \`import\`. \`check:fragments\` (Check 5) now validates subcommand coverage within fragment files — not just file existence. When adding new subcommands, always update the corresponding fragment in \`docs/src/fragments/commands/\` AND run \`pnpm run check:fragments\` to verify coverage. - - -* **debug-files upload: partial size-drop silently exits 0 — doUpload must receive oversizedCount**: Trap: \`doNothingToUpload\` (all-dropped path) correctly calls \`setExitCode(1)\` when \`oversizedCount > 0\`, establishing the contract that oversized files → non-zero exit. But \`doUpload\` (partial-drop path) did not receive \`oversizedCount\`/\`maxFileSize\` — so uploading 3 files where 1 was oversized exited 0 silently. Fix (PR #1146): pass \`oversizedCount\` and \`maxFileSize\` to \`doUpload\`; append scan-oversize message to failures hint; call \`setExitCode(1)\` when \`oversizedCount > 0\` even with no upload failures. Warden finding 9LL-87A on PR #1140. - - -* **delta-upgrade intermediate files: always alternate .patching.a/.b — never write to source path**: Trap: writing patch output to the same path as input looks like a simple in-place update. Fix: \`applyPatchesSequentially()\` alternates between \`${destPath}.patching.a\` and \`${destPath}.patching.b\` because the old binary is mmap'd for reading — writing to the source path truncates it and corrupts output. Single-patch chains apply old→dest directly (safe, different paths). \`cleanupIntermediates()\` called in \`finally\` block removes both intermediates via \`unlinkSync\` (ignoring errors) — always runs even on failure. This is a standing directive: 'Always clean up intermediate files, even on failure' and 'never target the same path.' - * **DEVELOPMENT.md hand-written prose is not covered by any staleness check**: DEVELOPMENT.md hand-written prose is not covered by any staleness check - -* **difFromCandidateBuffer: PE entries must be allowed through format filter when portablepdb is requested**: Trap: the format filter in \`difFromCandidateBuffer\` rejects PE files when \`--type portablepdb\` is specified, because \`peekFormat\` returns \`'pe'\` not \`'portablepdb'\`. But embedded PPDBs live inside PE files — blocking the PE at the format-filter stage means the PPDB is never extracted. Fix: when \`filters.formats\` includes \`'portablepdb'\`, also allow \`'pe'\` through the format gate so the parser can inspect the PE for an embedded PPDB. The PE itself is still dropped after extraction if it has no features. - - -* **embeddedPpdbDif: oversized extracted PPDB dropped silently — oversizedCount not incremented**: Trap: \`embeddedPpdbDif()\` (scan.ts:628-633) returns \`null\` when the decompressed PPDB exceeds \`maxFileSize\`, but does NOT increment \`oversizedCount\`. Looks correct because the PPDB is a derived artifact, not a scanned file. But the user gets no signal that a PPDB was found and dropped — silent data loss. The existing directive (PR #1146) requires \`oversizedCount > 0\` → non-zero exit and scan-oversize message. Fix: increment \`oversizedCount\` (or return a sentinel) when an extracted PPDB is size-gated out, so the caller can report it. - * **encodeChunk silently mislabels payloads if called with 'zstd' on zstd-less runtime**: Trap: encodeChunk in chunk-upload.ts (lines 186-201, 252-254) accepts 'zstd' as input but silently falls back to gzip when zstdCompressAsync is null. The content-encoding header still claims 'zstd', causing the server to reject the payload. Fix: When zstdCompressAsync is null, reject 'zstd' input explicitly rather than silently mislabeling. - -* **error-reporting.ts: three independent 4xx classifiers must stay in sync — isUserError, classifySilenced, isUserApiError**: Trap: \`classifySilenced\` (Sentry capture gate), \`isUserError\` in \`errors.ts\` (upgrade nudge via \`getErrorUpdateNotification\`), and \`isUserApiError\` in \`telemetry.ts\` (span attribution) all classify 4xx errors independently. Adding a new silence rule to only one leaves the others out of sync — e.g., query-parse 400s were silenced in \`classifySilenced\` but still triggered the upgrade nudge until \`isUserError\` was updated. Fix: whenever a new error classification is added (e.g., \`isSearchQueryParseError\`, \`isNetworkError\`), update all three classifiers. Export shared predicates from \`errors.ts\` so all three import the same function. - - -* **eval-skill-fork.yml has no setup-node step — floating Node version bypasses pins**: Trap: \`.github/workflows/eval-skill-fork.yml\` job \`eval\` has no \`actions/setup-node\` step at all — it runs on the runner image's system Node (e.g. 24.17.0/22.23.0), the exact versions the Node-pin PR (#1145) aimed to avoid. The PR's claim 'every setup-node step is pinned' is technically true but the intent is unmet because this job never calls setup-node. Fix: add a pinned \`actions/setup-node@v6\` step with \`node-version: ${{ env.NODE\_VERSION\_24 }}\` before \`pnpm install\` in the \`eval\` job. Discovered via subagent adversarial review of PR #1145. - - -* **eval-skill-fork.yml missing setup-node — fork PRs exposed to Node regression**: Trap: PR #1145 pinned Node versions in all 4 main workflow files and the PR description claimed completeness. But \`.github/workflows/eval-skill-fork.yml\` job \`eval\` (lines 27–57) has no \`actions/setup-node\` step — it runs on the runner's system Node. This is the exact workflow exhibiting \`ERR\_STREAM\_PREMATURE\_CLOSE\` (CVE-2026-48931 regression in Node 24.17.0/22.23.0). Fix: add \`actions/setup-node@v6\` with \`node-version: ${{ env.NODE\_VERSION\_24 }}\` before \`pnpm install\` in that job. When pinning Node versions across CI, always grep ALL workflow files for \`setup-node\` AND check for jobs that invoke Node without an explicit setup step. - - -* **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\`). - * **Frontify brand portal cannot be accessed programmatically — requires auth**: Trap: \`https://brand.getsentry.com/share/wLssCFiQ5ZzmQmKCWym4\` returns HTTP 200 and looks like a static asset server. It's a JS-rendered SPA — no static asset URLs are extractable from HTML. API probes (\`/api/share/…\`, \`/api/shares/…\`) return 404 or HTML. CDN URLs found embedded in the SPA shell (\`media.ffycdn.net\`) are portal chrome assets (OG illustration, favicon), not the actual brand files. Fix: use the pre-signed download endpoint \`https://brand.getsentry.com/api/screen/download/\\` — token must be extracted from the authenticated session or provided by the user. Always ask the user to provide Frontify asset URLs or download tokens directly. - -* **generate-docs-sections.ts still references Bun — extractBunVersion() silently falls back to hardcoded '1.3'**: generate-docs-sections.ts still references Bun — extractBunVersion() silently falls back to hardcoded '1.3' - * **getCurlInstallPaths trusts stale stored install path — must guard with existsSync(dirname)**: Trap: \`getCurlInstallPaths()\` in \`src/lib/upgrade.ts\` reads the stored install path from SQLite and uses it directly — looks correct because the path was valid at install time. But macOS cleans \`/tmp\` on reboot, and users may delete test install dirs, leaving a stale DB entry. Fix: guard the stored-path branch with \`existsSync(dirname(stored.path))\` before trusting it; fall back to \`process.execPath\` startsWith-match against \`KNOWN\_CURL\_DIRS\` (\`\['.local/bin','bin','.sentry/bin']\`), then \`~/.sentry/bin\` default. Conservative: only add the guard — do NOT prefer \`execPath\` over stored path (breaks npm→nightly migration flow). NFS edge case is self-resolving: if binary runs from NFS mount, mount must be active so \`existsSync\` passes. * **git add -A during rebase sweeps in stray untracked files and .lore.md conflicts**: Trap: \`git add -A\` looks like a safe 'stage everything' shortcut during rebase conflict resolution. But it stages untracked stray files (e.g. \`sentry-lightning-talk.md\`) and auto-stages \`.lore.md\` conflict markers, polluting the commit. Fix: always stage specific file paths explicitly (e.g. \`git add src/commands/debug-files/read-file.ts\`) during rebase resolution. If the rebase is complex, abort with \`git rebase --abort\`, reset to \`origin/main\`, and re-apply edits manually. - -* **GitHub CI skips pull\_request workflows entirely when PR has merge conflicts**: Trap: missing CI jobs on a PR look like a workflow trigger bug or branch filter issue — easy to spend time investigating ci.yml triggers. Fix: check \`mergeable\`/\`mergeStateStatus\` first. When a PR is \`CONFLICTING\` (GitHub cannot compute the merge ref), ALL \`pull\_request\`-triggered workflows are silently skipped — only \`pull\_request\_target\` and CodeQL/external checks still run. Confirmed on sentry-cli (TypeScript) PR #1123: full Build/lint/test suite absent because \`mergeStateStatus: DIRTY\`. Resolution: rebase or resolve conflicts, then CI triggers normally. - - -* **isNetworkError must NOT match ApiError(status=0) — TLS cert errors share status 0**: Trap: \`ApiError\` with \`status === 0\` looks like a network failure (no HTTP response received) and is tempting to include in \`isNetworkError()\`. But TLS certificate errors are also wrapped as \`ApiError(status=0)\` — once wrapped, \`isTlsCertError()\` cannot reliably re-detect them. Silencing all status-0 errors would suppress actionable TLS config issues. Fix: \`isNetworkError()\` matches ONLY \`error instanceof TypeError && error.message === 'fetch failed'\` (the unambiguous undici network signature). \`ApiError(status=0)\` is handled separately via an explicit \`error.status === 0\` branch in \`isUserError()\` with a comment explaining the TLS overlap. Callers that want status-0 treated as user-env check it explicitly. - * **issue list --sort recommended: client-side re-sort must be skipped for single-project results**: Trap: applying \`getComparator('recommended')\` to single-project results looks like consistent behavior. Fix: the \`isMultiProject\` guard at list.ts:1144-1148 is the ONLY client-side issue sort — it's intentionally skipped for single-project results because re-sorting would silently replace the server's recommended ranking with a \`lastSeen\` fallback. \`getComparator('recommended')\` returns \`compareDates(a.lastSeen, b.lastSeen)\` — same as \`date\` sort — so applying it to single-project results would corrupt the server's relevance ranking without any visible error. @@ -272,21 +215,18 @@ * **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\`. + * **node-sqlite3-wasm param passing incompatibility**: Trap: node-sqlite3-wasm uses array parameter binding while node:sqlite uses spread syntax. Looks like drivers should be interchangeable. Fix: adapter layer in sqlite.ts converts between formats. WAL not supported in WASM version. - -* **OOM on large non-DIF files — header peek before full read**: Trap: \`prepareDifs\` reads entire file into memory before calling \`parseDebugFile\`. Large files that aren't DIFs (e.g. videos, logs) cause OOM. Fix: call \`peekFormat\` on a small header chunk first to reject non-object files early, then only read the full file if it passes. Pattern documented in \`src/lib/dif/scan.ts\` with \`peek(16)\` + \`peekFormat(header)\`. - -* **OpenCode memoizes skill list at session start — changes to SKILL.md not picked up until restart**: Trap: modifying \`~/.claude/skills/sentry-cli/SKILL.md\` and seeing count=6 (sentry-cli absent) in the current session looks like a parse/load failure. Fix: OpenCode's \`InstanceState.make\` caches skill discovery once per instance at session start — \`opencode debug skill\` from a fresh invocation shows the true live count (7 including sentry-cli). Stale session snapshots always show the count from when the session started. To verify skill loading, always run \`opencode debug skill\` from a new shell rather than checking \`available\_skills\` in an already-running session. +* **OpenCode memoizes skill discovery at session start — no hot-reload of skill files or SKILL.md changes**: Trap: modifying \`~/.claude/skills/sentry-cli/SKILL.md\` and seeing count=6 (sentry-cli absent) in the current session looks like a parse/load failure. Fix: OpenCode's \`InstanceState.make\` caches skill discovery once per instance at session start — \`opencode debug skill\` from a fresh invocation shows the true live count (7 including sentry-cli). Stale session snapshots always show the count from when the session started. To verify skill loading, always run \`opencode debug skill\` from a new shell rather than checking \`available\_skills\` in an already-running session. * **OpenCode not detected for skill installation — only .claude and .agents roots are supported**: Trap: OpenCode is detected in \`src/lib/detect-agent.ts\` via \`OPENCODE\_CLIENT\` env var and \`PROCESS\_NAME\_AGENTS\` map — looks like it should drive skill installation. But detection is for telemetry only. \`installAgentSkills()\` and \`src/commands/cli/uninstall.ts\` hardcode \`agentRoots = \['.claude', '.agents']\` — OpenCode is never a skill install target. Fix: to add OpenCode skill support, add its root dir to \`agentRoots\` in both \`agent-skills.ts\` and \`uninstall.ts\`, and add a \`detectOpenCode()\` function parallel to \`detectClaudeCode()\`. - -* **OpenCode skill scan is session-scoped — new skill files installed mid-session are invisible until restart**: Trap: \`installAgentSkills()\` writes skill files to \`~/.claude/skills/sentry-cli/\` and the files appear on disk immediately — looks like OpenCode will pick them up. But OpenCode scans skills once per session at startup via \`InstanceState.make\` and memoizes the result for the session lifetime — no hot-reload. Fix: any session that started before the skill directory was created will show the old count forever. Users must start a new OpenCode session after \`sentry setup\` installs skills. Confirmed: \`init count=6\` appeared in every log across 2 days because \`~/.claude/skills/sentry-cli/\` didn't exist until Jun 25 19:38 UTC; \`debug skill\` immediately returned 7 in a fresh process. - * **OutputError must not be preceded by a yield — causes double-render**: Trap: \`OutputError\` looks like a normal error you can throw after yielding a partial result, since other error types allow prior yields. Fix: \`OutputError\` (src/lib/errors.ts:292) is handled in \`src/lib/command.ts:723\` by re-rendering \`err.data\` via \`handleYieldedValue()\` then re-throwing — so any prior \`yield\` of the same data causes double-render. For FAILED/NOT\_RAN terminal states in \`build size\`, throw \`OutputError(result)\` directly without yielding first. @@ -297,7 +237,13 @@ * **pnpm nested script invocation loses TTY — inline tsx to fix**: Trap: pnpm nested script invocation loses TTY — inline tsx to fix — Trap: \`"cli": "pnpm tsx src/bin.ts"\` creates nested pnpm invocations (pnpm → /bin/sh → pnpm → /bin/sh → tsx → node). Each inner pnpm layer pipes stdio, so \`process.stdin.isTTY\` and \`process.stdout.isTTY\` are \`undefined\` in the final Node process. Fix: inline tsx directly — \`"cli": "tsx --import ./script/require-shim.mjs src/bin.ts"\` and same for \`dev\`. -* **pnpm test triggers generate:docs + generate:sdk pre-steps — always times out in CI-like environments**: Trap: \`pnpm test\` looks like the standard way to run tests. But \`test:unit\` = \`pnpm run generate:docs && pnpm run generate:sdk && vitest run test/lib test/commands test/types --coverage\` — the doc/SDK generation pre-steps cause 120s+ timeouts. Fix: run vitest directly on specific test files: \`npx vitest run test/lib/dif test/commands/debug-files\` (or similar scoped paths). Use \`pnpm test\` only for final pre-commit validation. \`test:init-eval\` is the only script without the preamble (uses \`--testTimeout 600000\` instead). +* **pnpm test runs generate:docs + generate:sdk pre-steps — causes multi-minute runs/timeouts**: Trap: \`pnpm test\` looks like the standard way to run tests. But \`test:unit\` = \`pnpm run generate:docs && pnpm run generate:sdk && vitest run test/lib test/commands test/types --coverage\` — the doc/SDK generation pre-steps cause 120s+ timeouts. Fix: run vitest directly on specific test files: \`npx vitest run test/lib/dif test/commands/debug-files\` (or similar scoped paths). Use \`pnpm test\` only for final pre-commit validation. \`test:init-eval\` is the only script without the preamble (uses \`--testTimeout 600000\` instead). + + +* **PR auto-merge completes before manual squash-merge step**: Trap: after confirming CI is green, issuing an explicit "squash merge PR #N" command looks like the required next step — but if auto-merge was enabled on the PR (e.g. by the bot that opened it), GitHub merges it automatically the instant CI turns green, with no explicit merge action from the assistant. Confirmed on getsentry/cli PR #1287: \`gh pr view\` returned mergeStateStatus/mergeable=UNKNOWN and state=MERGED before any merge command was issued. Fix: after CI goes green, re-fetch PR state (\`gh pr view --json state,mergeStateStatus,mergeCommit\`) before attempting to merge — if already MERGED, just verify the merge commit has exactly 1 parent (confirms squash, not a regular merge) and move on. + + +* **prepareDifs: peek header before full read (OOM) and extract helper (Biome complexity cap 15)**: Gotcha: prepareDifs in scan.ts: peek header before full read (OOM) and extract per-file helper to stay under Biome complexity cap of 15. Trap: adding a ZIP expansion branch to \`prepareDifs\` looks like a small addition, but it pushes cognitive complexity above Biome's 15 cap. Also, calling \`Archive\` parsing on oversized files risks OOM. Fix: extract per-file processing into a private helper \`prepareFileDif(path, filters, maxFileSize)\` returning \`{ dif: PreparedDif | null; oversized: boolean }\`, and peek only the first few header bytes (e.g. \`peekFormat()\` over first 4096) before doing a full parse. Pattern: when adding conditional branches to existing complex functions, check Biome complexity score first and pre-extract helpers. * **prepareZipDifs null return means 'not a zip' OR 'container skipped' — non-null (even empty) means fully handled**: Trap: \`null\` from \`prepareZipDifs\` previously meant only 'not a ZIP'. After the maxTotalSize refactor, \`null\` also means 'container skipped wholesale (too large / malformed)' — both cases should fall through to normal file handling. Fix: the contract is now 'non-null result (even empty array) means the path was a \`.zip\` and is fully handled; \`null\` means fall through.' The \`continue\` in \`prepareDifs\` fires on any non-null result, so an empty array correctly skips the file without treating it as a non-ZIP. Document this dual-null semantics in JSDoc on \`prepareZipDifs\`. @@ -317,11 +263,8 @@ * **ruzstd partial decompression: must validate output size explicitly**: Trap: \`ruzstd::StreamingDecoder\` (unlike \`zstd::bulk::decompress\`) silently returns a partial result when passed a too-small \`size\` — it does NOT error. Fix: read \`size + 1\` bytes into the output buffer, then assert \`decompressed.len() == size\`; return \`None\` on mismatch. This matches \`zstd::bulk::decompress\` error-on-mismatch semantics. Confirmed via test: exact(560)→Some(560)✓, toosmall(550)→None✓, toolarge(570)→None✓. - -* **scan.ts difFromBuffer: calls embeddedPpdbDif for ALL formats — doubles WASM memory for non-PE files**: Trap: \`difFromBuffer()\` (scan.ts:662) calls \`embeddedPpdbDif()\` unconditionally for every buffer regardless of format — ELF, Mach-O, Breakpad all trigger a full \`new Archive(data)\` + \`objects()\` + \`asPe()\` before returning \`undefined\`. Looks correct because \`asPe()\` returns \`undefined\` for non-PE. But it contradicts the peek-before-parse strategy and doubles peak WASM linear memory across the whole scan. Fix: gate \`embeddedPpdbDif\` on \`format === 'pe'\` by threading \`peeked.format\` (on-disk path, scan.ts:511) or \`format\` (zip path, scan.ts:722) into \`difFromBuffer\`; or cheap-gate inside \`embeddedPpdbDif\` via \`peekFormat(header)\` before full parse. - - -* **scan.ts: oversized PE early-return must not skip embedded PPDB extraction**: Trap: in \`prepareFileDif\` (src/lib/dif/scan.ts), gating on a PE's on-disk size vs \`maxFileSize\` looks like a safe early-exit optimization — but it isn't PPDB-aware. A large assembly can contain a small embedded .pdb well within the limit, yet the pre-fix code returned before \`embeddedPpdbDif\` ever ran, silently dropping the extractable PDB (Bugbot finding, PR #1163, bug 47). Fix: \`embeddedPpdbDif\` cheap-peeks only the first \`PEEK\_HEADER\_BYTES\` (4096) via \`peekFormat()\` to confirm PE format before doing a second full \`Archive\` parse, so oversized-container gating never blocks embedded-content extraction. The oversized signal instead threads end-to-end: \`embeddedPpdbDif\` → \`difFromBuffer\` → \`readMatchedDif\` → \`prepareFileDif\` → \`prepareDifs\` (\`oversizedCount+=1\`), and is only counted when \`portablepdb\` format is actually requested via \`formatMatches\`. + +* **scan.ts: oversized/format-filtered PE gating must not skip embedded PPDB extraction**: Gotcha: scan.ts: oversized PE early-return must not skip embedded PPDB extraction — Trap: in \`prepareFileDif\` (src/lib/dif/scan.ts), gating on a PE's on-disk size vs \`maxFileSize\` looks like a safe early-exit optimization — but it isn't PPDB-aware. A large assembly can contain a small embedded .pdb well within the limit, yet the pre-fix code returned before \`embeddedPpdbDif\` ever ran, silently dropping the extractable PDB. Fix: \`embeddedPpdbDif\` cheap-peeks only the first \`PEEK\_HEADER\_BYTES\` (4096) via \`peekFormat()\` to confirm PE format before doing a second full \`Archive\` parse, so oversized-container gating never blocks embedded-content extraction. * **sentry-cli SKILL.md has non-standard frontmatter fields (version, requires) — safe for OpenCode but worth knowing**: SKILL.md frontmatter includes \`version\` and \`requires: {bins: \["sentry"], auth: true}\` — fields not in the OpenCode skill spec. OpenCode's \`isSkillFrontmatter()\` only validates \`name: string\` and optional \`description: string\`; extra fields are silently ignored. gray-matter (js-yaml) parses the nested \`requires\` object without error. Trap: nested objects in frontmatter look like they'd cause a YAML parse failure or schema rejection. They don't — confirmed via gray-matter test and zero "failed to load skill" log entries. The skill loads correctly; absence from a session is always a stale-snapshot issue, not a parse issue. @@ -345,10 +288,7 @@ * **streamDecompressToFile: never emit 'drain' on ENOSPC — race drain against error to avoid hang**: streamDecompressToFile: never emit 'drain' on ENOSPC — race drain against error to avoid hang -* **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 0.7.0, stripping is built into fossilize itself — it strips the copied binary (already unsigned for macOS/Windows) BEFORE calling postject. Cross-strip from Linux to macOS silently fails (caught); native macOS runners strip correctly with \`strip -x\`. Windows skipped (no debug symbols). \`stripCachedNodeBinaries()\` was removed from \`script/build.ts\` in fossilize 0.7.0 update — fossilize handles it natively. - - -* **SUPERSEDED — org listing no longer fans out per region**: 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\`. +* **strip fails on Node SEA binaries — must strip BEFORE fossilize injection; UPX destroys ELF notes**: Strip debug symbols must happen BEFORE fossilize SEA injection. Trap: \`strip --strip-unneeded\` on a plain Node binary saves ~17 MiB and still runs — looks like it should work on the final SEA binary too. But after postject injects the SEA blob, \`strip\` fails: 'section .text can't be allocated in segment 2'. Fix: as of fossilize 0.7.0, stripping is built into fossilize itself — it strips the copied binary (already unsigned for macOS/Windows) BEFORE calling postject. Cross-strip from Linux to macOS silently fails (caught); native macOS runners strip correctly with \`strip -x\`. Windows skipped (no debug symbols). \`stripCachedNodeBinaries()\` was removed from \`script/build.ts\` in fossilize 0.7.0 update — fossilize handles it natively. * **symbolic-wasm: JS callback errors silently swallowed via .ok()? — must propagate as JsError**: Trap: using \`.ok()?\` on \`call1\` (JS function invocation) and \`dyn\_into::\()\` looks like idiomatic Rust error-to-Option conversion. Fix: both failures must propagate as \`JsError\` to the caller — thrown JS exceptions yield partial bundles with no feedback; non-\`Uint8Array\` returns (ArrayBuffer, plain arrays) silently skip files. Pattern: capture callback error in \`Option\\` outside closure, set it on failure, check after closure returns. Make \`with\_object\` generic over \`E: From\\` so both error paths unify. Flagged by Cursor Bugbot (Medium) and Sentry Seer (Medium) on PR #991. @@ -356,26 +296,20 @@ * **Symlink cycle hang in recursive file collection — use lstat + visited-realpath set**: Trap: \`collectFiles\` uses \`stat\` (follows symlinks) with no cycle detection. Directory symlinks pointing to ancestors cause unbounded recursion — never returns. macOS \`.framework\`/dSYM trees routinely contain cyclic symlinks. Fix: use \`lstat\`, skip symlinked directories, and track visited realpaths in a \`Set\` to break cycles. File symlinks are safe to follow. + +* **test/e2e/bundle.test.ts flaky 60s beforeAll timeout**: Trap: the E2E "npm bundle" test's beforeAll hook (test/e2e/bundle.test.ts) runs \`pnpm run bundle\` (esbuild) with a 60s timeout; it can flake-timeout even on unrelated PRs (e.g. docs-only dependency bumps that never touch anything the bundle consumes), making it look like the PR broke something. Fix: check whether main is green on the same test/job to rule out a regression, then just re-run the failed CI job — no code change needed. Confirmed on getsentry/cli PR #1287 (docs-only @sentry/starlight-theme 0.7.0→0.8.0 bump): main was green on the same test, re-run passed, PR merged clean. + * **Upload assembly \`not\_found\` after deadline is a real failure — must set exit code 1**: Trap: upload assembly only treated \`"error"\` state as failure; \`"not\_found"\` was treated as incomplete (exit 0 with debug log). But after deadline, \`not\_found\` means chunks were never delivered — a genuine failure. Fix: treat \`not\_found\` as failure with \`log.warn\` + exit code 1. Also upgrade deadline-break log from \`debug\` to \`warn\`. Discovered during self-review of \`debug-files upload\` PR. - -* **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. - -* **WASM handles (Archive/ObjectFile/PeFile) must be freed — new DIF functions multiply live handles**: Gotcha: scan.ts: oversized PE early-return must not skip embedded PPDB extraction — Trap: in \`prepareFileDif\` (src/lib/dif/scan.ts), gating on a PE's on-disk size vs \`maxFileSize\` looks like a safe early-exit optimization — but it isn't PPDB-aware. A large assembly can contain a small embedded .pdb well within the limit, yet the pre-fix code returned before \`embeddedPpdbDif\` ever ran, silently dropping the extractable PDB. Fix: \`embeddedPpdbDif\` cheap-peeks only the first \`PEEK\_HEADER\_BYTES\` (4096) via \`peekFormat()\` to confirm PE format before doing a second full \`Archive\` parse, so oversized-container gating never blocks embedded-content extraction. - * **WASM SQLite close() can delete different process's live lock**: Trap: WASM SQLite close() calls rmdirSync on the lock directory, which looks like correct cleanup. But if another CLI process has the same database open and created the lock directory, this rmdirSync will succeed (directory is empty) and delete the other process's lock directory out from under it. Fix: Only rmdirSync if this.db.\_isLocked() returns true — never delete a lock directory that this connection didn't create. Applies to src/lib/db/sqlite.ts lines 327-339. -* **wasm-pack test --node never catches js\_name/js\_class binding bugs like Object shadowing**: Trap: \`wasm-pack test --node\` looks like a complete test of the WASM package — it runs Rust tests compiled to WASM. But it builds its own JS glue and never loads the \`--target web\` artifact. So \`export class Object\` shadowing the JS global \`Object\` passes all wasm-pack tests. Fix: use the two-layer approach — (1) \`wasm\_bindgen\_test\` + \`wasm-pack test\` for bulk behavior, (2) artifact smoke test that does \`npm pack\` → install into temp dir → \`import "@sentry/symbolic"\` → assert API loads. The smoke test catches packaging regressions that wasm-pack misses. Fix for Object shadowing: \`#\[wasm\_bindgen(js\_name = "ObjectFile")]\` + \`#\[wasm\_bindgen(js\_class = "ObjectFile")]\`; Rust struct name \`Object\` unchanged. - - -* **wasm-pack test never loads the published package — builds its own glue instead**: Trap: \`wasm-pack test\` looks like the right way to test the published \`@sentry/symbolic\` package (it builds WASM and runs tests). Fix: \`wasm-pack test\` builds its own glue (\`--target web\`, \`symbolic.js\`, \`initSync\`) and never loads what the package actually ships via \`exports\`/\`files\`. Testing the published package requires loading it through the actual package entry points, not wasm-pack's test harness. Confirmed by Burak Yigit Kaya on Jun 22 2026. +* **wasm-pack test never tests the published package — builds its own glue instead**: Trap: \`wasm-pack test --node\` looks like a complete test of the WASM package — it runs Rust tests compiled to WASM. But it builds its own JS glue and never loads the \`--target web\` artifact. So \`export class Object\` shadowing the JS global \`Object\` passes all wasm-pack tests. Fix: use the two-layer approach — (1) \`wasm\_bindgen\_test\` + \`wasm-pack test\` for bulk behavior, (2) artifact smoke test that does \`npm pack\` → install into temp dir → \`import "@sentry/symbolic"\` → assert API loads. The smoke test catches packaging regressions that wasm-pack misses. Fix for Object shadowing: \`#\[wasm\_bindgen(js\_name = "ObjectFile")]\` + \`#\[wasm\_bindgen(js\_class = "ObjectFile")]\`; Rust struct name \`Object\` unchanged. * **Whole-buffer matchAll slower than split+test when aggregated over many files**: Grep/scan traps in \`src/lib/scan/\`: (1) Whole-buffer \`regex.exec\` 12× faster per-file but ~1.6× SLOWER over 10k files — early-exit at \`maxResults\` via \`mapFilesConcurrent.onResult\` wins. (2) Literal prefilter is FILE-LEVEL gate (\`indexOf\`→skip); per-line verify breaks cross-newline patterns and Unicode length-changing \`toLowerCase\`. (3) Extractor \`hasTopLevelAlternation\`+\`skipGroup\` must call \`skipCharacterClass\`. (4) Wake-latch race: use latched \`pendingWake\` flag, not \`let notify=null; await new Promise(r=>notify=r)\`. (5) \`mapFilesConcurrent\` filters \`null\` but NOT \`\[]\` — return \`null\` for no-op files. (6) \`collectGlob\`/\`collectGrep\` must NOT forward \`maxResults\` to iterator; drain uncapped, set \`truncated=true\`. Worker pool: lazy singleton, size \`min(8, max(2, availableParallelism()))\`. Matches encoded as \`Uint32Array\` quads transferred via \`postMessage\` (~40% faster). \`new Worker(new URL(...))\` HANGS in SEA binaries — use Blob+URL.createObjectURL. FIFO \`pending\` queue per worker. \`ref()\`/\`unref()\` idempotent — only unref when \`inflight\` drops to 0. Disable via \`SENTRY\_SCAN\_DISABLE\_WORKERS=1\`. @@ -403,14 +337,11 @@ * **atomicWriteFile in agent-skills.ts: same-dir temp + rename guarantees no partial reads**: \`atomicWriteFile(destPath, content)\` at \`src/lib/agent-skills.ts:72\`: writes to \`.\.\.\.tmp\` in the same directory as \`destPath\`, then calls \`rename()\` into place. Same-directory placement guarantees same filesystem → POSIX atomic rename. Concurrent readers never observe a truncated or partially-written file. Temp file is cleaned up on error. Used by \`writeSkillFiles()\` (replacing in-place \`writeFile\`). Skills are written on every version — write-if-changed optimization was explicitly rejected as unnecessary. - -* **bundle-sources exit-code convention: manual exitCode=1 vs OutputError for no-sources path**: In \`src/commands/debug-files/bundle-sources.ts\`, the no-sources-found path uses \`this.process.exitCode = 1\` (not \`OutputError\`). This is a known Medium deviation from the framework-blessed pattern: \`OutputError\` (exit 60) is robust against framework finalization resetting exitCode; manual assignment is fragile. The \`-o\` flag also does NOT create parent directories (unlike \`bundle-jvm\`), so \`writeFile\` throws raw ENOENT if the parent doesn't exist. Both are accepted as-is in PR #1126 — do not 'fix' them without a follow-up PR discussion. Exit code map: ValidationError→21, success→0, no-sources→1. - * **CI Node version pinning: centralized env block per workflow file, ternary for matrix jobs**: Node CVE-2026-48931 fix: \`NODE\_VERSION\_22="22.23.1"\`, \`NODE\_VERSION\_24="24.18.0"\` (22.23.0 had the vulnerability; fix landed in 22.23.1 via nodejs/node#64004). Pattern: add top-level \`env:\` block to each workflow file (ci.yml, release.yml, sentry-release.yml, docs-preview.yml) with both constants + rationale comment. Reference via \`${{ env.NODE\_VERSION\_22 }}\`. Matrix jobs (build-npm) use ternary: \`${{ matrix.node == '24' && env.NODE\_VERSION\_24 || env.NODE\_VERSION\_22 }}\` — matrix labels stay as bare majors (\`\["22","24"]\`) for job naming. Gotcha: \`eval-skill-fork.yml\` has no \`setup-node\` step at all — must add one explicitly \[\[019f03bb-f9cf-7208-a183-d4f0074480f9]]. -* **clack-utils.ts 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. +* **clack-utils.ts rename deferred (still pre-monorepo path)**: \`src/lib/init/clack-utils.ts\` filename kept (not renamed to \`wizard-utils.ts\`) to keep PR 4 diff focused on clack removal. No clack references remain in the file. \`WizardCancelledError\` lives here. \`abortIfCancelled\()\` return type uses \`Exclude\\` to narrow union types. \`FEATURE\_DISPLAY\_ORDER\` and \`CANONICAL\_STEP\_ORDER\` (12 steps) also defined here. Post-monorepo: full path is \`packages/cli/src/lib/init/clack-utils.ts\`. Rename still intentionally deferred — pre-shape merge does NOT depend on it. * **createSourceBundle: object selection, sync provider contract, writer lifecycle**: \`createSourceBundle(data, objectName, readSource)\` in \`src/lib/dif/index.ts\`: selects \`objects.find(o => o.hasDebugInfo) ?? objects\[0]\`; returns \`{bundle:null, debugId:null, fileCount:0}\` if no objects. \`SourceBundleWriter.writeObject\` is synchronous — provider/filter callbacks must be sync (\`readFileSync\` in bundle-sources.ts). Writer is single-use: \`writeObject\` calls \`\_\_destroy\_into\_raw()\` (zeroes ptr, unregisters FinalizationRegistry). Provider returning \`null\` signals skip (WASM glue checks \`arg0 == null\`). \`bundle === null || fileCount === 0\` correctly catches manifest-only ZIPs with zero source files. @@ -445,6 +376,9 @@ * **idle.ts eviction: upstream uses per-function cleanup in idle.ts, not centralized evictSession in pipeline.ts**: Upstream (main branch) puts session eviction logic directly in \`idle.ts\` rather than a centralized \`evictSession()\` in \`pipeline.ts\`. \`idle.ts\` imports cleanup functions individually: \`evictSession as evictGradientSession\` from \`@loreai/core\`; also \`deleteSessionAuth\`, \`clearAuthStale\` from \`./auth\`; \`deleteSessionCosts\` from \`./cost-tracker\`; \`deleteBillingPrefix\` from \`./cch\`; \`clearWarmupAuthDisabled\` from \`./cache-warmer\`. The \`startIdleScheduler\` signature uses \`onEvict?: (sessionID: string) => void\` (upstream) vs \`onEvictSession?: (sessionID: string) => boolean\` (branch). Upstream inline \`onEvict\` in \`pipeline.ts\` cleans 5 Maps: \`headerSessionIndex\`, \`ltmSessionCache\`, \`ltmPinnedText\`, \`stableLtmCache\`, \`cwdWarned\`. When merging, adopt upstream's per-function approach and add any missing cleanup calls. + +* **Monorepo split: packages/cli scripts must preserve root script semantics + paths**: Post-monorepo-split \`packages/cli/package.json\` scripts must mirror root scripts verbatim — including: (1) \`generate:docs\` chains \`generate:banner\` + \`generate:parser\` BEFORE other generators (parser outputs under \`src/generated/\` are gitignored); (2) \`typecheck\`, \`build\`, \`bundle\` MUST run \`generate:docs\` + \`generate:sdk\` as prerequisites (\`src/sdk.generated.ts\` is gitignored and imported by \`src/index.ts\` — bare \`tsc --noEmit\` fails on clean checkout); (3) \`test:e2e\` and \`test:changed\` use direct \`vitest\` invocation; (4) \`check:\*\` scripts point at the real script filenames (\`generate-api-schema.ts\`, \`check-no-deps.ts\`, \`check-error-patterns.ts\`, \`check-stale-references.ts\`, \`generate-banner-sixel.ts\`, \`generate-docs-sections.ts --check\`). Bugbot B1 + B2 caught PR #1254 dropping these chain prerequisites in the rebased base. Fix: take main's \`scripts\` block verbatim into \`packages/cli/package.json\` rather than re-deriving. ADDITIONAL gotcha: ci.yml \`code\` paths-filter must include root workspace config files (\`package.json\`, \`.npmrc\`, \`pnpm-workspace.yaml\`) — \`pnpm.patchedDependencies\`/\`pnpm.overrides\`/\`node-linker\` moved there post-split. + * **Node version floor documentation pattern**: Uses dual version floors: engines.node (>=18.0) for runtime minimum, devEngines.runtime (>=22.15) for development tooling. Documentation generators use dev floor for dev prereqs, runtime floor for library usage. @@ -454,9 +388,6 @@ * **Node.js version matrix testing pattern for npm package**: Build npm package on development Node floor (22.15+), smoke-test on matrix Node versions. CI env vars: NODE\_VERSION\_20: "20.20.2" (WASM-SQLite floor), NODE\_VERSION\_22: "22.23.1", NODE\_VERSION\_24: "24.18.0". Build job: setup-node uses env.NODE\_VERSION\_22. Smoke-test job: setup-node uses matrix.node with fallback to env vars. Ensures fallback path is exercised in CI. - -* **parseIssueArg multi-line handling: first non-blank line wins (CLI-1G1)**: Issue identifiers are always single-line atomic tokens. \`parseIssueArg\` in \`src/lib/arg-parsing.ts\` uses \`LINE\_SPLIT\_PATTERN = /\r?\n/\` to split input, trims each line, and takes the first non-empty line (PR #1148, CLI-1G1). Rationale: \`.trim()\` only strips leading/trailing whitespace — embedded newlines from command substitution or agent note-appending survive and reach \`validateResourceId\`. Joining lines (like the \`api.ts\` LINE\_BREAK\_PATTERN approach) is wrong for atomic tokens because \`CLI-ABC\n\\` would produce garbage. Splitting on \`\n\` is safe because newlines are control characters already rejected by \`validateResourceId\` — splitting never breaks project display names with spaces. - * **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\`. @@ -472,9 +403,6 @@ * **sensitive argv flags must never reach telemetry — redactArgv() in cli.ts**: \`SENSITIVE\_ARGV\_FLAGS = new Set(\['token', 'auth-token'])\` in \`src/cli.ts\`. \`redactArgv()\` replaces values of these flags with \`\[REDACTED]\` before any telemetry call. This is an absolute invariant — never pass raw \`process.argv\` to telemetry without running through \`redactArgv()\` first. - -* **Sentry SDK tree-shaking patches must be regenerated via bun patch workflow**: Sentry SDK tree-shaking patches must be regenerated via pnpm patch workflow. Regenerate \`@sentry/core\` and \`@sentry/node-core\` patches after bumping to catalog version 10.54.0. - * **sentry-cli banner local test commands (isTTY-gated)**: Banner only renders in interactive terminals (\`if (process.stdout.isTTY)\`). Test commands: \`pnpm cli help\` or \`pnpm cli\` (fastest, tsx via src/bin.ts, no build); \`pnpm cli init\` (Ink wizard banner path). Quick sanity check without CLI: \`FORCE\_COLOR=3 pnpm exec tsx -e "import('./src/lib/banner.ts').then(m => console.log(m.formatBanner()))"\`. Env vars: \`FORCE\_COLOR=3\` forces color output; \`NO\_COLOR\` or \`SENTRY\_PLAIN\_OUTPUT\` strips gradient. @@ -501,89 +429,29 @@ ### Preference - -* **Always add new check scripts to both package.json and CI workflow**: When introducing a new check or validation script, the user expects it to be registered in two places simultaneously: (1) as a named script in \`package.json\` alongside other \`check:\*\` scripts, and (2) as a \`- run: pnpm run \\` step in \`.github/workflows/ci.yml\`. Never add to only one location. This applies to any new linting, validation, or verification script added to the project. - - -* **Always check CI/PR status after opening or updating a pull request**: After creating or pushing changes to a pull request, the user consistently checks the CI check status (e.g., via 'gh pr checks' or similar tooling) to monitor pass/fail/pending state of automated checks such as lint, typecheck, unit tests, security scans, and bot code reviews (Cursor Bugbot, Seer Code Review). When assisting, proactively run or offer to run PR status checks after PR creation/updates, summarize pass/pending/fail counts, and flag any pending or failing checks that need attention before considering the PR ready to merge. - - -* **Always clarify that the repo uses plain git (not jj) when jj commands fail**: When a jj command fails with 'no jj repo in .', the user consistently clarifies that the repo is a plain git repo and that jj's 'never fails on conflict' behavior is being referenced conceptually — meaning conflicts should be recorded/resolved rather than aborting operations. The agent should: (1) fall back to git commands immediately without retrying jj, (2) handle merge conflicts by stashing, pulling, and resolving (e.g., \`git checkout --theirs\` for files like \`.lore.md\`), and (3) not attempt \`jj git init\` or any jj initialization. This pattern appears at the start of every build session. - - -* **Always compare PR branch against main before reviewing changes**: When reviewing a PR, the user consistently wants to understand exactly what changed in the PR branch versus main before diving into the content. This means fetching the remote branch if not available locally, running \`git log main..origin/\\` to see commits, and \`git diff\` (with stat) to understand the scope of changes. The user explicitly frames this as needing to know 'what changes were made vs what actually exists on main.' Always establish this baseline diff context first before analyzing or discussing PR content. - - -* **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 create a dedicated branch when updating fossilize versions**: When a new version of fossilize is released, always create a branch named \`chore/fossilize-{version}\` tracking origin/main, update the dependency, remove any functionality now handled natively by fossilize (e.g., \`stripCachedNodeBinaries()\` removed in 0.7.0), verify the build succeeds, then commit with \`chore: update fossilize to X.Y.Z\`. Follow this exact pattern: branch → update dep → remove superseded code → build verify → commit → PR. - - -* **Always Directs to Resolve Merge Conflicts and Ensure Compatibility**: The user consistently directs the assistant to resolve merge conflicts, ensure compatibility, and verify code changes before merging. This includes rebasing branches, resolving conflicts, and verifying code changes. The user also emphasizes the importance of backwards and forwards compatibility during transitions and releases. - -* **Always ensures merge readiness before merging**: The user consistently seeks to ensure that all prerequisites are met before merging, including assessing the impact of changes, verifying CI checks, evaluating test coverage, and confirming the correctness of the code and its documentation. The user also tends to consider the user-facing aspects and potential issues that may arise after merging. - - -* **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 ensure merge readiness before merging**: Always ensures merge readiness before merging: assess change impact, verify all CI checks pass, evaluate test coverage, confirm code and documentation correctness, and consider user-facing implications. Standard flow: push, wait for CI green, diagnose any failures (distinguishing flaky/infra from real regressions), then squash-merge only when mergeStateStatus=CLEAN. * **Always fetchable from the base repo with github**: User stated always fetchable from the base repo with github. - -* **Always flag import framework mismatches as blocking CI issues in PR reviews**: When reviewing PRs, the user consistently identifies test files using the wrong import framework (e.g., \`bun:test\` instead of \`vitest\`) as a BLOCKING issue, not a non-blocking suggestion. This applies when the project has migrated frameworks and all other test files use the new one. The user expects the reviewer/assistant to explicitly label it as blocking (B1, B2, etc.) and distinguish it from non-blocking issues (N1, N2, etc.), using a clear severity classification system in PR review feedback. - - -* **Always follow a standard workflow for PR creation and merging**: The user consistently enforces a standard workflow for creating and merging Pull Requests (PRs). This workflow includes creating a branch with a specific naming convention (e.g., \`fix/\-\\`), committing changes with a \`.lore.md\` file, creating a PR, and waiting for CI checks and bot reviews to complete before merging. The user also emphasizes the importance of proper issue tracking, cross-linking tasks, and regenerating patches when necessary. - * **Always guard Node.js API availability at module top-level**: User consistently identifies and fixes module-level calls to Node.js APIs that may not exist in certain versions. They require wrapping such calls in runtime availability checks (e.g., \`typeof api === 'function'\`) to prevent import-time crashes. This applies particularly to newer APIs like \`zlib.zstdCompress\` that aren't available in older Node.js versions. - -* **Always inject debug IDs and sourcemaps for local builds**: The user consistently requires debug IDs to be injected into JavaScript bundles during local builds, even when no authentication token is present. This ensures local development always has debug information available. The user also mandates that sourcemaps are never shipped to end users in server-side deployments, maintaining security while preserving debuggability during development. + +* **Always implement robust lock management with safety guarantees**: The user consistently implements and refines database lock management with strong safety guarantees. They ensure locks are never re-touched after release, never deleted while potentially in use by another process, and never leave empty directories behind. The user methodically tests edge cases like PID reuse scenarios and stale lock recovery, always adding comprehensive tests for each lock management behavior. They prioritize preventing cross-process database corruption through careful lock cleanup strategies. - -* **Always inject debug IDs even without auth token**: Always inject debug IDs (even without auth token); upload is gated inside the plugin. This ensures debug information is available for analysis regardless of authentication status. - - -* **Always investigate build configurations and dependencies when researching Node version compatibility**: The user consistently researches build configurations (esbuild, bundle scripts) and dependency management when evaluating Node version compatibility. They specifically examine how external dependencies like SQLite modules are handled, including import patterns, externalization in bundlers, and fallback mechanisms. The user wants to understand the complete dependency chain and build pipeline implications when supporting different Node versions. - - -* **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 inject debug IDs (even without auth); never ship sourcemaps to users**: Always inject debug IDs into JS bundles during local builds, even when no auth token is present — upload is gated inside the plugin. This ensures debug info is always available for analysis regardless of auth status. Also: never ship sourcemaps to end users in server-side deployments, while preserving debuggability during development. -* **Always maintain zero runtime dependencies with bundled WASM fallback**: User consistently requires that the CLI maintain zero runtime dependencies. For features requiring native modules (like SQLite), they prefer bundling a WASM fallback driver in the npm package for older Node versions, while using native modules on newer Node versions. The standalone SEA binary must always use native modules and never bundle WASM. All dependencies must be devDependencies, bundled at build time via esbuild, with CI enforcing zero runtime dependencies. +* **Always maintain zero runtime deps with WASM fallback for older Node**: Always maintain zero runtime dependencies: all deps are devDependencies bundled at build time via esbuild; CI enforces this (e.g., \`bun run check:deps\` / \`pnpm run check:deps\`). For features needing native modules (like SQLite), use native on newer Node versions and bundle a WASM fallback driver in the npm package for older Node; the standalone SEA binary must always use native modules and never bundle WASM. * **Always migrate Bun-specific APIs and tooling to Node.js equivalents**: Always migrate Bun-specific APIs and tooling to Node.js equivalents: 🔴 Directive (repeated 25+ sessions): ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. NEVER proactively create documentation files (\*.md) or README files — only create documentation files if explicitly requested by the user. - -* **Always Monitor CI Status**: The user consistently checks and verifies the status of CI checks, including Validate generated files, CodeQL, and other workflows. They also investigate and address failures, often by identifying root causes and applying fixes. Additionally, the user monitors and confirms the resolution of transient issues and ensures that all required checks pass before proceeding with the PR. - - -* **Always monitor PR CI checks until fully green before proceeding**: After opening or updating a PR, the user consistently wants the assistant to poll and report GitHub Actions CI status repeatedly (build jobs, unit tests, E2E tests, lint, security scans, etc.) until all checks pass. When a check is pending, keep polling rather than stopping early. When a check fails, investigate the specific failing job/step (fetch run ID, job ID, logs) to determine if it's a genuine issue or flaky test before deciding next steps. Only consider the PR ready (e.g., for merge) once mergeStateStatus is CLEAN and all required checks are pass/skipping. Report status updates with concrete details: check names, pass/fail/pending state, durations, job IDs, and run URLs. - - -* **Always perform comprehensive adversarial code reviews before merging**: The user consistently requests and performs detailed adversarial code reviews before merging significant changes. This includes: 1) Providing complete code context and test results, 2) Analyzing edge cases and failure scenarios, 3) Identifying security and reliability issues across multiple categories (BLOCKING, SHOULD-FIX, NIT), 4) Verifying fixes through systematic testing, 5) Ensuring CI passes across all configurations. The review covers both implementation details and broader architectural implications, with particular focus on error handling, resource management, and cross-process interactions. - - -* **Always perform rigorous, read-only code reviews with specific focus areas**: The user consistently conducts critical, adversarial code reviews in a read-only manner, focusing on specific technical areas. They provide detailed context about the repository, branch, and changes under review, and specify exact focus areas like path correctness, dependency management, build configurations, and security implications. The user maintains strict read-only discipline, prohibiting any modifications to source files, and often works within isolated worktrees. They expect thorough verification of technical changes against documented requirements and prior decisions. - - -* **Always perform thorough codebase exploration before designing or implementing fixes**: When investigating a bug or feature, the user consistently requests comprehensive upfront exploration across multiple files before any code changes. This includes: reading relevant command and API files completely, searching for all references to key terms/parameters, checking type definitions in SDK/node\_modules, and understanding the full data flow from flags to API calls. The user expects the assistant to map out the entire call chain, identify misleading comments, and surface all related code paths before proposing a solution. Do not jump to fixes — first read all relevant files thoroughly and report findings. - - -* **Always perform thorough quality reviews of PRs distinguishing blocking vs non-blocking issues**: When reviewing PRs, the user expects a structured quality review that: (1) categorizes issues as BLOCKING vs non-blocking/low-priority, (2) verifies each claimed change against the actual codebase, (3) flags LLM-generated planning artifacts (e.g., DOCS-AUDIT.md) that violate repo conventions as blocking issues, (4) checks for missed/inconsistent changes across all affected files, and (5) confirms correct changes are working as intended. The user wants specific file paths and line numbers cited for each issue. Non-blocking issues should still be reported but clearly distinguished from blockers. - - -* **Always plan systemic fixes with structured multi-problem breakdowns before implementation**: When the user identifies documentation or tooling issues, they consistently organize them as numbered problems with precise file locations, line numbers, and root causes before any code is written. They expect the assistant to engage at the planning level first — proposing detection strategies, fix approaches, and tradeoffs — and to consolidate related problems (e.g., merging overlapping tasks) rather than treating each in isolation. Plans are written to files and iterated on. Implementation only follows after the plan is agreed upon. The user prefers systemic/automated fixes (e.g., derive patterns from package.json) over one-off patches. - -* **Always plans for testing or verification of tasks**: The user consistently implies a need for testing or verification of tasks, as seen in multiple instances where they either directly request tests or imply the necessity for verification. This pattern is observed across various sessions where the user is planning and executing tasks, indicating a preference for ensuring that tasks are validated or tested. - - -* **Always prefer systemic/automated solutions over one-off fixes**: When the user identifies errors, gaps, or problems, they explicitly direct the assistant to create or fix systems that prevent the entire class of errors in the future, rather than applying isolated one-off fixes. This applies especially when evaluating code quality, reviewing PRs, or addressing bugs. The user wants automated checks (e.g., CI steps, lint rules, scripts) and general solutions that scale, not patches that only address the immediate symptom. When planning or executing fixes, always ask: 'Can this be automated or systematized?' and prefer that approach. +* **Always plan for and request testing/verification of tasks**: User consistently plans for and requests testing/verification of tasks: either directly requesting tests or implying the necessity for verification after implementation. Observed as a pre-commit/merge requirement — ensure tasks are validated and tested before considering them complete. * **Always prefers or chooses practical, immediate solutions with minimal effort over ideal but more costly alternatives**: The user consistently selects or acquiesces to options that provide immediate relief or unblock tasks with the least effort, even if they are not the most optimal or long-term solutions. This is evident in instances where the assistant presents multiple options for resolving issues, such as shipping a codemod or addressing documentation. The user tends to favor quicker, albeit potentially less ideal, solutions like using 'npx jscodeshift' over more involved approaches like porting to 'jssg' or setting up registry packages. This pattern suggests the user prioritizes expediency and practicality in their decision-making process. @@ -592,40 +460,10 @@ * **Always prefers pnpm over Turbo for task orchestration in monorepos**: The user consistently expresses a preference for using pnpm-native orchestration over Turbo for task management in monorepos. In multiple sessions, the user has indicated a dislike for manual and fragile configuration of Turbo and has sought automated configuration derived from pnpm workspace dependencies. The user has explicitly stated that they want to get rid of Turbo for task orchestration in the \`getsentry/toolkit\` monorepo and instead use pnpm-native orchestration. -* **Always provide complete technical context including source code, diffs, and test results**: The user consistently shares full source code files, specific function implementations, and relevant code snippets when investigating issues. They provide detailed context about how components interact, including constructor behavior, method implementations, and error handling logic. This pattern shows the user expects technical discussions to be grounded in actual code, not just descriptions. - - -* **Always provide comprehensive code evidence when explaining system behavior**: The user consistently provides detailed source code evidence when explaining how a system component works. They share specific code snippets, function implementations, and class structures to support their claims about system behavior, especially regarding database operations, locking mechanisms, and error handling. This pattern shows the user expects technical discussions to be grounded in concrete code evidence rather than abstract descriptions. - - -* **Always provide comprehensive technical context for implementation decisions**: The user consistently provides detailed technical context when making implementation decisions, including code diffs, test results, build configurations, and dependency analysis. This pattern shows the user expects technical discussions to be grounded in concrete evidence and implementation details, with changes justified by specific requirements like Node version compatibility or WASM driver behavior. - - -* **Always provide comprehensive verification artifacts during code reviews**: The user consistently provides detailed verification artifacts when submitting code for review. This includes sharing commit hashes, full code snippets with context, test results across multiple environments, CI status updates, and bot review comments. The user expects reviewers to have complete visibility into the changes and their impact, and proactively shares all relevant information to facilitate thorough review and fast iteration. - - -* **Always read and document full file details before proceeding with analysis or implementation**: When exploring a codebase, the user consistently reads files in full and records comprehensive structured details: exact line counts, all imports, every exported type/interface with their fields, all constants, all function signatures with their logic, and any notable comments or assertions. This applies to both source files and build/tooling scripts. The user expects the assistant to capture and reference these details precisely rather than summarizing loosely. When examining related files (e.g., a module and its consumers), the user reads each completely before drawing conclusions. This pattern applies during architecture exploration, feature planning, and documentation generation tasks. - - -* **Always read source files thoroughly before asking questions or making changes**: The user consistently reads full source files (often 400-900+ lines) and traces complete data flow pipelines across multiple modules before taking action. They examine types, constants, function signatures, and cross-module dependencies in depth. They do not ask clarifying questions upfront — instead they investigate the codebase themselves to build a complete mental model. When helping this user, proactively read all relevant files in full, trace imports and data flows end-to-end, and present comprehensive findings rather than asking what they want to know. Assume they want the full picture, not a summary. - - -* **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 requests exhaustive and detailed research**: The user consistently demonstrates a preference for thorough planning and exhaustive research before making changes to the codebase or repository structure. This includes creating detailed migration plans, researching repository structures, build processes, release infrastructure, and deployment processes. The user also values precision and accuracy in the information provided, often requesting exhaustive and precise inventories of repository structures, build processes, and other relevant details. +* **Always provide full technical context: source, diffs, test results**: Always provide complete technical context for implementation decisions: share full source code files, specific function implementations, constructor behavior, method implementations, error handling logic, code diffs, test results, build configurations, and dependency analysis. Changes must be grounded in concrete code evidence (with line refs) and justified by specific requirements (e.g., Node version compatibility, WASM driver behavior). -* **Always requests exhaustive and precise information**: The user consistently requests detailed and thorough information about repository structures, build, release, deployment, and website infrastructure. They emphasize the need for precision, specifying requirements such as file paths, package names, versions, and config values. The user also often prohibits writing any code, indicating that the focus is on research and reporting. This pattern is observed across multiple sessions, including instances where the user requests information about the sentry-mcp and getsentry/cli repositories. - - -* **Always requests tests after implementation**: The user consistently requests tests or verification after each implementation or code change. This is observed in multiple sessions where the user asks for tests or implies the need for testing/verification after tasks are executed or code is modified. - - -* **Always research distribution architecture before implementation**: The user consistently performs deep architectural research on distribution mechanisms before making implementation decisions. This includes examining package.json (bin, files, exports, engines), build scripts, polyfills, runtime detection, CI/CD workflows, and documented policies. The research is read-only and focuses on understanding how the software is built, bundled, and distributed across different platforms and environments. - - -* **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 request exhaustive and precise research/inventories**: User consistently requests exhaustive and precise information about repository structures, build/release/deployment infrastructure, and package metadata (file paths, package names, versions, config values). Focus is on read-only research and reporting — often explicitly prohibiting writing code. Applies to repos like sentry-mcp, getsentry/cli, etc. * **Always resolve a string (or change the type definition)**: User stated always resolve a string (or change the type definition). @@ -639,23 +477,8 @@ * **Always specify exact version requirements and constraints**: The user consistently provides precise version requirements and technical constraints when discussing dependencies, platform support, and compatibility. This includes specifying exact Node.js version floors (e.g., '>=18.0'), defining version ranges for conditional logic (e.g., 'Node 18–22.14'), and setting strict version pins in CI configurations. The user expects these constraints to be accurately implemented and documented across all relevant files. - -* **Always stage all modified files before committing, not just already-staged ones**: When preparing to commit, the user reviews git status and expects ALL modified files to be staged together — not just files already in the index. If unstaged modified files exist alongside staged ones, the user treats this as an incomplete commit state that needs to be resolved before proceeding. The user reviews the full list of changed files (staged + unstaged) as a checklist against completed tasks, and expects the commit to encompass all related changes from the session as a single coherent unit. - - -* **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. - - -* **Always switch from plan mode to build mode before executing changes**: The user consistently uses a two-phase workflow: first planning (read-only exploration, writing a plan file), then explicitly approving a switch to build/agent mode before any changes are executed. When the user approves the mode switch, the assistant should immediately begin executing the existing plan file — typically by re-reading the key files to be modified. Never execute changes while still in plan mode, even if the plan is complete and approved. Wait for the explicit mode-switch approval before acting. - - -* **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 track pre-existing failures separately from introduced regressions**: When running tests, the user consistently distinguishes between failures that existed before their changes and failures caused by their changes. They verify pre-existing failures by checking out main/stashing changes and confirming the same failures reproduce. Only new failures introduced by the current branch are treated as actionable. When reporting test results, always clarify which failures are pre-existing (with evidence) versus newly introduced, and never treat pre-existing failures as blockers for the current fix. - - -* **Always update configuration files systematically**: The user consistently updates configuration files in a methodical and systematic way, focusing on specific sections or blocks. This pattern is observed in multiple sessions where the user or assistant updates files like \`docs-preview.yml\`, \`ci.yml\`, and others, starting with specific sections such as paths-filter globs and version-read, and proceeding with a detailed examination and edits of the file contents. + +* **Always update .lore.md before committing; always take HEAD on conflicts**: Always verify and update .lore.md before committing; on rebase/cherry-pick conflicts, always take HEAD (main's version) for \`.lore.md\` since it is auto-regenerated. Resolved by stashing with key prefix \`lore-md-pre-rebase-\` before rebase, then restoring and re-committing after. * **Always use absolute language when describing system behavior**: The user consistently uses absolute terms like 'always' and 'never' when describing system behavior, especially regarding code execution paths, API availability, and module usage. This indicates a preference for precise, unambiguous language when documenting or discussing technical implementation details. The assistant should mirror this communication style by using definitive language when describing system behavior. @@ -669,128 +492,50 @@ * **Always Verify and Publish Code Changes with Documentation**: The user consistently requests to publish code changes and file issues for them. They also verify the documentation and ensure it is correct before publishing. The user checks the configuration files, such as configuration.md, which is generated by a script, and ensures that the documentation is up-to-date and accurate. The user also confirms that the CI builds are correct and that the documentation is valid. - -* **Always verify and update .lore.md before committing**: The user consistently checks and updates the .lore.md file before committing changes. This is observed in multiple instances where the user verifies the content of .lore.md, resolves conflicts in it, and ensures it is staged before committing changes to other files. The user also checks that .lore.md is not gitignored and verifies its presence in commit history. - - -* **Always verify and update file/directory paths in CI/CD configurations after repository restructuring**: The user consistently demonstrates a pattern of thoroughly checking and updating file and directory paths in CI/CD configuration files (ci.yml, docs-preview.yml, eval-skill-fork.yml) following repository restructuring. This includes verifying paths for build jobs, cache configurations, test commands, documentation generation, and artifact handling. The user provides specific line numbers and path details, confirming that paths are updated to reflect new locations (e.g., packages/cli/, apps/cli-docs/) and that absolute paths are used where necessary to avoid ambiguity. - - -* **Always Verify and Validate Code Correctness**: The user consistently verifies and validates code correctness across multiple sessions. This includes checking the correctness of functions, ensuring proper scoping of catch blocks, verifying variable destructuring, and confirming the integrity of specific code elements. The user also tends to ask questions about code behavior, compatibility, and potential issues, demonstrating a thorough approach to code review and validation. - - -* **Always verify bot findings and test coverage before merging**: The user consistently demonstrates a pattern of thorough verification before merging changes. They always review bot findings (sentry-warden, Cursor Bugbot, etc.) in detail, share relevant code snippets to verify findings against current code, and run comprehensive tests including edge cases. The user expects to see test results for all scenarios, especially for database operations and signal handling. They also verify that fixes address the root cause and don't introduce new issues. - - -* **Always verify code claims against actual file contents before accepting them as true**: When evaluating PRs, documentation, or assertions about code behavior, the user systematically cross-checks every claim against the actual source files at specific line numbers. They expect the assistant to read the real files, confirm exact line locations, quote the relevant code/comments, and flag discrepancies between what is claimed and what the code actually does. The user marks confirmed findings with 🟡 (verified) and actionable directives with 🔴 (user assertion/directive). Never accept a PR description or assertion at face value — always ground-truth it against the codebase with precise line references. - - -* **Always verify PR claims against actual codebase before accepting changes**: When reviewing a PR, the user consistently directs the assistant to check each stated claim against the real source files on the main branch rather than trusting the PR description or commit messages. This applies especially to documentation PRs: the user wants specific file paths, line numbers, and code excerpts cited as evidence. The user also cross-checks automated tooling (scripts, CI configs) against what they actually produce. When a PR introduces fixes, the user wants confirmation that the underlying problem genuinely existed and that the fix is correct — not just that the PR author says so. Always run the relevant check scripts and grep the codebase directly rather than reasoning from PR metadata alone. - - -* **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 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 write tests alongside implementation**: Behavioral pattern detected across 8 sessions (action: requested-tests). The user consistently demonstrates this behavior. + +* **Always verify Sentry CLI auth and org access before querying issues**: Before running any \`sentry issue list\` or similar query, the user expects verification of: (1) authenticated identity and token expiry, (2) which orgs are accessible, and (3) which org/project the relevant telemetry flows into. If access is insufficient (e.g., 403 on internal org), the user switches accounts to gain access to the correct org (e.g., from \`ben@byk.im\` to \`byk@sentry.io\`). The user also wants proactive identification of which org/project maps to the code being debugged — never assume; always confirm before pulling issue data. * **Call plan\_exit to indicate planning done**: Always call plan\_exit to indicate that planning is done. - -* **Designer Steven Lewis directs Dammit Sans for headings, Rubik for body**: Steven Lewis is the designer for the sentry-cli docs site. His direction: use Dammit Sans for headers, Rubik for body. Dammit Sans will eventually be scoped mainly to larger headings (h1 + some h2s). Dammit Sans missing glyphs: \`/ \* \ | ^ \_ \\\`\` plus \`#\` and \`~\` — fall back to Rubik Variable. - - -* **Enforce internal wrapper conventions over direct Stricli imports**: When implementing or reviewing CLI commands in this codebase, always use the project's internal wrapper modules instead of importing directly from '@stricli/core'. Specifically: import \`buildRouteMap\` from \`../../lib/route-map.js\` (which auto-injects standard aliases: list→ls, view→show, delete→remove/rm, create→new — do not add these manually), and use \`buildCommand\` from \`../../lib/command.js\` for all commands (which injects hidden flags like --log-level, --verbose, --org, --project, --fields, and handles auth/skipRcUrlCheck logic). When adding a new route/command, always update \`src/app.ts\` in two places: the alphabetical import block and the top-level \`routes\` object (plus \`hideRoute\` only if needed). Before implementing new features, research existing similar commands/patterns in the codebase (e.g., mirror \`build upload\` for \`build download\`, reuse streaming logic from \`upgrade.ts\`) rather than writing from scratch. - - -* **Enforce read-only, verification-first workflow for code reviews and asset/config work in getsentry/cli**: When the user requests a code review, PR audit, or investigation in the getsentry/cli repo (or similar), they consistently mandate a strict read-only discipline: never edit, write, or 'fix' files during review; record sha256 checksums (or diffs) of all files that will be touched before running any commands; restore files byte-identically if anything gets mutated; and confirm git status is clean afterward. They also expect rigorous, adversarial verification of concrete claims (e.g., exact character/column widths, banner never wraps, PR description matches actual diff) rather than taking descriptions at face value. When exploring history (PR commits, branch state, prior related PRs/issues), the user wants tool-based evidence (commit SHAs, diffs, metadata) cited rather than assumptions. Apply this pattern whenever asked to review, audit, or verify code/PRs/assets: default to read-only, checksum before/after, and independently verify specific technical assertions. - - -* **Enforce strict read-only adversarial code review protocol for PR reviews**: When asked to review a PR or commit in the getsentry/cli repo, treat it as a critical/adversarial, READ-ONLY review: never modify, write, or fix source files. Before running any commands, record checksums (sha256sum) of files expected to change; if any mutation occurs, restore files byte-identically and confirm \`git status --short\` is clean afterward. Builds/tests are allowed only if output is isolated (e.g., to dist/) and don't touch tracked source. Verify claims empirically rather than trusting the PR description — actually measure/compute things like column widths, row/array lengths, hash equality between duplicated files, and test boundary conditions. Explicitly confirm the scoped set of changed files matches expectations, and flag any invariant (e.g., 'never wraps', behavior parity) that isn't verifiably guaranteed across all consumers/code paths. - - -* **Ensure banner-related file changes stay synchronized across duplicate implementations**: When modifying the CLI banner (BANNER\_ROWS, BANNER\_GRADIENT, or related quadrant-block art/colors), the user expects perfect consistency between all files that duplicate this data, especially src/lib/banner.ts and src/lib/init/ui/ink-ui.ts. Always verify row counts, gradient stops, and byte-for-byte content match (e.g., via sha256 checksums) across duplicated definitions. When requesting reviews, treat this synchronization as a key correctness criterion. Also expect the user to favor incremental, tiered solutions (e.g., full/wordmark/text fallback) for responsive design problems, deferring more complex approaches (like sixel graphics) for later. When given recommended options in prompts, the user typically accepts the assistant's suggested/recommended choice rather than customizing further. + +* **Diagnose CI failures by reading context thoroughly before applying fixes**: When CI fails on a rebased branch, the user expects a methodical root-cause investigation: read the failing job logs, identify the exact import/module path mismatch (e.g., file moved during rebase but import path not updated, git treating rename as delete+add causing files to land at old paths), classify conflicts by category (UU/AU/DU/D), and propose a targeted fix — not a speculative change. Pattern shows the user values understanding WHY a failure occurred (e.g., git rename detection behavior, monorepo path conventions) before committing to a fix, and wants conflicts categorized into resolvable groups before resolution begins. * **Follow consistent code style conventions**: Behavioral pattern detected across 3 sessions (action: corrected-style). The user consistently demonstrates this behavior. - -* **Follow the established git workflow (branch, PR, review)**: Behavioral pattern detected across 6 sessions (action: enforced-workflow). The user consistently demonstrates this behavior. + +* **Follow standard PR workflow: branch, .lore.md commit, wait for CI+reviews**: Always follow a standard workflow for PR creation and merging: create a branch with naming convention (e.g., \`fix/\-\\`), commit changes with a \`.lore.md\` file, create the PR, then wait for CI checks and bot reviews (warden, Bugbot, Seer) to complete before merging. Emphasize proper issue tracking, cross-linking tasks, and regenerating patches when necessary. * **Ink import behavior**: User stated code should 'never calls \`import("ink")\` at runtime —', indicating a preference against runtime imports of Ink library. - -* **Iterate on brand/design assets with review checkpoints and strict read-only verification**: User is actively managing a documentation site rebrand (logos, OG images, accent colors, wordmarks) across multiple sessions, iterating incrementally: choosing among assistant-recommended options for colors/design (preferring the 'recommended' choice), deferring some items to later commits, then circling back to complete them. User also engages in a separate strict adversarial review pass of the same PR, mandating read-only discipline (sha256 checksums before/after, byte-identical restoration, clean git status verification) and expects the assistant to catch discrepancies between PR descriptions and actual diffs. When resuming design work, expect the user to ask to 'proceed with' previously deferred/planned items rather than starting fresh. When reviewing, never modify files—always checksum before touching, verify after, and flag stale/inaccurate PR descriptions relative to the real commit diff. - - -* **Monorepo setup for toolkit**: Always resolve merge conflicts manually in .lore.md — applies to CLI and MCP. This ensures that changes to the changelog are reviewed and intentional. - * **Never throws - errors are caught and reported to Sentry**: Never throws - errors are caught and reported to Sentry. This ensures that the application remains stable and provides useful error information. - -* **Never treat incomplete operations as successful — always surface silent failures**: When reviewing code, the user explicitly asserts that incomplete operations must never report success. Examples from \`debug-files upload\`: \`not\_found\` after deadline is a failure (exit 1), not success (exit 0). Symlink cycles must not hang silently. Missing requested IDs must be surfaced. Any failure path that silently exits 0 or produces no diagnostic is a blocker. - * **Never use node\_modules/**: User stated never to use 'node\_modules/.'. - -* **Never uses form-data at runtime — only dev transitive dependency via @types/node-fetch**: User stated never uses form-data at runtime — only dev transitive dependency via @types/node-fetch. + +* **Perform rigorous read-only adversarial code reviews**: Always perform rigorous, read-only adversarial code reviews: focus on specific technical areas (path correctness, dependency management, build configs, security implications), provide detailed context about repo/branch/changes, maintain strict read-only discipline (often in isolated worktrees), categorize findings (BLOCKING, SHOULD-FIX, NIT), and verify changes against documented requirements and prior decisions. + + +* **Poll PR CI until green then verify merge readiness**: After pushing to a PR, poll CI status across all checks (CodeQL, Unit/E2E Tests, Lint & Typecheck, Seer Code Review, Bugbot, Build Docs, dependency-review, warden, etc.), track pass/fail/pending counts, wait for slow checks to settle, distinguish transient infra failures from real regressions, and confirm merge readiness via mergeStateStatus=CLEAN, mergeable=MERGEABLE, zero non-pass/non-skip before considering work complete. CI green is the authoritative merge gate. - -* **Perform rigorous code-only reviews cross-checked against legacy Rust CLI**: When reviewing ported TypeScript Sentry CLI features (e.g., 'build upload', VCS metadata), the user requests a critical, objective code review without writing any code. Always: (1) fetch and read the corresponding legacy Rust implementation fully to cross-check behavior/parity, (2) read all relevant TS source and test files in full, (3) run \`pnpm exec tsc --noEmit\` and \`pnpm exec vitest run\` on affected test paths and report exit codes/results, (4) produce findings categorized strictly by severity (Critical/High/Medium/Low/Nit), each with exact file:line references and a concrete, specific fix suggestion, (5) be skeptical and find real functional bugs or behavioral divergences from Rust, not style nits, (6) explicitly flag any deviations from documented CLI behavior or use cases as blockers if they break primary workflows. Do not modify code during these review sessions—only report findings and let the user decide on fixes. + +* **Preference for deep SQLite understanding**: User repeatedly asks to understand HOW SQLite is used across multiple sessions, indicating a strong preference for deep technical understanding of database usage patterns. * **Prefers Bun-native APIs over Node**: prefer Bun-native APIs over Node. - -* **Prefers freshly-created location over existing**: \`installAgentSkills()\` in \`src/lib/agent-skills.ts\` returns \`results.find(location => location.created) ?? results\[0] ?? null\` — prefers freshly-created location over existing. Installs to \`~/.agents/skills/sentry-cli/\` and \`~/.claude/skills/sentry-cli/\` only. Detection: \`detectClaudeCode()\` checks \`existsSync(join(homeDir, '.claude'))\`; installer never creates top-level agent roots — presence of root dir is the detection signal. OpenCode is detected via \`OPENCODE\_CLIENT\` env var in \`detect-agent.ts\` for telemetry only — NOT for skill installation. No OpenCode skill install path exists anywhere in the repo. - - -* **Refine ASCII/block-art CLI banner rendering with iterative visual verification**: When working on the Sentry CLI's ASCII/Unicode block-art banner (src/lib/banner.ts), the user is meticulous about pixel-accurate, legible wordmark reproduction. They provide reference images (e.g., sentry-ref.webp), scrutinize letterform legibility (e.g., flagged the 'E' being unreadable), and expect the assistant to: (1) verify image dimensions/scale, (2) analyze font/duty-cycle characteristics, (3) test multiple sampling/rendering strategies (threshold-downsample, monospace grid mapping, block-glyph density), (4) compare aspect ratios and column/row counts against source, and (5) generate multiple candidate options with tradeoffs (legibility vs. scanline texture) for user selection. When reviewing related PRs, the user also enforces strict read-only discipline (sha256sum verification, no edits, clean git status). Always iterate empirically on rendering parameters and present concrete side-by-side comparisons rather than a single guess. - - -* **Request read-only critical code reviews with severity-tagged findings**: When user asks for a code review of a PR/feature (often ports of legacy Rust sentry-cli functionality into the TypeScript CLI), they explicitly forbid writing or editing code — only produce findings. Provide output organized by severity levels (Critical/High/Medium/Low/Nit), with each finding including a concrete file:line reference and a specific suggested fix. User expects a rigorous, skeptical, objective review that surfaces real bugs and correctness/security issues rather than style nits. User typically specifies exact files/functions to read in full and provides context like repo path, branch, PR number, and that the code is a port from the legacy Rust implementation for cross-checking behavioral parity. Assistant should read all specified files fully, compare against the Rust reference when applicable, and compile a structured severity-ranked report without modifying any code. - - -* **Request rigorous port-fidelity code reviews against legacy Rust implementation**: When reviewing TypeScript ports of legacy Rust sentry-cli functionality (e.g., snapshots diff, build VCS metadata), the user expects a critical, objective, findings-only code review (no code changes) that: (1) reads all relevant new files fully, (2) cross-checks logic and semantics line-by-line against the original Rust source and any third-party libraries/binaries it wrapped (e.g., odiff, pixelmatch), (3) verifies wire-format/field names, default option values, and edge-case behavior (e.g., threshold scaling, antialiasing inversion, decoder output formats) match exactly, (4) runs tsc --noEmit and relevant vitest suites as verification, and (5) organizes findings by severity (Critical/High/Medium/Low/Nit) with file:line references and concrete fixes. Assistant should proactively investigate subtle divergences from Rust behavior (e.g., missing event-payload parsing, gitignore handling differences) rather than assuming the port is equivalent. - - -* **Require adversarial read-only subagent review for merged dependency/patch-bump PRs**: When a PR bumps patched dependencies or regenerates patches (e.g., @sentry/core, @sentry/node-core, @stricli/core version bumps with custom tree-shaking patches), after it's merged the user wants a separate, critical, adversarial, read-only review performed in an isolated git worktree checked out at the merged commit with a fresh install. The review must: (1) never modify source, patch, or lockfile files—builds/tests only writing to dist/; (2) verify checksums/git status to prove read-only discipline; (3) check for dangling references in both CJS and ESM builds after export stripping; (4) verify completeness of stripped exports vs new version's additions; (5) confirm no unintended lockfile/dependency changes; (6) validate PR description's numeric claims; (7) run check:patches/tsc and assess safety-net gaps. Use a subagent (or dedicated review pass) with a detailed, prioritized checklist and deliver a clear verdict (e.g., PASS / FOLLOW-UP NEEDED / BLOCKER) at the end. - - + * **Respect explicitly rejected approaches**: Behavioral pattern detected across 13 sessions (action: rejected-approach). The user consistently demonstrates this behavior. - -* **Review code before committing**: Behavioral pattern detected across 9 sessions (action: requested-review). The user consistently demonstrates this behavior. - - -* **Review code before committing**: Behavioral pattern detected across 7 sessions (action: requested-review). The user consistently demonstrates this behavior. - - -* **Rigorously verify pnpm patch files after dependency version bumps**: When updating pinned dependencies (e.g., @sentry/core, @sentry/node-core, @stricli/core) that have corresponding pnpm patchedDependencies, the user requires thorough verification of the patch files: (1) confirm all originally stripped exports/imports are still stripped in the new version, plus any new-in-version exports that reference stripped modules are also stripped, (2) verify no dangling references remain to stripped code across all build targets (cjs/esm, index/light variants), (3) check patch hygiene — no build artifacts, no unintended sourceMappingURL edits, no whitespace-only churn, (4) enumerate all file targets touched by each patch with line numbers, and (5) verify package.json patchedDependencies key format includes the version pin. When assisting with dependency/patch updates, proactively run these completeness and hygiene checks and report exact counts/diffs rather than assuming the patch still applies correctly. - - -* **SQLite usage research for Node 20 support**: User is researching Node 18+ support for Sentry CLI (getsentry/cli) and needs to understand how SQLite is used. Specifically investigating the lack of built-in node:sqlite module in Node 18-22.14 and exploring alternatives like node-sqlite3-wasm fallback. The CLI is distributed both as an npm package and as standalone SEA binaries. The standalone binary always embeds modern LTS Node and uses node:sqlite. The npm bundle MUST inline the WASM driver (it's a devDependency, not shipped as a runtime dep). The project has zero runtime dependencies (all deps are devDependencies, bundled at build time via esbuild). CI enforces zero runtime dependencies with bun run check:deps / pnpm run check:deps. + +* **Run isolated test, full suite, lint, then verify CI after fix**: After applying a fix, follow this verification cascade: (1) run the targeted/isolated test for the changed file, (2) run the full test suite (e.g., \`pnpm test\`, vitest full) to confirm no regressions, (3) verify lint/typecheck remain clean across the monorepo, (4) audit repo for related stragglers (grep), (5) commit with a conventional-commit message, (6) force-push and wait for end-to-end CI green (Unit, E2E, lint, typecheck, security, CodeQL), tracking which previously-failing checks now pass. - -* **Understanding SQLite usage patterns**: User repeatedly asks to understand HOW SQLite is used across multiple sessions, indicating a strong preference for deep technical understanding of database usage patterns. - - -* **Verify patch strip-sets with empirical cross-checks before trusting them**: When maintaining patches that strip exports/modules from vendored packages (e.g., @sentry/core, @sentry/node-core) across version upgrades, the user expects rigorous empirical verification rather than assumption-based patching. This includes: reconstructing pristine pre-patch source to diff against new upstream versions; checking both CJS and ESM builds for strip parity (same names removed in both formats); verifying no dangling references (exports pointing to stripped modules that themselves weren't stripped); confirming CJS re-exports don't resolve to undefined and ESM re-exports don't throw SyntaxErrors; identifying newly-added upstream exports and deciding whether to keep or strip them; and running actual runtime checks (node --check, dynamic import) as proof rather than static review alone. When updating a patch for a new package version, always cross-verify strip-set completeness and consistency across module formats before finalizing. - - -* **Verify pnpm lockfile dependency trees and versions before finalizing dependency changes**: When updating or auditing dependencies (e.g., pnpm-lock.yaml), the user expects thorough verification of the resulting lockfile: checking transitive dependency entries (like @spotlightjs/spotlight's nested @sentry/core, @sentry/node, hono), confirming exact resolved versions, cross-referencing multiple lockfile entries for the same package (patched vs unpatched, different peer contexts), and grepping for security-relevant packages to confirm no vulnerable versions remain. When a grep or script doesn't produce expected confirmation output, re-check by inspecting the raw lockfile snapshot directly rather than trusting a single query. Always confirm both package.json version ranges and pnpm-lock.yaml resolved versions match expectations after any install/update, especially around security patches or CVE fixes. - - -* **Verify TypeScript CLI ports against both legacy Rust source and live server implementation**: When reviewing or implementing a TypeScript port of a legacy Rust CLI feature (e.g., sentry-cli snapshots download), the user expects the assistant to cross-check three sources: (1) the legacy Rust implementation being ported from, (2) the actual live Sentry server-side API endpoint source code (permissions, rate limits, error responses, side effects), and (3) the new TypeScript client code under review. The user wants a rigorous, objective comparison — not just style review — that surfaces real behavioral discrepancies (e.g., streaming-to-disk vs buffering-in-memory, missing rate-limit handling, mismatched error codes, auth token leakage risks). When asked for a review, do not write code; produce findings organized by severity with file:line references and concrete fixes, grounded in actual source inspection rather than assumptions. - - -* **Verify TypeScript port matches legacy Rust sentry-cli behavior exactly**: When porting sentry-cli functionality from Rust to TypeScript, always cross-reference the legacy Rust implementation (e.g., collect\_git\_metadata, find\_head\_sha, find\_base\_sha, VcsInfo struct, CI/GitHub Actions env var handling, clap flag semantics like conflicts\_with) against the new TS code and tests. Actively look for behavioral discrepancies — such as flag conflict handling, error vs. silent-override behavior, and missing env var reads (e.g., GITHUB\_EVENT\_PATH) — and flag them as bugs/blockers rather than assuming the port is correct. Use probe tests to empirically confirm flag-parsing edge cases (single flag, combined flags, malformed flags) and compare results against Rust's documented behavior. Prioritize fidelity to legacy behavior unless an intentional deviation is explicitly decided upon. + +* **Update configuration files systematically and verify paths after restructuring**: Always update configuration files systematically: update specific sections/blocks methodically (e.g., paths-filter globs, version-read), with detailed examination before edits. Applies to CI/CD configs (ci.yml, docs-preview.yml, eval-skill-fork.yml), with paths verified after repository restructuring (e.g., packages/cli/, apps/cli-docs/) and absolute paths used where necessary. - -* **Verify visual/terminal output changes across all render surfaces with rigorous validation**: When making changes to CLI banner/visual rendering (colors, ASCII/block art, responsive layout), the user consistently: (1) requires the assistant to check ALL rendering call sites (help.ts, ink-app.tsx, ink-ui.ts, wizard-runner.ts) since banner code is duplicated/must stay in sync across multiple files; (2) cares deeply about pixel/character-level legibility and exact visual fidelity to source art (e.g., verifying individual letterforms aren't distorted); (3) expects careful tradeoff analysis (aspect ratio, duty cycle, column width) with multiple candidate options presented before deciding; (4) sometimes requests strict read-only adversarial review workflows requiring sha256 verification of files before/after and clean git status confirmation; (5) expects existing tests to be updated in sync with visual changes. When editing shared visual assets, always search for all consumers, verify byte-identical duplication where files must mirror each other, and present tiered/graduated solutions for terminal-width responsiveness. + +* **Verify PR/code claims against actual source before accepting**: Always verify code/PR claims against actual file contents before accepting them as true: read the real source files at specific line numbers, quote exact code/comments, and flag discrepancies. Mark confirmed findings with 🟡 and actionable directives with 🔴. Cross-check automated tooling (scripts, CI configs) against what they actually produce. When a PR introduces a fix, confirm the underlying problem genuinely existed. Never accept a PR description or assertion at face value — ground-truth it against the codebase with precise line references. diff --git a/.npmrc b/.npmrc index d67f374883..1aa498c4e2 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -node-linker=hoisted +node-linker=isolated diff --git a/AGENTS.md b/AGENTS.md index 798b0250b9..3316fa78e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,1004 +1,3 @@ -# AGENTS.md - -Guidelines for AI agents working in this codebase. - -## Project Overview - -**Sentry CLI** is a command-line interface for [Sentry](https://sentry.io), built with [Bun](https://bun.sh) and [Stricli](https://bloomberg.github.io/stricli/). - -### Goals - -- **Zero-config experience** - Auto-detect project context from DSNs in source code and env files -- **AI-powered debugging** - Integrate Seer AI for root cause analysis and fix plans -- **Developer-friendly** - Follow `gh` CLI conventions for intuitive UX -- **Agent-friendly** - JSON output and predictable behavior for AI coding agents -- **Fast** - Native binaries via Bun, SQLite caching for API responses - -### Key Features - -- **DSN Auto-Detection** - Scans `.env` files and source code (JS, Python, Go, Java, Ruby, PHP) to find Sentry DSNs -- **Project Root Detection** - Walks up from CWD to find project boundaries using VCS, language, and build markers -- **Directory Name Inference** - Fallback project matching using bidirectional word boundary matching -- **Multi-Region Support** - Automatic region detection with fan-out to regional APIs (us.sentry.io, de.sentry.io) -- **Monorepo Support** - Generates short aliases for multiple projects -- **Seer AI Integration** - `issue explain` and `issue plan` commands for AI analysis -- **OAuth Device Flow** - Secure authentication without browser redirects - -## Cursor Rules (Important!) - -Before working on this codebase, read the Cursor rules: - -- **`.cursor/rules/bun-cli.mdc`** - Bun API usage, file I/O, process spawning, testing -- **`.cursor/rules/ultracite.mdc`** - Code style, formatting, linting rules - -## Quick Reference: Commands - -> **Note**: Always check `package.json` for the latest scripts. - -```bash -# Development -bun install # Install dependencies -bun run dev # Run CLI in dev mode -bun run --env-file=.env.local src/bin.ts # Dev with env vars - -# Build -bun run build # Build for current platform -bun run build:all # Build for all platforms - -# Type Checking -bun run typecheck # Check types - -# Linting & Formatting -bun run lint # Check for issues -bun run lint:fix # Auto-fix issues (run before committing) - -# Testing -bun test # Run all tests -bun test path/to/file.test.ts # Run single test file -bun test --watch # Watch mode -bun test --filter "test name" # Run tests matching pattern -bun run test:unit # Run unit tests only -bun run test:e2e # Run e2e tests only -``` - -## Rules: No Runtime Dependencies - -**CRITICAL**: All packages must be in `devDependencies`, never `dependencies`. Everything is bundled at build time via esbuild. CI enforces this with `bun run check:deps`. - -When adding a package, always use `bun add -d ` (the `-d` flag). - -When the `@sentry/api` SDK provides types for an API response, import them directly from `@sentry/api` instead of creating redundant Zod schemas in `src/types/sentry.ts`. - -## Rules: Use Bun APIs - -**CRITICAL**: This project uses Bun as runtime. Always prefer Bun-native APIs over Node.js equivalents. - -Read the full guidelines in `.cursor/rules/bun-cli.mdc`. - -**Bun Documentation**: https://bun.sh/docs - Consult these docs when unsure about Bun APIs. - -### Quick Bun API Reference - -| Task | Use This | NOT This | -|------|----------|----------| -| Read file | `await Bun.file(path).text()` | `fs.readFileSync()` | -| Write file | `await Bun.write(path, content)` | `fs.writeFileSync()` | -| Check file exists | `await Bun.file(path).exists()` | `fs.existsSync()` | -| Spawn process | `Bun.spawn()` | `child_process.spawn()` | -| Shell commands | `Bun.$\`command\`` ⚠️ | `child_process.exec()` | -| Find executable | `Bun.which("git")` | `which` package | -| Glob patterns | `new Bun.Glob()` | `glob` / `fast-glob` packages | -| Sleep | `await Bun.sleep(ms)` | `setTimeout` with Promise | -| Parse JSON file | `await Bun.file(path).json()` | Read + JSON.parse | - -**Exception**: Use `node:fs` for directory creation with permissions: -```typescript -import { mkdirSync } from "node:fs"; -mkdirSync(dir, { recursive: true, mode: 0o700 }); -``` - -**Exception**: `Bun.$` (shell tagged template) has no shim in `script/node-polyfills.ts` and will crash on the npm/node distribution. Until a shim is added, use `execSync` from `node:child_process` for shell commands that must work in both runtimes: -```typescript -import { execSync } from "node:child_process"; -const result = execSync("id -u username", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }); -``` - -## Architecture - -The full project-structure tree — including the live command/subcommand list and the -domain API modules — is generated from the route tree and lives in -[`docs/src/content/docs/contributing.md`](docs/src/content/docs/contributing.md) -(the `project-structure` block produced by `script/generate-docs-sections.ts`). It is -kept in sync automatically, so it is **not** duplicated here to avoid drift. For the -current command list run `ls src/commands/` or `sentry --help`. - -Top-level layout: - -- **`src/bin.ts`** — entry point; **`src/app.ts`** — Stricli application setup; - **`src/context.ts`** — dependency-injection context. -- **`src/commands/`** — one directory per command group (`auth`, `cli`, `dashboard`, - `event`, `issue`, `log`, `org`, `project`, `release`, `replay`, `repo`, `sourcemap`, - `span`, `team`, `trace`, `trial`, `local`, …) plus standalone command files - (`api.ts`, `explore.ts`, `help.ts`, `init.ts`, `schema.ts`). -- **`src/lib/`** — shared utilities. Key subtrees: `api/` (domain API modules), - `db/` (SQLite layer), `dsn/` (DSN detection, with per-language extractors under - `dsn/languages/`), and `formatters/` (output formatting). See the file-locations - table below and the JSDoc in each module for details. -- **`src/types/`** — TypeScript types and Zod schemas. -- **`test/`** — tests mirroring `src/` (unit, `*.property.test.ts`, - `*.model-based.test.ts`, `e2e/`, `fixtures/`, `mocks/`). -- **`docs/`** — documentation site (Astro + Starlight); **`script/`** — build/utility - scripts; **`.cursor/rules/`** — Cursor AI rules; **`biome.jsonc`** — lint config. - -## Key Patterns - -### CLI Commands (Stricli) - -Commands use [Stricli](https://bloomberg.github.io/stricli/docs/getting-started/principles) wrapped by `src/lib/command.ts`. - -**CRITICAL**: Import `buildCommand` from `../../lib/command.js`, **NEVER** from `@stricli/core` directly — the wrapper adds telemetry, `--json`/`--fields` injection, and output rendering. - -Pattern: - -```typescript -import { buildCommand } from "../../lib/command.js"; -import type { SentryContext } from "../../context.js"; -import { CommandOutput } from "../../lib/formatters/output.js"; - -export const myCommand = buildCommand({ - docs: { - brief: "Short description", - fullDescription: "Detailed description", - }, - output: { - human: formatMyData, // (data: T) => string - jsonTransform: jsonTransformMyData, // optional: (data: T, fields?) => unknown - jsonExclude: ["humanOnlyField"], // optional: strip keys from JSON - }, - parameters: { - flags: { - limit: { kind: "parsed", parse: Number, brief: "Max items", default: 10 }, - }, - }, - async *func(this: SentryContext, flags) { - const data = await fetchData(); - yield new CommandOutput(data); - return { hint: "Tip: use --json for machine-readable output" }; - }, -}); -``` - -**Key rules:** -- Functions are `async *func()` generators — yield `new CommandOutput(data)`, return `{ hint }`. -- `output.human` receives the same data object that gets serialized to JSON — no divergent-data paths. -- The wrapper auto-injects `--json` and `--fields` flags. Do NOT add your own `json` flag. -- Do NOT use `stdout.write()` or `if (flags.json)` branching — the wrapper handles it. - -### Command File Structure - -Command files in `src/commands/` should focus on three concerns: -1. **Argument parsing** — positional args, flags, URL detection -2. **API orchestration** — fetching data, error handling, enrichment -3. **Output dispatch** — `yield new CommandOutput(data)` - -Formatting and rendering logic belongs in `src/lib/formatters/.ts`. If a command file exceeds ~400 lines, extract formatting helpers into a dedicated formatter module. - -Reference: `src/lib/formatters/replay.ts` (extracted from `replay/view.ts`), `src/lib/formatters/trace.ts`, `src/lib/formatters/human.ts`. - -Lint enforcement: `stderr.write()` is banned in command files (GritQL rule). Use `logger` for diagnostics and `CommandOutput` for data output. - -### Route Maps (Stricli) - -Route groups use Stricli's `buildRouteMap` wrapped by `src/lib/route-map.ts`. - -**CRITICAL**: Import `buildRouteMap` from `../../lib/route-map.js`, **NEVER** from `@stricli/core` directly — the wrapper auto-injects standard subcommand aliases based on which route keys exist: - -| Route | Auto-aliases | -|----------|----------------| -| `list` | `ls` | -| `view` | `show` | -| `delete` | `remove`, `rm` | -| `create` | `new` | - -Manually specified aliases in `aliases` are merged with (and take precedence over) auto-generated ones. Do NOT manually add aliases that are already in the standard set above. - -```typescript -import { buildRouteMap } from "../../lib/route-map.js"; - -export const myRoute = buildRouteMap({ - routes: { - list: listCommand, - view: viewCommand, - create: createCommand, - }, - defaultCommand: "view", - // No need for aliases — ls, show, and new are auto-injected. - // Only add aliases for non-standard mappings: - // aliases: { custom: "list" }, - docs: { - brief: "Manage my resources", - }, -}); -``` - -### Positional Arguments - -Use `parseSlashSeparatedArg` from `src/lib/arg-parsing.ts` for the standard `[//]` pattern. Required identifiers (trace IDs, span IDs) should be **positional args**, not flags. - -```typescript -import { parseSlashSeparatedArg, parseOrgProjectArg } from "../../lib/arg-parsing.js"; - -// "my-org/my-project/abc123" → { id: "abc123", targetArg: "my-org/my-project" } -const { id, targetArg } = parseSlashSeparatedArg(first, "Trace ID", USAGE_HINT); -const parsed = parseOrgProjectArg(targetArg); -// parsed.type: "auto-detect" | "explicit" | "project-search" | "org-all" -``` - -Reference: `span/list.ts`, `trace/view.ts`, `event/view.ts` - -### Markdown Rendering - -All non-trivial human output must use the markdown rendering pipeline: - -- Build markdown strings with helpers: `mdKvTable()`, `colorTag()`, `escapeMarkdownCell()`, `renderMarkdown()` -- **NEVER** use raw `muted()` / chalk in output strings — use `colorTag("muted", text)` inside markdown -- Tree-structured output (box-drawing characters) that can't go through `renderMarkdown()` should use the `plainSafeMuted` pattern: `isPlainOutput() ? text : muted(text)` -- `isPlainOutput()` precedence: `SENTRY_PLAIN_OUTPUT` > `NO_COLOR` > `FORCE_COLOR` (TTY only) > `!isTTY` -- `isPlainOutput()` lives in `src/lib/formatters/plain-detect.ts` (re-exported from `markdown.ts` for compat) - -Reference: `formatters/trace.ts` (`formatAncestorChain`), `formatters/human.ts` (`plainSafeMuted`) - -### Create & Delete Command Standards - -Mutation (create/delete) commands use shared infrastructure from `src/lib/mutate-command.ts`, -paralleling `list-command.ts` for list commands. - -**Delete commands** MUST use `buildDeleteCommand()` instead of `buildCommand()`. It: -1. Auto-injects `--yes`, `--force`, `--dry-run` flags with `-y`, `-f`, `-n` aliases -2. Runs a non-interactive safety guard before `func()` — refuses to proceed if - stdin is not a TTY and `--yes`/`--force` was not passed (dry-run bypasses) -3. Options to skip specific injections (`noForceFlag`, `noDryRunFlag`, `noNonInteractiveGuard`) - -```typescript -import { buildDeleteCommand, confirmByTyping, isConfirmationBypassed, requireExplicitTarget } from "../../lib/mutate-command.js"; - -export const deleteCommand = buildDeleteCommand({ - // Same args as buildCommand — flags/aliases auto-injected - async *func(this: SentryContext, flags, target) { - requireExplicitTarget(parsed, "Entity", "sentry entity delete "); - if (flags["dry-run"]) { yield preview; return; } - if (!isConfirmationBypassed(flags)) { - if (!await confirmByTyping(expected, promptMessage)) return; - } - await doDelete(); - }, -}); -``` - -**Create commands** import `DRY_RUN_FLAG` and `DRY_RUN_ALIASES` for consistent dry-run support: - -```typescript -import { DRY_RUN_FLAG, DRY_RUN_ALIASES } from "../../lib/mutate-command.js"; - -// In parameters: -flags: { "dry-run": DRY_RUN_FLAG, team: { ... } }, -aliases: { ...DRY_RUN_ALIASES, t: "team" }, -``` - -**Key utilities** in `mutate-command.ts`: -- `isConfirmationBypassed(flags)` — true if `--yes` or `--force` is set -- `guardNonInteractive(flags)` — throws in non-interactive mode without `--yes` -- `confirmByTyping(expected, message)` — type-out confirmation prompt -- `requireExplicitTarget(parsed, entityType, usage)` — blocks auto-detect for safety -- `DESTRUCTIVE_FLAGS` / `DESTRUCTIVE_ALIASES` — spreadable bundles for manual use - -### List Command Pagination - -All list commands with API pagination MUST use the shared cursor-stack -infrastructure for **bidirectional** pagination (`-c next` / `-c prev`): - -```typescript -import { LIST_CURSOR_FLAG } from "../../lib/list-command.js"; -import { - buildPaginationContextKey, resolveCursor, - advancePaginationState, hasPreviousPage, -} from "../../lib/db/pagination.js"; - -export const PAGINATION_KEY = "my-entity-list"; - -// In buildCommand: -flags: { cursor: LIST_CURSOR_FLAG }, -aliases: { c: "cursor" }, - -// In func(): -const contextKey = buildPaginationContextKey("entity", `${org}/${project}`, { - sort: flags.sort, q: flags.query, -}); -const { cursor, direction } = resolveCursor(flags.cursor, PAGINATION_KEY, contextKey); -const { data, nextCursor } = await listEntities(org, project, { cursor, ... }); -advancePaginationState(PAGINATION_KEY, contextKey, direction, nextCursor); -const hasPrev = hasPreviousPage(PAGINATION_KEY, contextKey); -const hasMore = !!nextCursor; -``` - -**Cursor stack model:** The DB stores a JSON array of page-start cursors -plus a page index. Each entry is an opaque string — plain API cursors, -compound cursors (issue list), or extended cursors with mid-page bookmarks -(dashboard list). `-c next` increments the index, `-c prev` decrements it, -`-c first` resets to 0. The stack truncates on back-then-forward to avoid -stale entries. `"last"` is a silent alias for `"next"`. - -**Hint rules:** Show `-c prev` when `hasPreviousPage()` returns true. -Show `-c next` when `hasMore` is true. Include both `nextCursor` and -`hasPrev` in the JSON envelope. - -**Navigation hint generation:** Use `paginationHint()` from -`src/lib/list-command.ts` to build bidirectional navigation strings. -Pass it pre-built `prevHint`/`nextHint` command strings and it returns -the combined `"Prev: X | Next: Y"` string (or single-direction, or `""`). -Do NOT assemble `navParts` arrays manually — the shared helper ensures -consistent formatting across all list commands. - -```typescript -import { paginationHint } from "../../lib/list-command.js"; - -const nav = paginationHint({ - hasPrev, - hasMore, - prevHint: `sentry entity list ${org}/ -c prev`, - nextHint: `sentry entity list ${org}/ -c next`, -}); -if (items.length === 0 && nav) { - hint = `No entities on this page. ${nav}`; -} else if (hasMore) { - header = `Showing ${items.length} entities (more available)\n${nav}`; -} else if (nav) { - header = `Showing ${items.length} entities\n${nav}`; -} -``` - -**Three abstraction levels for list commands** (prefer the highest level -that fits your use case): - -1. **`buildOrgListCommand`** (team/repo list) — Fully automatic. Pagination - hints, cursor management, JSON envelope, and human formatting are all - handled internally. New simple org-scoped list commands should use this. - -2. **`dispatchOrgScopedList` with overrides** (project/issue list) — Automatic - for most modes; custom `"org-all"` override calls `resolveCursor` + - `advancePaginationState` + `paginationHint` manually. - -3. **`buildListCommand` with manual pagination** (trace/span/dashboard list) — - Command manages its own pagination loop. Must call `resolveCursor`, - `advancePaginationState`, `hasPreviousPage`, and `paginationHint` directly. - -**Auto-pagination for large limits:** - -When `--limit` exceeds `API_MAX_PER_PAGE` (100), list commands MUST transparently -fetch multiple pages to fill the requested limit. Cap `perPage` at -`Math.min(flags.limit, API_MAX_PER_PAGE)` and loop until `results.length >= limit` -or pages are exhausted. This matches the `listIssuesAllPages` pattern. - -```typescript -const perPage = Math.min(flags.limit, API_MAX_PER_PAGE); -for (let page = 0; page < MAX_PAGINATION_PAGES; page++) { - const { data, nextCursor } = await listPaginated(org, { perPage, cursor }); - results.push(...data); - if (results.length >= flags.limit || !nextCursor) break; - cursor = nextCursor; -} -``` - -Never pass a `per_page` value larger than `API_MAX_PER_PAGE` to the API — the -server silently caps it, causing the command to return fewer items than requested. - -Reference template: `trace/list.ts`, `span/list.ts`, `dashboard/list.ts` - -### ID Validation - -Use shared validators from `src/lib/hex-id.ts`: -- `validateHexId(value, label)` — 32-char hex IDs (trace IDs, log IDs). Auto-strips UUID dashes. -- `validateSpanId(value)` — 16-char hex span IDs. Auto-strips dashes. -- `validateTraceId(value)` — thin wrapper around `validateHexId` in `src/lib/trace-id.ts`. - -All normalize to lowercase. Throw `ValidationError` on invalid input. - -### Sort Convention - -Use `"date"` for timestamp-based sort (not `"time"`). Export sort types from the API layer (e.g., `SpanSortValue` from `api/traces.ts`), import in commands. This matches `issue list`, `trace list`, and `span list`. - -### Generated Docs & Skills - -All command docs and skill files are generated via `bun run generate:docs` (which runs `generate:command-docs` then `generate:skill`). This runs automatically as part of `dev`, `build`, `typecheck`, and `test` scripts. - -- **Command docs** (`docs/src/content/docs/commands/*.md`) are **gitignored** and generated from CLI metadata + hand-written fragments in `docs/src/fragments/commands/`. -- **Skill files** (`plugins/sentry-cli/skills/sentry-cli/`) are **committed** (consumed by external plugin systems) and auto-committed by CI when stale. -- Edit fragments in `docs/src/fragments/commands/` for custom examples and guides. -- `bun run check:fragments` validates fragment ↔ route consistency. -- Positional `placeholder` values must be descriptive: `"org/project/trace-id"` not `"args"`. - -### Zod Schemas for Validation - -All config and API types use Zod schemas: - -```typescript -import { z } from "zod"; - -export const MySchema = z.object({ - field: z.string(), - optional: z.number().optional(), -}); - -export type MyType = z.infer; - -// Validate data -const result = MySchema.safeParse(data); -if (result.success) { - // result.data is typed -} -``` - -### Type Organization - -- Define Zod schemas alongside types in `src/types/*.ts` -- Key type files: `sentry.ts` (API types), `config.ts` (configuration), `oauth.ts` (auth flow), `seer.ts` (Seer AI) -- Re-export from `src/types/index.ts` -- Use `type` imports: `import type { MyType } from "../types/index.js"` - -### SQL Utilities - -Use the `upsert()` helper from `src/lib/db/utils.ts` to reduce SQL boilerplate: - -```typescript -import { upsert, runUpsert } from "../db/utils.js"; - -// Generate UPSERT statement -const { sql, values } = upsert("table", { id: 1, name: "foo" }, ["id"]); -db.query(sql).run(...values); - -// Or use convenience wrapper -runUpsert(db, "table", { id: 1, name: "foo" }, ["id"]); - -// Exclude columns from update -const { sql, values } = upsert( - "users", - { id: 1, name: "Bob", created_at: now }, - ["id"], - { excludeFromUpdate: ["created_at"] } -); -``` - -### Error Handling - -All CLI errors extend the `CliError` base class from `src/lib/errors.ts`: - -```typescript -// Error hierarchy in src/lib/errors.ts -// Exit codes are defined in the EXIT constant object — use EXIT.* constants -// when constructing errors, never hardcode numeric exit codes outside errors.ts. -CliError (base, exitCode=1) -├── HostScopeError (exitCode=13) -├── ApiError (exitCode=30 — HTTP/API failures) -├── AuthError (exitCode=10–12 by reason — 'not_authenticated' | 'expired' | 'invalid') -├── ConfigError (exitCode=20 — configuration/DSN) -├── OutputError (exitCode=60 — data rendered, but operation failed) -├── ContextError (exitCode=22 — missing context) -├── ResolutionError (exitCode=23 — value provided but not found) -├── ValidationError (exitCode=21 — input validation) -├── DeviceFlowError (exitCode=51 — OAuth flow) -├── SeerError (exitCode=40–42 by reason — 'not_enabled' | 'no_budget' | 'ai_disabled') -├── TimeoutError (exitCode=31 — operation timed out) -├── UpgradeError (exitCode=50 — upgrade failures) -└── WizardError (exitCode=61–64 by workflow step — init wizard error) -``` - -> Exit code ranges: 1x=auth, 2x=input/config, 3x=API/network, 4x=feature/billing, -> 5x=operations, 6x=command-specific. See `EXIT` in `src/lib/errors.ts` and -> https://cli.sentry.dev/exit-codes/ for the full reference. - -**Choosing between ContextError, ResolutionError, and ValidationError:** - -| Scenario | Error Class | Example | -|----------|-------------|---------| -| User **omitted** a required value | `ContextError` | No org/project provided | -| User **provided** a value that wasn't found | `ResolutionError` | Project 'cli' not found | -| User input is **malformed** | `ValidationError` | Invalid hex ID format | - -**ContextError rules:** -- `command` must be a **single-line** CLI usage example (e.g., `"sentry org view "`) -- Constructor throws if `command` contains `\n` (catches misuse in tests) -- Pass `alternatives: []` when defaults are irrelevant (e.g., for missing Trace ID, Event ID) -- Use `" and "` in `resource` for plural grammar: `"Trace ID and span ID"` → "are required" - -**CI enforcement:** `bun run check:errors` scans for `ContextError` with multiline commands, `CliError` with ad-hoc "Try:" strings, and silent `catch` blocks (advisory). - -```typescript -// Usage examples -throw new ContextError("Organization", "sentry org view "); -throw new ContextError("Trace ID", "sentry trace view ", []); // no alternatives -throw new ResolutionError("Project 'cli'", "not found", "sentry issue list /cli", [ - "No project with this slug found in any accessible organization", -]); -throw new ValidationError("Invalid trace ID format", "traceId"); -``` - -**Fuzzy suggestions in resolution errors:** - -When a user-provided name/title doesn't match any entity, use `fuzzyMatch()` from -`src/lib/fuzzy.ts` to suggest similar candidates instead of listing all entities -(which can be overwhelming). Show at most 5 fuzzy matches. - -Reference: `resolveDashboardId()` in `src/commands/dashboard/resolve.ts`. - -### Catch Block Logging - -Silent `catch` blocks are prohibited in `src/` production code. Biome's `noEmptyBlockStatements` catches syntactically empty `catch {}` blocks, but blocks with only a `return` statement and no logging are equally problematic — errors vanish silently, making debugging impossible. - -Every `catch` block must either: -1. Re-throw the error -2. Log with `log.debug()` or `log.warn()` for diagnostic visibility -3. Return a fallback value **with** a `log.debug()` call explaining the suppression - -```typescript -// WRONG — error vanishes silently -try { data = await fetchOptionalData(); } -catch { return []; } - -// RIGHT — error is visible in debug logs -try { data = await fetchOptionalData(); } -catch (error) { - log.debug("Failed to fetch optional data", error); - return []; -} -``` - -Use `logger.withTag("command-name")` for tagged logging in command files. - -**CI enforcement:** `bun run check:errors` includes a silent-catch scan that flags -`catch` blocks which are empty, comment-only, or return-only without surfacing the -error. It is currently **advisory** (warns, does not fail CI) because of a pre-existing -backlog; run with `SENTRY_STRICT_SILENT_CATCH=1` to enforce. Do not add new silent -catches — they will appear in the scan output during review. - -### Auto-Recovery for Wrong Entity Types - -When a user provides the wrong type of identifier (e.g., an issue short ID -where a trace ID is expected), commands should **auto-recover** when the -user's intent is unambiguous: - -1. **Detect** the actual entity type using helpers like `looksLikeIssueShortId()`, - `SPAN_ID_RE`, `HEX_ID_RE`, or non-hex character checks. -2. **Resolve** the input to the correct type (e.g., issue → latest event → trace ID). -3. **Warn** via `log.warn()` explaining what happened. -4. **Show** the result with a return `hint` nudging toward the correct command. - -When recovery is **ambiguous or impossible**, keep the existing error but add -entity-aware suggestions (e.g., "This looks like a span ID"). - -**Detection helpers:** -- `looksLikeIssueShortId(value)` — uppercase dash-separated (e.g., `CLI-G5`) -- `SPAN_ID_RE.test(value)` — 16-char hex (span ID) -- `HEX_ID_RE.test(value)` — 32-char hex (trace/event/log ID) -- `/[^0-9a-f]/.test(normalized)` — non-hex characters → likely a slug/name - -**Reference implementations:** -- `event/view.ts` — issue short ID → latest event redirect -- `span/view.ts` — `traceId/spanId` slash format → auto-split -- `trace/view.ts` — issue short ID → issue's trace redirect -- `hex-id.ts` — entity-aware error hints in `validateHexId`/`validateSpanId` - -### Async Config Functions - -All config operations are async. Always await: - -```typescript -const token = await getAuthToken(); -const isAuth = await isAuthenticated(); -await setAuthToken(token, expiresIn); -``` - -### Adding New Utility Files - -Before creating a new `src/lib/*.ts` utility file, check whether existing shared modules already cover your use case: - -| If you need... | Check first... | -|----------------|---------------| -| Duration formatting | `src/lib/formatters/time-utils.ts` (`formatDurationCompact`, `formatDurationVerbose`) | -| Hex ID validation/normalization | `src/lib/hex-id.ts` (`validateHexId`, `tryNormalizeHexId`, `normalizeHexId`) | -| Relative time display | `src/lib/formatters/time-utils.ts` (`formatRelativeTime`) | -| Table/markdown output | `src/lib/formatters/` directory | -| Pagination | `src/lib/db/pagination.ts`, `src/lib/list-command.ts` | -| Error classes | `src/lib/errors.ts` (never create ad-hoc error types) | -| Search query building | `src/lib/search-query.ts`, `src/lib/arg-parsing.ts` | - -If an existing module covers ≥80% of what you need, extend it with new exported functions rather than creating a new file. New files are appropriate when the domain is genuinely new (e.g., `replay-search.ts` for replay-specific field resolution). - -Every new `src/lib/**/*.ts` file must start with a module-level JSDoc comment describing the module's purpose. - -### Imports - -- Use `.js` extension for local imports (ESM requirement) -- Group: external packages first, then local imports -- Use `type` keyword for type-only imports - -```typescript -import { z } from "zod"; -import { buildCommand } from "../../lib/command.js"; -import type { SentryContext } from "../../context.js"; -import { getAuthToken } from "../../lib/config.js"; -``` - -### List Command Infrastructure - -Two abstraction levels exist for list commands: - -1. **`src/lib/list-command.ts`** — `buildOrgListCommand` factory + shared Stricli parameter constants (`LIST_TARGET_POSITIONAL`, `LIST_JSON_FLAG`, `LIST_CURSOR_FLAG`, `buildListLimitFlag`). Use this for simple entity lists like `team list` and `repo list`. - -2. **`src/lib/org-list.ts`** — `dispatchOrgScopedList` with `OrgListConfig` and a 4-mode handler map: `auto-detect`, `explicit`, `org-all`, `project-search`. Complex commands (`project list`, `issue list`) call `dispatchOrgScopedList` with an `overrides` map directly instead of using `buildOrgListCommand`. - -Key rules when writing overrides: -- Each mode handler receives a `HandlerContext` with the narrowed `parsed` plus shared I/O (`stdout`, `cwd`, `flags`). Access parsed fields via `ctx.parsed.org`, `ctx.parsed.projectSlug`, etc. — no manual `Extract<>` casts needed. -- Commands with extra fields (e.g., `stderr`, `setContext`) spread the context and add them: `(ctx) => handle({ ...ctx, flags, stderr, setContext })`. Override `ctx.flags` with the command-specific flags type when needed. -- `resolveCursor()` must be called **inside** the `org-all` override closure, not before `dispatchOrgScopedList`, so that `--cursor` validation errors fire correctly for non-org-all modes. -- `handleProjectSearch` errors must use `"Project"` as the `ContextError` resource, not `config.entityName`. -- Always set `orgSlugMatchBehavior` on `dispatchOrgScopedList` to declare how bare-slug org matches are handled. Use `"redirect"` for commands where listing all entities in the org makes sense (e.g., `project list`, `team list`, `issue list`). Use `"error"` for commands where org-all redirect is inappropriate. The pre-check uses cached orgs to avoid N API calls — when the cache is cold, the handler's own org-slug check serves as a safety net (throws `ResolutionError` with a hint). - -3. **Standalone list commands** (e.g., `span list`, `trace list`) that don't use org-scoped dispatch wire pagination directly in `func()`. See the "List Command Pagination" section above for the pattern. - -### Project Filtering in API Calls - -Different Sentry API endpoints use different project filtering mechanisms. Never apply both simultaneously: - -| API Endpoint | Project filter | Helper | -|-------------|---------------|--------| -| Discover/Events (`queryEvents`) | `project:` in query string | `buildProjectQuery()` | -| Replay index (`listReplays`) | `projectSlugs` parameter | Direct parameter | -| Issue index (`listIssuesPaginated`) | `project` parameter or query string | Varies by mode | - -When adding a new dataset to `explore`, verify which filtering mechanism the underlying API expects and handle it in `resolveDatasetConfig`. The `explore` command centralizes dataset-specific behavior (sort, query, fetch, field validation) in `resolveDatasetConfig` — add new datasets there rather than scattering `if (dataset === ...)` checks through the `func` body. - -## Commenting & Documentation (JSDoc-first) - -### Default Rule -- **Prefer JSDoc over inline comments.** -- Code should be readable without narrating what it already says. - -### Required: JSDoc -Add JSDoc comments on: -- **Every exported function, class, and type** (and important internal ones). -- **Types/interfaces**: document each field/property (what it represents, units, allowed values, meaning of `null`, defaults). - -Include in JSDoc: -- What it does -- Key business rules / constraints -- Assumptions and edge cases -- Side effects -- Why it exists (when non-obvious) - -### Inline Comments (rare) -Inline comments are **allowed only** when they add information the code cannot express: -- **"Why"** - business reason, constraint, historical context -- **Non-obvious behavior** - surprising edge cases -- **Workarounds** - bugs in dependencies, platform quirks -- **Hardcoded values** - why hardcoded, what would break if changed - -Inline comments are **NOT allowed** if they just restate the code: -```typescript -// Bad: -if (!person) // if no person -i++ // increment i -return result // return result - -// Good: -// Required by GDPR Article 17 - user requested deletion -await deleteUserData(userId) -``` - -### Prohibited Comment Styles -- **ASCII art section dividers** - Do not use decorative box-drawing characters like `─────────` to create section headers. Use standard JSDoc comments or simple `// Section Name` comments instead. - -### Goal -Minimal comments, maximum clarity. Comments explain **intent and reasoning**, not syntax. - -## Testing (bun:test + fast-check) - -**Prefer property-based and model-based testing** over traditional unit tests. These approaches find edge cases automatically and provide better coverage with less code. - -**fast-check Documentation**: https://fast-check.dev/docs/core-blocks/arbitraries/ - -### Testing Hierarchy (in order of preference) - -1. **Model-Based Tests** - For stateful systems (database, caches, state machines) -2. **Property-Based Tests** - For pure functions, parsing, validation, transformations -3. **Unit Tests** - Only for trivial cases or when properties are hard to express - -### Test File Naming - -| Type | Pattern | Location | -|------|---------|----------| -| Property-based | `*.property.test.ts` | `test/lib/` | -| Model-based | `*.model-based.test.ts` | `test/lib/db/` | -| Unit tests | `*.test.ts` | `test/` (mirrors `src/`) | -| E2E tests | `*.test.ts` | `test/e2e/` | - -### Test Environment Isolation (CRITICAL) - -Tests that need a database or config directory **must** use `useTestConfigDir()` from `test/helpers.ts`. This helper: -- Creates a unique temp directory in `beforeEach` -- Sets `SENTRY_CONFIG_DIR` to point at it -- **Restores** (never deletes) the env var in `afterEach` -- Closes the database and cleans up temp files - -**NEVER** do any of these in test files: -- `delete process.env.SENTRY_CONFIG_DIR` — This pollutes other test files that load after yours -- `const baseDir = process.env[CONFIG_DIR_ENV_VAR]!` at module scope — This captures a value that may be stale -- Manual `beforeEach`/`afterEach` that sets/deletes `SENTRY_CONFIG_DIR` - -**Why**: Bun's test runner uses `--isolate --parallel` (see `test:unit` in `package.json`), so each test file runs in a fresh global environment within a worker process. That bounds most cross-file leaks to a single worker, but `process.env` is still shared within a file's lifecycle — if your `afterEach` deletes the env var, the next describe/test's module-level code (or a beforeEach that re-reads env) gets `undefined`, causing `TypeError: The "paths[0]" property must be of type string`. Also, `TEST_TMP_DIR` is namespaced by `BUN_TEST_WORKER_ID` in `test/constants.ts` so parallel workers don't wipe each other's temp state during preload. - -```typescript -// CORRECT: Use the helper -import { useTestConfigDir } from "../helpers.js"; - -const getConfigDir = useTestConfigDir("my-test-prefix-"); - -// If you need the directory path in a test: -test("example", () => { - const dir = getConfigDir(); -}); - -// WRONG: Manual env var management -beforeEach(() => { process.env.SENTRY_CONFIG_DIR = tmpDir; }); -afterEach(() => { delete process.env.SENTRY_CONFIG_DIR; }); // BUG! -``` - -### Property-Based Testing - -Use property-based tests when verifying invariants that should hold for **any valid input**. - -```typescript -import { describe, expect, test } from "bun:test"; -import { constantFrom, assert as fcAssert, property, tuple } from "fast-check"; -import { DEFAULT_NUM_RUNS } from "../model-based/helpers.js"; - -// Define arbitraries (random data generators) -const slugArb = array(constantFrom(..."abcdefghijklmnopqrstuvwxyz0123456789".split("")), { - minLength: 1, - maxLength: 15, -}).map((chars) => chars.join("")); - -describe("property: myFunction", () => { - test("is symmetric", () => { - fcAssert( - property(slugArb, slugArb, (a, b) => { - // Properties should always hold regardless of input - expect(myFunction(a, b)).toBe(myFunction(b, a)); - }), - { numRuns: DEFAULT_NUM_RUNS } - ); - }); - - test("round-trip: encode then decode returns original", () => { - fcAssert( - property(validInputArb, (input) => { - const encoded = encode(input); - const decoded = decode(encoded); - expect(decoded).toEqual(input); - }), - { numRuns: DEFAULT_NUM_RUNS } - ); - }); -}); -``` - -**Good candidates for property-based testing:** -- Parsing functions (DSN, issue IDs, aliases) -- Encoding/decoding (round-trip invariant) -- Symmetric operations (a op b = b op a) -- Idempotent operations (f(f(x)) = f(x)) -- Validation functions (valid inputs accepted, invalid rejected) - -**See examples:** `test/lib/dsn.property.test.ts`, `test/lib/alias.property.test.ts`, `test/lib/issue-id.property.test.ts` - -### Model-Based Testing - -Use model-based tests for **stateful systems** where sequences of operations should maintain invariants. - -```typescript -import { describe, expect, test } from "bun:test"; -import { - type AsyncCommand, - asyncModelRun, - asyncProperty, - commands, - assert as fcAssert, -} from "fast-check"; -import { createIsolatedDbContext, DEFAULT_NUM_RUNS } from "../../model-based/helpers.js"; - -// Define a simplified model of expected state -type DbModel = { - entries: Map; -}; - -// Define commands that operate on both model and real system -class SetCommand implements AsyncCommand { - constructor(readonly key: string, readonly value: string) {} - - check = () => true; - - async run(model: DbModel, real: RealDb): Promise { - // Apply to real system - await realSet(this.key, this.value); - - // Update model - model.entries.set(this.key, this.value); - } - - toString = () => `set("${this.key}", "${this.value}")`; -} - -class GetCommand implements AsyncCommand { - constructor(readonly key: string) {} - - check = () => true; - - async run(model: DbModel, real: RealDb): Promise { - const realValue = await realGet(this.key); - const expectedValue = model.entries.get(this.key); - - // Verify real system matches model - expect(realValue).toBe(expectedValue); - } - - toString = () => `get("${this.key}")`; -} - -describe("model-based: database", () => { - test("random sequences maintain consistency", () => { - fcAssert( - asyncProperty(commands(allCommandArbs), async (cmds) => { - const cleanup = createIsolatedDbContext(); - try { - await asyncModelRun( - () => ({ model: { entries: new Map() }, real: {} }), - cmds - ); - } finally { - cleanup(); - } - }), - { numRuns: DEFAULT_NUM_RUNS } - ); - }); -}); -``` - -**Good candidates for model-based testing:** -- Database operations (auth, caches, regions) -- Stateful caches with invalidation -- Systems with cross-cutting invariants (e.g., clearAuth also clears regions) - -**See examples:** `test/lib/db/model-based.test.ts`, `test/lib/db/dsn-cache.model-based.test.ts` - -### Test Helpers - -Use `test/model-based/helpers.ts` for shared utilities: - -```typescript -import { createIsolatedDbContext, DEFAULT_NUM_RUNS } from "../model-based/helpers.js"; - -// Create isolated DB for each test run (prevents interference) -const cleanup = createIsolatedDbContext(); -try { - // ... test code -} finally { - cleanup(); -} - -// Use consistent number of runs across tests -fcAssert(property(...), { numRuns: DEFAULT_NUM_RUNS }); // 50 runs -``` - -### When to Use Unit Tests - -Use traditional unit tests only when: -- Testing trivial logic with obvious expected values -- Properties are difficult to express or would be tautological -- Testing error messages or specific output formatting -- Integration with external systems (E2E tests) - -### Avoiding Unit/Property Test Duplication - -When a `*.property.test.ts` file exists for a module, **do not add unit tests that re-check the same invariants** with hardcoded examples. Before adding a unit test, check whether the companion property file already generates random inputs for that invariant. - -**Unit tests that belong alongside property tests:** -- Edge cases outside the property generator's range (e.g., self-hosted DSNs when the arbitrary only produces SaaS ones) -- Specific output format documentation (exact strings, column layouts, rendered vs plain mode) -- Concurrency/timing behavior that property tests cannot express -- Integration tests exercising multiple functions together (e.g., `writeJsonList` envelope shape) - -**Unit tests to avoid when property tests exist:** -- "returns true for valid input" / "returns false for invalid input" — the property test already covers this with random inputs -- Basic round-trip assertions — property tests check `decode(encode(x)) === x` for all `x` -- Hardcoded examples of invariants like idempotency, symmetry, or subset relationships - -When adding property tests for a function that already has unit tests, **remove the unit tests that become redundant**. Add a header comment to the unit test file noting which invariants live in the property file: - -```typescript -/** - * Note: Core invariants (round-trips, validation, ordering) are tested via - * property-based tests in foo.property.test.ts. These tests focus on edge - * cases and specific output formatting not covered by property generators. - */ -``` - -```typescript -import { describe, expect, test, mock } from "bun:test"; - -describe("feature", () => { - test("should return specific value", async () => { - expect(await someFunction("input")).toBe("expected output"); - }); -}); - -// Mock modules when needed -mock.module("./some-module", () => ({ - default: () => "mocked", -})); -``` - -## File Locations - -| What | Where | -|------|-------| -| Add new command | `src/commands//` | -| Add API types | `src/types/sentry.ts` | -| Add config types | `src/types/config.ts` | -| Add Seer types | `src/types/seer.ts` | -| Add utility | `src/lib/` | -| Add DSN language support | `src/lib/dsn/languages/` | -| Add DB operations | `src/lib/db/` | -| Build scripts | `script/` | -| Add property tests | `test/lib/.property.test.ts` | -| Add model-based tests | `test/lib/db/.model-based.test.ts` | -| Add unit tests | `test/` (mirror `src/` structure) | -| Add E2E tests | `test/e2e/` | -| Test helpers | `test/model-based/helpers.ts` | -| Add documentation | `docs/src/content/docs/` | -| Hand-written command doc content | `docs/src/fragments/commands/` | - -## Automated Fix PRs (BugBot / agents) - -Automated bug-fix PRs (e.g. Cursor BugBot) must follow these rules to avoid the -duplication and staleness that caused five overlapping PRs to pile up: - -1. **Check for existing work first.** Before opening a PR, search open PRs and - recently-closed PRs/issues for the same file + symbol: - ```bash - gh pr list --state open --search "in:title " - gh issue list --state all --search "" - ``` - If an open PR already touches the target function, **comment on it** or extend - it instead of opening a duplicate. Multiple BugBot PRs independently re-fixed - the same `JSON.parse` guard, `withTTY` helper, and pagination code. - -2. **Rebase before review.** A PR that is many commits behind `main` may fail CI - on unrelated drift (e.g. a lint error in a file the PR never touched) and its - fix may already be superseded. Rebase onto `main` and re-verify the bug still - exists before requesting review. Verify against current `main`, not the - snapshot the PR was generated from. - -3. **Separate correctness fixes from opinion.** A real bug (wrong output, crash, - skipped data) is in scope. A subjective UX change (different hint wording, - different default) is **not** a bug — `main`'s current behavior is often - deliberate. Do not bundle UX opinions into bug-fix PRs; they waste review - cycles and are usually dropped. - -4. **Prefer shared helpers over re-deriving fixes.** If a correct implementation - already exists (e.g. `autoPaginate()` for pagination, `safeParseJson()` for - cached JSON), use it rather than hand-rolling a one-off fix. The recurring - pagination-overshoot and parse-crash bugs were classes solved once centrally. - ## Long-term Knowledge diff --git a/README.md b/README.md index c4c800ac6c..a201255c5b 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,27 @@ -

- Sentry CLI -

+# Sentry Toolkit (pre-shape) -

- The command-line interface for Sentry. Built for developers and AI agents. -

+This repository is being reshaped into a pnpm-workspace monorepo ahead of merging +the Sentry CLI and Sentry MCP server into a single `getsentry/toolkit` monorepo. -

- Documentation | - Getting Started | - Commands -

+## Workspace layout ---- - -## Installation - -### Install Script (Recommended) - -```bash -curl https://cli.sentry.dev/install -fsS | bash -``` - -### Homebrew - -```bash -brew install getsentry/tools/sentry -``` - -### Package Managers - -```bash -npm install -g sentry -pnpm add -g sentry -yarn global add sentry -bun add -g sentry -``` - -> The npm/pnpm/yarn packages require Node.js 18+. On Node.js 22.15+ the CLI uses the built-in `node:sqlite`; on Node.js 18–22.14 it transparently falls back to a bundled WASM SQLite driver. - -### Run Without Installing - -```bash -npx sentry@latest -pnpm dlx sentry --help -yarn dlx sentry --help -bunx sentry --help -``` - -## Quick Start - -```bash -# Authenticate with Sentry -sentry auth login - -# List issues (auto-detects project from your codebase) -sentry issue list - -# Get AI-powered root cause analysis -sentry issue explain PROJ-ABC - -# Generate a fix plan -sentry issue plan PROJ-ABC -``` - -## Features - -- **DSN Auto-Detection** - Automatically detects your project from `.env` files or source code. No flags needed. -- **Seer AI Integration** - Get root cause analysis and fix plans directly in your terminal. -- **Monorepo Support** - Works with multiple projects, generates short aliases for easy navigation. -- **JSON Output** - All commands support `--json` for scripting and pipelines. -- **Open in Browser** - Use `-w` flag to open any resource in your browser. - -## Commands - -Run `sentry --help` to see all available commands, or browse the [command reference](https://cli.sentry.dev/commands/). - -## Configuration - -Credentials are stored in `~/.sentry/` with restricted permissions (mode 600). - -## Library Usage - - -Use Sentry CLI programmatically in Node.js (≥18.0) without spawning a subprocess: - - -```typescript -import createSentrySDK from "sentry"; - -const sdk = createSentrySDK({ token: "sntrys_..." }); - -// Typed methods for every CLI command -const orgs = await sdk.org.list(); -const issues = await sdk.issue.list({ orgProject: "acme/frontend", limit: 5 }); -const issue = await sdk.issue.view({ issue: "ACME-123" }); - -// Nested commands -await sdk.dashboard.widget.add({ display: "line", query: "count" }, "my-org/my-dashboard"); - -// Escape hatch for any CLI command -const version = await sdk.run("--version"); -const text = await sdk.run("issue", "list", "-l", "5"); -``` - -Options (all optional): -- `token` — Auth token. Falls back to `SENTRY_AUTH_TOKEN` / `SENTRY_TOKEN` env vars. -- `url` — Sentry instance URL for self-hosted (e.g., `"sentry.example.com"`). -- `org` — Default organization slug (avoids passing it on every call). -- `project` — Default project slug. -- `text` — Return human-readable string instead of parsed JSON (affects `run()` only). -- `cwd` — Working directory for DSN auto-detection. Defaults to `process.cwd()`. -- `signal` — `AbortSignal` to cancel streaming commands (`--follow`, `--refresh`). - -Streaming commands return `AsyncIterable` — use `for await...of` and `break` to stop. - -Errors are thrown as `SentryError` with `.exitCode` and `.stderr`. - ---- +- [`packages/cli/`](./packages/cli) — the Sentry CLI (npm package `sentry`, + binary `sentry`). See [packages/cli/README.md](./packages/cli/README.md). +- [`apps/cli-docs/`](./apps/cli-docs) — the CLI documentation site + (Astro + Starlight, published to `cli.sentry.dev`). ## Development -### Prerequisites - - -- [Node.js](https://nodejs.org) v22.15+ and [pnpm](https://pnpm.io) v10.11+ - - -### Setup - -```bash -git clone https://github.com/getsentry/cli.git -cd cli -pnpm install -``` - -### Running Locally +This is a [pnpm workspace](https://pnpm.io/workspaces). Common tasks are exposed +as delegating scripts at the root and forwarded to the relevant package: -```bash -# Run CLI in development mode -pnpm run cli -- --help - -# With environment variables (create .env.local first, see DEVELOPMENT.md) -pnpm run cli -- --help -``` - -### Scripts - - -```bash -pnpm run build # Build for current platform -pnpm run typecheck # Type checking -pnpm run lint # Check for issues -pnpm run lint:fix # Auto-fix issues -pnpm run test:unit # Run unit tests -pnpm run test:e2e # Run end-to-end tests -pnpm run generate:docs # Regenerate command docs and skills +```sh +pnpm install # install all workspace packages +pnpm run build # build the CLI package +pnpm run typecheck # typecheck the CLI package +pnpm run lint # lint +pnpm run test # run the CLI unit tests ``` - - -See [DEVELOPMENT.md](DEVELOPMENT.md) for detailed setup and [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. - -## License -[FSL-1.1-Apache-2.0](LICENSE.md) +To work directly within a package, use pnpm filters, e.g. +`pnpm --filter sentry run