Skip to content

Docs: split AGENTS.md so it stops being silently truncated - #3284

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:docs/agents-md-split
Aug 23, 2026
Merged

lahma merged 1 commit into
sebastienros:mainfrom
lahma:docs/agents-md-split

Conversation

@lahma

@lahma lahma commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Docs: split AGENTS.md so it stops being silently truncated

AGENTS.md was 135,147 bytes (~17,700 words) in one file. OpenAI Codex reads a project doc up to
project_doc_max_bytes, whose default is 32768 — confirmed at today's HEAD in
codex-rs/config/defaults.toml
lines 8–9. Overflow is not a skip and not a warning the user sees: read_agents_md() does a hard
data.truncate(remaining) mid-file and emits a tracing::warn!, which is below the RUST_LOG=error
that codex exec defaults to. At 4.13× the budget, a Codex user was getting roughly the first quarter
of the file and no signal that the rest existed.

Two further facts from that source made the budget tighter than the headline number suggests, and both are
now recorded in the file itself:

  • remaining is a running total across the whole root→cwd chain, decremented per file — a fat root file
    starves a nested one to zero bytes.
  • Codex never reads below the working directory, so the co-located files below do not reach it at all.
    The root index is what reaches it.

This is a move, not an edit. No prose was rewritten, shortened or paraphrased.


1. The "stays inline" list — the judgement call, ranked by what a violation costs

These four gotchas stay in the root file. The test I applied: would an agent break this before it knew
which area it was working in?
Each of them looks like an ordinary internal refactor right up to the moment
an embedder's bounded execution stops being bounded, or an embedder's object model stops being visible.

# Rule kept inline Why it cannot move
1 Constraints bound one entry into the engine, never a host-driven sequence of them (3,185 B) The highest-cost rule in the file. It is not a rule about Jint/Constraints/ — it is a rule about Engine.ExecuteWithConstraints and about what an embedder's foreach (var row in rows) predicate.Call(row) does. An agent "simplifying" a reset, or writing a test that assumes a host loop is bounded, never opens the constraints file first. Measured consequences are in the text: MaxStatements(100) does not stop 1000 host calls; TimeoutInterval(200ms) does not fire across 3 s.
2 GetOwnProperties is not the enumeration hook (844 B) A real integrator already shipped an object whose keys were invisible to every script-visible enumeration. The trap is that the wrong hook compiles, runs and looks right; nothing points you at Jint/Native/Object/AGENTS.md before you make the mistake, because you do not know you are making one.
3 FastSetProperty / FastSetDataProperty always create an own property (646 B) Reads like a fast setter, is actually a shadowing, validation-skipping, shape-deoptimising setup-time primitive. It is reached from all over the engine and from host code, so its blast radius is not the directory it is declared in.
4 Sharing a JsValue across engines is unsupported (333 B) Nothing validates or guards it — there is no test, no XML doc, no exception. A rule with zero enforcement has to be where it will be read, and it costs 333 bytes.

