lapis

Version, currently 4.8-dev75 versions

github.com/sol-vin/lapis

Lapis: High-performance Crystal bindings and 2-way host integration for Godot Engine 4.8+ using LibGodot and GDExtension.

11 stars
6 dependents
License: MIT

Installation

# Add this to your shard.yml
dependencies:
  lapis:
    github: sol-vin/lapis
    version: ~> 4.8-dev7

Then run:

shards install

shard.yml

Crystal
>= 1.20.0, <= 1.21.0
License
MIT
Author
Ian
Targets
  • game from src/main.cr
  • lapis from tools/lapis/src/lapis.cr
  • libgodot from src/libgodot.cr

Dependencies

Runtime Dependencies

  • opal*github: sol-vin/opal, branch: main
  • carbon*github: sol-vin/carbon, branch: main
  • jasper*github: sol-vin/jasper, branch: main
  • bakelite*github: sol-vin/bakelite, branch: main
  • cradare2*github: sol-vin/cradare2, branch: master

README

Lapis for Crystal

Crystal Lapis Godot Tests Release Docs Benchmarks Slides License

Lapis for Crystal provides high-performance Crystal bindings and a bidirectional runtime integration for Godot Engine 4.8+ using LibGodot and GDExtension. It empowers developers to build Godot games with native LLVM machine speed, compile-time static type safety, and Ruby-like syntax elegance.

▢️ Watch the Video Tour: https://youtu.be/EKMw_zQjovc
πŸ“– Official Documentation: https://sol-vin.github.io/lapis/
πŸ“Š Interactive Benchmarks: https://sol-vin.github.io/lapis/benchmarks.html
πŸ“‘ Interactive Slide Deck: https://sol-vin.github.io/lapis_slides/


Why Lapis?


Table of Contents


Architecture: Dual-Paradigm Integration

Lapis provides two execution paradigms designed for both rapid in-editor iteration and lean standalone production shipping:

graph TD
    subgraph Mode A: GDExtension In-Editor Workflow
        GE[Godot Editor 4.8] -->|Loads| GDX[addons/crystal_integration/crystal.gdextension]
        GDX -->|Loads| CB[bin/crystal_bridge.dll]
        CB -->|1. Initializes Boehm GC| CRT[Crystal Runtime]
        CB -->|2. Shadow loads| GDL[bin/game_loaded_pid_ts.dll]
        GDL -->|3. crystal_godot_init| REG[ClassDB & EditorHelp]
        REG -->|4. Exposes Nodes & Inspector| GE
        GE -->|F5 / Build Hook| EB[EditorPlugin._build]
        EB -->|Recompiles & Hot-Reloads| GDL
    end

    subgraph Mode B: Standalone LibGodot Host Paradigm
        EXE[bin/game.exe] -->|1. Owns main Entry Point| CRTM[Crystal Boehm GC]
        EXE -->|2. In-Memory Boot| LGD[bin/libgodot.dll]
        LGD -->|3. libgodot_create_godot_instance| GINST[Godot Engine Instance]
        GINST -->|4. Passes GDExtension C-API Table| EXE
        EXE -->|5. Steps Main Loop & Dispatches| WIN[Game Window & Audio]
    end

Windows Shadow Loading

On Windows, dynamic libraries loaded via LoadLibraryA are locked by the OS kernel, preventing compilers from overwriting game.dll. In development mode, crystal_bridge.cpp copies game.dll to a timestamped shadow file (game_loaded_<PID>_<TIMESTAMP>.dll) and loads the shadow copy. bin/game.dll remains completely unlocked so Crystal can recompile freely while the Godot Editor stays open.


Production Gameplay DSL & Ergonomics

Showcase Quickstart Node

Here is an idiomatic Lapis character node demonstrating property exports, @onready path unrolling, signals, tweening, pattern matching, direct space physics queries, and dead-pointer safety:

require "lapis"

