VibeTags v1.2.0
[1.2.0] - 2026-08-13
A minor release: guardrails a library declares on a package now reach the projects that depend on
it. Both halves are file-presence opt-ins that do not exist by default, so upgrading changes nothing
for a project that does not ask for it — example/ and example-multimodule-indexed/ regenerate
byte-for-byte against 1.1.1. Thirteen annotations gained ElementType.PACKAGE, which is
source-compatible: nothing that compiled before stops compiling.
Added
-
Transitive guardrails: package-level rules travel from a library into the projects that depend
on it. An agent working in an application reads the application'sCLAUDE.md, not the one
belonging to a dependency — so a constraint the library's author knows about is invisible at
exactly the moment it matters. A library now annotates itspackage-info.java, and any consuming
project that opts in renders those rules into its own AI configuration under## Inherited Guardrails (dependencies)(the six safety buckets) and## Inherited Context (dependencies)
(everything else), after everything the project says about itself.Both halves are file-presence opt-ins, like every other VibeTags output:
.vibetags-manifestto
publish,.vibetags-transitiveto consume. Neither exists by default, so upgrading the processor
changes nothing for anyone —example/andexample-multimodule-indexed/regenerate byte-for-byte
against the previous release.Thirteen annotations gained
ElementType.PACKAGE:@AISecure,@AIPrivacy,@AICore,
@AIAudit,@AIRegulation,@AIArchitecture,@AIPublicAPI,@AIBannedApi,@AIThreadSafe,
@AIImmutable,@AIDeprecated,@AIContext,@AIStrictClasspath. Widening@Targetis
source-compatible; nothing that compiled before stops compiling. Only package-level annotations
propagate — class- and method-level guardrails stay local, because propagating them would scale a
manifest with the library's whole API surface and a consumer cannot act on a rule about a class it
never sees.New options:
-Avibetags.manifest.origin,-Avibetags.manifest.dir,
-Avibetags.manifest.packages,-Avibetags.manifest.max. See
PROCESSOR.md.Three findings shaped the design, all measured against
javac 26rather than assumed:- A manifest under
META-INF/cannot be read by an annotation processor at all. javac's
CLASS_PATHlocation skips archive directories whose names are not valid Java package
identifiers:Filer.getResourcethrowsFileNotFoundExceptionwith the JAR provably on the
classpath, and javac's own file manager lists zero entries there even with--add-exports
granted. The manifest therefore lives atvibetags/manifests/<package>.json, and
TransitiveGuardrailLifecycleE2ETestrepacks a fixture JAR underMETA-INF/to assert the
conventional location stays broken — moving it back fails the build instead of silently
disabling the feature. - There is no supported way to enumerate the compile classpath.
ClassLoader.getResourcessees the processor path, not the classpath, and returns nothing under
the documentedannotationProcessorPathssetup. Listing works only behind
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED, which a library may not
demand of every consumer. Discovery is therefore driven by the packages the compilation actually
imports, which also bounds the volume of inherited text structurally rather than by a filter
applied afterwards. - A processor is not invoked at all when nothing in the round matches its supported types. A
project that inherits all of its guardrails annotates nothing itself, so the feature would have
appeared to do nothing.getSupportedAnnotationTypes()now returns"*"— but only for
projects carrying the.vibetags-transitivemarker, never by default.
The tier a rule renders under is re-derived from its annotation on read rather than trusted from
the manifest, so a JAR cannot claim the always-on tier for advisory advice.An annotation's attributes are ordered by name in both the manifest and the rendered output.
Class.getDeclaredMethods()has no specified order and genuinely differs between releases — JDK
26 reported@AIContext'sfocusbeforeavoids, JDK 25 the reverse — so leaving it as-reported
made the same sources publish different manifests and regenerate different files depending on
which JDK compiled them. Same class of defect, and the same fix, asGuardrailModelsorting its
buckets rather than keepinggetElementsAnnotatedWith's unspecified order.
TransitiveManifestMemberOrderTestpins it. - A manifest under
Changed
- A reactor module that inherits guardrails now contributes a region to the merged root, even
with no annotations of its own. Previously the root aggregated only modules whose own sources
carried annotations. A module that imports an instrumented dependency genuinely has something to
say about its own code, so it appears — carrying its inherited rules and nothing else. What has
not changed is that.vibetags-mirroralone still creates no region: mirrored rules are scoped
files, and they still never reach the aggregate. Visible inexample-multimodule/, where the
testsmodule now appears for exactly this reason.
Fixed
-
Transitive guardrails did nothing under Gradle. Gradle hands every annotation processor an
IncrementalProcessingEnvironmentrather than javac's own, andTrees.instancerejects anything
that is not javac's. Discovery therefore reportedtransitive.skip reason=trees-unavailableand
inherited nothing on every Gradle build — green, silent, and with the section simply missing from
the generated file. Found by wiring the feature up inasync-test-lib, which publishes and
consumes with both build tools: Maven inherited two rules, Gradle inherited none from the same
sources.VibeTags already had the answer.
SourcePositionResolver.treesForunwraps that same wrapper so
@AILockedpositions survive a Gradle build; the new reader calledTrees.instancedirectly and
the two drifted apart the moment the second one was written. Discovery now goes through
treesFor, which is one call site instead of two implementations.
TransitiveGuardrailLifecycleE2ETest.aWrappedProcessingEnvironmentStillDiscoversManifests
compiles through a Gradle-shaped wrapper and fails when the unwrap is bypassed — it was written
against the broken code first and reproduced it. -
A split package no longer misattributes its second artifact. The inherited-rule block grouped
bullets under one header per package, so when two artifacts published rules for the same package
the second rendered under the first one's coordinate — telling the reader a constraint came from a
dependency that never made it. Grouping is now by package and origin. Reachable by combining
-Avibetags.manifest.dirwith classpath discovery, which run additively by design. -
Check mode's documented guarantee is now precise. It said "writes nothing"; it publishes
dependency manifests intoCLASS_OUTPUT, and has to. In a reactor that both publishes and
consumes, one module's manifest is what the next reads off the classpath, so a check-mode run that
skipped publishing leaves every consuming module inheriting nothing and reporting drift on a build
where nothing is wrong.CLASS_OUTPUTis the compiler's own directory, which javac fills with
class files regardless; the guarantee that matters — and that is unchanged — is that no file
VibeTags manages in the project is touched. -
The JSON reader rejects malformed numbers. The scan accepted a character set rather than a
grammar, so-,.,--5,1.2.3and1e+e-.3all parsed as numbers — in a parser whose
stated contract is that anything malformed raises rather than being guessed at, reading documents
from JARs the consuming build did not write. -
Discovery no longer probes class names. Candidate keys expanded every prefix of the raw
import, soimport a.b.Calso looked upa.b.C— a name that cannot host a manifest, costing one
guaranteed-miss classpath lookup per import in the project. Type and member segments are now
dropped first, static imports included. -
AnnotationCollector.anyAnnotationsFound()now counts inherited rules too. It gates
hasNewRules, which decides whether an existing generated file may be rewritten. Counting only
local annotations meant a project whose guardrails all come from dependencies wrote its files
exactly once and then refused every update with "no annotations found in this module, preserving
existing rules" — so a dependency upgrade could never reach the file.
Documentation
- The
vibetags-usageskill documents transitive guardrails.USAGE.mdandPROCESSOR.md
gained the feature when it shipped; the packaged skill did not, so an agent answering "how do I
use VibeTags" from the skill alone would have said package annotations do not exist. It now
carries the publish/consume walkthrough, the thirteen annotations that accept
ElementType.PACKAGE, and the four-Avibetags.manifest.*options. scripts/consumer-sweep.shcan build a Gradle consumer that already mentions
mavenLocal(). The injection it used skipped any build file containing that string, and
codekarta has one behindif (project.hasProperty("useMavenLocal"))— present in the text, off
in the build. The sweep injected nothing and reported codekarta FAIL for a resolution error that
said nothing about VibeTags. Gradle builds now get--init-scriptinstead, which needs no edit
to a consumer's file and leaves nothing to restore.pluginManagementis deliberately untouched:
declaring one repository there removes Gradle's implicitgradlePluginPortal(), which broke
codekarta's shadow plugin on the first attempt.DocumentationLinksTestwalked the build output it was running inside. Its skip list
filtered the results ofFiles.walkinstead of pruning the walk, so it still descended into
build/,target/and.gradle/while Gradle was writing there. A file that vanished between
the listing and the visit surfaced asUncheckedIOException: NoSuchFileExceptionand failed a
test about links in documentation — red on CI, green everywhere else, and about nothing. It now
prunes those directories in aFileVisitor, which checks exactly the same files and is faster.