What I deliberately did not keep inline, and why — the fourth candidate named in the brief:

  • The shareable-vs-engine-affine contract (### Engine-affine vs shareable state, 4,891 B) moved to
    Jint/Runtime/Interpreter/AGENTS.md. The decisive question is who breaks it, and the answer is
    whoever stashes engine-affine state on AST UserData — i.e. someone editing
    Jint/Runtime/Interpreter/**, which is exactly where JintStatement.Build's INVARIANT comment already
    lives. The root index row for that file names the contract and its cost explicitly. This is the one
    inline/move call I would most welcome being overruled on; see §7.

2. Byte table, before and after

Before — one file, 135,147 bytes (working-tree CRLF; 134,604 as the LF blob git stores). Section shares:

section bytes share
Third-party integration surface 74,653 55%
ECMAScript compliance (incl. Web APIs + WPT) 26,602 20%
Benchmarks 11,524 9%
Conventions 9,588 7%
Architecture 6,305 5%
Build & Test 4,712 3%
Modules / Constraints & security / AOT 829 <1%

After — 14 files, none within 20% of the cap:

file bytes % of 32 KiB cap
AGENTS.md (root) 23,197 71%
Jint/AGENTS.md 25,396 78%
Jint/WebApi/AGENTS.md 19,986 61%
Jint/Native/Object/AGENTS.md 18,267 56%
Jint/Runtime/Modules/AGENTS.md 12,891 39%
Jint.Benchmark/AGENTS.md 12,541 38%
Jint/Runtime/Interpreter/AGENTS.md 9,467 29%
Jint/Constraints/AGENTS.md 7,763 24%
Jint.Tests/Wpt/AGENTS.md 5,466 17%
Jint/Runtime/Interop/AGENTS.md 5,404 16%
Jint/Extensions/AGENTS.md 4,041 12%
Jint.Tests.Test262/AGENTS.md 3,623 11%
Jint/Native/AGENTS.md 2,783 8%
Jint.Tests.PublicInterface/AGENTS.md 1,205 4%
total 152,030

Plus, not moved content: .claude/rules/ 13 files totalling 7,217 B, .github/copilot-instructions.md
837 B, CLAUDE.md 12 B (unchanged, still @AGENTS.md).

Root is 23,197 B, under the 24 KiB (24,576 B) target with 1,379 B of headroom, and 9,571 B under the
hard cap.

Delta accounting: +16,883 bytes, explained line by line

component bytes
13 co-located file headers (title, "read this when", back-pointer to root) +6,652
6 re-emitted ### Gotchas headings + their one-line pointer +1,353
root: ## How these instructions are laid out + the 13-row triggered index +5,225
root: ## The size budget, and which agents load what + the 13-row tool table +2,744
root: pointer paragraph under ## Third-party integration surface +321
root: "do not add a new gotcha here" pointer +202
root: one line pointing at the size budget from the index +185
7 link retargets (a relative path is longer than a #anchor) +169
blank-line seams where a section boundary became a file boundary +32
total +16,883

Every byte of growth is navigation. No moved text grew.


3. The file map — every heading and every gotcha bullet

Produced by the splitter itself, which is heading-keyed rather than line-number-keyed and aborts if it
finds a unit the manifest does not route (see §6).

AGENTS.md (root) — 14,434 B of retained text

unit kind bytes
Agent Instructions for Jint (title + intro) section 397
## Build & Test section 1,197
### Quick manual testing with Jint.Repl section 434
## Architecture section 107
### Execution pipeline section 625
### Test projects section 1,515
## Third-party integration surface (intro) section 471
### Gotchas heading + preamble section 70
Constraints bound one entry into the engine, never a host-driven sequence of them. gotcha 3,185
FastSetProperty / FastSetDataProperty always create an own property. gotcha 646
GetOwnProperties is not the enumeration hook. gotcha 844
Sharing a JsValue across engines is unsupported gotcha 333
## Conventions section 281
### Performance is critical section 758
### Code patterns (includes the Throw.* rule and the spec-reference convention) section 2,199
### Data structures section 341
### Visibility: internal-first section 1,029

Jint/AGENTS.md — 24,530 B moved

unit kind bytes
### Key types section 1,075
### Namespace organization section 3,026
### Type co-location section 447
### Unsigned-cast bounds check (`(uint) i < (uint) length`) section 1,185
## AOT compatibility section 170
### What counts as a public contract section 12,346
The handler-tree caches engage only on the second evaluation… gotcha 843
An *Async entry has two ways out… gotcha 2,499
RestoreGlobalSnapshot bumps version counters, it never restores them. gotcha 1,471
Discarding the event loop is a fence, not a flush. gotcha 1,006
A suspended EvaluateAsync is invisible to the ordinary signals. gotcha 462

Jint/Native/AGENTS.md — 2,246 B moved

unit kind bytes
## ECMAScript compliance section 2,246

Jint/Native/Object/AGENTS.md — 17,488 B moved

unit kind bytes
### The subclassing cliff section 2,486
### Host-contract verification section 12,208
### When you add a fast lane, decide who can reach it section 1,765
A global installed after construction must invalidate… gotcha 1,029

Jint/Runtime/Interop/AGENTS.md — 4,648 B moved

unit kind bytes
A registered IObjectConverter used to disable the compiled member-read lane… gotcha 1,201
Dictionary-valued reads re-wrap on every access… gotcha 3,069
A non-default IReferenceResolver used to disable the inline caches… gotcha 378

Jint/Runtime/Interpreter/AGENTS.md — 8,724 B moved

unit kind bytes
### Engine-affine vs shareable state section 4,891
Coverage counters live engine-side, keyed on AST node identity… gotcha 1,264
A warmed member-read site retains its last receiver. gotcha 2,569

Jint/Runtime/Modules/AGENTS.md — 12,186 B moved

unit kind bytes
## Modules section 280
### Asynchronous module loading section 10,187
A module's location is the name its loader chose… gotcha 1,719

Jint/Constraints/AGENTS.md — 6,995 B moved

unit kind bytes
## Constraints & security section 396
Execution constraints and the interpreter's tight-loop lane. gotcha 1,117
An engine-driven fan-out is one entry, not one per callback… gotcha 1,707
Cancellation is the one thing that does span host calls… gotcha 1,537
MaxRecursionDepth counts one function's occurrences, not stack depth… gotcha 1,781
Saturated sentinels register nothing. gotcha 457

Jint/Extensions/AGENTS.md — 3,429 B moved

unit kind bytes
### Write against the modern BCL, and polyfill downwards section 3,429

Jint/WebApi/AGENTS.md — 19,491 B moved

unit kind bytes
### Web APIs section 15,440
#### Diagnostics and reportError section 4,051

Jint.Tests/Wpt/AGENTS.md — 4,967 B moved · Jint.Tests.Test262/AGENTS.md — 3,126 B · Jint.Tests.PublicInterface/AGENTS.md — 747 B

unit kind bytes
### Web platform tests section 4,967
### Updating the test262 suite section 3,126
### Where integrator-facing tests belong section 747

Jint.Benchmark/AGENTS.md — 12,136 B moved

unit kind bytes
## Benchmarks section 1,292
### The measurement environment section 7,155
### Adding a new benchmark section 556
### Never warm one engine with more than one row's workload section 2,609
### Benchmarking host-object shapes section 524

The seven prose edits, in full

These are the only characters of existing text that changed. Each is a #anchor that now lives in another
file, retargeted to a relative path.

in from to
root ## Build & Test [Host-contract verification](#host-contract-verification) …(Jint/Native/Object/AGENTS.md#host-contract-verification)
root ### Test projects [Web platform tests](#web-platform-tests) …(Jint.Tests/Wpt/AGENTS.md#web-platform-tests)
root ### Test projects [Updating the test262 suite](#updating-the-test262-suite) …(Jint.Tests.Test262/AGENTS.md#updating-the-test262-suite)
Jint/AGENTS.md namespace map See [Web APIs](#web-apis). See [Web APIs](WebApi/AGENTS.md#web-apis).
Jint/AGENTS.md contract table is derived `Exotic` (below) is derived `Exotic` (see [the subclassing cliff](Native/Object/AGENTS.md#the-subclassing-cliff))
Jint/Native/AGENTS.md see [Web APIs](#web-apis) for how it works and why see [Web APIs](../WebApi/AGENTS.md#web-apis) …
Jint.Benchmark/AGENTS.md […](Jint.Benchmark/README.md) […](README.md)

4. Tool discovery — verified against primary sources, 2026-08-23

Two research passes; every row below was fetched from vendor documentation, a changelog, or source. The
brief's own claim was half right
, and the correction matters: Copilot's cloud agent auto-loads nested
AGENTS.md, but Copilot's CLI does not — it walks upward only, and downward discovery is an open
feature request. Claude Code does not read AGENTS.md at all.

Tool / surface Loads at repo root Nested AGENTS.md below cwd Size behaviour
OpenAI Codex AGENTS.override.md → AGENTS.md; codex.md is entirely gone No — "does not read subdirectory files below where you're working" project_doc_max_bytes = 32768, a running budget over the whole chain; silent mid-file truncation
Claude Code CLAUDE.md, .claude/CLAUDE.md, CLAUDE.local.md. Does not read AGENTS.md ("create a CLAUDE.md that imports it") Nested CLAUDE.md loads when Claude reads a file in that directory; .claude/rules/*.md with paths: fires on a match CLAUDE.md over 4 MiB is skipped, not truncated
Copilot cloud agent (ex-"coding agent") AGENTS.md since 2025-08-28 and .github/copilot-instructions.md and .github/instructions/** — all sets are supplied, none wins Yes, **/AGENTS.md; "the nearest AGENTS.md file in the directory tree will take precedence" none documented
Copilot code review AGENTS.md, root only, since 2026-06-18 No a 4,000-char cap was documented until 2026-01-12, then removed; treat as UNVERIFIED
Copilot CLI AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, ~/.copilot/… Upward only, cwd → git root (v1.0.11, 2026-03-23). Downward discovery is open issue #3051 none documented; runtime truncation reported in open issue #2111
Copilot in VS Code AGENTS.md — chat.useAgentsMdFile default true (GA v1.105, 2025-10-09) chat.useNestedAgentsMdFiles default false, experimental; and even when on it injects the path, not the content none documented
Copilot in Visual Studio / JetBrains .github/copilot-instructions.md only — no AGENTS.md support No none documented
Cursor AGENTS.md, CLAUDE.md, .cursor/rules/*.mdc (.cursorrules legacy) Yes — "supports AGENTS.md in the project root and subdirectories", combined with parents, more specific wins none documented
Amp AGENTS.md; per-directory fallback to AGENT.md / CLAUDE.md Yes — "Subtree AGENTS.md files are included when the agent reads a file in the subtree" none documented
Windsurf → Devin Desktop AGENTS.md, .devin/rules/*.md, .windsurf/rules/*.md, .windsurfrules Yes — a subdirectory file "is treated as a glob rule with an auto-generated pattern of <directory>/**" 6,000 chars for global_rules.md; 12,000 chars per workspace rule file; no total cap and no truncation sentence any more
Devin CLI / Local agent (now the default) AGENTS.md, AGENTS.local.md, AGENT.md, CLAUDE.md, .cursor/rules/* Yes, lazily 32 KiB per always-on rule file, truncated with a pointer to the source path (v3000.3.22, 2026-07-29)
Devin (cloud) AGENTS.md "in your project root (or anywhere else)"; ingests rules files into Knowledge UNVERIFIED — no scoping model documented none documented
Gemini CLI GEMINI.md; AGENTS.md only if context.fileName names it Just-in-time and upward from a touched path to the trusted root; the old eager downward scan no longer exists none documented, none in code
Jules AGENTS.md at the repository root (since 2025-06-20) UNVERIFIED — never mentioned in the docs none documented
Aider nothing automatically. Needs read: [AGENTS.md] in .aider.conf.yml No none

Sources. Codex: defaults.toml,
agents_md.rs,
docs/install.md,
AGENTS.md guide,
config reference. ·
Claude Code: memory, hooks,
skills,
CHANGELOG (.claude/rules/ landed in
v2.0.64; paths: accepting a YAML list fixed in v2.1.84),
issue #16853. ·
Copilot: 2025-08-28 changelog,
2026-06-18 code review changelog,
custom-instructions support matrix,
response customization,
Copilot CLI changelog,
VS Code v1.104 / v1.105,
VS Code custom instructions. ·
Cursor: rules, help,
CLI changelog. ·
Amp: manual, multiple AGENT.md files,
AGENTS.md rename. ·
Cognition: Devin Desktop memories,
Devin Desktop AGENTS.md,
Devin CLI changelog,
Devin AGENTS.md. ·
Gemini CLI: GEMINI.md, memport. ·
Jules: docs, changelog. ·
Aider: conventions, config.

A condensed version of this table is now in the root AGENTS.md itself, under
## The size budget, and which agents load what, so the next person to grow the file sees the constraint.

Consequences that shaped the design

  1. Co-location beats docs/agents/ — but only for four ecosystems. Amp, Cursor, Devin Desktop/CLI and
    Copilot's cloud agent auto-load a nested AGENTS.md; a docs/ path would reach none of them. That is
    the whole justification and it holds, with the correction that Copilot's CLI is not one of them.
  2. Claude Code needed a separate mechanism, since it does not read AGENTS.md and does not walk into a
    nested one. .claude/rules/*.md with paths: frontmatter is that mechanism, and I verified the details
    rather than assuming: the key is paths:, not globs: (that is Cursor) and not applyTo: (that is
    Copilot); patterns are matched with gitignore semantics via the ignore package with a trailing
    /** stripped, so Jint/WebApi/** does match Jint/WebApi/Console/JsConsole.cs; and a path-scoped rule
    fires "when Claude reads files matching the pattern, not on every tool use."
    One gitignore consequence worth flagging: a slash-free pattern matches at any depth, which is why the
    single-component rules here are written anchored (/Jint/**, /Jint.Benchmark/**).
  3. The root index is load-bearing, not decorative. For Codex, Copilot CLI, Copilot code review, Jules,
    Gemini CLI and Aider the co-located files are never auto-loaded — they are reachable only because the
    index names them and those agents can read files. That is why the index is a table with a trigger
    column and a cost column, and why the four rules in §1 could not be delegated to it.

5. .github/copilot-instructions.md: committed, as a pointer

It had never been committed, was 4.6 KB, and duplicated AGENTS.md — and it had already drifted, still
naming FluentAssertions after the repository moved to AwesomeAssertions. That is the failure mode the
brief warned about, demonstrated.

Deleting it outright was tempting but wrong: Copilot in Visual Studio and in JetBrains read
.github/copilot-instructions.md and have no AGENTS.md support at all, so deletion would leave those
two surfaces with nothing. And committing the duplicate was worse: the cloud agent supplies
.github/copilot-instructions.md and AGENTS.md together — "all sets of relevant instructions are
provided to Copilot" — so a copy is not a fallback, it is a second, diverging voice in the same prompt.

The committed file is 837 bytes, states no rule of its own, points at AGENTS.md, and says in one
paragraph why it contains nothing else. The stale untracked copy was removed from the working tree.


6. Verification

Build. dotnet build -c Release for the solution: exit 0, 0 errors. The one warning is a
pre-existing MSB3277 NuGet version conflict in Jint.Tests.CommonScripts on net472, unrelated to this
change. (Note .github/workflows/build.yml has paths-ignore: '**.md', so a docs-only change skips that
workflow anyway.)

Diff shape. git diff --cached --stat: 28 files, all .md — 14 AGENTS.md, 13 .claude/rules/*.md,
1 .github/copilot-instructions.md. No source, project or configuration file touched. CLAUDE.md is
byte-identical (12 bytes, @AGENTS.md).

Nothing lost — scripted, not eyeballed. The splitter parses AGENTS.md into 60 addressable units
(heading-rooted sections, plus one unit per gotcha bullet keyed on its bold lead-in) and asserts every byte
belongs to exactly one unit. It then routes each unit through a manifest and aborts on any unit the
manifest does not name — which is precisely the guard against a concurrently landed edit being swallowed.
A separate verifier then compares the result against a snapshot of the original:

==============================================================================
1. HEADINGS
==============================================================================
  OK   ## Build & Test                       -> AGENTS.md
  OK   ### Updating the test262 suite        -> Jint.Tests.Test262/AGENTS.md
  …  (37 headings, each in exactly one destination; `### Gotchas` intentionally
     appears in 7 files, one per area that received bullets)
  OK   ## AOT compatibility                  -> Jint/AGENTS.md

==============================================================================
2. GOTCHA BULLETS
==============================================================================
  … 21 gotcha bullets + every other `- **bold**` bullet in the file, each
    found byte-identical in exactly one destination
  OK   Constraints bound one entry into the engine…        -> AGENTS.md
  OK   `RestoreGlobalSnapshot` bumps version counters…     -> Jint/AGENTS.md
  OK   `Jint.Tests.Test262`** — Official TC39 conformance… -> AGENTS.md (link retargeted)

==============================================================================
3. EVERY NON-BLANK LINE ACCOUNTED FOR
==============================================================================
  original non-blank distinct lines : 364
  present verbatim in the new files : 357
  needing a declared link retarget  : 7

    RETARGETED ->AGENTS.md                     Setting `JINT_HOST_CONTRACT_VERIFICATION=1` runs …
    RETARGETED ->Jint.Benchmark/AGENTS.md      The cross-engine comparison (`EngineComparisonBenchmark`) …
    RETARGETED ->Jint/AGENTS.md                - `Jint.WebApi` — The opt-in WHATWG web platform APIs …
    RETARGETED ->AGENTS.md                     - **`Jint.Tests`** — Main unit tests (xUnit v3, …
    RETARGETED ->AGENTS.md                     - **`Jint.Tests.Test262`** — Official TC39 conformance suite …
    RETARGETED ->Jint/AGENTS.md                **`PropertyFlag.CustomJsValue` is the supported lazy-value hook.** …
    RETARGETED ->Jint/Native/AGENTS.md         **That rule stops at the language.** The WHATWG web APIs …

==============================================================================
4. BYTES
==============================================================================
  TOTAL    152030
  BEFORE   135147
  DELTA    +16883

ALL CHECKS PASSED

Exit code 0. Each of the seven is a declared retarget from §3, and the verifier proves the transformed
line is present rather than taking my word for it.

Concurrency. Cut against upstream/main @ 50d4219fc, which is the tree the byte figures above
describe. PR #3278 was open and rewriting one of the moved bullets while this was drafted, so I ran the
splitter against #3278's head (42419e3d3) as a rehearsal: it routes cleanly, the rewritten bullet lands
whole in Jint/AGENTS.md, and that file comes out at 25,547 B — still well under the cap. If #3278 lands
after this, its rebase will conflict because AGENTS.md no longer contains that bullet; the resolution is
to put #3278's new bullet, wholesale, into Jint/AGENTS.md under ### Gotchas
, not to reinstate it in
the root. The same applies to #3273 or anything else that appends: the root file now says explicitly do not
add a new gotcha here.


7. Against the verdict — what this split makes worse

A nested file an agent never triggers is a rule nobody reads. That is the real cost here, and it is not
hypothetical. Honestly, these are the rules now most at risk:

  1. The polyfill discipline (Jint/Extensions/AGENTS.md). It governs every call site in the assembly
    but now sits in a directory almost nobody edits. It is the single worst fit for co-location in this
    change. Mitigations: .claude/rules/modern-bcl-polyfills.md matches Jint/**/*.cs — the one rule whose
    paths: is deliberately not the directory it points at — and the root index row names it. For every
    other agent, an engine edit no longer surfaces it at all. If one thing in this PR should be reverted
    into the root, it is this.
  2. The engine-affine vs shareable-state contract (Jint/Runtime/Interpreter/AGENTS.md). Placed by
    who breaks it, not by who reads it. An agent editing Jint/Engine.cs — where PrepareScript lives,
    and where the "may be shared across engines" promise is made — will get Jint/AGENTS.md and not this.
    §1 explains the reasoning; it is the call I am least sure of.
  3. ## ECMAScript compliance (Jint/Native/AGENTS.md). "Read the normative text before writing code"
    and "do not introduce non-standard language extensions" are close to constitutional, and they now fire
    only under Jint/Native/. Someone adding syntax works in Jint/Runtime/Interpreter/ and will not see
    them.
  4. ## Constraints & security — "CLR access is disabled by default" moved to
    Jint/Constraints/AGENTS.md. It is a configuration snippet rather than a rule, but it is the only
    statement of a security default and it is no longer in the file everyone reads.
  5. ### Key types and ### Namespace organization left the root. The brief required root to still
    answer "where things live"; it does — pipeline, test projects, and a 13-row index that names every area —
    but the namespace-by-namespace map is now one hop away for anyone not already inside Jint/.

Two more honest costs:

  • ### Gotchas is now seven lists in seven files. Nobody can read "all the gotchas" in one place any
    more, and the count in the root's pointer sentence ("the other seventeen") is a hard-coded number that
    will rot the first time someone adds one. That is a deliberate trade for making each list appear where it
    bites, but it is a real loss of a document that people did read end-to-end.
  • Total bytes went up by 16,698. A human reading everything now reads 12% more. The split optimises
    for the machine's budget, not the human's.

And two things this does not fix:

  • Devin Desktop documents 12,000 characters per workspace rule file. Whether that cap applies to an
    AGENTS.md its rules engine processes is not documented. If it does, six of the fourteen files exceed
    it. The root file's budget section says so rather than pretending otherwise.
  • Aider, Copilot CLI, Copilot code review, Jules, Gemini CLI and Codex will never auto-load a co-located
    file.
    For them the change is: the root file is no longer truncated (real, and the point), and everything
    else is one deliberate Read away if the index does its job. If the index does not, this split has
    moved rules out of reach for six of the fourteen surfaces surveyed.

8. Proposals, not changes

Nothing below was acted on; the brief said to list rather than remove.

  • The root intro still says "CLAUDE.md imports it", which is true but now under-describes the layout; the
    new ## How these instructions are laid out section immediately below it covers the same ground from a
    different angle. Candidate for a one-line merge in a follow-up.
  • ### Test projects (root) and Jint.Tests.PublicInterface/AGENTS.md overlap by one sentence each about
    InternalsVisibleTo. Both were left verbatim.
  • ### Host-contract verification contains a self-referential "(below)" pointing at itself, which predates
    this change. Left as found.
  • ## Modules (the 280-byte usage snippet) and ### Asynchronous module loading are now adjacent in one
    file and could be introduced by a single sentence. Left unmerged.

Rebase note — the #3278 collision, resolved as predicted

Cut against 50d4219fc; #3278 landed in between and rewrote the RestoreGlobalSnapshot gotcha bullet from 1,463 to 3,337 bytes. AGENTS.md was the only conflicted file, and the resolution was the one rehearsed: take the split root wholesale, and put #3278's new bullet — not the old one the split had carried — into Jint/AGENTS.md under ### Gotchas. Jint/AGENTS.md is therefore 27,276 B rather than the 25,396 B the pre-rebase table states; still 83% of the cap, and still the largest file.

Re-verified after the rebase, and this is the check that matters, because a clean-looking conflict resolution is exactly how somebody's new bullet gets dropped into a section that no longer exists:

  • 37 of 37 headings from upstream/main's AGENTS.md present in exactly one destination.
  • 59 of 59 - **bold** bullets present verbatim.
  • 364 distinct non-blank lines: 357 verbatim, 7 accounted for. Six are link retargets (](#anchor) → ](path/AGENTS.md#anchor)); the seventh is the PropertyFlag.CustomJsValue paragraph, whose bare (below) became (see [the subclassing cliff](Native/Object/AGENTS.md#the-subclassing-cliff)) — a required fix, since "below" stopped being true the moment that section moved to another file. Zero content lost.
  • dotnet build -c Release for the solution: 0 errors. git diff --name-only against main: markdown only, no source file touched.
  • Every instruction file under the 32,768-byte cap: Jint/AGENTS.md 27,276 (83%), root 23,197 (71%), Jint/WebApi/AGENTS.md 19,986 (61%), Jint/Native/Object/AGENTS.md 18,267 (56%), the remaining ten between 1,205 and 12,891.

The mechanism claims were also re-verified independently against Claude Code's own documentation before merging, since the whole Claude Code half of the split rests on them: .claude/rules/*.md with a paths: key is real, matches gitignore-style globs, and injects when Claude reads a matching file rather than at launch; a rule with no paths: loads at launch; nested CLAUDE.md auto-loads on demand and concatenates with its ancestors rather than overriding them; @ imports expand at launch with a 4-hop limit; and Claude Code does not read AGENTS.md natively — the @AGENTS.md line in CLAUDE.md is load-bearing and must stay.

AGENTS.md was 135,147 bytes. OpenAI Codex reads a project doc up to
project_doc_max_bytes, default 32768 (codex-rs/config/defaults.toml), and
overflow is a hard mid-file byte truncation whose only signal is a
tracing::warn! below the level `codex exec` prints at. At 4.1x that budget a
Codex user was getting roughly the first quarter of the file and no indication
the rest existed. The budget is also a running total across the whole
root-to-cwd chain, not a per-file allowance.

This is a move, not an edit. The root file is now 23,012 bytes and holds what
every agent needs before its first edit — build and test commands, the
Release-only rule, the branch to target, the architecture map, the conventions
that apply everywhere, and a triggered index of the rest. Everything else moved
verbatim into an AGENTS.md beside the code it governs, each far under the cap:

  AGENTS.md                             23,012
  Jint/AGENTS.md                        25,396
  Jint/WebApi/AGENTS.md                 19,986
  Jint/Native/Object/AGENTS.md          18,267
  Jint/Runtime/Modules/AGENTS.md        12,891
  Jint.Benchmark/AGENTS.md              12,541
  Jint/Runtime/Interpreter/AGENTS.md     9,467
  Jint/Constraints/AGENTS.md             7,763
  Jint.Tests/Wpt/AGENTS.md               5,466
  Jint/Runtime/Interop/AGENTS.md         5,404
  Jint/Extensions/AGENTS.md              4,041
  Jint.Tests.Test262/AGENTS.md           3,623
  Jint/Native/AGENTS.md                  2,783
  Jint.Tests.PublicInterface/AGENTS.md   1,205

Four gotchas stay in the root, because they are the ones an agent breaks before
it knows which file to open: constraints bounding one entry rather than a host
loop, FastSetProperty always creating an own property, GetOwnProperties not
being the enumeration hook, and JsValue not being shareable across engines.

Co-location is not decoration. Amp, Cursor, Devin Desktop/CLI and Copilot's
cloud agent auto-load a nested AGENTS.md; a docs/ path would reach none of
them. Claude Code reads CLAUDE.md rather than AGENTS.md and does not walk into
a nested one, so .claude/rules/*.md with `paths:` frontmatter (gitignore
semantics, trailing /** stripped) carries a pointer per area instead. That is
also the only way Jint/Extensions/AGENTS.md — the polyfill discipline, which
governs every call site in the assembly and not just its own directory — fires
at all; its rule matches Jint/**/*.cs.

.github/copilot-instructions.md had never been committed and was a 4.6 KB
duplicate that had already drifted (it still named FluentAssertions). It is now
a pointer that adds no rules of its own, kept only because Copilot in Visual
Studio and JetBrains read it and have no AGENTS.md support at all.

Nothing was lost and no prose was rewritten. Every heading and every gotcha
bullet is byte-identical in exactly one destination; seven lines changed
because a #anchor became a relative path to the file the anchor moved to. The
+16,698 bytes of growth are entirely the new navigation: 13 file headers,
6 re-emitted "### Gotchas" headings, the index, the size-budget table, two
pointer paragraphs, and 32 bytes of blank-line seams.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd
@lahma
lahma merged commit d84920a into sebastienros:main Aug 23, 2026
5 checks passed
@lahma
lahma deleted the docs/agents-md-split branch August 23, 2026 09:44
lahma added a commit that referenced this pull request Aug 23, 2026
…he README (#3291)

The 4.x branch exists now, cut at the last commit before the opt-in Web API
foundation and the security-defaults stack — both of which change what a default
engine is and therefore belong to 5.x, not to a maintenance line. Three pieces of
housekeeping follow from the branch existing at all.

`build.yml` filtered its push trigger to `[ main, 3.x ]`, so a push to 4.x built
nothing and published no preview package. 4.x joins the list. `pr.yml` needs no
change: it deliberately carries no `branches:` filter, so a pull request based on
4.x already gets the full matrix.

`VersionPrefix` moves to 4.16.1, which is what the MyGet preview stream coming off
this branch should be numbered. It is not what a release will carry — `release.yml`
derives that from the pushed `vX.Y.Z` tag and passes it to `dotnet pack`, so the
property only ever names the preview feed's version.

The README's "Branches and releases" section described `main` alone, which was true
when `main` was the only branch anyone was asked to target. It now says what each of
the three live branches is for and how a release is actually cut.

What is deliberately *not* here: the AGENTS.md split (#3284). The maintenance branch
keeps its single pre-split AGENTS.md, so a backport cherry-picked from main never
conflicts on documentation layout.


Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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