pyre
AGENTS.md6 starsChanged 26 days agoMITView on GitHub
PyPy's implementation shape is the default and module presence follows PyPy. Before implementing any module, first verify that the real pypy3 oracle imports it and identify its owner in pypy/, rpython/, or lib_pypy/. If PyPy has no such module, it is not a pyre porting target unless the user explicitly expands the scope. A skipped CPython test does not override this boundary; fix PyPy's real public-module owner or fallback instead. See “Module presence follows PyPy” below for the full rule…
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,439 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository7 other files it gives its agents.
- codex-reviewSkill
- paritySkill
- apple-containerSkill
- codex-reviewSkill
- mergeSkill
- paritySkill
- rebaseSkill
The file
We split this AGENTS.md into the rules it gives an agent
11 rules. About 0 are common, 3 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 13
Hard stop: if neither the real pypy3 import nor an upstream PyPy owner can be verified, do not add the module or keep it in the …
- Very specific: line 67
A descent wall is a lowering gap, not a reason to fold: When descending a helper does not finish or aborts (an unlowered constructor such …
- Very specific: line 164
Do not delete an RPython method to "simplify": If optimizer.py has ensure_ptr_info_arg0, the port has it — the shortcut diverges, and the …
- Very specific: line 255
Do you hold an admissible 3.15 artefact — an in-tree lib-python/3/… assertion, a measured run at the pinned version, or C source read at …
- Very specific: line 258
Do the two upstreams actually disagree?
- Distinctive: line 279
If a parity fix regresses, find the root cause before reverting.
- Distinctive: line 290
Never check a running local build or CI job more often than once every five minutes.
- Very specific: line 379
New code: Run the tests that exercise it: the touched crate's tests, the relevant check.py --synthetic-pattern fixtures, and the regression …
- Very specific: line 384
The last CI run on this branch was red: Before the next push, run every failed job's command locally and see it pass.
- Very specific: line 418
When rebasing or cherry-picking, check the fix isn't already on main (git log main --grep=…).
- Distinctive: line 423
Before running the test, verify the traced path is actually reached — check gating and feature flags.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AGENTS.md
Primary porting boundary: PyPy's module surface
PyPy's implementation shape is the default and module presence follows
PyPy. Before implementing any module, first verify that the real pypy3
oracle imports it and identify its owner in pypy/, rpython/, or
lib_pypy/. If PyPy has no such module, it is not a pyre porting target unless
the user explicitly expands the scope. A skipped CPython test does not override
this boundary; fix PyPy's real public-module owner or fallback instead. See
“Module presence follows PyPy” below for the full rule and examples.
Hard stop: if neither the real pypy3 import nor an upstream PyPy owner can
be verified, do not add the module or keep it in the implementation backlog.
Known exclusions: _testlimitedcapi is a CPython-only test helper, and
PyPy has no _datetime extension module. Do not implement either one; preserve
and repair PyPy's pure-Python datetime path when the public module is broken.
The JIT is generated from the interpreter source
pyre is structured like PyPy: pyre-interpreter is the RPython-interpreter
analog, and the JIT is not hand-written. majit-translate reads the
interpreter's Rust source and generates it — front/ast.rs (parse) →
flowspace/ (flowcontext.py/framestate.py) → annotator/ (annrpython.py)
→ rtyper/ → codewriter/ (jtransform.py/codewriter.py, emits JitCode) —
the same pipeline RPython's translator runs over PyPy.
So "Rust can't be meta-traced" is never a valid excuse for a deviation. Whatever the interpreter source expresses is what the generated JIT must preserve; a JIT that diverges from the interpreter has a generation defect to fix. Never justify a mismatch by appeal to the implementation language.
One red frame per frame
PyPy keeps one frame object per inlined Python call (MIFrame tracing,
BlackholeInterpreter resuming), each with its own
jitcode → pycode → w_globals → locals. LOAD_GLOBAL reads it off the live
frame (pyframe.py get_w_globals); resume rebuilds one frame per encoded
jitcode header (resume.py rebuild_from_resumedata). No shared frame slot
exists, so namespace confusion is impossible.
The frame is the loop's single red input, pycode the green. Thread
that red frame through every frame, inlined non-portal callees included.
Collapsing callees onto one anchor (a single portal_frame_reg, a single
bridge-resume root frame) drops the callee's own pycode/globals/locals — the one
root cause behind the pycode-names miscompile, the LOAD_GLOBAL namespace
mismatch, bridge-resume inline-frame globals and vable-resident root locals. Fix
it by restoring the per-frame red frame, never by baking an anchor's value as a
constant.
No new hand folds
pyjitpl.py has no hand-written specializations. Every fast path PyPy has
comes from tracing the interpreter's own body (space.add, int_w,
w_float_new as new_with_vtable + setfield) plus interpreter-side hints
(@jit.elidable, @jit.look_inside_iff, promote, @jit.unroll_safe) and
the optimizer (intbounds, heap, virtualize, unroll). pyre's
try_walker_specialize_* functions and spec_folds! rows are the manual
traces the descent work exists to retire, and their count is monotonic:
- The census must not go up. Rows:
sed -n '/^spec_folds! {/,/^}/p' pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs | rg -cF '=> ("'. Functions:rg -o 'fn try_walker_specialize_' pyre/ majit/ -g '*.rs' | wc -l. A change that raises either number is rejected in review; record both numbers before and after in the commit message when a change touches them. - A descent wall is a lowering gap, not a reason to fold. When descending
a helper does not finish or aborts (an unlowered constructor such as
w_float_new/w_complex_new, a guard with a null class pointer, a trace length or recursion limit), fix the codewriter, walker or optimizer so the body records, and cite the upstream shape that records it. Emitting the helper's result by hand, or residualizing the helper to make the walk stop early, re-creates the manual trace under a new name. - A renamed fold is still a fold. A
try_emit_exact_*or similar "exact operand" shortcut consulted before a descent is a hand fold gated under a descent row and counts toward the census. Do not hide one under a_descentrow. - Speed is recovered upstream's way. Where a descent is slower than the
fold it replaced, the missing piece is an interpreter hint or an optimizer
pass the fold was standing in for; port that. Reinstating the fold,
raising a
pyre-checkratio gate, or re-recording.jitstatswithout a root cause are not fixes.
Charon LLBC extraction — the prepass input
The annotator/rtyper prepass and the PYRE_RTYPER_VERBOSE census read
pre-extracted .ullbc under target/llbc/, not the Rust source. A change to
pyre-interpreter / pyre-object / pyre-module / pyre-jit is invisible until re-extraction;
majit-translate changes take effect immediately, because the translator runs
live over the frozen bodies.
Charon is shared and pre-installed at ../.pyre-build/charon/<platform>/<version>/charon
(pinned nightly-2026.10.29), not on PATH — which charon finding nothing
does not mean it is missing.
python3 scripts/install-charon.py # idempotent, usually a no-op
python3 scripts/extract-llbc.py # pyre-object pyre-interpreter pyre-module pyre-jit
touch pyre/pyre-jit-trace/build.rs
PYRE_RTYPER_VERBOSE=1 cargo build --release -p pyre-jit-trace # runs the prepass
# census: target/release/build/pyre-jit-trace-*/stderr, rg -c 'PREPASS phaseA fail'extract-llbc.pyskips a crate whose source fingerprint is unchanged;--force/LLBC_FORCE_REEXTRACT=1overrides.pyre-interpreter.ullbcis ~300 MB and takes minutes.- Worktrees share a content-addressed LLBC store (
<shared>/llbc-store, orLLBC_STORE) and import a matching artefact via clone/reflink instead of re-running Charon.LLBC_STORE=offdisables it;--prune-storedrops unused entries. cargo cleandropstarget/llbc; the next extract re-imports it from the LLBC store. The prepass cache lives in<shared>/jit-trace-cache.- A Linux container never extracts into the host's
target/llbc. Mount a per-container directory over/workspace/pyre/target/llbcatcontainer run(see/apple-container, step 2). - On Windows, keep
CHARON_TARGET_DIRshort. cargo passes one-L dependency=...per dependency build dir, so under a long target pathpyre-interpreter's rustc line passes the 32767-character limit; cargo then hands charon-driver an@argfile, charon-driver reads--targetfrom its unexpanded argv, does not find it, and compiles the crate against the host sysroot, failing every--externwithE0463: can't find crate. The wall of missing crates is that, not a bad install or a stale pin. CI's Windows job setsCHARON_TARGET_DIR=D:\ct; a local path of that length works, while.pyre-build/charon-target-<worktree>under a home directory does not. - A nonzero
extract-llbc.pydoes not stop a shell gate on its own, and thecargo testafter it then runs against stale LLBC. Abort the script on it. - It internally sets
MAJIT_LLBC_EXTRACTION=1to break thepyre-jit → pyre-jit-trace → pyre-jit.ullbcbootstrap cycle, which makesbuild.rsemit placeholder artifacts. Never set it for an ordinary build. - Interpreter-source work ⇒ re-extract, then rebuild the prepass. Translator-only work ⇒ prepass rebuild alone.
rgnote: a stray--replacein user config mangles long lines; passrg --no-configwhen bucketing.
The wasm gate runs on every OS
Its only prerequisite beyond the native backends is the
wasm32-unknown-unknown target — wasmtime is linked into pyre-wasm-runner,
and the guest builds on stable with no -Z build-std. Once the target is
installed DEFAULT_BACKENDS adds wasm by itself, so a bare
python3 pyre/check.py runs three backends and --backend dynasm,cranelift
narrows it.
CI installs that target on the ubuntu leg alone because wasm output is platform-independent — a cost decision, not a capability limit. Never defer a wasm-only failure to CI on the grounds that the host cannot reproduce it.
Data structure parity with RPython/PyPy
majit and pyre are line-by-line ports, so the container choice is part of the
port. Treat every HashMap and every thread_local! as suspicious: find the
upstream owner and storage shape first.
- Look up the upstream attribute before choosing a container. A Rust
HashMapis not the default translation of a Pythondict— over a small/dense key space, or where insertion order or index lookup suffices,VecMap/IndexMap/Vecis closer. Prove the semantics from upstream. - Side-tables are usually wrong. RPython stores per-box information on the
box (
box._forwarded,PtrInfo,IntBound, descr attributes). Reaching forHashMap<OpRef, _>means you skipped that machinery; route throughOptContext::with_intbound_mut/set_ptr_infoinstead. - A borrow-checker workaround is acceptable only when every alternative was tried, the deviation is minimal, and a comment cites the RPython original.
- Do not delete an RPython method to "simplify". If
optimizer.pyhasensure_ptr_info_arg0, the port has it — the shortcut diverges, and the next porter'sheap.pyport stops compiling for no visible reason. - TLS is almost never the right owner. Type objects, module state,
registries and semantic caches are process-global or interpreter-owned in
PyPy and stay shared here; never duplicate them per thread to satisfy
Sync. TLS is right only where upstream makes the state itself thread-specific (current thread/execution context, errno) or for a disposable cache that cannot affect identity, semantics, lifetime or GC reachability. Anything else needs an upstream citation in the code.
The measured cascade this prevents: deleting ensure_ptr_info_arg0 for a
side-table OptHeap.array_min_lengths left postprocess_arraylen_gc unable to
read it, so it was crippled to a hardcoded IntBound::nonnegative(), which then
forced a parallel ExportedValueInfo::int_lower_bound. One non-orthodox
HashMap, four files of divergence.
The PyPy oracle: run it before arguing about orthodoxy
rpython/ says what upstream claims; a real pypy3 shows what it does. For
"is this orthodox or our deviation?", run the oracle first — before reading
source, forming a theory, or recording a verdict.
PYPYLOG=jit-summary:- pypy3 pyre/bench/synth/<fixture>.py # vs MAJIT_STATS=1Most pyre/bench/synth fixtures need no stdlib, so this costs one command. Read
Total # of loops/bridges, forcings, virtualizables forced, every
abort: *, nvirtuals. A differing counter is a pointer, not the answer —
find the upstream line behind it, usually a single JIT hint
(@jit.look_inside_iff, dont_look_inside, elidable, unroll_safe), and cite
it. PYPYLOG=jit-log-opt:FILE dumps the optimized trace when the summary is too
coarse. pypy3 is 3.11, so trim newer syntax out of the fixture.
This overturned a weeks-old "unfixable by construction" verdict on
getframe_inline_subwalk_multiframe in one command: the oracle reported
forcings: 0, and the @jit.look_inside_iff on getframe
(pypy/module/sys/vm.py) named the reason.
Spec follows CPython 3.15t; engineering follows PyPy
Pursue PyPy parity to the extreme as engineering, and CPython 3.15t as spec; where they collide, take the 3.15t behaviour and engineer it the way PyPy would. Neither goal yields wholesale — the axis decides which one governs the line in front of you.
The t is the free-threaded build: "CPython does X" is an answer only once X
holds without the GIL. Correct-because-a-global-lock-serialises-it is not
on-spec.
Module presence follows PyPy
Do not implement a module that PyPy does not have merely to unskip a CPython
test. Whether a module exists, and which layer owns it, is part of the PyPy
implementation shape. Before adding any builtin, extension, compatibility, or
test-support module, verify that the real pypy3 oracle imports it and locate
its owner in pypy/, rpython/, or lib_pypy/. If neither exists, the module
is not a pyre porting target unless the user explicitly expands the scope.
This applies to builtin-type and CPython-test-suite sweeps too: exclude an
absent module from the backlog instead of treating its skipped tests as missing
implementation work. The goal is to complete PyPy's module/type surface in the
PyPy way, not to reproduce CPython's private extension-module inventory.
In particular, CPython-only test helpers such as _testlimitedcapi are not
product modules. PyPy also has no _datetime extension module: it provides
datetime through its pure-Python implementation, so pyre must preserve the
working pure-Python path rather than invent an _datetime stub or accelerator.
A CPython test that directly imports one of these absent private modules may
skip or remain blocked; that is not by itself an implementation gap. If the
public stdlib feature is broken, port PyPy's actual owner/fallback and fix that
root cause. Never add an empty or partial module just to make the import
succeed, because that suppresses the intended fallback and turns a clean skip
into misleading failures.
The spec governs only what a caller can observe — return value, exception type/message/attributes, identity, encoding-and-errors contract, accepted argument shapes. Everything else follows PyPy unconditionally: names, module paths, control-flow order, data structures, storage owner, JIT hints. A structural divergence does not become a spec fix by sitting next to one.
It is not a 3.11-vs-3.15 question — six of seven adjudicated cases had no
version delta at all (sched_setscheduler since 3.3, PyUnicode_FSConverter
since 3.3, PEP 529, PEP 471). "3.15" pins which CPython you read
(lib-python/stdlib-version.txt); a missing delta is not grounds to refuse the
exception, and a real one earns nothing by itself.
Six tests, in order; stop at the first leaf. The full procedure is in
/parity under "SPEC-DEVIATION" — do not invoke this ruling without reading it.
- Can a Python snippet print a difference? No → ordinary parity finding.
- Do you hold an admissible 3.15 artefact — an in-tree
lib-python/3/…assertion, a measured run at the pinned version, or C source read at that tag? Prose is not admissible; a comment in pyre's own source is never the artefact. - Do the two upstreams actually disagree? If they agree and pyre differs from both, it is a plain regression that no spec reasoning rescues.
- Is PyPy's shape load-bearing for a mechanism pyre also has? Search the whole
definition, decorators included, in
rpython/andpypy/for@jit.*,_immutable_*,_attrs_,unrolling_iterable,make_sure_not_resized,rgc.*. A hint governing the value you are changing → STOP, follow PyPy. Record the negative search too. - Per-site artefact plus a blast-radius census: every departing site needs the
artefact forcing that site ("consistency with a sibling" is not one), then
rgpyre's own readers,pyre-jit*andmajit*included. - Does pyre land on 3.15t across the whole decision? Landing where neither upstream sits is a defect however well one axis matched.
File the result under the review's ## 4. Structural adaptations as
[3.15-spec] <ours> ↔ <pypy> — <observable>; evidence: <route + cite>, and
comment at the site citing both sides.
Porting discipline
- Strict line-by-line structural parity. No shortcuts, no reimplementation from scratch, no declaring a phase complete without the literal refactor.
- If a parity fix regresses, find the root cause before reverting. Structural alignment skipped is not success, even with green benchmarks.
- Cite upstream by symbol, not
file:line. Numbers rot silently and a rotted citation still reads as authoritative; a symbol stays checkable withrg. Use a line number only where no symbol pins the claim, and name the enclosing symbol beside it. - Confirm the worktree (
git rev-parse --show-toplevel) before editing and before staging — dozens of sibling worktrees share one.git.
Build and CI polling
- Never check a running local build or CI job more often than once every five minutes. While it runs, continue useful independent work when any is available; otherwise wait for the five-minute boundary. Do not spend tokens on repetitive status-only updates.
- CPython reference results for
pyre/check.pysynthetic fixtures andparity_tests/run.pyare cached at<shared>/oracle-cache(override withPYRE_ORACLE_CACHE;PYRE_ORACLE_CACHE=offdisables).--no-oracle-cacherefreshes entries without reading them. - A red
cargo testjob cancels the whole run. TheCancel the run (cargo test failed)step inpyre-ci.ymlcallsgh run cancel. Every other job then concludescancelled,gh pr checkslists them asfail, and--log-failedprints nothing. This is not a manual cancel, and the cancelled legs say nothing about the tree. To find the job that failed, rungh api repos/<owner>/<repo>/actions/runs/<id>/jobsand look for a step whoseconclusionisfailure. Read that job withgh run view <id> --log --job <job-id>. Fix the failure, then push again.
Iterate on the core; gate on the product
The product binary links pyre-module, the crate that owns the builtin
modules under pyre/pyre-module/src/module/ (math, _json, _ssl,
_cffi_backend, unicodedata, zlib, …). Work on majit, the JIT, the GC or
the interpreter core does not need them, so the edit–build–test loop leaves
them out:
export CHARON_TARGET_DIR=$HOME/Projects/.pyre-build/charon-target-<worktree>
pyre/scripts/build-jit-core.sh # extract the 4 core crates, pyre-dynasm without pyre-module (`jit-core` profile)
pyre/scripts/build-jit-core.sh --check # same extraction, `cargo check` only
python3 pyre/check.py --backend cranelift # check.py's cranelift leg is the core build
cargo test -p pyre-jit --no-default-features --features dynasm,prepass- Core-green is not product-green. The core compiles nothing behind
cfg(feature = "pyre-module"), including the JIT registrations that name those modules. A change that touchespyre-moduleor those registrations needs the product build before it is judged. - A fixture that imports a
pyre-modulemodule names it in its header (# pyre-check: requires-modules=math).check.pyreads each binary'ssys.builtin_module_namesand printsskip (no math)on a binary built without it, whatever its backend. Never exempt a backend (skip-backends=) for a module it lacks: whether the module is there is a property of the build, not of the backend. build-jit-core.shwritestarget/jit-core/pyre-dynasmunder thejit-corecargo profile (release optimisation, no LTO, 16 incremental codegen units), so its timings are not the product's.python3 pyre/check.py --build no --backend dynasm target/jit-core/pyre-dynasmmeasures it, with the samerequires-modulesskips as the cranelift leg. JIT counters (.jitstats) are comparable; wall-clock ratios are not.- The core loop does not replace the full gate below. When the full gate is
due (see "How much to verify"), it is the product
cargo testand a barepython3 pyre/check.py, not their core variants. - Which crate owns a module has three criteria, and PyPy's
default_modulestier is not one of them. A module belongs inpyre-interpreterwhen the interpreter reaches it by name --import,absolute_import,import_module, or upstream'sspace.getbuiltinmodule, sobootstrap_importlib_modules->_imp,eval.rs build_template_op->_templateandimporting.py read_compiled_module->marshalall count. The last shows why a pyre-side string search is not the whole test: pyre reachesmarshalfrominterp_imp.rsas a direct Rust call, so only upstream names it -- read the upstream owner before concluding a module is unreached. A module belongs there too when PyPy'sessential_modulesnames it (_opcode,__pypy__), or when CPython'sModules/Setup.bootstrapdoes (_abc,_functools,_stat,_symtable,_types,faulthandler,pwd,time). Everything else stays inpyre-module, and the test runs in both directions: a module inpyre-interpreterthat answers none of the three belongs inpyre-module. Note thatModules/Setup.bootstrapis narrower than CPython's always-built-in table inconfig.c.in--_tokenizeis in the latter only and is not a criterion hit. Adefault_moduleshit is not a reason to move:pypyoption.pygives every module aBoolOption(modname, default=modname in default_modules), so that tier means "on by default, switchable off" -- which is whatpyrex'sdefault = [..., "pyre-module"]already expresses.mathandcmatharedefault_modulesand stay optional; a fixture that needs one takes the skip marker above instead.
How much to verify
The full gate (cargo test --all plus a bare pyre/check.py) costs over an
hour. It is not the default for every commit. Scale verification to what
changed since the last green signal, local or CI:
- Rebase, cherry-pick or history rewrite of a tree that was green. Do not
rerun the tests. Confirm the rewrite kept the work: the commit list, and the
patch-ids of the commits that did not conflict. Run
cargo checkon the crates whose conflicts you resolved by hand. A conflict resolved by blending logic from both sides also gets the tests that exercise that region. CI judges the rest. - New code. Run the tests that exercise it: the touched crate's tests, the
relevant
check.py --synthetic-patternfixtures, and the regression test for the bug. Widen to the full gate when the change is cross-cutting (translator front end, codewriter, GC rooting protocol, backend ABI, a shared runtime helper), and once before opening a PR. - The last CI run on this branch was red. Before the next push, run every
failed job's command locally and see it pass. Use
/apple-containerfor an ubuntu-only red. A push that only hopes the red is fixed spends another CI run. - Before every push, which takes seconds:
cargo fmt --all -- --checkand codespell. Thepre-commitjob gates every other CI job. Also make sure the tip containsorigin/main(git merge-base --is-ancestor origin/main HEAD). A PR that is behind main gets no workflow run at all.
The full gate
cargo test --all --no-default-features --features dynasm,pyre-module. All three parts matter.--features dynasmselects a backend;--no-default-featuresis what keeps the selection consistent across crates. With default features on, the workspace's example interpreters default tocraneliftand supply it to the one sharedmajit-metainterp, whilepyre-jitcompiles in its dynasm registrations — so the binary dispatches through one backend and holds the other's hooks. Nothing emits an error; the tests simply run against a mismatched pair.pyre-modulenames the crate that owns the optional builtin modules; it is apyrexdefault, and because the dependency is optional,--no-default-featuresleaves the crate out of the launcher the integration tests run. CI's cargo test steps pass it, and the tests that need those modules are gated on the feature, so leaving it out here does not fail — it silently runs fewer tests than CI does.python3 pyre/check.py— every backend the host can build. A perf regression is a finding to explain, not an automatic veto: if the slower code is the line-by-line port and the faster was a shortcut, the port stands — record it and name the upstream optimization that would recover it (/parityPrinciple 4). Revert only when the regression has no such explanation. This file is loaded every session and Principle 4 is not, so do not restate that rule here in a form that contradicts it.- Re-record a
.jitstatsbaseline only when the new number is the one that should hold. "The recorded number no longer matches" is never on its own a reason: a gate is a target to reach, not a figure to refit. - When rebasing or cherry-picking, check the fix isn't already on main
(
git log main --grep=…).
Debugging discipline
- Before running the test, verify the traced path is actually reached — check gating and feature flags.
- Fix the interpreter/JIT root cause. Do not build workarounds (fallback modules, special cases) around it.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