# Player character with physics movement, combat raycasting, and animations
node Player < CharacterBody3D do
  # Scene unique node references resolved and cached during _ready
  onready visual_mesh : MeshInstance3D = ~"%VisualMesh"
  onready collider : CollisionShape3D = ~"CollisionShape3D"

  # Inspector export system with ranges, steps, and tooltips
  @[Export(range: 1.0_f32..25.0_f32, step: 0.5_f32)]
  property move_speed : Float32 = 8.0_f32

  @[Export(range: 1.0_f32..30.0_f32, step: 0.5_f32)]
  property jump_velocity : Float32 = 9.5_f32

  @[Export(range: 10..500, step: 10)]
  property max_health : Int32 = 100

  # First-class type-safe signals
  signal health_changed(current : Int32, max_health : Int32)
  signal died

  @current_health : Int32 = 100

  def _ready : Void
    @current_health = @max_health
    Godot.print("Player initialized: #{name}")
  end

  def _physics_process(delta : Float64) : Void
    vel = velocity

    # Apply gravity when airborne
    unless is_on_floor
      vel.y -= 24.0_f32 * delta.to_f32
    end

    # Handle jump impulse
    if Input.is_action_just_pressed("jump") && is_on_floor
      vel.y = @jump_velocity
    end

    # Vectorized movement direction
    input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
    dir = (transform.basis * Vector3.new(input_dir.x, 0.0_f32, input_dir.y)).normalized

    if dir.length > 0.001_f32
      vel.x = dir.x * @move_speed
      vel.z = dir.z * @move_speed
    else
      vel.x = Math.move_toward(vel.x, 0.0_f32, @move_speed * delta.to_f32)
      vel.z = Math.move_toward(vel.z, 0.0_f32, @move_speed * delta.to_f32)
    end

    self.velocity = vel
    move_and_slide

    # Fire weapon raycast on attack action
    if Input.is_action_just_pressed("fire")
      fire_weapon
    end
  end

  # Performs a direct space physics raycast
  private def fire_weapon : Void
    start_pos = global_position + Vector3.new(0.0_f32, 1.2_f32, 0.0_f32)
    target_pos = start_pos + (-transform.basis.z * 50.0_f32)

    # One-liner direct space query returning typed PhysicsHit3D
    if hit = raycast_to(target_pos, from: start_pos)
      # Expression pattern matching with dead-pointer safe downcasting
      match hit.collider do
        is Enemy do |enemy|
          enemy.take_damage(25)
          flash_hit_marker
        end
        default do
          Godot.print("Hit scenery at #{hit.position}")
        end
      end
    end
  end

  # Statement-based tween orchestration with auto call-peeling and typed identifiers
  private def flash_hit_marker : Void
    tween(self) do
      animate(visual_mesh.scale, to: Vector3.new(1.15_f32, 1.15_f32, 1.15_f32), in: 0.08.seconds)
      trans(Trans.Back)
      ease(Ease.Out)
      chain()
      animate(visual_mesh.scale, to: Vector3.new(1.0_f32, 1.0_f32, 1.0_f32), in: 0.12.seconds)
      trans(Trans.Cubic)
      ease(Ease.In)
    end
  end

  # Inflicts damage, emits signals, and handles death
  def take_damage(amount : Int32) : Void
    return if @current_health <= 0
    @current_health = Math.max(0, @current_health - amount)
    emit_health_changed(@current_health, @max_health)

    if @current_health <= 0
      emit_died
      queue_free
    end
  end
end

Typed Scene Pipeline & Queries

Lapis introduces expressive pipeline and query operators that eliminate boilerplate typecasts:

# 1. Preload Pipeline (>): Preload PackedScene, instantiate, and cast to concrete node
enemy = "res://scenes/enemy.tscn" > Enemy do |e|
  e.position = Vector3.new(10.0, 0.0, 5.0)
end

# 2. Dynamic Load Pipeline (>>): Load dynamically at runtime without caching
boss = "res://scenes/boss.tscn" >> Boss

# 3. Symmetrical Upward Ancestor Resolution (<<): Find nearest parent in hierarchy
player = hitbox << Player     # Strict: returns Player or raises NodeNotFoundError
boss = hitbox << Boss?        # Nilable: returns Boss? or nil if not found

