Stripe Projects
fossasia/eventyay
A skill your agent uses when the user wants to provision infrastructure or third-party services using Stripe Projects.
Author event-driven LunCoSim scenarios for missions, waypoints, reactions, or multi-entity coordination.
$ npx skills add LunCoSim/lunco-sim --skill author-scenario -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install LunCoSim/lunco-sim author-scenario --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/LunCoSim/lunco-sim.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/author-scenario .claude/skills/author-scenario && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "author-scenario" agent skill from https://github.com/LunCoSim/lunco-sim/tree/main/skills/author-scenario into .claude/skills/author-scenario/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "author-scenario", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/LunCoSim/lunco-sim/tree/main/skills/author-scenarioType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add LunCoSim/lunco-sim --skill author-scenario -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install LunCoSim/lunco-sim author-scenario --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/LunCoSim/lunco-sim.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/author-scenario .agents/skills/author-scenario && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "author-scenario" agent skill from https://github.com/LunCoSim/lunco-sim/tree/main/skills/author-scenario into .agents/skills/author-scenario/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "author-scenario", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add LunCoSim/lunco-sim --skill author-scenario -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install LunCoSim/lunco-sim author-scenario --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/LunCoSim/lunco-sim.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/author-scenario .cursor/skills/author-scenario && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "author-scenario" agent skill from https://github.com/LunCoSim/lunco-sim/tree/main/skills/author-scenario into .cursor/skills/author-scenario/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "author-scenario", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/LunCoSim/lunco-sim.git --path skills/author-scenario--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add LunCoSim/lunco-sim --skill author-scenario -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install LunCoSim/lunco-sim author-scenario --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/LunCoSim/lunco-sim.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/author-scenario .gemini/skills/author-scenario && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "author-scenario" agent skill from https://github.com/LunCoSim/lunco-sim/tree/main/skills/author-scenario into .gemini/skills/author-scenario/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "author-scenario", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install LunCoSim/lunco-sim author-scenarioInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add LunCoSim/lunco-sim --skill author-scenario -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/LunCoSim/lunco-sim.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/author-scenario .github/skills/author-scenario && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "author-scenario" agent skill from https://github.com/LunCoSim/lunco-sim/tree/main/skills/author-scenario into .github/skills/author-scenario/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "author-scenario", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add LunCoSim/lunco-sim --skill author-scenario -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install LunCoSim/lunco-sim author-scenario --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/LunCoSim/lunco-sim.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/author-scenario .opencode/skills/author-scenario && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "author-scenario" agent skill from https://github.com/LunCoSim/lunco-sim/tree/main/skills/author-scenario into .opencode/skills/author-scenario/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "author-scenario", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
author-scenarioAuthor event-driven LunCoSim scenarios for missions, waypoints, reactions, or multi-entity coordination.
Author Scenario is an agent skill from LunCoSim/lunco-sim. Author event-driven LunCoSim scenarios for missions, waypoints, reactions, or multi-entity coordination. Use when working with task, mission, onevent, RunScenario, navto, emit, or persistent this state. Scenarios own sequencing and policy; Modelica owns continuous control math, USD owns scene structure and wiring, and authoring-vessel-controllers owns vessel GNC.
Its SKILL.md is about 12k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Backend & APIs, covering Event-driven systems. The repository describes itself as: Collaborative Multiphysics Cosimulator For Space Missions 🌎🚀🌚. The licence is Apache-2.0.
7 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 43f1301. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
cargoFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Author Scenario loads about 12k tokens when it runs. Until then it costs about 99 tokens; SKILL.md has 5,944 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from LunCoSim/lunco-sim at commit 43f1301, republished under its Apache-2.0 licence (© LunCoSim). 5,944 words, ~11,701 tokens.
.claude/skills/author-scenario/SKILL.md (or your agent's skills folder).A scenario is a rhai program attached to an entity. Production scenarios
are task/event-driven policy. They must not define on_tick; that hook is
reserved for authored tests under assets/scenarios/tests/ to sample live
telemetry and publish a bounded verdict. Continuous rover dynamics remain in
fixed-step physics/Modelica. Runtime plugins install their own cycles from
the selected application composition. A scenario may declare its peer-role
execution target with // @peer host|client|both and its cadence with
// @timing simulation; host includes standalone authoritative mode. These
directives do not choose its Core/Application/Twin owner or install schedules.
Rust assigns the owner route and retains schedule ownership.
Unsupported metadata disables only that program and publishes a document
diagnostic for its source revision. Keep runtime errors visible; do not catch
and erase them as successful no-ops.
Scenario actors submit, commit, and run in GlobalEntityId order from the
source-owned component, not from ECS query order or the API lookup index. A
local-only host without a global identity uses a world-local Bevy key and is not
part of cross-session replay ordering. If one scenario's synchronous command
must be visible to another in the same pass, test that handoff through a
production Rhai scene with stable actor identities.
For mission operations, read the generic mission and engineering quality gates before authoring the scenario. Treat the ConOps, mode transitions, command and telemetry contract, timing/resources, nominal path, and contingency/recovery paths as explicit requirements. Exercise fault stimuli and safe/degraded modes through event-driven policy and tests; do not turn missing telemetry or an invalid command into a silent fallback.
For live route edits, Rhai owns the route policy and calls the generic typed USD
operation command. The reusable waypoint_editor tool authors ordinary USD
route points, whether their route scope is inline or composed from a separate
route-plan asset. Document authoring composes the base, runtime, and view layers
at their actual strengths before validating selected-variant children; the
route_variant_lifecycle production gate covers add, move, and delete on that
route shape. It uses a local active=false opinion when a point comes from
a reference arc, and updates the disposable ribbon in the document's @view@
layer under the selected route scope from the committed USD route. An edit can
use the selected route point to resolve its enclosing route and does not require
rover possession; multiple routes still require an explicit route or subject
selection. A selection-only delete resolves the selected point when no pointer
target is present. Bind a route program to its composed USD document and build
its ribbon from the matching usd.document.projected event; that event is the
authoritative boundary for the route identity and does not wait for Modelica
admission. Keep on_visualization(me, ctx) for presentation preparation that
does not depend on a document attachment that may still be settling. This
one-shot callback runs in PreUpdate, after the time spine and before the first
fixed tick, top-level initialization, or on_start; use the host id, parameters,
and read-only queries. Rhai blocks world writes and events there;
ApplyUsdTransientOps is the only available command and always edits the
disposable view layer. Keeping the ribbon under that scope
preserves the route points' USD parent frame. The subject and route owner remain live:
route edits do not rebuild
or reset their physics, Modelica state, pose, possession, or Rhai this state.
The authored inputs:enabled value is only an initial policy; F changes the
scenario's runtime state and must not write it back to USD. Do not implement
this policy with a polling on_tick loop or move USD identity resolution,
terrain sampling, collision events, or fixed-step steering into Rhai.
Document-backed route edits remain valid during the short projection interval:
the generic resolver uses a locally authored target before its live ECS entity
exists. If the canonical child list is not synchronized, the editor reports a
retryable error rather than guessing a point name or writing to a different
program. Composed-only paths still require the mounted canonical stage.
When a source is hot-swapped, the program emits the generic typed
program.ready event after on_start; one-shot UI or control actions wait for
that lifecycle edge rather than relying on a timer. Sensor events may carry a
nested collider; match the entrant through the generic parent() chain to the
authored subject instead of adding a route-specific child relationship.
If the mounted document is attached after on_start, defer document-backed
route reads and task actions until the matching usd.document.projected event
binds it; do not retry against an empty identity each task pass.
Initial scenario admission waits for all world and entity readiness holds
because a program may depend on entities outside its own hierarchy. After
admission, an entity hold idles only scenarios attached within that subtree;
release resumes them without a stop/start cycle. Fixed-step hooks run after
SimTickSet and release only events stamped before the current tick. Paused
simulations deliver discrete events on the next Update without advancing the
tick. The first on_start does not replay events from before the program
started. Query current state from the owning subsystem for startup decisions,
then use on_event for later transitions.
Connected co-simulation event outputs are edge-detected on admitted simulation
ticks after SimTickSet and the full scripting pass. During startup holds, an
edge may occur before scenario execution opens and is not replayed to a later
on_start; read the current state from its owning subsystem during startup.
Event time comes from MissionClock at the producer's SimTick; later edges
are delivered on an eligible scenario pass.
Use simulation_dependencies(me, ctx) to declare simulation-clock Modelica
port/event dependencies, generic live entities accessed through direct
get, port, ReadPorts, or direct mutation, and broad API query providers.
The owner uses the plan for startup admission and runtime access checks. Return
all five plan arrays even when empty. Omitting a field is a source error:
fn simulation_dependencies(me, ctx) {
#{
modelica_entities: [find_path("/Rover/Controller")],
entity_reads: [],
entity_writes: [],
query_reads: [],
required_inputs: [
#{ owner: "sysml.twin-analysis", identity: ctx.twin_name },
],
}
}Modelica entities join the shared fixed-step causal barrier. Required input
keys name an owner namespace and an exact identity; the generic runtime waits
on owner-published Pending/Ready/Failed state before top-level initialization
and on_start. A pending scenario keeps its existing activation hold and
resumes after the owner publishes a new state revision, so scripts should not
poll analysis from on_tick. A missing owner or failed input produces a
diagnostic. Keep the selection policy in Rhai and use the key contract exposed
by the domain owner. Every modelica_entities id must resolve to a live
Modelica participant; a live but unrelated entity fails the source revision
before initialization. Declare every Modelica participant this scenario reads
or writes through a port or whose event it consumes. The aggregate solver
barrier is not an access grant: membership through USD connections or another
scenario's plan does not authorize this scenario to read that participant.
modelica_entities grants both read and write access to those live Modelica
participants and adds them to the shared simulation barrier. entity_reads
and entity_writes grant directional access to any live entity; a Modelica
entity in either set also joins the shared barrier. Direct reflected and port
reads require read access, while direct reflected writes, port writes, and
structural verbs require write access.
Entity-targeted query providers report the ids they read. Declare those target
ids in entity_reads too; this includes QueryEntity, QueryPhysicsState,
ReadPorts, GetPort, SolarPose, and single-entity ListPorts calls.
Mounted-scene USD queries without doc_id use the committed Twin generation.
For broad providers such as EntitiesInRadius, Raycast, or CosimStatus,
list the public provider name in query_reads. This declares the provider's
whole snapshot as a coarse dependency. Broad queries cannot run while the plan
is being resolved; after commit, the same declaration authorizes the
scenario-owned top-level initialization and lifecycle hooks. Undeclared
simulation-clock access through direct get,
port, or ReadPorts requires the matching direction. Targeted Modelica commands and
Modelica event delivery require the target or producer in this scenario's own
plan; aggregate barrier membership is not authorization.
Hierarchy and spatial identity queries remain available for discovery. If a
scenario uses their returned ids in direct get, port, or mutation calls, it
must include those ids in the corresponding access set.
When a typed simulation command creates an entity whose id was not available
while the plan was prepared, call track_entity_read(id) or
track_entity_write(id) after the command has materialized the live entity
and before accessing it. This commits access in serialized simulation order;
if the entity is a Modelica participant, it also joins the scenario's barrier.
The reusable route marker is a translucent, unlit, shadowless annotation. Its
unvisited colour is bright green and its visited colour is gray in standard
primvars:displayColor; route_follow applies the visited colour through
waypoint_editor's transient USD view operation. Route progress is keyed by USD
point path and survives autopilot stop/start. At on_start, the program reads
the current Avian contact for only the first unvisited route point through the
generic SensorOccupants query. A simultaneous occupancy snapshot has no visit
order, so it cannot mark later points. A later sensor enter marks a visit only
when its zone is the first unvisited point; out-of-order arrivals are ignored
until that point is entered after its predecessors. Active route progression
uses the same order. This is presentation state, not a vessel component or a
second route fact.
The route context gesture opens its authored menu without selecting the point;
selection and gizmo activation require the menu's explicit select action, and
click-to-place movement requires the separate Move action. Pass the resolved
document, route, and direct point in each menu action context so a later
callback does not depend on viewport query scope. Resolve a route-bearing
pointer target before a previously selected or controlled subject, especially
when one subject has multiple route programs. User possession is a
ControlLink/SessionRegistry lifecycle, while a route program is guidance
policy. Releasing possession hides the vessel HUD and releases manual input
holds; an enabled route then republishes its active guidance target without
claiming the user's session. Route policy owns any stop setpoint and writes it
explicitly. Repossession restores the HUD without restarting the route.
Script source edits made by a user go through the ScriptDocument host, so
undo, redo, and the Twin journal see the same typed ScriptOp. A file-backed
or USD-embedded source refresh uses the shared external-baseline path instead;
it advances the runtime generation without duplicating the source owner's
journal entry. The disposable ribbon follows the same rule: its typed @view@
projection is rebuilt from the committed route and is never authored as Twin
content or placed in user history.
Reusable authored programs are managed through the generic Rhai
program_editor tool. It lists LunCoProgramAPI children, selects or opens a
program in the existing editor, atomically switches its info:sourceAsset arm,
and can attach a new source-backed program without knowing whether the owner is
a rover, lander, route, or another model. Twin policy may enable the standard
program-browser surface on any USD scope with
program_editor::enable_hud(doc, scope, visibility). The surface emits typed
semantic actions; it does not contain vessel-specific Rust or an ad-hoc text
input protocol. A subject may bind several programs through rel programs; the
browser and generic scene selection keep one canonical program path active, so
editing or F control never falls back to a hardcoded /Route.
Host = mechanism, script = policy. A scenario touches the world only through the same command/query API the HTTP API, MCP, and UI use — so it inherits every command for free and stays decoupled from physics.
Scene construction is an authoring/preflight concern, not production scenario
logic. Use the generic Rhai model_authoring::scene_recipe before launching a
scenario to place authored assemblies/terrain, define cameras and initial
state, and record route/program hand-offs. Use model_context,
readiness_report, and port_graph/wiring_plan to validate the exact
document, paths, and generation first. Apply returned USD ops through
assembly_edit; pass route entries to waypoint_editor and program entries to
assembly_edit::attach_program. Keep mission sequencing here, continuous math
in Modelica, and do not turn the recipe into an on_tick loop. See the
model-authoring guide.
Scope boundary — do not blur these:
authoring-vessel-controllers.vadd vsub vscale vlen norm_squared vdot vcross vnorm qrot clamp
angle_deg yaw_delta_deg are Rust (lunco_scripting_rhai_core::rhai_math, on glam).
Existing array operands remain supported; hot-loop code should use native
Vec3/Quat from world_pos3, world_forward3, and
world_rotation_quat, lowering with vec3_array/quat_array only at a
command, telemetry, or USD-literal boundary. Reimplementing one in rhai is
how four scripts ended up with four copies of the same broken acos guard.nav_to, drive, cmd), reacts to events, and sequences phases.The two rules that make the math surface safe:
() when there is
nothing to measure — a () input, a wrong-length array, or a degenerate
orientation. Native constructors/operations instead raise a script error
for malformed values, so an authored program cannot silently continue with
a poisoned native pose:let d = yaw_delta_deg(this.fprev, world_forward3(me));
if d != () { this.yaw += d; } // skip the tick, don't poison the sumyaw_delta_deg saturates at 180°, so a total
swept angle is accumulated from deltas — never measured start-to-end. Past
half a revolution a direct measure folds back and reads as a turn the other
way.Full reference: docs/scripting-guide.md. The
authoritative callable surface in one place: the ScriptingCatalog query.
Define any subset. First param (me) is the host entity id. Production
progression is returned by task(me, ctx) and advanced by the native behavior
kernel; mission(me, ctx) supplies durable objective tracking. Lifecycle/event hooks
remain available for setup, reactions, and teardown.
fn task(me, ctx) { seq([wait_until(|m| arrived(m, GOAL, 2.0))]); }
fn mission(me, ctx) { [objective("survey", #{})]; } // optional
fn on_start(me, ctx) { this.i = 0; } // once, after declared inputs are ready
fn on_event(me, evt, ctx) { if evt.name == "GO" { /* … */ } } // event-driven policy
fn on_stop(me, ctx) { brake(me); } // hot-reload / detach / despawn
// Bounded sampled observer (tests only):
// fn on_tick(me, ctx) { this.samples.push(query("rover_status", #{id: me})); }The state rule that trips up everyone (get this right first):
fns are pure — they CANNOT see top-level lets. Thread all
persistent state through this.this is the persistent scenario-state map. Direct lifecycle/mission drivers
receive the host entity id as me; task leaves receive that same id as their
one positional argument and are authored as anonymous closures (|me| ...).
The native task driver binds this while invoking those closures, and owns
the task cursor/dwell/event state. Named callbacks are also supported as
Fn("name") when declared fn name(me); the driver binds the same
persistent state map as this.this; helper-side field assignments alone do not persist.on_stop before installing the new program state; initialize
all required this fields in the new run.simulation_dependencies declares admission, the module body initializes
per-instance state after admission, and on_start is the ready callback.
Do not add duplicate init/ready hooks. on_event runs in the scenario's
eligible Simulation pass (or paused Lifecycle pass); a listener does not
select the producer's clock. Any future UI, Interaction, or Presentation
listener needs its own cycle owner, inbox, and state, with typed messages
across owners.Rhai hooks inherit the owner scope chosen by their Rust host. A Twin-owned
scenario carries that Twin's stable ID and scene generation in
execution_context(); application and Core policy runtimes keep their own
scope across Twin transitions. Do not infer ownership from the currently active
Twin. A script projected from USD is scene-owned and is stopped when that scene
is replaced; a file-backed tutorial launched for a Twin uses an isolated host
and is stopped, with its source handles and document, at TwinClosed.
For a persistent Twin-owned host using reload_policy: "Retain", scene
replacement preserves this and does not repeat initialization or on_start.
The owner reruns simulation_dependencies against the new scene generation
before admitting subsequent hooks. Resolve scene entity identities in that
hook; retaining VM state does not retain outgoing entity access rights.
The outgoing Twin ID remains available to on_stop even after workspace
selection changes. Pending asset completions are accepted only while that
specific Twin remains mounted. Application-owned scenarios on WorldRoot are
not replaced by Twin tutorial launches and are not recompiled because another
Twin's scene generation changed.
| Verb | Purpose |
|---|---|
cmd(name, #{params}) | WRITE — fire any #[Command] by name; returns #{id,ok,data,error} (data carries e.g. a spawned gid) |
query(name, #{params}) | READ — any read-only query provider (Raycast, Nearest, GroundHeight, CausalTrace, …) |
get(id,"Comp.field") / set(id,"Comp.field",v) | reflected component read / write |
world_pos(id) / world_forward(id) | float-origin-correct array pose (use these, never raw Transform) |
world_pos3(id) / world_forward3(id) / world_rotation_quat(id) | native glam Vec3/Quat pose for hot loops; lower explicitly at wire boundaries |
find(name) / name(id) / usd_path(id) / parent/children | entity lookup + hierarchy; name is presentation, and usd_path reads stable USD identity metadata and is available during dependency planning |
owner_of(id) / controller(id) / is_controlled(id) | who's driving (human vs AI vs unowned) |
emit(name, value?) | fire a TelemetryEvent stamped with the simulator sim_secs and sim_tick (fixed-step delivery waits for a later SimTick; a paused simulation uses the next Update pass); scalar, array, and map payloads keep their typed structure. During a world-level readiness hold, the shared scenario gate is closed and events are not queued. |
sim_tick() / dt() / elapsed_seconds() | available only in simulation-cycle calls; each returns a Rhai error in paused lifecycle and one-shot REPL/tool calls |
execution_context() | read-only owner scope, cycle, phase, clock sample, logical sequence, and event producer stamp |
rand() / rand_range(lo,hi) | deterministic RNG (seeded by entity, event producer or cycle sequence, and hook; discrete lifecycle hooks use a sequence-free seed) |
despawn(id) / add/remove(id,"Comp",…) | structural. Spawn: cmd("SpawnEntity", #{entry_id, position, producer_id?}) — no generic spawn. Twin Rhai uses its actor identity; actorless Rhai supplies a stable producer id. |
notify(msg) / notify_kind(msg,kind) | HUD notification |
JSON appears only at the cmd/query params seam. get/set are native
reflect — no JSON round-trip.
assets/scripting/prelude/*.rhai, one file per topic. Read them for the full
list. Highlights:
drive(rover,fwd,steer), brake(rover), nav_to(entity,target,speed,radius) (returns true on arrival). New missions return task trees. goto is a reserved word — use nav_to.distance, arrived, velocity3/velocity/speed, raycast, obstacle_ahead, ground_height, nearest, entities_in_radius.all_of_type, nearest_where, count_where, min_by/max_by.seq/par_all/par_race/repeat/forever, leaves step/once/act_for/wait/wait_until/wait_for/wait_for_from, and failure nodes check/sel/retry/invert/force_ok/force_fail/reactive_seq/reactive_sel. Return the tree from task(me, ctx); the kernel owns event delivery and there is one task progression path.prelude/auto_tests.rhai): t_range t_max t_true t_rel t_present t_bounded t_moved report_verdict fail_fast expect_fault expect_runtime_fault expect_scene_load_failure seg find_or_none r2/r4.Add helpers freely — edit the prelude, no rebuild.
For a reusable helper rather than mission-local policy, use a named Rhai tool
library under assets/scripting/tools/ or the active Twin's tools/ directory.
Follow author-rhai-tool: registered tools are
called as name::function(...), do not use dynamic import for them, and a
tool used to author USD must return typed operations to the document owner
instead of writing USDA or mutating ECS.
A test scenario is an ordinary scenario whose last act is a verdict. Take the
assertions from prelude/auto_tests.rhai — do not paste private copies of
r2/t_range/t_report into a new test.
Where it goes is what makes it a test, and there is no name convention to remember:
assets/scenes/tests/<name>.usda | the rig |
assets/scenarios/tests/<name>.rhai | its scenario |
scripts/run_scene_tests.sh runs everything in scenes/tests/, the Scene menu
hides it (AssetVisibilitySettings, one checkbox in Settings), and
every_test_scene_carries_a_scenario fails on any of them that asserts nothing.
A rig written into scenes/luncosim/ instead gates nothing however carefully it
asserts — no_test_scene_hides_outside_the_tests_directory is the check that
says so. Do NOT suffix the file _test: the folder already said it.
A check returns "" on pass and a MESSAGE on failure; collect them so every
check runs and the report names all of them:
fn verdict(s) {
let f = [];
f.push(t_bounded(s.hull_pos, 100.0, "hull")); // still a vehicle
f.push(t_range(s.tilt, 0.0, 5.0, "tilt at rest (deg)"));
f.push(t_moved(s.distance, 1.0, "rover travel")); // and it actually drove
report_verdict(f, "LANDING LEGS", "LANDING_LEGS"); // prints, emits, toasts
}report_verdict(fails, title, channel) prints the greppable <title>: PASS|FAIL
line, emits the verdict on channel — which is what sets luncosim test's exit
code — and raises a toast. Call it once after assertions. For a test that
intentionally loads a scene expected to fail, call
expect_scene_load_failure(path, detail_contains) before the verdict and call
load_scene(path) after it. The runner keeps the result open until it observes
that exact typed transition outcome and verifies the failed mount released its
load and admission state. Use fail_fast for setup failures (a find that returned -1, the
wrong scene) so a broken run stops on tick one instead of ticking silently to
the limit.
For a tutorial, this scenario is an observer, not a second lesson. Attach it
to the same production scene fixture as the tutorial and observe its public
cmd:* events, mission verdict, and live state. Count the mechanism that
matters (cmd:AcquireControl plus a real port write, for example), then verify
the resulting movement or value. Never make the observer send the same control
commands as the lesson, and never accept MISSION_COMPLETE by itself.
This keeps tutorial regression tests in Rhai, where they can be edited and run without rebuilding the Rust core:
"$LUNCOSIM_BIN" test \
--scene scenes/tests/tutorial_first_drive.usda --max-ticks 6000Scene loading and asynchronous Modelica participant readiness are bounded by
wall time, not update count. Use --readiness-timeout SECS when a machine needs
a different compile budget; the shell gate uses its separate READINESS_TIMEOUT
startup budget. The shell's larger SCENE_TIMEOUT wall-clock backstop allows
valid long-running missions to keep advancing, while --max-ticks remains the
simulated-time liveness bound after readiness. A readiness timeout is a
no-verdict failure and must be diagnosed at the worker/readiness owner, not
hidden by increasing an update-count constant.
The generic Rust contract may still compile every embedded script and exercise the shared hook seam. Keep it content-agnostic; a lesson's steps, required events, and expected command sequence belong in an authored Rhai observer.
A silent pass is not a pass. A scenario fails silently in every direction
that matters: a hook that never fires, a phase that never advances, a find
that missed. So assert that something was MEASURED (t_present) and that
something MOVED (t_moved, or t_rel's both-near-zero rejection), and print a
per-sample table — a run with no sample rows proves nothing.
For a user-visible mission or scene review, run the production luncosim
headfully with --api PORT, keep the window open, and capture/inspect the
viewport at each material phase. Do not silently replace that session with
--no-ui or --offscreen; the user needs to see whether the authored vehicle
actually lands, deploys, connects, or falls.
Run the deterministic numeric assertion headlessly only when visual acceptance is not part of the request:
"$LUNCOSIM_BIN" test \
--scene scenes/tests/landing_legs.usda --max-ticks 500When a source build is required, build the production binary in the current
worktree and set LUNCOSIM_BIN to that executable. An installed production
build can be used directly. Test commands consume the selected production
binary; they do not use cargo run.
For USD/Rhai-only iteration, reuse it without rebuilding:
./scripts/run_scene_tests.sh --no-build --exact <scene-name>
./scripts/run_scene_tests.sh --no-build -j 4 <scene-substring>The runner defaults to four independent headless production processes.
-j/--jobs N changes only that process bound; each gate process still uses
--threads 1 --jitter 0, and graphics assertions run in their separate serial
offscreen pass. Use -j 1 when diagnosing ordering or resource interactions.
For a standalone assertion against a running scene, use the live no-restart wrapper:
./scripts/api/run_rhai_test.sh 4101 assets/scripting/tests/test_usd_query.rhai /SandboxScene/BoxThe helper assembles the prelude and delegates to the native luncosim rhai --stdout client, which sends the test through RunRhai; edit the .rhai file
and run it again in the same API session. Keep generic command/lifecycle/cache tests in
Rust, and move authored mission or vehicle outcomes into a discovered
scenes/tests + scenarios/tests pair.
An authored test on_tick is a bounded fixed-step verdict hook, not a
replacement for the task/event machinery:
Hook-bound this is not available inside helpers. In the production host,
ordinary map arguments are passed by value and script-defined map helpers do not
resolve as mutable methods. Therefore test phase helpers should be reducers:
accept an explicit state map, return the updated map, and let the test observer
copy the returned keys into this. This both controls parser complexity and
makes phase logic independently testable.
The test observer owns its execution domain. Existing observers are headless by default; a test whose assertion is rendered pixels, UI state, or a graphics diagnostic declares the GPU-backed path with a top-level literal:
const TEST_KIND = "graphics";The scene still binds the observer through LunCoProgramAPI and
info:sourceAsset. Native asset sources must follow the
typed native admission contract
through the originating live Twin; keep asset labels separate from filesystem
characters. The runner discovers that composed binding and reads the
literal without executing the script, so USD does not carry a second test-mode
field and the shell gate does not maintain an exception list. Valid values are
"headless", "graphics", "render-contract", and "editor". Use
"graphics" when the assertion consumes rendered pixels, "render-contract"
when it consumes production GPU/render diagnostics without requiring a valid
color-phase item (for example a negative shader contract), and "editor" when
the assertion requires the windowed workbench. Omit the declaration for the
deterministic headless default. Never weaken the ordinary graphics readiness
gate to accommodate a diagnostic-only fixture.
A comparative assertion is only as good as its ability to fail. Two traps, both of which produce a confidently green test that measures nothing.
The anti-trivial guard. "The two sides mirror" is satisfied perfectly by a rig
that never moved: 0 ≈ -0 passes. So assert the driven side ACTUALLY MOVED before
asserting anything about the other one.
f.push(t_true(s.peak_l > 0.02,
"the driven rocker never moved — a rig at rest mirrors trivially, so " +
"nothing below would mean anything"));The control case. Ship a second scene with the mechanism DISABLED, and assert
it fails the same check. Without it, "coupled mirrors" might be measuring gravity,
symmetry, or nothing at all. differential_rig{,_nodiff} and
rocker_bogie{,_nodiff} are the worked pair.
The control invariant must match the fixture. A disabled or uncoupled control case must have an explicit expected response; do not infer it from a simplified stand or from a symmetric result. Derive the assertion from the mechanism's purpose and declare the case through a parameter.
Declare which case a stage is; never sniff it. Both stages reference one rig and differ only in whether the drive is live, so one scenario serves both — but it must be TOLD which:
def Scope "Test" (prepend apiSchemas = ["LunCoProgramAPI"]) {
uniform asset info:sourceAsset = @lunco://scenarios/tests/rocker_bogie.rhai@
float lunco:param:coupled = 1.0 # read: param(me, "coupled", -1.0)
}Reading it off the coupling's own stiffness would make the expectation depend on the very authoring the test exists to check.
t_rel(a, b, tol_pct, what) takes a PERCENTAGE. 0.2 means 0.2%, not 20%.
Write the numeric tolerance and its percent meaning together.this. on_tick has it; anything it calls does
not. Pass every measurement through the verdict map — which is also what keeps
the verdict a pure function of what was measured.if s.x != () { ... } makes it disappear when the port is absent. Wait for the
port to exist, then measure; count the assertions in the final verdict.() divided by 1000
THROWS, the scenario dies between its last print and report_verdict, and
luncosim test reports NO-VERDICT — the failure looks like a hang, not like the
assertion that was about to fail. Route every logged number through a formatter
that answers "(none)".soc_out belongs to
the Battery prim. When another solver consumes it, author
float inputs:engine_enable.connect = </Rover/Battery.outputs:soc_out> on the
consumer. The generated network projection resolves that member output to its
solver wrapper; do not invent a vessel-level SOC port or use a fallback value.
A script may read an intentionally published network boundary, but that
boundary must be an authored connection to the battery, never a second state.set_camera("RoverCam") rebinds the viewport
until another action rebinds it. Give every wait_until a bound and provide a
return-to-avatar beat when the mission uses a cinematic camera.examples/sequence.rhai): build a tree with
step/wait/wait_for and return it from task(me, ctx). Action and predicate
leaves are anonymous |me| ... closures or named Fn("name") callbacks
(fn name(me)); the native kernel owns progression,
state binding, and event delivery.examples/timeline.rhai): a mission as
pure data. Each step has exactly one operation word (move_to,
move_to_entity, possess, brake, cmd, emit, wait, or wait_event)
and only that operation's fields; compile_timeline lowers it inside
task(me, ctx). It is serialisable and can also be run through
RunTimeline/RunStoredTimeline.Progress is observable on the bus: TASK_COMPLETE/TASK_FAILED for the native
task root and OBJECTIVE_COMPLETE/PLAN_COMPLETE when mission policy emits
those application events.
requires_event mission objectives consume the native runtime's bounded
name/source identity projection. Full typed payloads belong only to a
matching user on_event hook; do not recreate a persistent Rhai event buffer or
retain a retired event-delivery API.
For complex reactive policy, compose the task tree in the scenario with the
prelude's reactive selectors, guards, waits, and event leaves. The generic
task kernel is shared and fast; no vessel-specific Rust driver or autopilot
command is required. Use wait_for when an owner can publish completion,
wait_until only for state with no suitable event, and wait(seconds) for a
deterministic simulation-time delay. A reactive_seq guard is the task-tree
interruption point: when an event handler updates the guarded state, a failed
check cancels the running child on the next task pass. Do not schedule Rhai
callbacks from an app-global wall-clock timer; callback execution stays on the
owning deterministic cycle. If a reusable timeout is needed, its clock,
cancellation, and failure result must be explicit task semantics.
emit(name, value) fires a TelemetryEvent; a running receiver handles it on
a later fixed step because the event carries the authoritative sim_tick and
scenario emissions occur after that tick boundary. A paused simulation delivers
discrete events on its next Update pass. The deterministic actor model makes
"A emits, B reacts" order-independent. Scripts interact ONLY through events +
shared ECS state, never by calling each other's functions (isolated VMs).
Producers also include
physics (COLLISION_START), lifecycle (SCENE_LOADED), and Modelica condition outputs
connected to LunCoEvent.inputs:trigger. The event prim adds the bus-facing name and
severity; threshold and hysteresis equations remain in Modelica.
Discrete vessel actions have a dedicated atomic edge surface:
intent_edge(target, intent, "pressed"|"released"|"pulse") or the shorter
intent_pulse(target, intent) helper. It emits intent.edge with
value.target_gid, value.correlation_id, value.intent, and value.edge;
value.correlation_id is the same unsigned command id returned by the helper.
The consuming Twin decides what the edge means and whether to write a port.
Use SimulateIntent/SetPorts
for held or continuous values, and never build a pulse from two ordered writes.
Held SimulateIntent state is keyed by target, intent, and producer identity:
API and direct typed producers use a stable nonzero producer_id; actorless
Rhai supplies one too. A Twin scenario uses its runtime route and stable actor
identity, so omit producer_id there. A release clears only that producer's
hold. External commands targeting fixed simulation state are admitted for the
next tick and publish intent.hold with producer identity, correlation id, and
input-order stamp; Simulation-clock Rhai behavior stays in its current pass,
and local-embodiment input uses the interaction cadence. The live held state is
not a durable session replay record.
The helper result's id correlates the edge with the read-only
query("CausalTrace", #{target: target, correlation_id: edge.id}) snapshot.
That snapshot exposes the authored mapping, selected port owner, connection
and native-joint admission, current measured channels, and classified producer
origin. An externally admitted edge also exposes its committed scene
generation, effective simulation tick, and per-tick sequence; a deterministic
simulation-hook edge has no external admission stamp. Missing or pending stages
remain visible as incomplete; they are not inferred as successful actuation.
Prefer the HTTP API (curl-first; canonical port 4101 — launch per the
test-via-api / run-modelica skills):
// attach + run (idempotent hot-reload); source is inline rhai OR an asset path
{"type":"ExecuteCommand","command":"RunScenario","params":{"target":<gid>,"source":"<rhai or path>","params":{"speed":1.5}}}
{"type":"ExecuteCommand","command":"SetScenarioPaused","params":{"target":<gid>,"paused":true}}
{"type":"ExecuteCommand","command":"StopScenario","params":{"target":<gid>}}params is a typed object; the script receives it as the explicit read-only ctx argument of its lifecycle and program hooks. Omitted parameters are {}.ScriptStatus {target} → compile/runtime health + located errors; ScriptInspect {target} → live this, hooks, generation, running/paused. print(...) goes to the process log.RunRhai {code} — full world access, stdout in the original deferred response.A script is a PRIM — give the entity a LunCoProgramAPI child and it auto-runs on spawn.
Delete the prim and the behaviour is gone:
def Xform "Rover_01"
{
def Scope "Patrol" (prepend apiSchemas = ["LunCoProgramAPI"]) {
uniform asset info:sourceAsset = @scenarios/route_follow.rhai@
# File-backed source is canonical for production programs.
# per-instance config: one typed attribute per key, read by param(me, "speed", 1.0)
custom float lunco:param:speed = 2.0
}
}Timelines persist via RegisterTimeline → <twin>/timelines/*.json; tool
libraries → <twin>/tools/*.rhai.
task), objective-tracked (mission), or
reactive (on_event). Use a Behavior Tree for reactive AI. Use on_tick
only in authored tests for bounded state sampling and verdicts; rover
continuous control/dynamics belong to native fixed-step systems or Modelica.|me| ...
closures or named callbacks. Keep persistent lifecycle state on this only
where a hook or task closure
genuinely needs it.nav_to/drive/cmd) — never a control loop (that's Modelica).emit/on_event (the next scenario pass delivers the event; paused simulations use Update while fixed simulation time is stopped).RunScenario on the target gid through the live API; verify with ScriptInspect; iterate by re-running (in-place hot-reload, no app restart).LunCoProgramAPI child prim on the target once it works.let or read from a helper — invisible/unbound. Use this, in hooks only.goto(...) — reserved word; use nav_to.emit to be seen in the same scenario pass — it arrives on the next pass.spawn(...) — use cmd("SpawnEntity", #{entry_id, position}) so clients reconstruct from the catalog. Actorless Rhai/API callers of raw-file scenes also supply a stable nonzero producer_id.Transform for position — use world_pos (float-origin correct).Fn("named_action") to once/step/wait_until when the
callback is declared fn named_action(me); the native driver binds the
persistent state as this../scripts/run_scene_tests.sh builds luncosim once and runs every gate scene through luncosim test
headless and deterministically (--threads 1 --jitter 0) using four production
processes by default. -j/--jobs N changes the process bound; it does not
change the gate's deterministic flags. Exit 0=PASS / 1=FAIL / 2=no verdict.
The set, and what each one is FOR:
| Scene | Guards |
|---|---|
drivetrain_parity · ackermann_parity · six_independent_parity | raycast ≡ physical for one authored parameter set (below) |
parts_attached | nothing falls off the vehicle. Drive the assembled rig and require every descendant to remain within the authored relative-distance tolerance. |
lint_selftest | the linter itself. A scene authored wrong on purpose, so RunLint → rules → LintReport can be shown to FIND the faults by rule id — and to stay silent on the correctly jointed wheel beside them |
Two lessons those last two encode, worth copying into any new gate:
parts_attached compares
|p_part − p_vessel| before and after a drive: a spinning wheel, a steering
knuckle and a stroking suspension all leave it alone, while a part left on the
ground changes it by the length of the drive. It walks children(), so it
needs no list of part names and covers parts added later.parts_attached excludes rucheyok for exactly that reason rather
than counting a frozen rover as a pass.When two authored realizations implement one contract, place them in one scene test and drive them with identical commands. Assert that both realizations move, compare physical outputs with tolerances appropriate to the contract, and check direction as well as magnitude. Add an independent bound so two equally wrong implementations cannot satisfy parity together.
The shipped drivetrain, attachment, and linter fixtures under
assets/scenes/tests/ and assets/scenarios/tests/ demonstrate this shape. Keep
the test scenario responsible for measured samples and the verdict; keep the
mechanism and its parameters in USD, Modelica, or the owning engine subsystem.
© LunCoSim, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in skills/author-scenario of LunCoSim/lunco-sim.
Open the folder on GitHubat commit 43f1301
Author Scenario next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Author Scenario this skillLunCoSim/lunco-sim | 107 | — | ~12k | Automated safety check: Pass | Apache-2.0 | |
| Stripe Projectsfossasia/eventyay | 1.7k | 5 repos | ~2k | Automated safety check: Notes | Apache-2.0 | |
| MQTTX CLIemqx/MQTTX | 5.1k | — | ~1.9k | Automated safety check: Pass | Apache-2.0 | |
| AWS Serverless Edazxkane/aws-skills | 367 | 4 repos | ~3.2k | Automated safety check: Pass | MIT | |
| Windmill Trigger Type Checklistwindmill-labs/windmill | 18k | — | ~4.7k | Automated safety check: Pass | Custom licence | |
| FoundatioFoundatioFx/Foundatio | 2.1k | — | ~3.9k | Automated safety check: Pass | Apache-2.0 |
fossasia/eventyay
A skill your agent uses when the user wants to provision infrastructure or third-party services using Stripe Projects.
emqx/MQTTX
Operates the `mqttx` command line client to connect, publish, subscribe, benchmark and simulate data against an MQTT broker, with TLS and MQTT 5 support.
zxkane/aws-skills
AWS serverless and event-driven architecture expert based on Well-Architected Framework.
windmill-labs/windmill
Checklist of every backend, frontend, CLI and capture change needed to add a new TriggerCrud-based trigger type, such as Azure, GCP or Kafka, to Windmill.
FoundatioFx/Foundatio
A skill your agent uses when working with Foundatio infrastructure abstractions for .NET -- caching, queuing, messaging, file storage, distributed locking, or background jobs.
xiaotianfotos/homerail
Design, start, supervise, inspect, and continue HomeRail DAG workflows from any agent with CLI or HTTP access.
LunCoSim/lunco-sim
Generate concise LunCoSim nightly GitHub release notes with platform downloads, installation guidance, an AI-agent mission prompt, and a changelog link.
LunCoSim/lunco-sim
Build or repair a reusable scene component through a live LunCoSim Editor session.
LunCoSim/lunco-sim
Build or review a componentized LunCoSim USD assembly with a realistic, dimensionally checkable presentation.
LunCoSim/lunco-sim
Author and review LunCoSim behavioral, asset-backed, component, mission, visual, and requirements-verification tests.
LunCoSim/lunco-sim
Create, extend, register, or debug a reusable LunCoSim Rhai tool library for live USD authoring, component linting, inspection, or test support.
LunCoSim/lunco-sim
Author an interactive tutorial, guided lesson, onboarding flow, coach-mark tour, or objectives checklist in LunCoSim.
Categories
Author event-driven LunCoSim scenarios for missions, waypoints, reactions, or multi-entity coordination. Author Scenario is an agent skill from LunCoSim/lunco-sim. Author event-driven LunCoSim scenarios for missions, waypoints, reactions, or multi-entity coordination.
Author Scenario fits situations like: working with task; persistent this state.
Run `npx skills add LunCoSim/lunco-sim --skill author-scenario -a claude-code`. Or copy the skill folder (skills/author-scenario in LunCoSim/lunco-sim) into .claude/skills/author-scenario in your project. Claude Code loads it when a task matches its description.
Run `npx skills add LunCoSim/lunco-sim --skill author-scenario -a codex`. Or copy the skill folder (skills/author-scenario in LunCoSim/lunco-sim) into .agents/skills/author-scenario in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add LunCoSim/lunco-sim --skill author-scenario -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/author-scenario, .gemini/skills/author-scenario, .github/skills/author-scenario and .opencode/skills/author-scenario in your project.
Going by SKILL.md and its folder, Author Scenario needs the command-line tools its instructions call (cargo).
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Author Scenario is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 12k tokens (SKILL.md is roughly 47k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Author Scenario: Stripe Projects (fossasia/eventyay, 1.7k stars), MQTTX CLI (emqx/MQTTX, 5.1k stars), AWS Serverless Eda (zxkane/aws-skills, 367 stars) and Windmill Trigger Type Checklist (windmill-labs/windmill, 18k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
LunCoSim (a GitHub organization) maintains it in LunCoSim/lunco-sim, which has 107 GitHub stars. The repository holds 40 skills in this directory. The repository was last updated on October 10, 2026.
Source: LunCoSim/lunco-sim on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.