engine-extend
Skill1 starsChanged 49 days agoMITView on GitHub
Adds or changes engine code (engine/, imported as '@engine') so it stays portable to mini-games, deterministic and inspectable. Covers module layout and barrels (engine/index.ts, export * name clashes), portability and determinism rules (no DOM, Math.random, setTimeout; seeded Rng), Node subclass conventions (kind, describe(), draw(ctx), anchor/local space, selectors, lint), tests next to the code, a lint-clean sandbox demo scene (sandbox/scenes/<module>.ts in sandbox/main.ts), and updating AGENTS.md plus the matching skill in the same change. Use when adding an engine module, feature, widget, Node subclass, system or platform capability, fixing an engine bug, or exporting something new from '@engine' ("engine module", "extend engine", "Node subclass", "custom node", "barrel", "export", "portability", "deterministic", "sandbox demo", "引擎", "新增模块", "扩展引擎", "节点子类", "自定义节点", "兼容性", "小游戏兼容", "示例场景", "沙盒").
- Reads credentials
In this repository11 other files it gives its agents.
- AGENTS.mdAGENTS.md
- audio-designSkill
- code-artSkill
- game-feelSkill
- input-and-i18nSkill
- make-a-gameSkill
- performanceSkill
- physicsSkill
- release-platformsSkill
- testing-and-shotsSkill
- ui-screensSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
16 rules. About 0 are common, 7 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 15
Check the export names you plan to add are free (see "Naming").
- Distinctive: line 16
Write the code with relative imports, following the portability, determinism and Node rules below.
- Very specific: line 18
Add a unit test next to the code: engine/<folder>/<file>.test.ts.
- Very specific: line 19
Add or extend a sandbox demo scene.
- Distinctive: line 20
Update AGENTS.md and the matching skill (see "Docs in the same change").
- Distinctive: line 21
Run the verification checklist at the bottom.
- Very specific: line 48
Never import '@engine' or any index.ts barrel from engine code: it creates import cycles, and class X extends Node then evaluates with Node …
- Very specific: line 57
Every public name from every barrel lands in one namespace.
- Very specific: line 60
Before adding a public name, search for it: rg -n "export (const|function|class|type|interface|enum) Sparkline\b" engine.
- Distinctive: line 67
Keep helpers that only one file needs unexported (or unlisted in an explicit barrel).
- Very specific: line 71
Only engine/platform/ and engine/testing/ may touch runtime globals.
- Very specific: line 73
No DOM or runtime globals: document, window, wx, tt, navigator, Image, OffscreenCanvas, createImageBitmap.
- Very specific: line 288
Run one file: pnpm test engine/chart/sparkline.test.ts.
- Distinctive: line 357
Look at it and lint it (quote comma lists in PowerShell):
- Very specific: line 362
Read the PNGs in .shots/ (sandbox-chart-sparkline-<device>.png).
- Distinctive: line 388
Keep doc snippets compiling: API names in docs must match the code you just wrote.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill 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
Extending the engine
engine/ is bundled into every platform build and runs in four runtimes: browser, wx/tt/tap
mini-game JS engines, and Node (headless tests, @napi-rs/canvas). Every change must keep working in all
of them, stay reproducible under a seed, and be visible to text tools (dumps, selectors, lint).
Workflow
- Pick the home for the code (table below). Read two neighbouring files in that folder and copy their style.
- Check the export names you plan to add are free (see "Naming").
- Write the code with relative imports, following the portability, determinism and Node rules below.
- Export it from the folder's barrel (and add a new folder barrel to
engine/index.ts). - Add a unit test next to the code:
engine/<folder>/<file>.test.ts. - Add or extend a sandbox demo scene; add a scene test in
tests/<module>-scenes.test.ts. - Update
AGENTS.mdand the matching skill (see "Docs in the same change"). - Run the verification checklist at the bottom.
Where code goes
| Folder | Holds | Barrel |
core/ | math, emitter, rng, color, Game loop, defineApp/runApp | one line per file in engine/index.ts |
gfx/ | Ctx2D/Surface types, Texture, textures registry, bakeTexture, roundRectPath | engine/index.ts |
platform/ | Platform interface (types.ts), platform(), adapters web/minigame/wx/tt/tap | only types, current, ads are exported |
scene/ | Node, selectors, dump, Sprite, Box, Text, Scene | engine/index.ts |
runtime/ | tweens, timers, juice, gestures, input, assets, save, pools, i18n | runtime/index.ts (export *) |
display/ | Graphics, AnimatedSprite, NineSlice, particles, masks, effects | display/index.ts (export *) |
world/ | World/Camera2D, tilemaps, iso, collision, physics, paths | world/index.ts (export *) |
ui/ | flex layout, widgets, ui.* builder, modal/toast, lint | ui/index.ts (explicit list) |
art/ | palettes, SVG, shapes, icons, pixel art, noise, creatures, atlas | art/index.ts (explicit list) |
audio/ | synth/DSP, sfx, notation, songs, WAV, analysis, AudioManager | audio/index.ts (export *) |
testing/ | Node-only harness, HeadlessPlatform, devices (@engine/testing) | testing/index.ts, never re-exported by @engine |
Rules:
- A feature that fits a folder goes there as a new file. Create a new top-level folder only for a new
area; give it an
index.tsbarrel with a one-line header comment and addexport * from './<folder>/index';toengine/index.tsunder "Feature modules". export *barrels pick up new files only when you add theexport * from './<file>';line.ui/index.tsandart/index.tslist names explicitly: add every new public name (and its types withtype) there, or it is not exported. Unlisted names stay internal on purpose.- Inside
engine/, import concrete files relatively (import { Node, type NodeOptions } from '../scene/node';). Never import'@engine'or anyindex.tsbarrel from engine code: it creates import cycles, andclass X extends Nodethen evaluates withNodeundefined. - Platform adapters are not exported from
@engineon purpose: the build entry imports only the target's factory (@engine/platform/wxetc.), so other targets' code never lands in the bundle. import typefor type-only imports (verbatimModuleSyntaxmakes a missingtypea compile error).- Node-only code (fs,
@napi-rs/canvas, CLIs) belongs inengine/testing/ortools/, never elsewhere.
Naming (avoid export * clashes)
Every public name from every barrel lands in one namespace. Two barrels exporting the same name is a
compile error (TS2308 "has already exported a member named ...") that breaks pnpm typecheck for everyone.
- Before adding a public name, search for it:
rg -n "export (const|function|class|type|interface|enum) Sparkline\b" engine. - Prefix generic names with the module:
drawArtShape,pcmFilter,uiColor,resolveUITexture,sparklinePoints. Types follow the owner:SparklineOptions,UIButtonSize. - Already taken at the top level (do not reuse):
clamp,clamp01,lerp,invLerp,remap,wrap,approach,damp,dist,vec2,rect,insets,TAU,after,every,wait,nextFrame,Timer,rng,Rng,hashString,parseColor,toCss,platform,textures,ui,buildUI. - Keep helpers that only one file needs unexported (or unlisted in an explicit barrel).
Portability rules
Only engine/platform/ and engine/testing/ may touch runtime globals. Everywhere else:
- No DOM or runtime globals:
document,window,wx,tt,navigator,Image,OffscreenCanvas,createImageBitmap. Get canvases, images, text files, storage, audio, time and vibration from the platform:platform().createCanvas(w, h),platform().loadImage(path),platform().readText(path),platform().storage,platform().audio,platform().now(),platform().vibrate('short'). - Portable canvas subset only (
engine/gfx/types.ts): noPath2D(usesvgPath/traceSvgPath), noctx.roundRect(useroundRectPath(ctx, x, y, w, h, r)), noctx.filter,ctx.letterSpacing,ctx.reset,createConicGradient. Blur/glow:shadowBlur/shadowColoror the art bake effects. - No
node:*,@napi-rs/canvas,require, orprocess(the build defines onlyprocess.env.NODE_ENV).pnpm buildfails whengame.jsreferences node modules. - Nothing at import time: no
platform()calls (it throws beforerunApp/createTestGame), no canvas creation, no registering into global registries from a module's top level (UI builder types are the one existing exception). This also keeps unused code tree-shakeable. - Output is ES2017 (esbuild lowers syntax, nothing is polyfilled). Stay within the
ES2020lib the typecheck allows; avoid regex lookbehind and very new APIs. - A new platform capability means: add it to
Platforminengine/platform/types.ts, implement it inweb.ts,minigame.ts(plusminigame-api.tstypes andwx.ts/tt.ts/tap.tsdifferences) andengine/testing/headless.ts(record calls so tests can assert them), and cover it inengine/platform/minigame.test.tswith the fake API there. The typecheck flags any missing implementation.
Self-check (only doc comments and the engine's own Graphics.roundRect method may match):
rg -n "Math\.random|Date\.now|performance\.now|setTimeout|setInterval|document\.|window\.|\bwx\.|\btt\.|from 'node:|@napi-rs|new Path2D|\.roundRect\(|ctx\.filter|letterSpacing|OffscreenCanvas|createConicGradient|createImageBitmap" engine --glob "!engine/platform/**" --glob "!engine/testing/**" --glob "!**/*.test.ts"Determinism and time
- Randomness: never
Math.random(). Take an optionalseed?: number | stringand build the generator likeParticleEmitterdoes:seed !== undefined ? new Rng(seed) : rng.fork()(rngis the shared default; the test harness reseeds it toseed ?? 1, so forks are reproducible too). Pure helpers take anRngparameter instead of reading a global. - Time: use the
dtpassed toupdate(dt)/ systems (seconds, already time-scaled) orgame.time(elapsed,realElapsed,dt,frame,timeScale). Delays:after(s, fn),every(s, fn),await wait(s)from the runtime timers. NeversetTimeout/setInterval/Date.now(): headless tests advance simulated time, so wall-clock code breaks tests and screenshots. - Per-frame services: implement
System(update(dt)) and register withgame.addSystem(sys, priority), which returns a remover (lower priority runs first; tweens/timers use -100). Keep one instance per game in aWeakMap<Game, ...>(seeengine/runtime/ticker.ts); resolve the game with an explicit parameter orGame.current. Offerattach(game, owner?)that removes itself whenowneremits'destroyed'(seePhysicsWorld.attachinengine/world/physics.ts). - Iteration order must not depend on
Map/Setinsertion races or object key order of numeric-like keys; sort explicitly when the order affects results.
Node subclass conventions
Local space: draw(ctx) paints in the node's own box (0,0)-(width,height); (x, y) places the anchor in
the parent; the anchor (fractions of width/height) is also the pivot for rotation, scale and skew.
render() applies the transform, alpha, blend and clip, then calls draw, children (by zIndex), drawOver.
Render contract (flat renderer, engine/scene/node.ts):
- Each node computes its world matrix (parent world x local, no allocation) and effective alpha and applies
them with
ctx.setTransform/globalAlpha(alpha only when it changes). There is nosave()/restore()per node: only nodes withclip,blendorisolate: trueget one. - So
draw()/drawOver()must put back any canvas state they change other than transform, alpha, fill/stroke style, lineWidth, font and text alignment: shadows (shadowBlur = shadowOffsetX/Y = 0),setLineDash([]),lineCap = 'butt',lineJoin = 'miter',miterLimit = 10, composite op, image smoothing.globalAlphais re-asserted afterdraw(). When resetting is awkward, setisolate: true(costs a save/restore). Localctx.translate/rotate/scaleinsidedraw()is fine: the next node sets its own transform. Under Vitest (Node.checkDrawState, on whenNODE_ENV === 'test') a leak prints one[render] Kind#id: draw() leaves ctx.<prop> ...warning per kind and property; set it totruein a script to audit a game. - Overriding
renderContent/renderChildren: use the protected helpers instead ofctx.save()+ctx.transform():renderDraw(ctx),renderChildren(ctx),renderDrawOver(ctx)(restores this node's transform first),renderChildrenOffset(ctx, dx, dy)/renderChildrenWith(ctx, m)(children as if a local offset/matrix were applied),applyLocalTransform(ctx)(back to this node's space and alpha after children, e.g. before drawing between them), andsaveRenderState(ctx)/restoreRenderState(ctx)around a custom clip. A rawctx.save()/restore()pair is fine only if it contains no child rendering. - Offscreen rendering:
renderContentTo(ctx, m)draws the node's content into another canvas withmas its root matrix (seeCacheContainer);Node.renderRoot(ctx, node, m)renders a whole subtree as a pass (Game.renderseeds(k, 0, 0, k, ox, oy)). Callingnode.render(ctx)outside a pass renders from an identity transform, not from the ctx's current one. - Hit tests skip subtrees whose
interactiveDescendantsis 0 (the count is kept byadd/removeand theinteractivesetter), so keepinteractivean assignment, never a class field redeclared in a subclass.
// engine/chart/sparkline.ts — example of a new module file
import type { Color } from '../core/color';
import type { Ctx2D } from '../gfx/types';
import { Node, type NodeOptions } from '../scene/node';
export interface SparklineOptions extends NodeOptions {
color?: Color;
thickness?: number;
/** Samples kept (default 64). */
capacity?: number;
}
/** Samples → flat [x0, y0, x1, y1, ...] spanning (0,0)-(w,h), min at the bottom. Pure, so it is unit-tested. */
export function sparklinePoints(values: readonly number[], w: number, h: number): number[] {
const out: number[] = [];
if (values.length === 0) return out;
let min = Infinity;
let max = -Infinity;
for (const v of values) {
if (v < min) min = v;
if (v > max) max = v;
}
const span = max - min || 1;
const step = values.length > 1 ? w / (values.length - 1) : 0;
values.forEach((v, i) => out.push(i * step, h - ((v - min) / span) * h));
return out;
}
/**
* Line chart of recent samples.
*
* const s = scene.add(new Sparkline({ width: 300, height: 80, color: '#38bdf8' }));
* s.push(score);
*/
export class Sparkline extends Node {
color: Color;
thickness: number;
capacity: number;
readonly values: number[] = [];
constructor(opts: SparklineOptions = {}) {
super();
this.width = 200;
this.height = 60;
this.color = opts.color ?? '#38bdf8';
this.thickness = opts.thickness ?? 4;
this.capacity = opts.capacity ?? 64;
this.set(opts);
}
override get kind(): string {
return 'Sparkline';
}
push(v: number): this {
this.values.push(v);
if (this.values.length > this.capacity) this.values.shift();
return this;
}
override draw(ctx: Ctx2D): void {
const pts = sparklinePoints(this.values, this.width, this.height);
if (pts.length < 4) return;
ctx.beginPath();
ctx.moveTo(pts[0]!, pts[1]!);
for (let i = 2; i < pts.length; i += 2) ctx.lineTo(pts[i]!, pts[i + 1]!);
ctx.strokeStyle = this.color;
ctx.lineWidth = this.thickness;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.stroke();
ctx.lineJoin = 'miter';
ctx.lineCap = 'butt';
}
override describe() {
const v = this.values;
return { ...super.describe(), points: v.length, last: v.length ? +v[v.length - 1]!.toFixed(2) : undefined };
}
}Rules the example follows:
- Constructor: primary arguments first, then
optsextendingNodeOptions. Callsuper()with no arguments, set your defaults (including defaultanchorX/anchorYor size), thenthis.set(opts)last so the caller'sNodeOptionswin.super(opts)followed by field initializers silently overwrites them. noImplicitOverrideis on: writeoverrideonkind,draw,drawOver,describe,update,hitTest,onDestroy,renderContent.draw()resets the stroke style it changed (lineJoin/lineCapabove): nodes share canvas state (see the render contract).kindreturns the class name; it is the selector type and the first word of each dump line.describe()spreadssuper.describe()and adds a few short, rounded values. They appear inpnpm shot --dumpand become selector attributes (Sparkline[points=32],[text*=分],[tex^=hero]; values are compared as strings). It runs for every node on each attribute query, so keep it cheap.- Give the node a real
width/height. Bounds in dumps,worldBounds(), tap hit tests, culling and lint all use the content box; a zero-size node prints@x,yand cannot be tapped. - Non-rectangular tap area: override
hitTest(lx, ly)(local coordinates; honourthis.hitPadding). - Per-frame logic: override
update(dt); release game-level hooks (systems,game.onlisteners, timers) inprotected override onDestroy().destroy()already removes node listeners andonUpdatecallbacks. - Text: prefer child
Text/Labelnodes so lint checks size, contrast and truncation. A node that paints its own text should exposeuiTextInfo(): { text, size, colors, stroke }(duck-typed, seeRichTextinengine/ui/richtext.ts) solintUIstill sees it. - Lint role:
UIViewsubclasses setlintRole; plain nodes use tags (lint-blocker,lint-surface,lint-decor,lint-control,lint-ignore). Interactive nodes without a role count as controls (88-unit minimum tap size). A new built-in UI widget also needs aui.<name>builder entry and areg([...])line inengine/ui/builder.ts, plus its names inui/index.ts(see the ui-screens skill). - Cache expensive drawing: bake static parts once into a texture (
bakeTexture,textures.getOrCreate) or recompute only when inputs change (seeNineSlice.sliceRects()).
Tests next to the code
Pure logic and node behaviour: engine/<folder>/<file>.test.ts (Vitest, node environment; @engine and
@engine/testing aliases work, relative imports too).
// engine/chart/sparkline.test.ts
import { describe, expect, it } from 'vitest';
import { Node } from '../scene/node';
import { Sparkline, sparklinePoints } from './sparkline';
describe('Sparkline', () => {
it('maps samples onto the box, min at the bottom', () => {
expect(sparklinePoints([0, 5, 10], 100, 50)).toEqual([0, 50, 50, 25, 100, 0]);
});
it('keeps the last `capacity` samples and exposes them to selectors', () => {
const root = new Node();
const s = root.add(new Sparkline({ id: 'spark', capacity: 3 }));
[1, 2, 3, 4].forEach((v) => s.push(v));
expect(s.values).toEqual([2, 3, 4]);
expect(root.find('Sparkline[points=3]')).toBe(s);
});
});- Anything that needs a canvas, textures, time or input needs a platform:
const t = await createTestGame()from@engine/testing, thenawait t.step(n)/await t.advance(s)/await t.tap(sel); callt.destroy()inafterEach. Pixel checks:createTestGame({ device: '750x1334@1' })(1 unit = 1 px) andt.platform.canvas.getContext('2d').getImageData(x, y, 1, 1)(seetests/display.test.ts). - Cross-module or sandbox-scene tests go in
tests/<module>-<topic>.test.ts. - Run one file:
pnpm test engine/chart/sparkline.test.ts; by name:pnpm test -t "Sparkline".
Sandbox demo scene
The sandbox (sandbox/) is the engine showcase and compatibility test pack; every module has
sandbox/scenes/<module>.ts exporting scenes: Record<string, SceneFactory>, spread into
defineApp({ scenes }) in sandbox/main.ts. The home scene lists every registered scene automatically.
- Existing module: add a class and a
'<module>-<topic>'entry to its file. - A file that exists but only holds
export const scenes: Record<string, SceneFactory> = {};is a placeholder already registered insandbox/main.ts: fill it, do not touchmain.ts. - New module: create
sandbox/scenes/<module>.ts, then insandbox/main.tsaddimport { scenes as <module> } from './scenes/<module>';and...<module>,insidescenes.
// sandbox/scenes/chart.ts
import { rng, Sparkline, Text, type SceneFactory } from '@engine';
import { DemoScene } from '../common';
class SparklineDemo extends DemoScene {
readonly title = 'Chart · Sparkline';
protected build(): void {
const { x, y, w } = this.content;
const spark = this.add(
new Sparkline({ id: 'spark', x: x + 48, y: y + 48, width: w - 96, height: 240, color: '#38bdf8' }),
);
for (let i = 0; i < 32; i++) spark.push(rng.float(0, 100));
this.add(new Text('32 seeded samples', { fontSize: 24, color: '#9aa3b8' }, { id: 'caption', x: x + 48, y: y + 312 }));
}
}
export const scenes: Record<string, SceneFactory> = {
'chart-sparkline': () => new SparklineDemo(),
};DemoScene (sandbox/common.ts) draws the background, a title bar and a #back button; build inside
this.content ({ x, y, w, h } below the title bar, above the bottom safe inset). Show every option and
state of the feature, label things with Text of fontSize 20 or more, and seed all randomness.
Scene test (behaviour plus lint on three devices):
// tests/chart-scenes.test.ts
import { afterEach, describe, expect, it } from 'vitest';
import { formatLint, lintUI, type AppDef } from '@engine';
import { createTestGame, type TestGame } from '@engine/testing';
import sandbox from '../sandbox/main';
let t: TestGame | null = null;
afterEach(() => {
t?.destroy();
t = null;
});
describe('chart demo scenes', () => {
for (const device of ['iphone-se', 'iphone-14', 'ipad'] as const) {
it(`chart-sparkline on ${device}`, async () => {
t = await createTestGame({ app: sandbox as AppDef, scene: 'chart-sparkline', device });
await t.advance(1);
expect(t.get('#spark').describe()).toMatchObject({ points: 32 });
const errors = lintUI(t.stage, t.game).filter((i) => i.severity === 'error');
expect(formatLint(errors)).toBe('UI lint: no issues');
});
}
});Look at it and lint it (quote comma lists in PowerShell):
pnpm shot --app sandbox --scene chart-sparkline --device "iphone-se,iphone-14,ipad" --lint --dump
pnpm shot --app sandbox --scene chart-sparkline --boundsRead the PNGs in .shots/ (sandbox-chart-sparkline-<device>.png). Game-world content that is not UI
belongs in a World (lint-ignored) or under a lint-ignore / lint-decor tag; see the ui-screens
skill for lint rules and fixes, and testing-and-shots for the harness and screenshot tools.
Docs in the same change
A change is not done until the docs agents read describe it:
AGENTS.md"Layout": the folder's line lists the new capability (new folder: add a line). "Core conventions": any new rule. "Commands": any new script or flag.- The skill for the area, in
.agents/skills/<skill>/SKILL.md(and itsreferences/when it lists APIs):
| Change in | Update skill |
ui/ | ui-screens |
art/, textures | code-art (texture memory: performance) |
audio/, tools/audio | audio-design |
platform/, tools/build | release-platforms |
world/ physics, collision | physics |
testing/, tools/shot | testing-and-shots |
| tweens, juice, particles, screen shake | game-feel |
| frame stats, pooling, caching | performance |
| input, gestures, joystick, i18n | input-and-i18n |
| app structure, game template | make-a-game |
| engine layout or these conventions | engine-extend (this file) |
- Keep doc snippets compiling: API names in docs must match the code you just wrote.
Verification checklist
pnpm typecheckclean (catches export clashes, missingoverride, missingtypeimports, unimplementedPlatformmembers).pnpm testgreen, including the newengine/**/<file>.test.tsandtests/<module>-scenes.test.ts.- Portability self-check
rgabove shows nothing new. pnpm shot --app sandbox --scene <module>-<topic> --device "iphone-se,iphone-14,ipad" --lintreports 0 errors, and the PNGs look right.pnpm build --target all --app sandboxsucceeds (no node imports, size table printed;233is skipped with instructions when its converter is missing).- New public names exported from the barrel (explicit list for
ui/,art/) and importable from'@engine'. AGENTS.mdand the matching skill updated.
Pitfalls
- Importing
'@engine'insideengine/works in some files and breaks others through cycles; always import the concrete file. export *of a name that another barrel already exports fails the typecheck for the whole repo.- Calling
platform()or baking textures at module top level throws in tests and on devices. - Passing
optstosuper(opts)and then declaring field defaults overwrites the caller's options. - A
describe()value that changes every frame (raw floats) makes dumps noisy; round it. - Headless shots and Vitest do not prove a runtime works: mini-game-only APIs need wx/tt devtools or a phone (see release-platforms).
- The sandbox is also the compatibility pack: a demo scene that throws or fails lint blocks everyone's
pnpm testandpnpm shot.
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.