# 4. Wildcard Scene Glob Queries (*):
all_enemies = self * "Enemies/*"                    # Array(Node)
all_lights  = self * "**/Light*"                    # Recursive globstar
all_areas   = self * {"HitBoxes/*", Area3D}         # Typed: Array(Area3D)
regex_boxes = self * /^HitBox_\d+$/                 # Regex: Array(Node)
hitbox      = self["Enemies/*/Hitbox", Area2D]?     # Nilable: returns nil if empty!

# 5. Zero-Allocation Streaming Iteration:
each_node("Enemies/*", Enemy) do |enemy|
  enemy.take_damage(50)
end

# 6. Fluent Group Queries (group):
group(:enemies).each(as: Enemy) do |enemy|
  enemy.alert!(player.global_position)
end
group(:enemies).call("wake_up")
alive_count = group(:enemies).size

Type-Safe Tween Orchestration

The tween macro peels apart block statements into a fluent pipeline, strictly verifying typed property identifiers at compile time:

# Statement-based tween pipeline with auto call-peeling, easing curves, and typed identifiers
tw = tween(hero) do
  animate(position, to: Vector2.new(120, 80), in: 4.seconds)
  chain()
  animate(modulate, from: Color::RED, to: Color::BLUE, in: 0.3.seconds)
  parallel()
  ease(Ease.Out)
  trans(Trans.Cubic)
  animate(scale, to: Vector2.new(1.2, 1.2), in: 0.3.seconds)
  chain()
  animate(modulate.a, to: 0.0, in: 0.25.seconds)
end

# Non-blocking cooperative await on the tween's completion
await(tw.finished)

Pattern Matching (match)

Game development frequently deals with polymorphic nodes, dynamic Variant payloads, and input dictionaries. The match macro handles these with zero-allocation inlined execution:

match event do
  # Variant unboxing & dead-pointer safe downcasting
  is Player do |player|
    Godot.print("Player HP: #{player.max_health}")
  end

  # Guard clauses with if: condition
  is Enemy, if: event.level > 50 do |boss|
    trigger_boss_music(boss)
  end

  # Symbol / String equivalence
  is :action_jump, :action_vault do
    player.execute_aerial_move
  end

  # Tuple destructuring
  is {state, on_floor?} do
    is :jumping, false do apply_air_control end
    is :jumping, true  do trigger_landing_dust end
  end

  default do
    handle_unhandled_event
  end
end

Gameplay Patterns: FSM, Signal Bus & Pooling

# 1. Zero-Allocation Typed Union FSM
struct Idle; end
struct Walking; getter speed : Float32 = 6.0_f32; end
struct Attacking; getter combo : Int32 = 1; end

alias State = Idle | Walking | Attacking

node CombatUnit < CharacterBody3D do
  fsm State, initial: Idle.new do
    on_enter Walking do |w|
      Godot.print("Walking at speed #{w.speed}")
    end
    on_exit Attacking do
      reset_combo_timer
    end
  end
end

# 2. Type-Safe Decoupled Signal Bus
signal_bus GameEvents do
  signal score_changed(new_score : Int32)
  signal player_died
end

# Emitter:
GameEvents.instance.score_changed.emit(100)

# Receiver:
GameEvents.instance.score_changed.connect do |score|
  ui_label.text = "Score: #{score}"
end

# 3. High-Performance Object Pool
class Arena < Node2D
  node_pool bullets : Bullet, capacity: 64

  def fire_volley(at_pos : Vector2) : Void
    bullet = bullets.acquire do |b|
      b.position = at_pos
      b.velocity = Vector2.new(500.0, 0.0)
    end
  end
end

In-Editor Action Driver ("Selenium for Godot Editor")

Lapis includes the Action Driver, a programmatic UI inspection, automation, and testing framework for the Godot Editor. It provides DOM traversal, synthetic inputs, state preservation across reloads, and Set-of-Marks AI Vision generation:

