Agent Workflow

SkillFiles & storage

Guide 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.

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:

  1. Clarify goal and constraints

    • Confirm expected behavior, non-goals, and compatibility constraints (target backend, public API stability, performance limits).
  2. Locate module/package boundaries

    • Find moon.mod (module root) and relevant moon.pkg files (package boundaries and imports).
  3. Discover APIs before coding

    • Prefer moon ide doc queries to discover existing functions/types/methods before adding new code.
    • Use moon ide outline, moon ide peek-def, and moon ide find-references for semantic navigation.
  4. 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:col when names are ambiguous.
    • Preserve compatibility with #alias(old_api, deprecated) when required.
  5. Validate in a tight loop

    • Run moon check after edits, adding --warn-list +unnecessary_annotation to enable warning 73 for redundant annotations and over-qualified constructors (--warn-list +73 is equivalent).
    • Run targeted tests with moon test [dirname|filename] --filter 'glob' and use moon test --update for snapshot changes.
  6. Finalize before handoff

    • Run moon fmt.
    • Run moon info to verify whether public APIs changed (pkg.generated.mbti diff).
    • Report changed files, validation commands, and any remaining risks.

Fast Task Playbooks

Use the smallest playbook that matches the request.

Bug Fix (No API Change Intended)

  1. Reproduce or identify the failing behavior.
  2. Locate symbols with moon ide outline, moon ide peek-def, moon ide find-references.
  3. Implement minimal fix in the current package.
  4. Validate with:
    • moon check
    • moon test [dirname|filename] --filter 'glob' (or closest targeted test scope)
    • moon fmt
    • moon info (confirm pkg.generated.mbti unchanged)

Refactor (Behavior Preserving)

  1. Confirm behavior/API invariants first.
  2. Prefer semantic rename/navigation tools:
    • moon ide rename
    • moon ide find-references
    • moon ide peek-def
    • If multiple symbols share a name, use moon ide rename <symbol> <new_name> --loc filename:line:col.
  3. Keep edits package-local and file-organization-focused.
  4. Validate with:
    • moon check
    • moon test [dirname|filename]
    • moon fmt
    • moon info (API should remain unchanged unless requested)

New Feature or Public API

  1. Discover existing idioms with moon ide doc before introducing new names.
  2. Add implementation in cohesive files with ///| delimiters.
  3. Add/extend black-box tests and docstring examples for public APIs.
  4. Validate with:
    • moon check
    • moon test [dirname|filename] (use --update for snapshots when needed)
    • moon fmt
    • moon info (review and keep intended pkg.generated.mbti changes)

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.mod file 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.pkg file in each directory. All subcommands of moon will still be executed in the directory of the module (where moon.mod is 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. The name in the moon.mod file 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 .mbt file 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Tests:

    • Place tests in dedicated test files (e.g. *_test.mbt) within the appropriate package. For a package (besides *_test.mbtfiles), *.mbt.md files are also blackbox test files in addition to Markdown files. The code blocks (separated by triple backticks) mbt check are treated as test cases and serve both purposes: documentation and tests. You may have README.mbt.md files with mbt check code examples. You can also symlink README.mbt.md to README.md to make it integrate better with GitHub.
    • It is fine — and encouraged — to have multiple small test files.
  6. Interface files (pkg.generated.mbti) pkg.generated.mbti files 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 using moon info and useful for code review. When you have a commit that does not change public APIs, pkg.generated.mbti files will remain unchanged, so it is recommended to put pkg.generated.mbti in version control when you are done. Do not modify pkg.generated.mbti directly, including whitespace-only cleanup; regenerate it with moon info and review its diff as the public API signal.

    For IDE navigation and symbol lookup commands, see the dedicated moon ide section below.

Common Pitfalls to Avoid

  • Don't use uppercase for variables/functions - compilation error
  • Don't forget mut for mutable record fields - immutable by default (note that Arrays typically do NOT need mut unless completely reassigning to the variable - simple push operations, for example, do not need mut)
  • Don't ignore error handling - either handle errors explicitly, or declare raise on the caller and let checked errors propagate
  • Don't use return unnecessarily - 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 + 1 or i += 1
  • Don't add explicit try for error propagation - inside a raise function, call error-raising functions normally; use catch to handle locally and try! only when aborting is intended
  • Legacy syntax: Legacy code may use function_name!(...) or function_name(...)? - these are deprecated; use normal calls for propagation.
  • Don't write an empty parameter list for main - use fn main { ... } or fn main raise { ... }, not fn main() { ... } or fn main() raise ... { ... }
  • Don't write record-style enum or error constructor fields - labeled constructor fields use label~ : Type, e.g. InvalidNumber(input~ : String), not InvalidNumber(input: String)
  • Prefer range for loops over C-style - for i in 0..<(n-1) {...} and for j in 0..=6 {...} are more idiomatic in MoonBit
  • Don't use for { ... } for infinite loops - write for ;; { ... } instead
  • Don't derive(Show) for debugging - derive Debug and use debug_inspect() for test/diagnostic output (\{Repr(value)} for interpolation of composed values). Reserve a manual impl Show for specialized display formats (JSON, XML, domain text)
  • Don't call @json.inspect() - use the prelude json_inspect(value, ...) without a package prefix
  • Async - MoonBit has no await keyword; do not add it. Async functions default to raising, so do not add raise; add noraise only 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 the async prefix (e.g. [pub] async fn ..., async test ...).

moon Essentials

Essential Commands

  • moon new my_project - Create new project
  • moon run cmd/main - Run main package
  • moon 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:
    cat hello.mbt | moon run -
    
    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:
    moon run - <<'EOF'
    fn main {
      println("Hello, MoonBit!")
    }
    EOF
    
    moon run -e 'fn main { println("Hello, MoonBit!") }'
    
    For multi-line -e snippets, especially snippets with import { ... }, pass real newlines. Do not put literal \n escapes 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 run and moon build both support --target; moon build also supports --diagnostic-limit <N>)
  • moon check - Type check without building, use it REGULARLY, it is fast (moon check also supports --target and --diagnostic-limit <N>)
  • moon info - Type check and generate mbti files. Run it to see if any public interfaces changed. (moon info also supports --target.)
  • moon check --target all - Type check for all backends moon check --output-json can be used with jq to filter the output, e.g,
    moon check --output-json 2>&1 | jq -R 'fromjson? | select(.message |
        contains("unused"))'
    
    or, for richer post-processing, pipe into a small MoonBit program via moon run -e. Use --target native (the default wasm-gc does not support async fn main or @stdio.stdin), a quoted heredoc (<<'EOF') so the shell does not expand $/backticks in the source, and a de-indented closing EOF:

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.mbt file (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 declare functions in different files thanks to our package organization.

  • Run moon test to check everything is correct.

  • declare is supported for functions, methods, and types.

  • The pub type Yaml line 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 prefer moon ide doc over other approaches when exploring what APIs are available, it is more powerful and accurate than grep_search or any regex-based searching tools.
  • moon ide outline . to scan a package,
  • moon ide find-references <symbol> to locate usages, and
  • moon ide peek-def for inline definition context and to locate toplevel symbols.
  • moon ide hover sym --loc filename:line:col to get type information at a specific location.
  • moon ide rename <symbol> <new_name> [--loc filename:line:col] to rename a symbol project-wide. Prefer --loc when 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 (grep displays 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 pkg and list all its exported symbols
    • Example: moon ide doc "@json" - explore entire @json package
    • Example: moon ide doc "@encoding/utf8" - explore nested 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