Repository navigation
VibeTags v1.4.0
[1.4.0] - 2026-10-08
Breaking, in a minor release. 1.4.0 stops writing every output that
1.3.5 and later deprecated (#641, #664 to #677, #845), each with a compiler warning naming the file
and its replacement. The warning said the next major version; the removal ships in 1.4.0
instead, by the owner's decision. A build that never saw that warning is unaffected. See Removed.
Removed
-
Four deprecated platform outputs are no longer written (#645). Each was deprecated in 1.3.5;
this is the removal that warning announced. Their keys are gone from the opt-in list, so an
existing file is neither regenerated nor warned about: it is left byte-identical and stops
tracking the annotations. Move any hand-written content to the replacement and delete the file.gemini_instructions.md, replaced byGEMINI.md(Gemini CLI) or.gemini/styleguide.md
(Gemini Code Assist). No Google product documents reading it; a last look before removal
(GitHub code search of thegoogle-geminiandgoogleapisorganisations, and a web search)
found no vendor source either..cody/config.jsonand.codyignore, replaced byAGENTS.md, which Amp reads. Sourcegraph
ended Cody Free and Pro on 23 July 2025, and its docs describe neither file..supermavenignore, replaced by.cursorignore, which Cursor Tab reads. Supermaven announced
its sunset on 21 November 2025..clinerulesas a single file, replaced by the.clinerules/directory, which VibeTags already
writes with an always-loaded+vibetags-safety.md. Cline's current docs describe only the
directory. A leftover file activates nothing, andvibetags init --platforms cline_granular
refuses to replace it.
CodyRendereris deleted with thePlatformconstantsGEMINI,CODY,CODY_IGNORE,
SUPERMAVEN_IGNOREandCLINE, andvibetags init --listno longer offers the five keys. The
README counts drop to 43 platforms and 60 config files.examples/basic,examples/multimodule
andexamples/gradle-multimodulelose the fixtures, andexamples/basicopts into the
.clinerules/directory instead, soExampleOptInCoverageTestneeds no exemption for it.
RemovedPlatformOutputsTestpins the removal; all 8 of its cases fail against the 1.3.8
processor. -
The other seventeen deprecated outputs are no longer written (#720), on the same terms. The
replacement each warning named is in docs/PLATFORMS.md, "Outputs removed in 1.4.0"..void/rules.md(Void is archived and read.voidrules),.mentatconfig.json(Mentat read
.mentat_config.json),sweep.yaml(Sweep is now a JetBrains assistant),.plandex.yaml
(Plandex never read it),.pearai/rules/,.ghostcoderignore,.doubleignore,
.piecesignoreand.ai/rules/(no tool documents them)..claudeignore,.copilotignoreand.antigravityignore: none of the three vendors documents
the file. Claude Code usesReaddeny rules, Copilot a Content exclusion setting, Antigravity
read_fileDeny rules or.gitignore..idx/airules.mdand.amazonq/rules/, whose products end on 22 March 2027 and 30 April 2027.
The owner chose to remove them in 1.4.0 rather than hold them to a later major version, as the
1.3.5 warning promised: use Antigravity's.agents/rules/and Kiro's.kiro/steering/, which
VibeTags writes..interpreter/profiles/vibetags.yamlandellipsis.yaml, which their tools no longer read, and
.zencoder/rules/, which Zencoder removed in May 2026.
With no deprecated output left,
DeprecatedServicesis deleted, and with it the deprecation
warning, theplatform.deprecatedlog event andvibetags init --list's[deprecated]marker.
MentatRenderer,SweepRenderer,PlandexRenderer,InterpreterRendererand
EllipsisRendererare deleted with their 17Platformconstants, and so are the two merge
shapes only they used:JsonRulesMerge(Mentat's JSON) andYamlMergeShape's keyed buckets
(Plandex). The prose formatter arms that.coderabbit.yaml,.pr_agent.toml,.roomodes,
.gemini/styleguide.mdand Greptile share wereINTERPRETER's and areCODERABBIT's now; every
one of those files is byte-identical. The README counts drop to 29 platforms, 47 config files and
16 scoped-rule directories; both reactors go from 53 active services to 38.
examples/multimodule-indexedopts into.aiexcludein place of.claudeignoreand
.copilotignore.RemovedPlatformOutputsTestcovers all 22 outputs; its 19 new cases fail
against the #645 commit.
Added
- A hand-written
.pr_agent.tomlgets the guardrails without losing its settings (#933). The
file is now merged the waygreptile.jsonis (#639): VibeTags owns a delimited span inside the
extra_instructionsstrings of[pr_reviewer]and[pr_code_suggestions], adds a table or key
the file lacks, and keeps every other byte. A document it cannot merge without guessing is left
alone with a warning naming why; the log recordswrite.skip reason=with the merge's reason
(duplicate-table,not-multiline-string,unclosed-spanand the rest). Until now such a file
was skipped outright, and before that overwritten. A[line inside a multi-line string or a
multi-line array is a value, not a table, so it neither ends a table early nor gets the key added
a second time, and a quoted name (["pr_reviewer"],'extra_instructions') is the same table
or key as the bare one; a UTF-8 byte order mark in front of the first header does not hide it.
TomlValueSpansTest(11 cases) and
aHandWrittenPrAgentConfig_keepsItsSettingsAndGainsTheGuardrailscover it; the latter failed before
the change. vibetags-cli/README.md. The CLI's flags, exit codes and limits were spread over a USAGE.md
section and the usage text inMain. The new README puts them in one place, with examples for
each command and check. The output it shows comes from running the CLI against scratch
projects. Linked from the README, the docs index and the repository layout notes.
Changed
- A release puts 84 files on Maven Central instead of 150 (#863). Central allows an
organization 1,167 files and 7 releases a month and rate-limits one that stays over, and
September 2026 was over both. The publishing plugin's default sent SHA-256 and SHA-512 beside
the MD5 and SHA-1 Central requires, and CycloneDX wrote each SBOM as XML as well as JSON. The
central-publishprofile now sets<checksums>required</checksums>and every published module
writes-cyclonedx.jsononly; anyone fetching the-cyclonedx.xmlclassifier will find no new
versions of it. Measured on a local bundle ofvibetags-annotationsbuilt with the upload
pointed at a closed port: 30 files before, 15 after, 36 and 20 once CI signs them.
CentralPublishingBudgetTestpins both settings.docs/RELEASING.mdand the release skill now
count the month's tags before a release, with a cadence of at most 3 a month. .aiexcludeis written besideGEMINI.md(#645). It neededgemini_instructions.mdor an
activeAGENTS.mdbeside it, so a Gemini user who followed the deprecation notice toGEMINI.md
would have lost.aiexcluderegeneration without a word. The orphan warning that suggests
creating.aiexcludefollows the same pairing, so a project withGEMINI.md,@AIIgnoreor
@AILocked, and no.aiexcludenow gets that warning.- Gemini's section wording is registered under
GEMINI_MD(#645). It lived under the removed
GEMINIplatform, whichGEMINI.mdreached as an alias throughPlatform.rendersAs(). No
platform is an alias now; the seam stays for the next one.GEMINI.mdprints the same bytes.
) - Test counts in the docs re-measured.
docs/architecture/testing.mdcarried a 0.7.x-era
per-class table and totals of 1484 and "724+" tests; it now points atdocs/TESTS.md, the
maintained map. The tier table inTESTS.mdis re-measured on 2026-10-06: 2655 tests in 208
classes formvn test, 3625 in 320 formvn test -Pe2e, against 957 and 1546 on 2026-08-10. - Three more of this repository's own classes carry
@AIContext. Each states a fact that
lives in another file or another build, so the class cannot show it:SourceDigest(an input
that shapes output but is not an option, opt-in or.vibetags-*config must be hashed, and a
test that compiles once never reaches the early exit to notice),TransitiveManifest(its JSON
is read out of JARs already on Maven Central by other processor versions) and
EnforcementBaseline(consumers commit the file, andGuardrailEnforceranswers an
unrecognised format with a warning and a pass, so dropping format 1 turns enforcement off on
upgrade).@AICorewas tried first and put the always-loaded block inCLAUDE.mdat 8,213
bytes against its 7,000 budget (#752). The facts matter when one of these files is edited,
which is when its scoped rule loads, so the block grows by 34 bytes instead. - PMD 7.27.0 -> 7.28.0 (#889). 7.28 adds
OnDemandImportto the best-practices category,
whichpmd-ruleset.xmlimports whole, and it fired 5 times: three wildcard imports in the
processor (AIGuardrailProcessor,FormatterRegistry,PlatformDescriptors) and the JMH
wildcard in bothload-testsbenchmarks. They are explicit imports now and the rule stays live,
since the rest ofsrc/mainalready follows it. A new formatter or renderer therefore needs its
import in the registry that lists it; the compiler says so if it is missing. The dependabot PR
failed 10 jobs on the processor's 3 findings and never reached the 2 inload-tests. - Dependencies: ArchUnit 1.5.0 -> 1.5.1 (#890), async-test-lib 1.12.2 -> 1.12.4 (#894, #897),
gradle/actions/wrapper-validation6.3.0 -> 6.4.0 (#893),anthropics/claude-code-action
1.0.235 -> 1.0.239 (#896), and the evals' pinned@anthropic-ai/claude-code2.1.283 -> 2.1.286
(#895). #890's red Gradle jobs were Maven Central answering 403 to the new ArchUnit POM, not
the bump.
Fixed
- A reactor root's Markdown aggregate states the scoped-rules index once (#930). Each module
rendered its own## Scoped Rules Indexheading and its 378-byte note, and the merge stacked them,
so they repeated once per module: 20% of blindbean'sGEMINI.mdand 24% of codekarta's. The merge
now writes the heading and note once, after the module blocks, with every module's lines under
them. It does so only when every module's section has the shape the renderer emits and the notes
agree; otherwise, as with two processor versions in one reactor, the stacked form is kept. The
sidecar format is unchanged.examples/multimodule-indexed'scopilot-instructions.mdloses its
second copy.aReactorRootStatesTheIndexPreambleOncefailed before the change. - The scoped-rules index names a role file once per package (#931). An element routed onto a
role file by.vibetags-roleskept a line of its own with the file repeated:
async-test-lib's rootGEMINI.mdhad 83 such lines, 8,744 bytes, pointing at 5 files. Elements
of one package in one role file now share a line,
<elements in="a.b" rules=".claude/rules/api.md">C, D</elements>inCLAUDE.mdand
- `a.b`: `C`, `D` → `.gemini/rules/api.md`in the Markdown aggregates. A role-file entry
for a single element keeps its one-element form, which is shorter.examples/all-tierschanges
accordingly.elementsSharingARoleFile_shareOneIndexLinefailed before the change. vibetags doctorchecks a reactor module by module (#929). It read only the root build file,
so a Maven reactor whose root only managesvibetags-processorand whose modules declare it was
told the annotations "will not compile" and exited 1, while a processor that was only managed
counted as wired. Doctor now follows the root's<modules>(profile-scoped and nested ones
included) or the Gradle settings file'sinclude(...), and checks each module whose sources
import the annotations, against its own build file and what it inherits from the root. Each
unwired module is named; a module with no annotations needs nothing. FourDoctorCommandTest
cases failed before the change; codekarta, the consumer in the issue, now reports6 use the annotations, all wiredand exits 0.- A nested type's scoped rule loads for the file its code is in. The glob was built from the
type's own simple name, soLedger.Entrygot**/Entry.java: no such file exists, and the rule
never loaded whenLedger.javawas opened, or an unrelatedEntry.javaelsewhere loaded it. A
member of a nested type files under that type and had the same glob. The glob now names the
outermost enclosing type's file,**/Ledger.java. The file a nested type is in reaches the build
fingerprint, so movingEntryinto a file of its own regenerates the glob; for every other
element the fingerprint is unchanged.NestedTypeRuleGlobTestfailed before the change. vibetags doctorreads a Gradleincludethat spans lines. Only the line holdinginclude
was read, soinclude(with one project per line below it listed no module and doctor fell back
to the root-only check above, and a Groovyinclude 'core',continued on the next line lost
every project after the first. The arguments are now read to the closing parenthesis, or across
every line ending in a comma. TwoDoctorCommandTestcases failed before the change.vibetags doctorreads a Gradle multi-project with no root build file. The settings file
alone lists the projects and each configures itself, but doctor looked only for
pom.xmlorbuild.gradle[.kts]at the root, reported "no build file found" and exited 1
without reading a module. A rootsettings.gradle[.kts]is now the build file when there is no
other.gradleReactorWithNoRootBuildFile_readsEachProjectfailed before the change.- Upgrading a pre-marker file keeps the hand-written text below a long legacy block (#936). The
end of an XML-shaped legacy block was looked for only in the first 2,000 characters after its
header, so a block longer than that (a dozen guardrails) read as running to the end of the file,
and everything below it was dropped. The block now ends at its first</project_guardrails>,
wherever it is, plus the<rule>lines a block closes with; a block without one keeps the
bounded search.aLongLegacyBlock_keepsTheHandWrittenSectionBelowItfailed before the change. - The orphan sweep looks past a UTF-8 byte order mark, as the writer already did. An editor that
saves UTF-8 with a BOM puts U+FEFF in front of the file. In a rule file with no front matter it
kept the first-line start marker from owning its line, so the sweep took the file for not ours and
a removed guardrail stayed in it; in one with front matter the block went, but the header left
behind no longer opened the file, so a rule file holding nothing but its globs was kept. The BOM
is set aside while the file is judged and written back with what remains.
deletesFile_whenAByteOrderMarkPrecedesTheMarkersand
aRuleFileSavedWithAByteOrderMark_isStillSweptWhenItsAnnotationGoesfailed before the change. - A hand-written
.pr_agent.tomlor.codex/config.tomlis no longer overwritten. TOML has
nowhere for a marker line, so VibeTags writes both files whole, and it replaced whatever was there:
a team's PR-Agent settings the first time.pr_agent.tomlcounted as an opt-in, and a project's
Codex configuration (model, MCP servers) the first timeAGENTS.mdwas managed, which writes
.codex/config.tomlimplicitly. Each is now replaced only when it is empty or carries the
generated header; otherwise it is left exactly as it is, with a warning that says why it carries no
guardrails and how to hand it over. The log recordswrite.skip reason=hand-written-whole-file.
aHandWrittenCodexConfig_isNotOverwrittenandaHandWrittenPrAgentConfig_isNotOverwritten
failed before the change. - A test-compile with no annotations no longer empties the ignore files. Ignore files
(.cursorignore,.aiderignoreand the rest) were rewritten from every round, even one that
found no annotation. In a single-module Maven or Gradle build, the test-compile round sees only
the test sources; when they carry no annotation it rendered an empty exclusion list and wrote it
over the main round's, so everymvn testormvn packageleft the ignore files with an empty
block. A reactor module with no annotations, compiled after the one that has them, did the same
to the shared root file. Now an ignore file is rewritten from an empty round only when the
content is the merge of every module's sidecar, which is what still retires the last
@AIIgnoreof a reactor.anUnannotatedTestRoundKeepsTheMainSourcesIgnoreGlobsand
anUnannotatedReactorModule_leavesTheSharedIgnoreFileAlonefailed before the change;
removingTheLastIgnoreInAReactor_clearsItFromTheSharedIgnoreFilepins the case the narrower rule
keeps, and fails if the ignore term is dropped outright. - A module leaving the reactor no longer deletes the hand-written text in its rule files. The
departed module's granular rule files were deleted by name, so a note the developer had added
below the markers went with them, the one thing invariant 2 says never happens. The orphan sweep
already removed only the generated block; the departure now does the same, deleting the file
only when nothing of the developer's is left, and leaving a same-named file with no markers
alone.moduleRemovedFromTheReactor_keepsTheHandWrittenTextInItsRuleFilefailed before the
change. - A rule file whose generated block could not be removed is retried by the next build. When the
orphan sweep takes a guardrail out of a granular rule file that also holds the developer's own
text, it rewrites the file instead of deleting it. A failed rewrite (a file an editor holds open
on Windows, a read-only directory) was swallowed, and the cache entry still vouched for the file,
so every later unchanged build short-circuited and the removed guardrail stayed in the file. The
failure is now a warning, and the entry is marked failed so the next build retries, as a failed
delete already was (#867).aRuleFileThatCouldNotBeRewritten_isScrubbedByTheNextBuildfailed
before the change. vibetags doctorno longer says the annotations "will not compile" when they arrive through
the processor. The processor's pom declaresvibetags-annotationsas a compile dependency, so a
build withvibetags-processoras a Maven<dependency>(scopeprovidedorcompile) or a
GradlecompileOnly/implementation/apidependency compiles the@AI*annotations without
naming them. Doctor only looked for the stringvibetags-annotationsin the build file, so it
reported a finding and exited 1 for four of the five consumer repositories in the regression
sweep, all of which compile. It now printsannotations dep: yes (through the vibetags-processor dependency). A processor reached only throughannotationProcessorPathsor Gradle's
annotationProcessor, or with atestorruntimescope, is still a finding. Four
DoctorCommandTestcases; the two healthy ones failed before the change.- A field or method annotated
@AIIgnoreno longer becomes a file glob (#926). Every
exclusion file (.cursorignore,.aiexclude, Greptile'signorePatternsand the rest) wrote
it as**/<memberName>.java: a glob for a file that does not exist, or for an unrelated file
that happens to share the name.examples/multimodule-indexedcarried
**/cachedExpiryEpochDay.javafor aprivate transient long. Only a type contributes a glob
now, as@AILockedalready did for.aiexclude; the member is still excluded in every prose
output that can name it. A module whose only@AIIgnoreis on a member contributes nothing to
an ignore file, rather than a header with no globs under it.MemberLevelAIIgnoreGlobTest
failed 2 of 2 before the change. - A nested type annotated
@AIIgnoreor@AILockedno longer becomes a file glob either. It
was written as**/<NestedName>.java, a file that does not exist, while its code is in the
outermost type's file: an unrelated file of that name was hidden and the code meant to be was
not. Only a top-level type contributes a glob now; the nested type is still excluded or locked
in every prose output. Hiding the whole outer file instead would hide code nobody marked.
aNestedTypeIsNotWrittenAsAFileGlobfailed before the change, for both annotations. - A module that gave up waiting for the generation lock released it for other processes on
Linux (#923). Every waiter opened its own channel on.vibetags-generate.lockand polled
tryLock(). The JDK takesfcntllocks on Linux, which belong to the process, so when a waiter
timed out or was interrupted and closed its channel, the holder's lock was gone while the holder
still reported it held. A second build on the same root, a Gradle daemon beside a Maven run,
could then generate concurrently, which is the interleaving #908 added the lock to prevent.
.vibetags-baseline.lockused the same poll. Waiters in one JVM now queue at an in-JVM gate
before any of them opens a channel, so only the thread about to hold the lock has the file open.
The gate is an interned string's monitor and a system property, because each processor
classloader has its own copy of the class. Windows locks per handle and never had the bug.
GenerationLockTeststarts a second JVM after a waiter gives up and asserts it cannot take the
lock; on Linux JDK 21 it failed before the fix. vibetags doctorjudged markers differently from the writer (#919). It flagged a file when
one marker literal appeared and the other did not. The writer only counts a start marker that
owns its line outside a code fence, closed by the first end line after it. So doctor passed an
end above its start, and a real block that lost its end next to a fenced example of the pair,
both of which the next build repairs by discarding text or refuses; and it failed a CI gate on a
marker quoted inside a sentence, which the writer ignores. Doctor now asks the writer through
GuardrailFileWriter.hasBrokenMarkers, built on the same search the write path uses. Three
DoctorCommandTestcases were red before the change.vibetags doctor --contextcounted hand-written text as generated (#919). The report found
the block with the same literal search, so a marker quoted in prose above the block made it
count everything from the quote down: 207 B reported as generated in a test file whose block
was 102 B. It now uses the writer'sindexOfBlockStartandindexOfMarkerLine, and reports the
one block the writer manages.- One unlistable directory turned off doctor's Groovy and Kotlin checks (#922). The project
was walked once per file type, and the first directory doctor could not list ended each walk,
so a root-owned directory such as a database volume left the checks with whatever came before
it, often nothing, and was reported once per file type. The project is now walked once; the
directory is one finding and every readable source is checked. vibetags doctor --classpathaborted on an unlistable directory in a class directory
(#921).Files.walkthrew it as anUncheckedIOExceptiontheIOExceptionhandler did not
catch, so doctor printed a bareAccessDeniedExceptionand stopped after the marker line, with
no Kotlin check and no result. The class directory is now walked file by file: an unreadable
directory or class file is one more finding and the rest still counts.vibetags doctor --helpandvibetags init --helpfailed with exit 2 (#920). Only a
--helpin first position was recognised; after a command it was a stray argument.--help
and-hnow print usage and exit 0 wherever they appear.vibetags init --platformsmishandled blank and repeated entries (#920).,cursoror
claude,,cursorfailed the whole command withunknown platform key(s):and no key named;
claude,claudereported the key as both created and already active. Blank entries are now
skipped, repeats collapse to one, and a list with no key left is the usual "Nothing created"
usage error.- A round skipped as unchanged flushed the shared write cache without the generation lock
(#916). When another processor generates sources after the first round was skipped, the round
clears its recorded source digest and flushes.vibetags-cache. That flush re-reads and rewrites
the root file, and the merge on flush (#907) is not a lock: racing a sibling's generation flush,
either side's changes could be lost, the clear included, leaving a digest that vouches for the
skip. In a reactor that flush now takes.vibetags-generate.locklike generation's do. The branch
is rare, so the ordinary early exit waits on nothing.ParallelReactorGenerationTestholds the
lock while a rebuild takes that branch through a wrapper processor that generates a source; the
rebuild finished while the lock was held before the change and waits for it after. - The
vibetags-self-checkpre-commit hook exited 127 on Windows outside Git Bash (#905).
Run from PowerShell or cmd, pre-commit found bash throughPATH, which there is the WSL
launcher inWindowsApps. WSL bash read the backslashed script path as escapes, so the hook
failed beforetools/self-check.shstarted. The hook now runs the script as a git!alias,
which Git runs with its own shell from the repository root on every platform. Verified from
PowerShell and from Git Bash: both run the 25 s regeneration and pass. - Two copies of the processor in one JVM could write through the same temp file. The root
.vibetags-baselineand.vibetags-cacheare written to<name>.<pid>-<n>.tmpand renamed
into place, with<n>from a static counter. A parallel reactor that loads the processor once
per module has one counter per copy, each starting at zero, so two modules wrote
.vibetags-baseline.<pid>-1.tmpat once: one rename moved bytes its writer never produced and
the other failed withNoSuchFileException. The name now carries a random tag drawn once per
class load,<name>.<pid>-<tag>-<n>.tmp. Found by the two-classloader test written for #911. - Enforcement baselines were recorded unlocked when a sibling module loaded the processor in
another classloader (#911).EnforcementBaseline.update()took.vibetags-baseline.lockwith
lock(). A file lock belongs to the whole JVM, and the per-root monitor in front of it only to
one copy of the class, so undermvn -Tor Gradle--parallelthe second module got
OverlappingFileLockExceptionand merged with no lock: the #554 lost update again. It now polls
tryLock()throughGenerationLock, which waits in both cases. Like generation, a wait over two
minutes proceeds unlocked instead of hanging the build; before, a lock held by another process
was waited on indefinitely. Two newEnforcementBaselineTestcases failed before the change: one
holds the lock outside the monitor and seesupdate()write anyway, and one records from two
classloaders at once and lost an approval in round 0. - In a parallel reactor the last write-cache flush dropped every sibling's cache entries
(#907). Each module's processor loads.vibetags-cachebefore any of them generates, and
WriteCache.flush()wrote that whole loaded view back, so the last module to flush erased the
entries, fingerprints, sidecar stamp and source digests its siblings had flushed since. Nothing
was wrong in the output, but the early exit and the per-file fast path missed on the build after
a parallel one. A flush now re-reads the file and replaces only what this instance changed: the
keys it recorded or removed, its own module section, and the root stamp if it set one. An entry
is still kept exactly as its writer recorded it, so the merge cannot pair one writer's hash with
another's file attributes.WriteCacheTestdrives two instances loaded before either flushes;
two of its three new cases failed before the change. Measured onexamples/multimodule(#915):
the second of twoclean verifybuilds under-T 4took the cache fast path for 285 of 544
file writes before the change and 544 of 544 after, the same as a serial build; the other 259
fell back to read-and-compare. Identical in three runs each. - A parallel reactor could drop a module's region from the root files (#908). Under
mvn -T
or Gradle--parallel, a module that read the sidecars before a sibling saved one could write
CLAUDE.mdand the other root files last, from a merge without that sibling, and the region
stayed missing until the next build. Module rounds now generate under an exclusive lock on
.vibetags-generate.lockat the root (empty; gitignore it), taken aroundgenerateFiles()in
process()so the locked step order is untouched. The lock pollstryLock()because a second
module in the same JVM getsOverlappingFileLockExceptionrather than a wait, and it never
fails a build: an unusable lock or a wait over two minutes proceeds unlocked and logs
generate.lock.skip reason=. The root round, single-module builds and a root with no opt-in
take no lock and create no file. Lock files no longer count as configuration in the early-exit
key, or the lock appearing would have cost every module its early exit on the next build
(SourceDigestEarlyExitEndToEndTestcaught it).ParallelReactorGenerationTestholds one module between its sidecar read and its write
through a test-only seam, since the window is milliseconds: red 3 of 3 before, green 3 of 3
after, and red again with the lock planted to give up onOverlappingFileLockException. - In a parallel reactor the write cache could vouch for another module's bytes. Every
module of amvn -Tor Gradle--parallelbuild writes the root files through its own
WriteCacheover the one.vibetags-cache. The writer stat'ed the file after moving its bytes
into place, and a file it found already current after reading it, so a sibling's write landing
in between paired this module's hash with the sibling's size and mtime. The cache then reported
the file unchanged while it held other content, and the write that would have fixed it was
skipped. How that reaches a user is reasoned, not reproduced end to end: a later build that
renders an earlier body again, such as after a module is removed, keeps the departed module's
region. A write now records the staging file's attributes, which a rename keeps. A file found
current is recorded only when a stat before the read and one after it agree on size, full
mtime and file key (the inode on Linux and macOS); otherwise it is marked for re-checking next
build. The second stat guards an interleaving reasoned from the code, a replacement before the
read followed by a third write of the same size in the same millisecond; no test has made it
fail.WriteCacheCrossInstanceAsyncTestreproduces the defect with 8 writers on one file:
reverting the write-path fix or the found-current fix fails it in 3 of 3 runs on Windows. .aiexcludeno longer gets an empty header block for each module with nothing to exclude.
AiExcludeRendereralways emitted its three-line header, and the reactor merge wraps every
non-blank contribution, so each module and each source set without an excludable type added a
header with no globs under it. This repository's own.aiexcludeheld two empty copies (main and
test rounds) and nothing else, and three of the six headers inexamples/multimodulehad no
globs under them.IgnoreFileRendererstopped doing this for the other ignore files in #328;
.aiexcludehas its own renderer and was missed. It now decides on the globs it emitted rather
than on the model, because a locked member is real but cannot be named by a glob. Pinned by
AiExcludeEmptyContributionEndToEndTest, which failed on the old renderer with the header-only
output.- A guardrail file saved as UTF-16 now says so instead of
Input length = 1(#878). Windows
PowerShell 5.1'secho "" > CLAUDE.mdwrites UTF-16 LE with a byte order mark (checked: the
file startsFF FE). VibeTags reads and writes UTF-8, so every build warned
Failed to write AI rules file: ... - Input length = 1and the file never got its guardrails,
with nothing saying why. The warning now names the encoding (it is saved as UTF-16LE, or
it is not valid UTF-8for a legacy single-byte file) and says to re-save the file as UTF-8,
and the log recordswrite.skip reason=not-utf8 encoding=. The file is still left
byte-for-byte unchanged: transcoding it would change its encoding under the user. A coding
failure that does not come from the file keeps the plain message. - Instruction Evals and the Inquisitor run on a Claude subscription token as well as an API key
(#632). Both workflows looked only forANTHROPIC_API_KEY, so a repository holding a
CLAUDE_CODE_OAUTH_TOKENsecret (fromclaude setup-token) skipped both gates for good. The
preflights now accept either secret and the model steps receive both; an unset one is dropped
before the eval harness starts.ModelCredentialGateTestpins the two halves together. The
workflow header andevals/README.mdalso quoted a default run of 15 and 12 sessions; with 6
tasks it is 18.