[ Lapis Action Driver ] ──TCP:9095──> [ Godot Editor 4.8 ]
       β”‚                                     β”‚
       β”œβ”€ click "Build"                      β”œβ”€ Finds Control via DOM predicate
       β”œβ”€ type "%SearchEdit" "Player"        β”œβ”€ Synthesizes InputEventKey / Mouse
       β”œβ”€ open-scene "res://test.tscn"       β”œβ”€ EditorInterface scene dispatch
       └─ vision --output reports/ai         └─ Generates Set-of-Marks AI Manifest

CLI Commands:

# Click a button in the Godot Editor UI
lapis driver click "Build"

# Type text into a LineEdit or search filter
lapis driver type "%FilterEdit" "Player"

# Switch main editor screens (2D, 3D, Script, Crystal)
lapis driver screen 3D

# Generate Set-of-Marks AI Vision manifest & cropped widget images
lapis driver vision --output reports/ai_vision

# Dump the live Godot Editor DOM tree hierarchy
lapis driver dom 8

# Launch interactive Action Driver REPL
lapis driver repl

Native In-Editor radare2 Debugger & Decompiler

Powered by sol-vin/cradare2, Lapis embeds a native multi-threaded debugger directly inside the Godot Editor:

  • Gutter Breakpoint Sync: Set breakpoints directly in Godot's Script Editor gutter (F9). Breakpoints are synchronized with radare2 via source line matching (dbl) instantly.
  • Dedicated In-Editor Tab: Docked Crystal Debugger tab featuring Continue (F5), Step Over (F10), Step Into (F11), Step Out (Shift+F11), call stack frame navigation, and CPU register inspection (RAX, RBX, RIP, RSP).
  • Live Pseudo-C Decompiler (pdc): Built-in side-by-side view with native pseudo-C (pdc) decompilation and annotated assembly (pdf/pdca) with zero external decompiler dependencies.
  • Multiplayer Lockstep Break: When any peer hits a breakpoint, all other active instances are automatically paused via cooperative interrupt (DebugBreakProcess / SIGINT) to eliminate network heartbeat timeouts (ENet / WebRTC).
  • Dead-Pointer Forensics: Automatically inspects faulting memory addresses, classifies loaded modules, and checks 64-bit ObjectDB IDs to diagnose dead pointers.
# Launch test project under native radare2 debugger
lapis run -d

# Launch Godot Editor under native radare2 debugger
lapis editor -d

# Decompile symbol directly from terminal
lapis decompile bin/game.dll "*Player*physics_process*" --side-by-side

Interactive Opal TUI Hub (lapis cli)

Lapis includes an interactive full-screen terminal command center powered by Opal (sol-vin/opal). Launch it with lapis cli (or lapis -i):

