ci(repo): detect throwing-stub-with-live-callers anti-pattern (closes #2410)#2412
Merged
alexey-pelykh merged 1 commit intomainfrom Apr 19, 2026
Merged
Conversation
Adds a CI gate that scans source for exported throwing stubs (body is a single `throw`) carrying any stub-calibration signal (variadic-unknown signature, fork-attributed throw message, or `// Gutted in RemoteClaw fork` marker comment) and flags symbols that have live non-test callers. Prevents recurrence of the class shipped in #2408 — `resolveAgentRuntimeOrThrow` left as an unconditional-throw stub with four live callers, crashing every agent dispatch in production because unit tests mocked the stub. Detection is AST-based (TypeScript compiler API) — legitimate typed error-throw helpers (`(err: unknown): never { ... }`) are not flagged because they lack the stub calibration signals. Known pre-existing violations are tracked in `.throwing-stub-callers-allowlist` with a remediation-issue reference. Current baseline: one entry for #2408. The check also warns on stale allowlist entries (no longer violating) so remediation can be locked in. Documentation: `CLAUDE.md` § Fork Stub Conventions explains the legitimate no-caller upstream-compat stub pattern and the allowlist mechanism. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 20, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a CI gate that detects exported throwing stubs (body is a single
throw) carrying any stub-calibration signal — variadic-unknownsignature, fork-attributed throw message, or
// Gutted in RemoteClaw forkmarker comment — and flags symbols that have live non-testcallers. Prevents recurrence of the class shipped in #2408, where
resolveAgentRuntimeOrThrowwas left as an unconditional-throw stubwith four live callers, crashing every agent dispatch in production
because unit tests mocked the stub.
Closes #2410.
Detection strategy
AST-based (TypeScript compiler API), scanning
src/,extensions/,ui/for exported function declarations and exportedconstbindingswith arrow/function-expression initializers. A function is classified
as a "throwing stub" when both:
throw new <Error>(...), and(...args: unknown[])/(..._args: unknown[])(supports
unknown[],readonly unknown[],Array<unknown>forms)./not available in RemoteClaw fork|\bgutted\b|upstream-compat/i./Gutted in RemoteClaw fork/i.Callers are found by walking import declarations in every non-test
file, binding imported local names to their stub identities, then
scanning the AST for
Identifierreferences outside import/exportclauses. Aliased imports (
import { foo as bar }) are tracked by localname.
Legitimate typed error-throw helpers are not flagged. The repo's
existing
exitHooksCliWithError(err: unknown): never,throwPathEscapesBoundary(params: {…}): never,throwGatewayAuthResolutionError(reason: string): never, andthrowUnresolvedGatewaySecretInput(path: string): neverall have typedscalar/object parameters and non-fork throw messages — so the check
scans exactly one stub on
main.Current baseline
Current inventory on
main:Remediation for this entry is tracked in #2408. The allowlist mechanism
lets the gate ship now and start catching new regressions while the
known-violation fix lands separately.
False-positive budget
node scripts/check-throwing-stub-callers.mjsonmain→ exit 0(single violation allowlisted). Once #2408 fix merges, removing the
allowlist line will let the default check pass without
--strict.--strictmode exits 1 onmaintoday, which is exactly the signalthat proves the gate will enforce after #2408 remediation.
Self-test evidence
Synthetic stub+caller pair injected via temporary fixtures, detection
output captured, fixtures removed. From the execution transcript:
Exit code 1 — the gate fires on the synthetic new violation while
continuing to allow the tracked one.
CI integration
New job
throwing-stub-callers-gatein.github/workflows/ci.yml,added to the
CIumbrella job'sneedslist and its result-checkshell conditional. Mirrors the existing
stub-debt-gateshape.Documentation
New § Fork Stub Conventions in
CLAUDE.md:comment, won't trip the gate).
live regression window is unavoidable.
--strict,--inventory).Also expanded § CI with a Fork-integrity gates subsection covering all
four gate jobs (rebrand, zombie-import, stub-debt, throwing-stub-callers,
obsolescence-audit).
Known limitations
./index.tsthat re-exportsthe stub) are not traced — module resolution matches the imported
path directly. Acceptable for MVP: current fork stubs are imported
via direct relative paths. Tighten in a follow-up if a barrel pattern
appears.
names scoped to the importing file, so collisions within a single
file are impossible. The only residual concern is a future stub whose
name collides with an unrelated identifier that happens to be
re-declared locally — not a pattern in the current codebase.
Test plan
node scripts/check-throwing-stub-callers.mjsonmainstate exits 0.--strictmode exits 1 onmainstate (demonstrates detection).pnpm check(format + typecheck + lint) passes.: neverhelpers not flagged (verified 4 existing helpers).🤖 Generated with Claude Code