basalt-ui 1.40.3 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli/index.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  readFileSync,
15
15
  rmdirSync,
16
16
  statSync,
17
+ realpathSync,
17
18
  unlinkSync,
18
19
  writeFileSync,
19
20
  } from 'node:fs'
@@ -338,7 +339,7 @@ export function resolveProjectDir(cwd: string): ProjectResolution {
338
339
  /**
339
340
  * Which shape of consumer a project-scoped command is looking at — DECLARED, never inferred
340
341
  * (`--tokens-only`, or `"basalt": { "profile": "tokens-only" }`), because the answer switches the
341
- * Mantine-remedy guard kinds off. Why that direction: package CLAUDE.md § CLI.
342
+ * Mantine-remedy guard kinds off. Why that direction: package MAINTAINING.md § CLI.
342
343
  */
343
344
  export function declaredProfile(cfg: BasaltConfig, flags: readonly string[]): DoctorProfile {
344
345
  if (flags.includes('--tokens-only')) return 'tokens-only'
@@ -452,7 +453,7 @@ export function waiverHintFor(relPath: string, profile: DoctorProfile): string {
452
453
  *
453
454
  * - `managed` : basalt owns it. sync refreshes it to the shipped version; a local edit is skipped
454
455
  * and reported (`--force` overwrites); `--check` exits 1 on drift. Applies to exactly
455
- * what Claude reads (.claude/rules/*, .claude/skills/*, the CLAUDE.md block) —
456
+ * what Claude reads (.claude/rules/*, .claude/skills/*, the AGENTS.md block) —
456
457
  * Claude Code cannot load rules or skills from node_modules, and that platform limit
457
458
  * is the only reason anything is copied at all. The sync diff is the review gate.
458
459
  * - `seed` : written once if absent, then owned entirely by the consumer. sync never overwrites
@@ -462,7 +463,7 @@ export function waiverHintFor(relPath: string, profile: DoctorProfile): string {
462
463
  * the package while the consumer still owns the file.
463
464
  *
464
465
  * A managed file with `markers: true` is spliced as a `<!-- basalt:begin -->…<!-- basalt:end -->`
465
- * region inside a host file the consumer otherwise owns (CLAUDE.md) — not a third mode, just
466
+ * region inside a host file the consumer otherwise owns (AGENTS.md) — not a third mode, just
466
467
  * managed ownership scoped to a region.
467
468
  */
468
469
  type Mode = 'managed' | 'seed'
@@ -518,11 +519,15 @@ type TemplateVars = {
518
519
  BASALT_BIN: string
519
520
  }
520
521
 
521
- // The block markers the CLAUDE.md template emits. The begin marker carries the framework version
522
+ // The block markers the AGENTS.md template emits. The begin marker carries the framework version
522
523
  // (`<!-- basalt:begin 1.0.0 -->`), so the region is matched by the begin PREFIX, not an exact string.
523
524
  const BLOCK_BEGIN_PREFIX = '<!-- basalt:begin'
524
525
  const BLOCK_END = '<!-- basalt:end -->'
525
526
  export const MANIFEST_PATH = '.basalt/manifest.json'
527
+ /** The host file the managed block is spliced into (and its manifest key). */
528
+ export const AGENTS_MD = 'AGENTS.md'
529
+ /** Where the block lived before it moved to {@link AGENTS_MD} — read only to migrate out of it. */
530
+ export const LEGACY_BLOCK_HOST = 'CLAUDE.md'
526
531
 
527
532
  export function sha256(text: string): string {
528
533
  return createHash('sha256').update(text, 'utf8').digest('hex')
@@ -633,6 +638,156 @@ function applyBlock(host: string, block: string): string {
633
638
  return base.length === 0 ? `${block.trim()}\n` : `${base}\n\n${block.trim()}\n`
634
639
  }
635
640
 
641
+ /** True when both paths resolve to one file (a symlinked pair). A missing path is simply "no". */
642
+ export function sameFile(a: string, b: string): boolean {
643
+ try {
644
+ return realpathSync(a) === realpathSync(b)
645
+ } catch (error) {
646
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
647
+ throw error
648
+ }
649
+ }
650
+
651
+ /** True when the host text carries a managed block (begin marker present). */
652
+ export function hasManagedBlock(host: string | null): boolean {
653
+ return host !== null && host.includes(BLOCK_BEGIN_PREFIX)
654
+ }
655
+
656
+ /**
657
+ * The block region of a host, or why it cannot be read: duplicate begin markers, or a begin marker
658
+ * with no end. The ONE detector `migrateBlockToAgentsMd` and `doctor` share, so they cannot disagree
659
+ * about whether a `CLAUDE.md` is well-formed.
660
+ */
661
+ export function scanBlock(host: string): {
662
+ region: { start: number; end: number } | null
663
+ problem: string | null
664
+ } {
665
+ const begin = hasManagedBlock(host)
666
+ if (!begin && !host.includes(BLOCK_END)) return { region: null, problem: null }
667
+ const bad = (problem: string): { region: null; problem: string } => ({ region: null, problem })
668
+ if (!begin) return bad(`\`${BLOCK_END}\` with no \`${BLOCK_BEGIN_PREFIX}\``)
669
+ let region: { start: number; end: number } | null
670
+ try {
671
+ region = findBlockRegion(host)
672
+ } catch (error) {
673
+ return bad((error as Error).message)
674
+ }
675
+ if (region === null) return bad(`\`${BLOCK_BEGIN_PREFIX}\` has no matching \`${BLOCK_END}\``)
676
+ // Exactly one end marker, after the begin: an orphan or extra one means the region is a guess.
677
+ if (host.indexOf(BLOCK_END) < region.start || host.indexOf(BLOCK_END, region.end) !== -1) {
678
+ return bad(`an extra or misplaced \`${BLOCK_END}\` outside the basalt block`)
679
+ }
680
+ return { region, problem: null }
681
+ }
682
+
683
+ /** One line that is exactly the `@AGENTS.md` import — not a prose or code-span mention, not `.bak`. */
684
+ export function isAgentsImportLine(line: string): boolean {
685
+ return line.trim() === `@${AGENTS_MD}`
686
+ }
687
+
688
+ /** Whether a `CLAUDE.md` imports `AGENTS.md`, so Claude Code still loads it. */
689
+ export function importsAgentsMd(text: string): boolean {
690
+ return text.split(/\r?\n/).some(isAgentsImportLine)
691
+ }
692
+
693
+ export type BlockMigration = {
694
+ /** The legacy host (`CLAUDE.md`) carried a managed block that has to move. */
695
+ pending: boolean
696
+ /** The legacy host was left empty (or `@AGENTS.md` only) and removed. */
697
+ deletedHost: boolean
698
+ /** Why nothing was moved and the run must fail: malformed markers, or two differing blocks. */
699
+ problem: string | null
700
+ }
701
+
702
+ /**
703
+ * Move the managed block out of `CLAUDE.md` into `AGENTS.md` (an install from before the move).
704
+ * Idempotent: a second run finds nothing and returns `pending: false`.
705
+ * 1. The ledger entry is re-keyed `CLAUDE.md` → `AGENTS.md` in memory (the caller persists it), so
706
+ * an unedited legacy block classifies `unchanged`, not untracked.
707
+ * 2. The region is cut out of `CLAUDE.md` and — unless `AGENTS.md` already has one — spliced into
708
+ * `AGENTS.md` AS IT IS, not as the new rendering: the normal three-way classify then sees a
709
+ * hand-edit and skips it like any drifted unit. The rest of `CLAUDE.md` is consumer-owned and
710
+ * stays, except the file is deleted when nothing but whitespace or `@AGENTS.md` is left.
711
+ * 3. Nothing is discarded silently: if `AGENTS.md` already has a block that differs (normalized,
712
+ * as the ledger compares) from the legacy one, or either file's markers are malformed, BOTH
713
+ * files are left byte-identical and `problem` says what to resolve by hand.
714
+ * `dryRun` (`sync --check`) writes nothing but still reports `pending` and `problem`.
715
+ */
716
+ export function migrateBlockToAgentsMd(
717
+ cwd: string,
718
+ manifest: Manifest,
719
+ opts: { dryRun?: boolean } = {},
720
+ ): BlockMigration {
721
+ const legacyHash = manifest.files[LEGACY_BLOCK_HOST]
722
+ if (legacyHash !== undefined) {
723
+ manifest.files[AGENTS_MD] ??= legacyHash
724
+ delete manifest.files[LEGACY_BLOCK_HOST]
725
+ }
726
+ const none: BlockMigration = { pending: false, deletedHost: false, problem: null }
727
+ const legacyAbs = resolve(cwd, LEGACY_BLOCK_HOST)
728
+ const agentsAbs = resolve(cwd, AGENTS_MD)
729
+ const legacy = readIfExists(legacyAbs)
730
+ if (legacy === null) return none
731
+ // A symlinked pair is one file: cutting the block out of "CLAUDE.md" would cut AGENTS.md too.
732
+ // Fail closed when that cannot be proven (EACCES, ELOOP, …) — and say so, not a stack trace.
733
+ try {
734
+ if (sameFile(legacyAbs, agentsAbs)) return none
735
+ } catch (error) {
736
+ const why = (error as NodeJS.ErrnoException).code ?? (error as Error).message
737
+ return { ...none, problem: `cannot tell whether ${LEGACY_BLOCK_HOST} is ${AGENTS_MD} (${why})` }
738
+ }
739
+ const legacyScan = scanBlock(legacy)
740
+ const { region } = legacyScan
741
+ if (legacyScan.problem !== null) {
742
+ return { ...none, problem: `${LEGACY_BLOCK_HOST}: ${legacyScan.problem}` }
743
+ }
744
+ if (region === null) return none
745
+
746
+ const agents = readIfExists(agentsAbs) ?? ''
747
+ const agentsScan = scanBlock(agents)
748
+ if (agentsScan.problem !== null)
749
+ return { ...none, problem: `${AGENTS_MD}: ${agentsScan.problem}` }
750
+ const legacyBlock = legacy.slice(region.start, region.end)
751
+ if (
752
+ agentsScan.region !== null &&
753
+ normalizeForLedger(agents.slice(agentsScan.region.start, agentsScan.region.end)) !==
754
+ normalizeForLedger(legacyBlock)
755
+ ) {
756
+ return {
757
+ ...none,
758
+ problem:
759
+ `${LEGACY_BLOCK_HOST} and ${AGENTS_MD} each carry a basalt block and they differ — ` +
760
+ `delete the stale block from ${LEGACY_BLOCK_HOST} (or the one in ${AGENTS_MD}, if ` +
761
+ `${LEGACY_BLOCK_HOST}'s is the one you want kept), then re-run.`,
762
+ }
763
+ }
764
+
765
+ const before = legacy.slice(0, region.start).trimEnd()
766
+ const after = legacy.slice(region.end).trimStart()
767
+ const remainder = before === '' || after === '' ? `${before}${after}` : `${before}\n\n${after}`
768
+ const deletedHost = remainder
769
+ .split(/\r?\n/)
770
+ .every((l) => l.trim() === '' || isAgentsImportLine(l))
771
+ if (opts.dryRun === true) return { pending: true, deletedHost, problem: null }
772
+
773
+ if (agentsScan.region === null) writeFileEnsuringDir(agentsAbs, applyBlock(agents, legacyBlock))
774
+ if (deletedHost) unlinkSync(legacyAbs)
775
+ else writeFileSync(legacyAbs, `${remainder.trimEnd()}\n`)
776
+ return { pending: true, deletedHost, problem: null }
777
+ }
778
+
779
+ /**
780
+ * The refusal `init` and `sync` run BEFORE any side effect (roots patch, package.json script,
781
+ * manifest): a dry-run migration against a scratch manifest, so a block that cannot be moved
782
+ * safely stops the command with nothing written. Null = safe to proceed.
783
+ */
784
+ export function blockMigrationProblem(cwd: string): string | null {
785
+ const { problem } = migrateBlockToAgentsMd(cwd, { version: 1, files: {} }, { dryRun: true })
786
+ return problem === null
787
+ ? null
788
+ : `${problem.replace(/\.$/, '')}. Nothing else was reconciled — resolve this by hand, then re-run.`
789
+ }
790
+
636
791
  /**
637
792
  * Explicit `--with-router` / `--with-query` CLI flags force-include a router/query scaffold even
638
793
  * when the peer isn't detected in the consumer's package.json yet (e.g. it's about to be added).
@@ -809,15 +964,17 @@ export function managedFiles(
809
964
  render: (ctx: RenderContext) => readSource(ctx.pkgRoot, `agent/skills/${name}/SKILL.md`),
810
965
  }))
811
966
 
812
- const claudeBlock: ManagedFile = {
813
- dest: 'CLAUDE.md',
967
+ // AGENTS.md, not CLAUDE.md: Claude Code loads a repo-root AGENTS.md directly, but ANY CLAUDE.md
968
+ // in or above the cwd suppresses it — so the block must not be the reason a CLAUDE.md exists.
969
+ const agentsBlock: ManagedFile = {
970
+ dest: AGENTS_MD,
814
971
  mode: 'managed',
815
972
  markers: true,
816
- source: 'agent/templates/CLAUDE-block.md.tpl',
973
+ source: 'agent/templates/AGENTS-block.md.tpl',
817
974
  // With markers, render() returns the fully-rendered region INCLUDING its begin/end markers.
818
- // The writer splices it into the consumer's host CLAUDE.md at apply time.
975
+ // The writer splices it into the consumer's host AGENTS.md at apply time.
819
976
  render: (ctx) => {
820
- const tpl = readSource(ctx.pkgRoot, 'agent/templates/CLAUDE-block.md.tpl')
977
+ const tpl = readSource(ctx.pkgRoot, 'agent/templates/AGENTS-block.md.tpl')
821
978
  if (tpl === null) return null
822
979
  return fillTemplate(tpl, ctx.vars).trim()
823
980
  },
@@ -946,7 +1103,7 @@ export function managedFiles(
946
1103
  // lefthook/CI config, and writing over or beside it is worse than not writing at all.
947
1104
  const rootTooling: ManagedFile[] = placement.isPackageRepoRoot ? [lefthook, ci] : []
948
1105
 
949
- return [...rules, ...skills, claudeBlock, design, oxfmt, ...rootTooling, oxlintrc, ...scaffolds]
1106
+ return [...rules, ...skills, agentsBlock, design, oxfmt, ...rootTooling, oxlintrc, ...scaffolds]
950
1107
  }
951
1108
 
952
1109
  /**
@@ -1086,6 +1243,18 @@ export function reportPrune(pruned: PruneResult): void {
1086
1243
  )
1087
1244
  }
1088
1245
 
1246
+ /** Name what {@link migrateBlockToAgentsMd} did — a file leaving a consumer's tree is never silent. */
1247
+ export function reportBlockMigration(migration: BlockMigration, command: 'init' | 'sync'): void {
1248
+ if (!migration.pending) return
1249
+ const tail = migration.deletedHost
1250
+ ? `${LEGACY_BLOCK_HOST} held nothing else and was deleted.`
1251
+ : `the rest of ${LEGACY_BLOCK_HOST} is yours; while it exists Claude Code loads it INSTEAD of ` +
1252
+ `${AGENTS_MD} unless it imports @${AGENTS_MD} — move your content over and delete it.`
1253
+ console.log(
1254
+ `\nbasalt-ui ${command}: moved the basalt block from ${LEGACY_BLOCK_HOST} to ${AGENTS_MD} — ${tail}`,
1255
+ )
1256
+ }
1257
+
1089
1258
  /**
1090
1259
  * Delete the managed rule/skill files this basalt version retired, and drop their manifest entries.
1091
1260
  *
@@ -1194,7 +1363,7 @@ type Classification = 'unchanged' | 'drifted' | 'missing' | 'current'
1194
1363
  * - Blank out the per-release `BASALT_VERSION` token in a marker's begin line
1195
1364
  * (`<!-- basalt:begin 1.0.1 -->` → `<!-- basalt:begin -->`). It changes on every release and is
1196
1365
  * the only per-release variable that lands INSIDE the hashed region (the rest of
1197
- * `CLAUDE-block.md.tpl` has no other `{{…}}` placeholders today), so stripping it is what lets
1366
+ * `AGENTS-block.md.tpl` has no other `{{…}}` placeholders today), so stripping it is what lets
1198
1367
  * one normalized rendering stand in for every version of a given template body instead of
1199
1368
  * needing one ledger entry per release. If a future marker template interpolates another
1200
1369
  * per-release var inside its region, extend this normalization the same way.
package/src/cli/init.ts CHANGED
@@ -10,9 +10,11 @@ import { deriveSpacing } from '../tokens'
10
10
  import type { MergeLintResult, ScaffoldFlags } from './index'
11
11
  import {
12
12
  MANIFEST_PATH,
13
+ blockMigrationProblem,
13
14
  findBasaltInstall,
14
15
  managedFiles,
15
16
  mergeOxlintExtends,
17
+ migrateBlockToAgentsMd,
16
18
  normalizeForLedger,
17
19
  patchPackageJson,
18
20
  placementNotices,
@@ -20,6 +22,7 @@ import {
20
22
  readManifest,
21
23
  reconcileRoots,
22
24
  renderContext,
25
+ reportBlockMigration,
23
26
  reportPrune,
24
27
  resolvePeerFlags,
25
28
  resolvePlacement,
@@ -52,6 +55,13 @@ const KEPT_FILE_COST: Record<string, string> = {
52
55
 
53
56
  /** Scaffold all managed files into the consumer repo, then write the manifest. Idempotent. Returns 0. */
54
57
  export function init(cwd: string = process.cwd(), scaffoldFlags: ScaffoldFlags = {}): number {
58
+ // First, before anything is written: a block that cannot be moved safely refuses the whole run.
59
+ const blockProblem = blockMigrationProblem(cwd)
60
+ if (blockProblem !== null) {
61
+ console.error(`✖ basalt-ui init: ${blockProblem}`)
62
+ return 1
63
+ }
64
+
55
65
  // ── Describe the repo BEFORE rendering anything ───────────────────────────
56
66
  // Every roots-derived seed (the CI oxfmt globs, DESIGN.md's series path, the default scan
57
67
  // exemption) renders from `basalt.roots`, so the key has to exist first. Writing it is what
@@ -86,6 +96,9 @@ export function init(cwd: string = process.cwd(), scaffoldFlags: ScaffoldFlags =
86
96
  const placement = resolvePlacement(cwd)
87
97
  const files = managedFiles(peers, placement)
88
98
 
99
+ // Before the loop: an install from before the move carries the block in CLAUDE.md.
100
+ const migration = migrateBlockToAgentsMd(cwd, manifest)
101
+
89
102
  const writtenFiles: string[] = []
90
103
  const keptFiles: string[] = []
91
104
  const missingSources: string[] = []
@@ -131,6 +144,7 @@ export function init(cwd: string = process.cwd(), scaffoldFlags: ScaffoldFlags =
131
144
  `basalt-ui init: ${writtenFiles.length} written, ${keptFiles.length} kept, manifest at ${MANIFEST_PATH}`,
132
145
  )
133
146
  reportPrune(pruned)
147
+ reportBlockMigration(migration, 'init')
134
148
  if (missingSources.length > 0) {
135
149
  console.log(
136
150
  `basalt-ui init: ${missingSources.length} shipped asset(s) not present, skipped: ${missingSources.join(', ')}`,
package/src/cli/sync.ts CHANGED
@@ -17,8 +17,12 @@ import {
17
17
  diffSummary,
18
18
  findBasaltInstall,
19
19
  inspectLefthookGate,
20
+ AGENTS_MD,
21
+ blockMigrationProblem,
20
22
  isStringArray,
23
+ LEGACY_BLOCK_HOST,
21
24
  managedFiles,
25
+ migrateBlockToAgentsMd,
22
26
  normalizeForLedger,
23
27
  noteRelocation,
24
28
  OXLINT_FILES,
@@ -32,6 +36,7 @@ import {
32
36
  reconcileRoots,
33
37
  relativePosix,
34
38
  renderContext,
39
+ reportBlockMigration,
35
40
  reportPrune,
36
41
  resolvePeerFlags,
37
42
  resolvePlacement,
@@ -149,6 +154,13 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
149
154
  return 1
150
155
  }
151
156
 
157
+ // A block that cannot be moved safely refuses the whole run BEFORE reconcileRoots writes anything.
158
+ const blockProblem = blockMigrationProblem(cwd)
159
+ if (blockProblem !== null) {
160
+ console.error(`✖ basalt-ui sync: ${blockProblem}`)
161
+ return 1
162
+ }
163
+
152
164
  // Before renderContext, which reads `basalt.roots` for the roots-derived template variables.
153
165
  const rootsState = reconcileRoots(cwd, { write: opts.check !== true })
154
166
  const ctx = renderContext(cwd)
@@ -157,6 +169,10 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
157
169
  const placement = resolvePlacement(cwd)
158
170
  const files = managedFiles(peers, placement)
159
171
 
172
+ // Before the loop: the block moved from CLAUDE.md to AGENTS.md, and the loop reads AGENTS.md.
173
+ // `--check` only reports it (and stays red until a real sync moves it).
174
+ const migration = migrateBlockToAgentsMd(cwd, manifest, { dryRun: opts.check === true })
175
+
160
176
  let updated = 0
161
177
  // `created` and `recreated` were one counter, and `0 updated, 20 recreated` is what a full
162
178
  // scaffold printed — a word that means "it was here, it went missing, I put it back" describing
@@ -272,10 +288,16 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
272
288
  `${retired.join(', ')} — run \`basalt-ui sync\`.`,
273
289
  )
274
290
  }
291
+ if (migration.pending) {
292
+ console.error(
293
+ `basalt-ui sync --check: the basalt block is still in ${LEGACY_BLOCK_HOST} — it now ` +
294
+ `lives in ${AGENTS_MD}. Run \`basalt-ui sync\` to move it.`,
295
+ )
296
+ }
275
297
  if (staleForCheck > 0) {
276
298
  console.error(`basalt-ui sync --check: ${staleForCheck} managed file(s) out of date.`)
277
- return 1
278
299
  }
300
+ if (staleForCheck > 0 || migration.pending) return 1
279
301
  const n = placementSkips.length
280
302
  const skips = n === 0 ? '' : ` (${n} permanent placement skip(s) — --verbose for detail)`
281
303
  console.log(`✓ basalt-ui sync --check: all managed files current.${skips}`)
@@ -291,6 +313,7 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
291
313
  `${skippedDrift} skipped (drift), ${pruned.removed.length} retired.`,
292
314
  )
293
315
  reportPrune(pruned)
316
+ reportBlockMigration(migration, 'sync')
294
317
  if (driftLines.length > 0) {
295
318
  console.log('Locally-edited files were skipped (run with --force to overwrite):')
296
319
  for (const l of driftLines) console.log(l)
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * The check + tooltip are the entire success signal — no notification (docs/CONTENT-SPEC.md §7).
8
8
  * `color="teal"` resolves through the bridged Mantine family, so it stays on the token layer in
9
- * both schemes (see the package CLAUDE.md on `cssVariablesResolver`).
9
+ * both schemes (see the package MAINTAINING.md on `cssVariablesResolver`).
10
10
  *
11
11
  * Not part of the public surface — reach it through `CodeBlock` / `HeadingAnchor`.
12
12
  */
@@ -4,7 +4,7 @@
4
4
  * hand-drawn ones that drift apart.
5
5
  *
6
6
  * Inline SVG rather than an icon dep: the framework takes icons as `ReactNode` from the consumer
7
- * (see the package CLAUDE.md) and ships none, but these are internal chrome of a shipped component,
7
+ * (see the package MAINTAINING.md) and ships none, but these are internal chrome of a shipped component,
8
8
  * not a consumer-facing slot. `currentColor` + `1em`-agnostic sizing keeps them steerable from CSS
9
9
  * — the heading anchor sizes its glyph as an optical ratio of whatever step the heading lands on
10
10
  * (docs/DESIGN-SPEC.md §3), while the code-block chrome pins a fixed 14px.
@@ -146,7 +146,7 @@ export type BasaltVirtualListHandle = {
146
146
  /**
147
147
  * The underlying `@tanstack/react-virtual` instance — the full escape hatch. Returns the RAW
148
148
  * `@tanstack/react-virtual` `Virtualizer`, so a future major of that library reaching this shape
149
- * ships here as a plain `feat:` under the no-majors doctrine (`../../CLAUDE.md` "Commit type
149
+ * ships here as a plain `feat:` under the no-majors doctrine (root `AGENTS.md` "Commit type
150
150
  * discipline"), not a silent break.
151
151
  */
152
152
  getVirtualizer: () => Virtualizer<HTMLDivElement, Element>
@@ -788,7 +788,7 @@ function insideHostTag(codeText: string, offset: number): boolean {
788
788
  *
789
789
  * KEBAB and PascalCase both count, because a repo picks one file-naming law and the convention has
790
790
  * to survive either: `edit-session-modal.tsx`, and `CbbiPanel.tsx` in a repo mandating
791
- * `PascalCase.tsx` for component files (basalt's own root `CLAUDE.md` included). Each needs the
791
+ * `PascalCase.tsx` for component files (basalt's own root `AGENTS.md` included). Each needs the
792
792
  * leading SUBJECT — a bare `modal.tsx` / `Panel.tsx` is a page module, not the convention.
793
793
  *
794
794
  * The trade is that a whole file goes unscanned on a naming convention — the same bargain