β”Œβ”€β”€ Lapis Engine Command Hub (v0.0.264) ───────────────────────── [Godot 4.8-dev7] ──┐
β”‚                                                                                    β”‚
β”‚   [+] New Project / Addon Wizard      Interactive folder & template scaffold       β”‚
β”‚   [#] Launch Godot Editor             Persistent launcher with log watching        β”‚
β”‚   [A] Action Driver Controller        DOM automation REPL & AI vision capturer     β”‚
β”‚   [B] Packaging & Export Center       Multi-target builds & release packaging      β”‚
β”‚   [D] Radare2 Native Debugger         Registers, disassembly & hex memory view     β”‚
β”‚   [L] Diagnostic Log Viewer           Real-time log tailing & channel filters      β”‚
β”‚   [#] Benchmark Visualizer            Cartesian charts, percentiles & memory delta β”‚
β”‚   [~] Run Game (Performance Monitor)  Live FPS/memory charts with Ctrl+K stop      β”‚
β”‚   [?] Test Suites Dashboard           Multi-phase test runner with split-screen    β”‚
β”‚   [+] Toolchain Doctor                Environment & dependency health check        β”‚
β”‚   [~] Synchronize Multi-Targets       Sync bridge DLLs and manifests across targetsβ”‚
β”‚   [x] Clean Build Artifacts           Prune binaries & unlock shadow DLLs          β”‚
β”‚   [=] Generate Documentation          Build static offline HTML documentation site β”‚
β”‚                                                                                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ [↑/↓] Navigate  β”‚  [Enter] Launch  β”‚  [Ctrl+P] Command Palette  β”‚  [Q] Exit        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Press Ctrl+P from any screen to open the global floating Command Palette for instant fuzzy search across all 35+ CLI tools and actions.


Memory Safety & Dead-Pointer Defense

Embedding a garbage-collected language (Crystal Boehm GC) inside a native C++ engine (Godot ObjectDB) introduces a critical hazard: when Godot frees an object (e.g. via queue_free()), standard C-API bindings retain a raw pointer to dead memory. Dereferencing it triggers an unrecoverable segmentation fault (ACCESS_VIOLATION / SIGSEGV).

[ Crystal Runtime ]                              [ Godot Engine / GDScript ]
  enemy = get_node("Enemy")
  enemy.@pointer = 0x7FFE_1234  ────────────>     Node instance at 0x7FFE_1234
                                                      β”‚
                                                      β”‚ GDScript: enemy.queue_free()
                                                      β–Ό
                                                   ObjectDB destroys Node & frees memory!
                                                   0x7FFE_1234 is now DEAD / UNMAPPED!
  enemy.position = Vector3.new(...)
        β”‚
        β–Ό
  [ Lapis check_alive! ]
        β”‚
        β”œβ”€β”€β”€β”€β”€ Query ObjectDB for monotonic 64-bit instance ID: ID is INVALID!
        β”œβ”€β”€β”€β”€β”€ Marks wrapper dead (@pointer = null)
        └───── Raises Godot::DisposedObjectError (Clean, catchable Crystal exception!)
               [ ZERO NATIVE CRASHES ]

Safety Invariants:

  1. Monotonic 64-bit Instance ID Tracking: Every Godot::Object wrapper tracks its engine-assigned ID. Godot's ObjectDB monotonic IDs never collide with recycled heap addresses.
  2. Pre-Dispatch Liveness Check (#check_alive!): Before dispatches or reflection calls, Lapis validates liveness in O(1) time (Bridge.is_instance_valid(instance_id)).
  3. Graceful DisposedObjectError: If an object is freed, Lapis marks the pointer null and raises Godot::DisposedObjectError instead of crashing.
  4. Defensive Inspection: Check node.alive? or node.destroyed? before interacting with transient entities.
  5. Main-Thread SceneTree Affinity: Scene graph mutations (add_child, remove_child, queue_free) enforce main-thread execution (assert_main_thread!) with automatic thread-safe deferral (call_deferred / Godot.on_main_thread).
  6. Dead-Pointer Armor (try? & if_alive): Safely invoke methods on nilable or transient objects with @heal_sfx.try?(&.play) or @target.try?(&.take_damage(spell.power)). try? automatically queries Godot's ObjectDB monotonic ID table and returns nil safely if the object was freed or nil, eliminating C#'s .NET ?. ObjectDisposedException trap.

Performance & Benchmarks

Lapis features an automated 1-to-1 cross-language benchmark suite comparing native compiled Crystal (GDExtension) against GDScript (Godot 4 Bytecode):

πŸ“Š Explore the Live Dashboard: https://sol-vin.github.io/lapis/benchmarks.html

Running Benchmarks Locally:

# Run all benchmarks and generate HTML/SVG reports
lapis benchmarks [ITERATIONS=3]

# Run specific categories or filters
crystal run benchmarks/runner.cr -- -c compute
crystal run benchmarks/runner.cr -- -c engine
crystal run benchmarks/runner.cr -- --filter=signals

Unified Multi-Tier Test Suite

Lapis enforces quality through an automated test harness covering language bindings, in-editor tool execution, and standalone runtimes:

  1. Engine Core Specs (spec/): Headless unit specs covering GC object retention, dynamic scaling, Variant type round-trips, and Vector math (spec/core_spec.cr, spec/math_spec.cr, spec/safety_spec.cr).
  2. Lapis CLI Specs (tools/lapis/spec/): CLI argument parsing, standalone execution, and packaging tests (tools/lapis/spec/cli_spec.cr).
  3. 85+ Modular Runtime Suites (spec/suites/): Comprehensive engine verification covering 2D/3D nodes, navigation, physics, multiplayer, shaders, audio servers, FSM, and memory lifecycle.
  4. Quantitative Zero-Leak Verification: Lapis::Test.assert_no_leak leverages Godot's Performance monitors (OBJECT_COUNT, OBJECT_NODE_COUNT, MEMORY_STATIC) and Crystal's GC.collect to mathematically verify zero object or memory leaks.
# Run the complete test suite (interactive TUI or streaming)
make test
lapis test

# Run headless specs only
make spec

Lapis CLI Toolchain Reference

The lapis CLI (bin/lapis / bin/lapis.exe) provides a unified developer toolchain:


Comprehensive In-Code Documentation (Lapis::Docs)

Lapis includes an extensive 10-track technical documentation system compiled directly into the code via Jasper (sol-vin/jasper):

Build and view documentation locally:

make docs
# Open docs/index.html in your browser

Prerequisites & Installation

System Dependencies

  • Crystal Compiler: 1.20+
  • radare2: 5.8+ (native debugger, disassembler, and decompiler)
  • Godot Engine: 4.8-dev7+ (Standard build, 64-bit)
  • C++ Compiler: GCC (g++) or Clang (for compiling the GDExtension loader bridge)
  • GNU Make: Workspace build & synchronization automation
  • Git: Shards dependency resolution & version control

Quick Install by Platform

Official Installers

  • Windows Installer (lapis-setup-windows-x86_64.exe): Installs Lapis CLI, bundles crystalline.exe (LSP) and radare2.exe (native debugger), configures user PATH, and registers PowerShell completions.
  • Debian Package (lapis_amd64.deb): Installs Lapis globally with automatic package dependency resolution (Depends: crystal, radare2).

Adding to an Existing Project (shard.yml):

dependencies:
  lapis:
    github: sol-vin/lapis
    branch: 4.8-dev7

Run shards install to install dependencies.


Repository Structure

lapis/
β”œβ”€β”€ src/                          # Reusable Lapis library & root host application
β”‚   β”œβ”€β”€ libgodot.cr               # Library root entry point (require "libgodot")
β”‚   β”œβ”€β”€ lapis.cr                  # Lapis prelude & engine extensions
β”‚   β”œβ”€β”€ main.cr                   # Root host game entry point & test runner panel
β”‚   β”œβ”€β”€ bridge/crystal_bridge.cpp # C++ GDExtension loader bridge
β”‚   └── libgodot/                 # Core engine C-API, macros, and generated bindings
β”œβ”€β”€ tools/                        # Built-in CLI toolchain
β”‚   └── lapis/                    # Compiled native CLI (bin/lapis)
β”œβ”€β”€ spec/                         # Unified Crystal specifications & test suites
β”‚   β”œβ”€β”€ spec_helper.cr            # Common spec helper and test nodes
β”‚   β”œβ”€β”€ editor_driver_spec.cr     # In-editor @tool and runtime tests via EditorDriver
β”‚   β”œβ”€β”€ suites/                   # 85+ modular engine test suites
β”‚   └── fixtures/                 # Spec test targets & fixtures
β”œβ”€β”€ scenes/                       # Root Godot host project scenes (main_test_runner.tscn, etc.)
β”œβ”€β”€ scripts/                      # GDScript test fixtures and interop nodes
β”œβ”€β”€ project.godot                 # Root Godot host project configuration
β”œβ”€β”€ addons/                       # GDExtensions & Editor Plugins
β”‚   β”œβ”€β”€ crystal_integration/      # Official GDExtension manifest & editor build hook
β”‚   └── dummy_*/                  # Isolated test addons for multi-addon stress tests
β”œβ”€β”€ examples/                     # Independent consumer showcase examples
β”œβ”€β”€ template/                     # Clean starter template for new games
β”œβ”€β”€ template-addon/               # Starter template for redistributable addons
β”œβ”€β”€ bin/                          # Output binaries, bridge DLL, and dependencies
└── AGENTS.md                     # Agent development guidelines

License

Distributed under the MIT License. Copyright (c) 2026 Ian and contributors.