Skip to content

ci(repo): detect throwing-stub-with-live-callers anti-pattern (closes #2410)#2412

Merged
alexey-pelykh merged 1 commit intomainfrom
ci/2410-throwing-stub-caller-gate
Apr 19, 2026
Merged

ci(repo): detect throwing-stub-with-live-callers anti-pattern (closes #2410)#2412
alexey-pelykh merged 1 commit intomainfrom
ci/2410-throwing-stub-caller-gate

Conversation

@alexey-pelykh
Copy link
Copy Markdown

Summary

Adds a CI gate that detects 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, where
resolveAgentRuntimeOrThrow was left as an unconditional-throw stub
with 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 exported const bindings
with arrow/function-expression initializers. A function is classified
as a "throwing stub" when both:

  1. Body is a single statement: throw new <Error>(...), and
  2. At least one of these signals fires:
    • Variadic-unknown signature: (...args: unknown[]) / (..._args: unknown[])
      (supports unknown[], readonly unknown[], Array<unknown> forms).
    • Throw message matches /not available in RemoteClaw fork|\bgutted\b|upstream-compat/i.
    • Leading comment matches /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 Identifier references outside import/export
clauses. Aliased imports (import { foo as bar }) are tracked by local
name.

Legitimate typed error-throw helpers are not flagged. The repo's
existing exitHooksCliWithError(err: unknown): never,
throwPathEscapesBoundary(params: {…}): never,
throwGatewayAuthResolutionError(reason: string): never, and
throwUnresolvedGatewaySecretInput(path: string): never all have typed
scalar/object parameters and non-fork throw messages — so the check
scans exactly one stub on main.

Current baseline

Current inventory on main:

Throwing-stub-with-live-callers inventory (1 violation):
  src/agents/agent-scope.ts::resolveAgentRuntimeOrThrow (line 491, signals: variadic-unknown, fork-message, marker-comment)
    callers: 4 sites
      - src/auto-reply/reply/agent-runner-execution.ts:350
      - src/commands/agent.ts:189
      - src/cron/isolated-agent/run.ts:342
      - src/cron/isolated-agent/run.ts:389

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.mjs on mainexit 0
(single violation allowlisted). Once #2408 fix merges, removing the
allowlist line will let the default check pass without --strict.

--strict mode exits 1 on main today, which is exactly the signal
that 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:

--- check output with fixture ---
Throwing-stub-with-live-callers inventory (2 violations):
  src/__self-test__/fake-stub.ts::fakeStubOrThrow (line 2, signals: variadic-unknown, fork-message)
    callers: 1 site
      - src/__self-test__/fake-caller.ts:3
  src/agents/agent-scope.ts::resolveAgentRuntimeOrThrow (line 491, signals: variadic-unknown, fork-message, marker-comment)
    callers: 4 sites
      …

Allowlisted (tracked for remediation): 1
  src/agents/agent-scope.ts::resolveAgentRuntimeOrThrow

FAIL: 1 throwing-stub with live callers not on allowlist:
  src/__self-test__/fake-stub.ts::fakeStubOrThrow (line 2, signals: variadic-unknown, fork-message)
    callers: 1 site
      - src/__self-test__/fake-caller.ts:3

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-gate in .github/workflows/ci.yml,
added to the CI umbrella job's needs list and its result-check
shell conditional. Mirrors the existing stub-debt-gate shape.

Documentation

New § Fork Stub Conventions in CLAUDE.md:

  • The legitimate no-caller upstream-compat stub pattern (with marker
    comment, won't trip the gate).
  • How to add an allowlist entry with a tracking issue when a temporary
    live regression window is unavoidable.
  • Local detection commands (default, --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

  • Barrel re-exports (importing through ./index.ts that re-exports
    the 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.
  • Cross-module identifier collisions: the AST binding tracks local
    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.mjs on main state exits 0.
  • --strict mode exits 1 on main state (demonstrates detection).
  • Synthetic stub+caller fixture is detected (self-test transcript above).
  • pnpm check (format + typecheck + lint) passes.
  • Legitimate : never helpers not flagged (verified 4 existing helpers).
  • Mock/test-helper files excluded from caller count (verified 4 callers match live production sites only).
  • CI green on PR.

🤖 Generated with Claude Code

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>
@alexey-pelykh alexey-pelykh enabled auto-merge (squash) April 19, 2026 07:54
@alexey-pelykh alexey-pelykh merged commit c803e6e into main Apr 19, 2026
22 of 24 checks passed
@alexey-pelykh alexey-pelykh deleted the ci/2410-throwing-stub-caller-gate branch April 19, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci(repo): detect throwing-stub-with-live-callers anti-pattern

1 participant