lmscript skill

For an LLM agent that writes or runs lmscript. lmscript looks like JavaScript or Python but is neither; most mistakes come from borrowing their syntax. Follow the rules below, then let lmscript --check and --dry-run prove the script before you run it. Verified against lmscript 0.2.348.

Purpose

lmscript automates local work: read and write files, transform CSV/JSON/TOML data, run other programs without a shell, and drive lmctl agent teams. A script is checked statically and simulated (dry pass) before every real run, so a correct script fails early instead of half-way.

When to use it

Install

Requires Node.js 24.15 or later.

npm install -g @lmctl-ai/lmctl
lmscript --version
lmscript 0.2.348

Commands

CommandUse
lmscript --check FILEStatic check only. Run after every edit.
lmscript --check-files FILECheck plus existence of path literals (i"…", o"…").
lmscript --dry-run FILE [ARGS]Simulate and print the plan of effects; changes nothing.
lmscript FILE [ARGS]Check, silent dry pass, then the real run.
lmscript -e 'CODE'Run a one-line program (quote it for the shell).
lmscript help NAMESignature, description and example of a function; lmscript help lists all; help topics lists topics (csv, json, cmd, dsl, path, methods, …).
lmscript --dsl-signatures SCHEMAList a DSL schema's functions and methods (experimental _dsl_ blocks).

Options go before the file; every word after the file is script data in ARGV.

Inputs and outputs

Syntax rules that differ from JavaScript and Python

Safety rules

Worked examples

Transform data with checked failures

rows = read_csv(i"orders.csv");          # fails before any effect if missing
require(len(rows) > 0, "orders.csv has no rows");
totals = {};
for (r in rows) {
  totals[r.customer] = totals.get(r.customer, 0) + r.amount;
}
write_json(o"totals.json", totals);
println(f"{len(keys(totals))} customers");

Run a tool and use its result

r = system git status --porcelain;
if (r.code != 0) {
  println(f"git failed: {trim(r.stderr)}");
  exit(1);
}
changed = [];
for (line in lines(r.stdout)) {
  if (!is_blank(line)) {
    push(changed, line);
  }
}
println(f"{len(changed)} changed file(s)");

Show the plan before deleting

lmscript --dry-run cleanup.lms
PLAN complete
1. /home/me/cleanup.lms:1:1 dummy write_file ["out/report.txt","done\n"]
2. /home/me/cleanup.lms:2:1 dummy delete_file ["out/notes.txt"]
3. /home/me/cleanup.lms:3:1 handled println ["cleaned up"]

Experimental: compile a _dsl_ program for a native host

Experimental; the syntax may change. A _dsl_ block holds code in a separate, statically typed language that a native host program (for example lmhtml or lmcad) runs. lmscript compiles it at check time against the host's schema and savedsl writes the bytecode; the host runs it.

  1. Get the schema from the host: lmhtml --schema --json > lmhtml.dsl.json. Read lmhtml --schema (or lmscript --dsl-signatures lmhtml.dsl.json) for the available context objects, functions and methods. Use only those.
  2. Write the block; the statement ends with ; on the marker line and the block closes with a line exactly end _dsl_:
check = _dsl_ schema="lmhtml.dsl.json";
let pages = html.load_dir("site");
for (i in range(0, pages.len())) {
  let page = pages.at(i);
  let links = page.links();
  for (j in range(0, links.len())) {
    let url = links.at(j);
    if (!html.link_exists(page, url)) {
      report.add("broken-link", page.path(), 0, url + " not found");
    }
  }
}
report.write_text("broken.txt");
end _dsl_
savedsl(check, "links.dslb");
lmscript links.lms
lmhtml --dsl links.dslb
{"ok": true, "result": null}
cat broken.txt
broken-link site/index.html:0: missing.html not found

Rules inside _dsl_: let x = expr; with inferred types, if/else, for (i in range(a, b)), while with break/continue; types int, float (write 1.0), string, bool and the schema's object types; int and float never mix (float(i) converts); strings join with + and str(x); host lists via xs.len() and xs.at(i). No lmscript functions, lists, maps or user functions inside the block. The compiled value is opaque: only savedsl(value, "file.dslb") accepts it. The host refuses bytecode compiled against a different schema (it checks a SHA-256 of the schema bytes), so recompile when the host changes. The built-in lmscript help dsl text in 0.2.348 is older than the compiler: while/break/continue do work.

Errors and exit codes

ExitMeaningWhat to do
0SuccessVerify the outputs (below).
1Run-time failure, failed require/assert, recorded error(), usage errorRead the error:FILE:LINE:COL line; fix the data handling or the guard.
2Check error: syntax, undeclared name, wrong arity, unknown function or method, bad data or _dsl_ block, missing i"…" pathNothing ran. Fix the reported position and run --check again.
Nexit(N)Script-defined.
Message (start)Fix
Expected ';' after the assignmentAdd ;.
Expected '{' after if (...)Add braces around the body.
Undeclared variable 'x'Assign x before the block, or fix the spelling.
Unexpected token '+' in expressionUsually x++ or x += 1: write x = x + 1;.
Unexpected token 'fn' in expressionNo anonymous functions; declare a top-level fn and pass its name.
Function declarations are only allowed at top levelMove the fn out of the other function.
map m has no key "k"m.has("k") or m.get("k", default).
Division by zeroGuard the divisor.
_dsl_: method T.m expects …Match the schema signature (--dsl-signatures).

How to verify

  1. lmscript --check script.lms prints nothing and exits 0. Repeat after every edit.
  2. lmscript --check-files script.lms when the script uses path literals.
  3. lmscript --dry-run script.lms ARGS: the plan lists exactly the writes, deletes and program runs you intend, with the right arguments.
  4. Run it; confirm exit code 0 (or the documented code) and that the expected files or output exist, for example by reading them back with read_file/read_json or ls.
  5. For _dsl_: the script exits 0 and writes the .dslb; HOST --dsl file.dslb prints {"ok": true, …} with exit 0; check the files the host program wrote.
  6. Report what you ran and what you checked; never claim success from exit code 0 alone when files were supposed to change.