Agent Workflow
SkillFiles & storageGuide for writing, refactoring, and testing MoonBit projects. Use when working in MoonBit modules or packages, organizing MoonBit files, using moon tooling (build/check/run/test/doc/ide etc.), or following MoonBit-specific layout, documentation, and testing conventions.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Agent Workflow skill
What this skill tells your AI
The instructions your AI receives, as published by golemcloud/golem in .agents/skills/moonbit-agent-guide/SKILL.md and read by ahel’s review.
For fast, reliable task execution, follow this order:
-
Clarify goal and constraints
- Confirm expected behavior, non-goals, and compatibility constraints (target backend, public API stability, performance limits).
-
Locate module/package boundaries
- Find
moon.mod(module root) and relevantmoon.pkgfiles (package boundaries and imports).
- Find
-
Discover APIs before coding
- Prefer
moon ide docqueries to discover existing functions/types/methods before adding new code. - Use
moon ide outline,moon ide peek-def, andmoon ide find-referencesfor semantic navigation.
- Prefer
-
Edit minimally and package-locally
- Keep changes inside the correct package, use
///|top-level delimiters, and split code into cohesive files. - For refactors, use
moon ide rename; add--loc filename:line:colwhen names are ambiguous. - Preserve compatibility with
#alias(old_api, deprecated)when required.
- Keep changes inside the correct package, use
-
Validate in a tight loop
- Run
moon checkafter edits, adding--warn-list +unnecessary_annotationto enable warning 73 for redundant annotations and over-qualified constructors (--warn-list +73is equivalent). - Run targeted tests with
moon test [dirname|filename] --filter 'glob'and usemoon test --updatefor snapshot changes.
- Run
-
Finalize before handoff
- Run
moon fmt. - Run
moon infoto verify whether public APIs changed (pkg.generated.mbtidiff). - Report changed files, validation commands, and any remaining risks.
- Run
Fast Task Playbooks
Use the smallest playbook that matches the request.
Bug Fix (No API Change Intended)
- Reproduce or identify the failing behavior.
- Locate symbols with
moon ide outline,moon ide peek-def,moon ide find-references. - Implement minimal fix in the current package.
- Validate with:
moon checkmoon test [dirname|filename] --filter 'glob'(or closest targeted test scope)moon fmtmoon info(confirmpkg.generated.mbtiunchanged)
Refactor (Behavior Preserving)
- Confirm behavior/API invariants first.
- Prefer semantic rename/navigation tools:
moon ide renamemoon ide find-referencesmoon ide peek-def- If multiple symbols share a name, use
moon ide rename <symbol> <new_name> --loc filename:line:col.
- Keep edits package-local and file-organization-focused.
- Validate with:
moon checkmoon test [dirname|filename]moon fmtmoon info(API should remain unchanged unless requested)
New Feature or Public API
- Discover existing idioms with
moon ide docbefore introducing new names. - Add implementation in cohesive files with
///|delimiters. - Add/extend black-box tests and docstring examples for public APIs.
- Validate with:
moon checkmoon test [dirname|filename](use--updatefor snapshots when needed)moon fmtmoon info(review and keep intendedpkg.generated.mbtichanges)
MoonBit Project Layouts
MoonBit uses the .mbt extension for source code files and interface files with the .mbti extension. At
the top-level of a MoonBit project there is a moon.mod file specifying
the metadata of the project. The project may contain multiple packages, each
with its own moon.pkg. Subdirectories may also contain moon.mod
files indicating that a different set of dependencies can be used for that subdir.
Legacy projects may still contain moon.mod.json; treat it as the old module
metadata format and migrate/update guidance to moon.mod instead of creating
new moon.mod.json files.
Example layout
my_module
├── moon.mod # Module metadata; source option can specify the source directory
├── moon.pkg # Package metadata (each directory is a package like Golang)
├── README.mbt.md # Markdown with tested code blocks (`test "..." { ... }`)
├── README.md -> README.mbt.md
├── cmd # Command line directory
│ └── main
│ ├── main.mbt
│ └── moon.pkg # executable package with `options("is-main": true)`
├── liba/ # Library packages
│ └── moon.pkg # Referenced by other packages as `@username/my_module/liba`
│ └── libb/ # Library packages
│ └── moon.pkg # Referenced by other packages as `@username/my_module/liba/libb`
├── user_pkg.mbt # Root packages, referenced by other packages as `@username/my_module`
├── user_pkg_wbtest.mbt # White-box tests (only needed for testing internal private members, similar to Golang's package mypackage)
└── user_pkg_test.mbt # Black-box tests
└── ... # More package files, symbols visible to current package (like Golang)
-
Module: characterized by a
moon.modfile in the project root directory. A MoonBit module is like a Go module; it is a collection of packages in subdirectories, usually corresponding to a repository or project. Module boundaries matter for dependency management and import paths. -
Package: characterized by a
moon.pkgfile in each directory. All subcommands ofmoonwill still be executed in the directory of the module (wheremoon.modis located), not the current package. A MoonBit package is the actual compilation unit (like a Go package). All source files in the same package are concatenated into one unit and thereby share all definitions throughout that package. Thenamein themoon.modfile combined with the relative path to the package source directory defines the package name, not the file name. Imports refer to module + package paths, NEVER to file names. -
Files: A
.mbtfile is just a chunk of source code inside a package. File names do NOT create modules, packages, or namespaces. You may freely split/merge/move declarations between files in the same package. Any declaration in a package can reference any other declaration in that package, regardless of file.
Coding/layout rules you MUST follow:
-
Prefer many small, cohesive files over one large file.
- Group related types and functions into focused files (e.g. http_client.mbt, router.mbt).
- If a file is getting large or unfocused, create a new file and move related declarations into it.
-
You MAY freely move declarations between files inside the same package.
- Each block is separated by
///|. Moving a function/struct/trait between files does not change semantics, as long as its name and pub-ness stay the same. The order of each block is irrelevant too. - It is safe to refactor by splitting or merging files inside a package.
- Each block is separated by
-
File names are purely organizational.
- Do NOT assume file names define modules, and do NOT use file names in type paths.
- Choose file names to describe a feature or responsibility, not to mirror type names rigidly.
-
When adding new code:
- Prefer adding it to an existing file that matches the feature.
- If no good file exists, create a new file under the same package with a descriptive name.
- Avoid creating giant "impl", “misc”, or “util” files.
-
Tests:
- Place tests in dedicated test files (e.g.
*_test.mbt) within the appropriate package. For a package (besides*_test.mbtfiles),*.mbt.mdfiles are also blackbox test files in addition to Markdown files. The code blocks (separated by triple backticks)mbt checkare treated as test cases and serve both purposes: documentation and tests. You may haveREADME.mbt.mdfiles withmbt checkcode examples. You can also symlinkREADME.mbt.mdtoREADME.mdto make it integrate better with GitHub. - It is fine — and encouraged — to have multiple small test files.
- Place tests in dedicated test files (e.g.
-
Interface files (
pkg.generated.mbti)pkg.generated.mbtifiles are compiler-generated summaries of each package's public API surface. They provide a formal, concise overview of all exported types, functions, and traits without implementation details. They are generated usingmoon infoand useful for code review. When you have a commit that does not change public APIs,pkg.generated.mbtifiles will remain unchanged, so it is recommended to putpkg.generated.mbtiin version control when you are done. Do not modifypkg.generated.mbtidirectly, including whitespace-only cleanup; regenerate it withmoon infoand review its diff as the public API signal.For IDE navigation and symbol lookup commands, see the dedicated
moon idesection below.
Common Pitfalls to Avoid
- Don't use uppercase for variables/functions - compilation error
- Don't forget
mutfor mutable record fields - immutable by default (note that Arrays typically do NOT needmutunless completely reassigning to the variable - simple push operations, for example, do not needmut) - Don't ignore error handling - either handle errors explicitly, or declare
raiseon the caller and let checked errors propagate - Don't use
returnunnecessarily - the last expression is the return value - Don't create methods without Type:: prefix - methods need explicit type prefix
- Don't forget to handle array bounds - use
get()for safe access - Don't forget @package prefix when calling functions from other packages
- Don't use ++ or -- (not supported) - use
i = i + 1ori += 1 - Don't add explicit
tryfor error propagation - inside araisefunction, call error-raising functions normally; usecatchto handle locally andtry!only when aborting is intended - Legacy syntax: Legacy code may use
function_name!(...)orfunction_name(...)?- these are deprecated; use normal calls for propagation. - Don't write an empty parameter list for
main- usefn main { ... }orfn main raise { ... }, notfn main() { ... }orfn main() raise ... { ... } - Don't write record-style enum or error constructor fields - labeled constructor fields use
label~ : Type, e.g.InvalidNumber(input~ : String), notInvalidNumber(input: String) - Prefer range
forloops over C-style -for i in 0..<(n-1) {...}andfor j in 0..=6 {...}are more idiomatic in MoonBit - Don't use
for { ... }for infinite loops - writefor ;; { ... }instead - Don't
derive(Show)for debugging - deriveDebugand usedebug_inspect()for test/diagnostic output (\{Repr(value)}for interpolation of composed values). Reserve a manualimpl Showfor specialized display formats (JSON, XML, domain text) - Don't call
@json.inspect()- use the preludejson_inspect(value, ...)without a package prefix - Async - MoonBit has no
awaitkeyword; do not add it. Async functions default to raising, so do not addraise; addnoraiseonly when the async body must not raise. Async functions and tests are characterized by those which call other async functions. To identify a function or test as async, simply add theasyncprefix (e.g.[pub] async fn ...,async test ...).
moon Essentials
Essential Commands
moon new my_project- Create new projectmoon run cmd/main- Run main packagemoon run - < hello.mbt- Run code from stdin (useful for quick experiments)moon run -e "code snippet"- Run code from command line argument (good for one-liners) Example:
This allows you to quickly test small snippets of MoonBit code without creating a full project. It can also be used with heredoc syntax for multi-line snippets:cat hello.mbt | moon run -moon run - <<'EOF' fn main { println("Hello, MoonBit!") } EOF
For multi-linemoon run -e 'fn main { println("Hello, MoonBit!") }'-esnippets, especially snippets withimport { ... }, pass real newlines. Do not put literal\nescapes inside single quotes; MoonBit will see backslash characters, not line breaks. Use command substitution with a quoted heredoc:moon run --target native -e "$(cat <<'EOF' import { "moonbitlang/x/sys" } fn main { println(@sys.get_cli_args().join("|")) } EOF )"moon build- Build project (moon runandmoon buildboth support--target;moon buildalso supports--diagnostic-limit <N>)moon check- Type check without building, use it REGULARLY, it is fast (moon checkalso supports--targetand--diagnostic-limit <N>)moon info- Type check and generatembtifiles. Run it to see if any public interfaces changed. (moon infoalso supports--target.)moon check --target all- Type check for all backends moon check --output-json can be used withjqto filter the output, e.g,
or, for richer post-processing, pipe into a small MoonBit program viamoon check --output-json 2>&1 | jq -R 'fromjson? | select(.message | contains("unused"))'moon run -e. Use--target native(the defaultwasm-gcdoes not supportasync fn mainor@stdio.stdin), a quoted heredoc (<<'EOF') so the shell does not expand$/backticks in the source, and a de-indented closingEOF:
moon check --output-json 2>&1 | moon run --target native -e "$(cat <<'EOF' import { "moonbitlang/async", "moonbitlang/async/stdio", "moonbitlang/core/json", }
async fn main { let seen = {} while @stdio.stdin.read_until("\n") is Some(line) { try @json.parse(line.trim()) catch { _ => () } noraise { {"level": "warning", "path": String(p), ..} => if !seen.contains(p) { seen[p] = () println(p) } _ => () } } } EOF )"
Get the diagnostics with "unused" in the message, which can be used to find unused code.
- `moon explain` - Show built-in documentation for compiler diagnostics and language topics.
- `moon explain --diagnostic` lists warning mnemonics and IDs.
- `moon explain --diagnostic 31` explains warning 31 (`unused_optional_argument`).
- `moon explain --diagnostic unused_optional_argument` explains the same warning by mnemonic.
- `moon explain --attribute` lists supported attributes such as `#deprecated`, `#alias`, `#cfg`, `#coverage.skip`, and `#warnings`.
- `moon explain --attribute deprecated` explains the `#deprecated` attribute and its supported forms.
- `moon add package` - Add dependency
- `moon remove package` - Remove dependency
- `moon fmt` - Format code - should be run periodically - note that the files may be rewritten
Note you can also use `moon -C dir check` to run commands in a specific directory.
### Profiling Hot Paths (`moon run --profile`)
`moon run --profile --target native --release cmd/<main>` runs a native release build under a sampling profiler and prints ranked **self-time** and **inclusive-time** tables plus a "runtime leaf costs attributed to MoonBit callers" section (which maps allocation, reference-counting, and string-equality costs back to *your* functions), alongside a `profile.json` and a `.trace` you can open in Instruments. On macOS it needs Xcode's `xcrun xctrace`, so install the full Xcode (not just the command-line tools) first. A single parse or compute is far too short to sample meaningfully, so point the profiled `main` at a loop that exercises the hot path a few hundred times over a representative fixture; this loop harness is throwaway and should never be committed.
Read **self-time** for *which function burns cycles* and **inclusive-time** for *which call subtree dominates*, then work a tight loop: profile, fix the top item, re-profile. Always re-baseline before trusting a delta — sampled timings drift with machine load, so build and benchmark the branch and `main` back-to-back (interleaved) rather than comparing against a number from an earlier session.
### Test Commands
- `moon test` - Run all tests
(`moon test` also supports `--target`)
- `moon test --update` - Update snapshots
- `moon test -v` - Verbose output with test names
- `moon test [dirname|filename]` - Test specific directory or file
- `moon coverage analyze` - Analyze coverage
- `moon test [dirname|filename] --filter 'glob'` - Run tests matching filter
```
moon test float/float_test.mbt --filter "Float::*"
moon test float -F "Float::*" // shortcut syntax
```
## `README.mbt.md` Generation Guide
- Output `README.mbt.md` in the package directory.
`*.mbt.md` file and docstring contents treats `mbt check` specially.
`mbt check` block will be included directly as code and also run by `moon check` and `moon test`. If you don't want the code snippets to be checked, explicit `mbt nocheck` is preferred.
If you are only referencing types from the package, you should use `mbt nocheck` which will only be syntax highlighted.
Symlink `README.mbt.md` to `README.md` to adapt to systems that expect `README.md`.
## Testing Guide
Use snapshot tests as it is easy to update when behavior changes.
- **Snapshot Tests**: write `inspect(value)` / `debug_inspect(value)` / `json_inspect(value)`, then run `moon test --update` (or `moon test -u`) to fill in `content=`.
- Use `inspect()` for values that implement `Show` (primitives, or types with a manual `impl Show`).
- Use `debug_inspect()` for any type that derives `Debug` — the default for your own data types.
- Use `json_inspect()` for complex nested structures (uses the `ToJson` trait, produces more readable output).
- It is encouraged to inspect the whole return value of a function if it is not huge; this keeps the test simple. Derive `Debug` and/or `ToJson` (or `impl Show`) on `YourType` accordingly.
- **Update workflow**: After changing code that affects output, run `moon test --update` to regenerate snapshots, then review the diffs in your test files (the `content=` parameter will be updated automatically).
- **Validation order**: Follow the canonical sequence in `Agent Workflow` and `Fast Task Playbooks`.
- Black-box by default: Call only public APIs via `@package.fn`. Use white-box tests only when private members matter.
- Grouping: Combine related checks in one `test "..." { ... }` block for speed and clarity.
- Panics: Name tests with prefix `test "panic ..." {...}`; if the call returns a value, wrap it with `ignore(...)` to silence warnings.
- Errors: For expected success, call error-raising functions directly. If a call unexpectedly raises, the test fails with the actual error. For expected failure, use `try ... catch ... noraise`, inspect the error in `catch`, and fail explicitly in `noraise`.
Default expected-failure shape: `try f() catch { err => inspect(err) } noraise { _ => fail("expected to fail") }`.
### Docstring tests
Public APIs are encouraged to have docstring tests.
````mbt check
///|
/// Return the sum of an `Array`.
///
/// # Example
/// ```mbt check
/// test {
/// inspect(sum_array([1, 2, 3, 4, 5, 6]), content="21")
/// }
/// ```
pub fn sum_array(xs : Array[Int]) -> Int {
xs.fold(init=0, (a, b) => a + b)
}
The MoonBit code in a docstring will be type checked and tested automatically
(using moon test --update). In docstrings, mbt check should only contain test or async test.
Spec-driven Development
- The spec can be written in a readonly
spec.mbtfile (name is conventional, not mandatory) with stub code marked as declarations:
///|
declare pub type Yaml
///|
declare pub fn Yaml::to_string(y : Yaml) -> String raise
///|
declare pub impl Eq for Yaml
///|
declare pub fn parse_yaml(s : String) -> Yaml raise
-
Add
spec_easy_test.mbt,spec_difficult_test.mbt, etc. to test the spec functions; everything will be type-checked(moon check). -
The AI or users can implement the
declarefunctions in different files thanks to our package organization. -
Run
moon testto check everything is correct. -
declareis supported for functions, methods, and types. -
The
pub type Yamlline is an intentionally opaque placeholder; the implementer chooses its representation. -
Note the spec file can also contain normal code, not just declarations.
moon ide [doc|peek-def|outline|find-references|hover|rename|analyze] for code navigation and refactoring
For project-local symbols and navigation, use:
moon ide doc <query>to discover available APIs, functions, types, and methods in MoonBit. Always prefermoon ide docover other approaches when exploring what APIs are available, it is more powerful and accurate thangrep_searchor any regex-based searching tools.moon ide outline .to scan a package,moon ide find-references <symbol>to locate usages, andmoon ide peek-deffor inline definition context and to locate toplevel symbols.moon ide hover sym --loc filename:line:colto get type information at a specific location.moon ide rename <symbol> <new_name> [--loc filename:line:col]to rename a symbol project-wide. Prefer--locwhen symbol names are ambiguous.moon ide analyze [path]to inspect public API usage of a package or module when planning safe refactors. These tools save tokens and are more precise than grepping (grepdisplays results in both definitions and call sites including comments too).
moon ide doc for API Discovery
moon ide doc uses a specialized query syntax designed for symbol lookup:
-
Empty query:
moon ide doc ''- In a module: shows all available packages in current module, including dependencies and moonbitlang/core
- In a package: shows all symbols in current package
- Outside package: shows all available packages
-
Function/value lookup:
moon ide doc "[@pkg.]value_or_function_name" -
Type lookup:
moon ide doc "[@pkg.]Type_name"(builtin type does not need package prefix) -
Method/field lookup:
moon ide doc "[@pkg.]Type_name::method_or_field_name" -
Package exploration:
moon ide doc "@pkg"- Show package
pkgand list all its exported symbols - Example:
moon ide doc "@json"- explore entire@jsonpackage - Example:
moon ide doc "@encoding/utf8"- explore nested package
- Show package
-
Multiple queries:
moon ide doc "query1" "query2" ...- Run multiple queries in one invocation and combine results
- Example:
moon ide doc "String" "Array" "@json"to explore multiple types and a package at once
-
Globbing: Use
*wildcard for partial matches, e.g.moon ide doc "String::*rev*"to find all String methods with "rev" in their name
moon ide doc Examples
# search for String methods in standard library:
$ moon ide doc "String"
type String
pub fn String::add(String, String) -> String
# ... more methods omitted ...
$ moon ide doc "@buffer" # list all symbols in package buffer:
moonbitlang/core/buffer
fn from_array(ArrayView[Byte]) -> Buffer
# ... omitted ...
$ moon ide doc "@buffer.new" # list the specific function in a package:
package "moonbitlang/core/buffer"
pub fn new(size_hint? : Int) -> Buffer
Creates ... omitted ...
$ moon ide doc "String::*rev*" # globbing
package "moonbitlang/core/string"
pub fn String::rev(String) -> String
Returns ... omitted ...
# ... more
pub fn String::rev_find(String, StringView) -> Int?
Returns ... omitted ...
Best practice: Treat this section as command reference; execution order is defined in Agent Workflow.
moon ide rename sym new_name [--loc filename:line:col] example
When the user asks: "Can you rename the function compute_sum to calculate_sum?"
$ moon ide rename compute_sum calculate_sum --loc math_utils.mbt:2
*** Begin Patch
*** Update File: cmd/main/main.mbt
@@
///|
fn main {
- println(@math_utils.compute_sum(1, 2))
+ println(@math_utils.calculate_sum(1, 2))
}
*** Update File: math_utils.mbt
@@
///|
-pub fn compute_sum(a: Int, b: Int) -> Int {
+pub fn calculate_sum(a: Int, b: Int) -> Int {
a + b
}
*** Update File: math_utils_test.mbt
@@
///|
test {
- inspect(@math_utils.compute_sum(1, 2))
+ inspect(@math_utils.calculate_sum(1, 2))
}
*** End Patch
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 2k
- Forks
- 212
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
moonbit-agent-guide- Source
- github.com/golemcloud/golem