Skip to content

feat(development): add the extension authoring skill - #4

Merged
jbachorik merged 1 commit into
agent/move-authoring-skillsfrom
agent/btrace-extension-authoring
Jul 28, 2026
Merged

feat(development): add the extension authoring skill#4
jbachorik merged 1 commit into
agent/move-authoring-skillsfrom
agent/btrace-extension-authoring

Conversation

@jbachorik

Copy link
Copy Markdown
Contributor

Closes btraceio/btrace#907. Stacked on #3, so this diff is the new skill alone.

Summary

Adds btrace-extension-authoring to btrace-development: designing a new extension against a
target library, framework, or runtime. #906's skill migrates an existing legacy integration; this
one starts from a target system with no integration yet, which is a build-time trigger, so it sits
beside btrace-extensions-and-permissions rather than in the incident plugin.

Bounded by what @ExternalType can actually express

The issue names @ExternalType as the technique for target interaction. Reading the adapter
emitter first-hand, its reach is narrower than it looks, and the limits are not stated anywhere an
author would encounter them:

  • The whole signature must be nameable at compile time. Adapters are emitted with an exact
    MethodType from erased declared types and handed to findVirtual/findStatic, so any target
    method that takes or returns a target-library type is unreachable. Overloaded names are rejected
    outright; fields, constructors, instanceof and non-public members are unsupported; and
    publicLookup() has no module read edge.
  • Static dispatch uses the thread context class loader, while virtual dispatch uses the
    receiver's defining loader. The defining loader is the documented guidance for target
    interaction, and a context loader is whatever the application thread happens to have set —
    frequently wrong under application servers, OSGi, and plugin loaders.
  • Class resolution runs on every call. The ClassValue cache holds only the method handle and
    is keyed by the resolved class, so a class that never resolves is never cached; the failure
    repeats and reaches the probe frame as an undeclared ClassNotFoundException.

The skill therefore teaches the boundary and routes anything richer — and anything version-variant
— to ClassLoadingUtil and MethodHandleCache. That cache never stores failures, by design, which
is what makes a caught lookup failure a usable capability check that recovers if the target appears
later. The gaps are recorded upstream as btraceio/btrace#931.

The implementation naming rule, stated as a rule

An earlier draft of this work argued the issue was wrong to call the META-INF/services provider
declaration required. That was incorrect. Resolution is ServiceLoader first, then the exact
name <serviceInterfaceFqcn>Impl — so an api/impl package split, which the issue's own design
requirements encourage, needs the provider declaration.

Skipping it is precisely what left the Spark example unable to resolve its own service
(btraceio/btrace#930).

Not claimed

The issue's ten-step workflow exceeds what a skill in this corpus should carry. Project layout,
permission scanning, and packaging are cross-referenced to the neighbouring skills rather than
restated; fat agents stay with the startup and packaging guidance. What survives here is what
nothing else covers: the analysis-first gate, the @ExternalType boundary and loader split, the
hand-written escape hatch, the version strategy, and the implementation naming rule.

Verification

scripts/validate-marketplace.sh passes: 9 eval cases, 2 plugins, 14 skills. Every mechanism the
skill asserts was read in the emitter and cache sources directly rather than taken from a summary.

Covers designing a new extension against a target library, which nothing in the
suite addressed. It sits in btrace-development because the trigger is building
something rather than diagnosing a running system, beside the permissions skill
it depends on.

The guidance is bounded by what @ExternalType can actually express, which is
narrower than it first appears and is not stated anywhere an author would look:

- The whole method signature must be nameable at compile time. Adapters are
  emitted with an exact MethodType from erased declared types and handed to
  findVirtual/findStatic, so any target method taking or returning a
  target-library type is unreachable. Overloads are rejected outright, and
  fields, constructors, instanceof and non-public members are unsupported.
- Static dispatch resolves the owning class through the thread context class
  loader while virtual dispatch uses the receiver's defining loader. The
  defining loader is the documented guidance, and a context loader is whatever
  the application thread happens to have set.
- Class resolution runs on every call. Only the method handle is cached, keyed
  by the resolved class, so a class that never resolves is never cached and the
  failure repeats at the probe call site as an undeclared exception.

So the skill routes anything richer, and anything version-variant, to
ClassLoadingUtil and MethodHandleCache, whose failures are deliberately not
cached and therefore work as a capability check that recovers if the target
appears later. The @ExternalType gaps are recorded upstream in btraceio/btrace#931.

It also states the implementation naming rule as an exact name or a provider
declaration, rather than treating the provider file as optional. Skipping it is
what left the Spark example unable to resolve its own service.
@jbachorik
jbachorik merged commit 869c854 into agent/move-authoring-skills Jul 28, 2026
2 checks passed
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.

1 participant