Synsemadocsv0.6.xENES

Operate

CLI

One static binary. The core commands:

synsema init [dir]             # scaffold: hello.syn (language tour + test), .env.example, .gitignore, .mcp.json (synsema-code)
synsema init [dir] --synfide   # + install the Synfide framework (durable workflows, approval
                               #   inbox, persistent kv), version-pinned from its latest release:
                               #   per-file sha256 verification, synfide/VERSION records the tag,
                               #   re-run to upgrade (framework files only — yours are never
                               #   overwritten, and a framework file YOU edited is kept too: the
                               #   new version lands beside it as <file>.new with a loud warning),
                               #   plus an app.syn starter and its test suite
synsema init [dir] --pwa       # an INSTALLABLE app (v0.6.15+): app.syn + index.html + public/{manifest.webmanifest,
                               #   sw.js, app.js, icon.svg} + the PNG icons generated from the SVG + push_keys.syn
                               #   (VAPID pair for native push) — see "Your app on the phone"
synsema run program.syn        # execute (exits when the program finishes); `-` reads the source from stdin
synsema run program.syn -- a b # everything after `--` is the program's own argv → args() == ["a", "b"]
synsema test program.syn       # run `test` blocks (a file or a directory)
synsema build program.syn -o app  # a single self-contained binary: the engine + your program baked in
synsema check program.syn      # no execution: parse + resolve all `use` imports + validate render("…") templates + (v0.6.19+) refuse what serve would refuse in an `export routes` group (stream/socket)
synsema code outline [path]    # code intelligence for agents (v0.6.13+): outline / symbol / refs / routes / caps /
                               #   check / search / deps — static, from the parser (--json); `synsema code --mcp` serves
                               #   the same tools as the `synsema-code` MCP server that `init` registers in .mcp.json
synsema openapi app.syn        # the /openapi.json the server would publish, from the source — no execution,
                               #   no port (--out openapi.json, --base-url https://api.example); exit 2 without `serve`
synsema serve program.syn      # stay alive for HTTP / crons / agents (--watch = restart on .syn changes)
synsema repl                   # interactive REPL
synsema conform --swarm app.syn  # post-run state dump (blackboard + agents) as JSON
synsema daemon start app.syn   # background daemon (see Deploy)
synsema llm status             # resolved LLM config + diagnosis (--json for scripting)

Re-running init is safe — and it repairs§

init never decides by mere existence. Each file it manages is classified by provenance:

Its contentWhat happens
identical to the current versionya está al día — untouched
identical to any version that shipped beforeactualizado (estaba sin ediciones tuyas) — it was still factory, so it gets the new one
matches no released versionit's yours: kept, and the new version lands beside it as <file>.new

Nobody types a file byte-identical to an old release by hand, so matching one is proof enough that there's no work of yours to protect. This is what lets a project that has been skipping upgrades catch up instead of staying frozen — and with --synfide, re-running also repairs a scaffold with deleted or stale files, even when synfide/VERSION already names the latest release (engine v0.5.9+; before that, existence alone counted as ownership).

init --pwa — an installable app (engine v0.6.15+)§

synsema init [dir] --pwa scaffolds a site that installs on Android, iOS and desktop: app.syn (the server: static "/" from "./public", the page and mount api.api), api.syn (v0.6.19+: the API as an export routes group — /api/ping and the push routes), index.html, public/manifest.webmanifest, public/sw.js (service worker), public/app.js, public/icon.svg, and push_keys.syn (prints the VAPID pair for .env). The PNG icons (icon-192.png, icon-512.png, apple-touch-icon.png) are generated from icon.svg with the engine's own rasterizer — edit the SVG and re-run init --pwa to refresh them; PNGs you brought yourself are left alone when icon.svg is the factory one, and never touched if you removed the SVG. No hello.syn (the starter is app.syn); the base files (.env.example, .gitignore, .mcp.json) come as usual, with the same provenance rules — re-running never overwrites yours. --pwa and --synfide are different starters: passing both is a usage error (exit 2), as is any unknown flag. The directory may go before or after the flag. Walkthrough: Your app on the phone (PWA). Since engine v0.6.19 the API lives in api.syn (an export routes api group that app.syn mounts) so the same routes can serve a desktop entry; a factory app.syn from an older init --pwa is refreshed to the modular one, an edited one is kept (app.syn.new beside it).

init --desktop — the same app as a desktop app (engine v0.6.19+)§

synsema init [dir] --desktop writes everything --pwa writes plus desk.syn — the desktop entry: bind "127.0.0.1", mount api.api, the page rendered with desktop: true, a socket route that counts open windows, the browser opened as an app window with run() under exec (platform() picks cmd / open / xdg-open), and shutdown() when the last window closes or when no window opened in 30 s — and public/desk.js, the socket each window opens (loaded by index.html only under { when desktop }). DESK_NO_WINDOW=1 skips the browser (tests, CI). synsema serve desk.syn opens the window; synsema build desk.syn -o desk --serve --no-console --icon public/icon.svg [--bundle] ships it. --desktop on an existing --pwa project just adds the two files. Walkthrough: Your app on the desktop.

What .env.example covers§

The generated .env.example is commented section by section: the LLM provider pairs (provider + its key), the host ceilings the program can't raise (SYNSEMA_SPEND_CEILING and the per-identity SYNSEMA_SPEND_CEILING_PER_IDENTITY), the human-approval knobs, and finally your own secrets — the ones your code names: JWT_KEY for jwt_sign, CAPTOKEN_ROOT_KEY for captoken_mint (attenuating needs no key, which is why a delegated sub-agent never sees it) and AGENT_SIGNING_KEY for http_sign (which also needs require sign("AGENT_SIGNING_KEY")). The name you choose is the capability scope, so add your own the same way. See Secrets and Agent identity.

synsema code — code intelligence for agents§

An agent working on a Synsema repo should not read whole files to learn what is in them. synsema code answers from the parser — it never runs the program and never talks to a running server:

synsema code outline                # every .syn: intent, requires, imports, tasks/agents/types/routes/tests with line ranges
synsema code routes app.syn         # the HTTP table a `serve` would publish (auth, stream/socket/proxy, expect, capabilities)
synsema code refs send_report       # where a task/agent/type is used, with the enclosing symbol
synsema code caps                   # declared vs needed capabilities, and what is MISSING (with the `require` to add)
synsema code check                  # parse + imports + templates over the whole project (exit 1 on errors; warnings = missing caps)
synsema code search "proxy to"      # text search over .syn/.html/.js/.css/… with the enclosing symbol; --regex, --kinds, --limit
synsema code deps                   # task → task call graph + imports

Add --json for the exact structure. The same eight tools are served over MCP by synsema code --mcp, registered as synsema-code in the .mcp.json that synsema init writes — so any agent that opens the folder discovers them. It is development-time tooling about the source in that folder, not the MCP of your application (that is discovery: /openapi.json, /llms.txt). Full reference: the code.md page of the agent skill.

synsema build — one program, one binary§

synsema build main.syn -o app produces a single self-contained executable: it copies the running engine and appends your program — the main .syn, the transitive use modules it imports, the templates it renders by a literal name, and whatever you add with --include — sealed with a trailer and a sha256. The result is a file, like docker is a file; no Python, no npm, nothing to install on the target.

synsema build lamp.syn -o lamp                       # the binary for this platform
synsema build lamp.syn -o lamp --include assets/     # bundle a dir (recursive) …
synsema build lamp.syn -o lamp --include "data/*.csv"  # … or a one-level glob, repeatable
synsema build lamp.syn -o lamp --sandbox             # bake a host ceiling into the binary
synsema build lamp.syn -o lamp --cap-set "stdout,net=api.example.com"   # tailored baked ceiling
synsema build lamp.syn -o lamp --profile pure        # bake the pure profile (no filesystem/exec/db…)
synsema build lamp.syn -o lamp-linux --engine-binary ./synsema-linux-x86_64  # "cross": a donor engine
synsema build app.syn -o app --serve --bind 127.0.0.1                  # a SERVER binary (v0.6.16+): runs the
synsema build app.syn -o app --serve --bind 0.0.0.0 --port 8080 --secure   #   serve runtime with these deployment
synsema build app.syn -o app --serve --bind 0.0.0.0 --domain app.example.com --tls-auto ops@example.com   # flags baked in
synsema build desk.syn -o desk --serve --no-console --icon icon.svg     # a DESKTOP app (v0.6.18+): desk.exe with your icon and
synsema build desk.syn -o desk --serve --icon icon.svg --bundle          #   no console window; .app / dir + install.sh on macOS / Linux

Prints built <out> (<n> files, <bytes>). The bundle is closed: a use whose path is a runtime expression can't be resolved statically and is a build error, not a silent gap; an --include that escapes the program's directory is rejected; assets larger than the engine are fine (the binary is ~30–60 MB either way — nobody weighs the Docker CLI).

A server in one binary (engine v0.6.16+). A program with a serve on block only runs under the serve runtime, so it must be built with --serve (without it the build stops with exit 2 and says so, instead of the binary failing at run time). A bind is required — a distributable must say where it listens (127.0.0.1 for a local app, 0.0.0.0 for a public one): --bind, or the serve block's bind "…" clause (v0.6.18+; the flag wins); --port, --domain, --tls-auto, --tls-cert/--tls-key and --secure are the same deployment knobs as synsema serve, baked in (TLS files are read from disk at start — the key never travels inside the binary). The static mounts of the serve block are bundled automatically (static "/x" from "./public"public/ goes in; a missing directory is a build error) and served from the bundle before the disk, with a content ETag — the whole installable app fits in the one file. Ctrl-C / SIGTERM is the ordered shutdown, exit 0. --serve with --profile pure is refused (a server binds a socket).

A desktop app in one binary (engine v0.6.18+). The serve block's bind "127.0.0.1" clause is baked when there is no --bind (without either, the build stops with exit 2). -o desk becomes desk.exe when the engine being wrapped is a Windows executable; --no-console opens no console window (stdout/stderr are discarded); --icon <svg|png|ico> puts your icon in the .exe, the .app or the Linux launcher; --bundle [--name "My App"] [--id com.example.app] writes My App.app/ (macOS) or <stem>/ + install.sh (Linux) — nothing to do on Windows, and the built line says so. All of it is decided by the engine's format (PE / Mach-O / ELF), never by the building machine. The recipe — the browser as an app window, a socket as the truth of "a window is open", shutdown() when the last one closes — is in Your app on the desktop.

The built binary runs your program. Its whole argv is the program's (args()), the baked ceiling and profile apply, and it reads its own bundle — a read_file("assets/x") on a bundled file needs no file capability (it's part of the program, like a use), and a disk write to a bundled path is refused. rendering a bundled template needs no capability either.

Reaching the engine: app --engine <subcommand> runs the plain engine CLI inside the built binary — the only way in, since --sandbox and friends are now the program's argv. app --engine version prints the engine version; app --engine run other.syn runs another program. (The stock synsema binary also accepts --engine as a no-op prefix, so scripts are uniform.) app --engine update is refused — a built program is rebuilt with synsema build, not updated in place. A tampered bundle (sha256 mismatch) refuses to run.

Deploy it straight from FROM scratch/distroless — see Deploy.

synsema llm status§

Prints the LLM configuration the runtime will actually use — each value with its source (environ / .env / default), key presence only (never values, prefixes, or lengths), which .env file was loaded, and a warning if several synsema binaries shadow each other in PATH. When offline it names the exact missing variable — including the "there's a key under DEEPSEEK_API_KEY: did you store it under the wrong variable name?" hint. Never touches the network. Exit 0 = live, 1 = offline (scriptable: synsema llm status && synsema serve app.syn). Full knob reference: Provider config.

Useful flags§

FlagCommandEffect
--flatrun / test / conformparse a .fsyn (flat document) file (also auto-detected by the .fsyn extension)
--explainrunrich error report on stderr (source context, call stack, suggestions)
--format jsonrunwithout --explain: the whole run as one JSON document on stdout — {ok, output, errors, audit, exit, llm_tokens} (the native form of the wasm run(); output is collected, not streamed). With --explain: structured error diagnostics
--provider <name>runforce the LLM provider (anthropic/openai/minimax/deepseek)
--sandboxrun / test / conform / servehost ceiling stdout,time — run untrusted code (see Capabilities). Under serve (engine v0.6.7+) it adds serve so the port can bind, and caps the preamble, handlers, cron ticks and spawned agents alike
--cap-set "<list>"run / test / conform / servetailored host ceiling (name or name=scope, e.g. "stdout,time,serve=8080,net=api.example.com"); --cap-set none = an empty ceiling (nothing, not even stdout); mutually exclusive with --sandbox
--profile native|purerun / test / conform / buildpure (default native) is a second wall: every filesystem/exec/socket/db/cron builtin fails with the truth of the environment, independent of the ceiling — see Sandboxing § the pure profile. serve --profile pure is a usage error (a server binds a socket)
--audit json|<path>|fd:Nrun / test / conform / serveone JSON line per capability check — {ts, context, capability, granted, source, reason, origin, file, line} — to stderr (json), a file, or an fd (Unix only), plus a final {"summary": {granted, denied, exit}}. Secret values never appear (only names). See Observability
--env-file <path> / --no-env-filealloverride / disable .env loading
--out <file> / --base-url <url>openapiwrite the document to a file instead of stdout / set OpenAPI servers (default: none — the running server derives it from domain/Host)
--watchservedev loop: restart on any .syn change (templates/statics already hot-reload per request)
--port / --domain / --tls-auto / --bind / --secureservedeployment knobs (see Deploy)
--serve --bind <addr> [--port --domain --tls-auto --tls-cert --tls-key --secure]build(v0.6.16+) a server binary: the serve runtime with these knobs baked in; a bind is required (--bind, or the block's bind "…" clause, v0.6.18+); the serve block's static mounts are bundled automatically
--no-console / --icon <svg|png|ico> / --bundle [--name <n>] [--id <id>]build(v0.6.18+) desktop: GUI subsystem (Windows), the app icon (.exe resource / .icns / launcher PNGs), a Name.app (macOS) or a dir/ + install.sh (Linux) — all by the engine's format; .exe is appended automatically for a Windows engine — see Your app on the desktop
-o <file> / --include <p> / --engine-binary <p>buildoutput path / bundle a file, dir (recursive) or */? glob (repeatable) / build against a donor engine binary (cross) — see synsema build
-- <args...>runend of engine flags: everything after -- is the program's own argv, readable with args()
--engine <subcommand>any binaryrun the plain engine CLI (a no-op prefix on synsema; the only way into the engine of a synsema build binary)

conform honors --sandbox/--cap-set/--profile/--audit with the same meaning as run (engine v0.6.14+; before that it silently ignored them — the one subcommand that didn't sandbox). Its stdout stays the {ok, out, err} JSON contract; a denial shows up inside err.

REPL§

synsema repl opens an interactive session: each line is a top-level statement, and state persists across lines (a let on one line is visible on the next). Results are shown with print/show — bare expressions are evaluated but not echoed:

$ synsema repl
>>> let who be "repl"
>>> show "hola " + who
hola repl
>>> print(type_of(who))
text

Exit with Ctrl+D (Ctrl+Z then Enter on Windows). It also works non-interactively — pipe statements in (printf '...' | synsema repl) to script quick checks.

Exit codes§

0 on success; 1 on a parse error, a runtime error, or if any spawned agent ended in ERROR; 2 on a usage error — a missing argument, synsema openapi on a file without serve, or (engine v0.6.14+) an unknown --flag, which is now rejected instead of silently ignored on run/test/conform. Plain run prints the stable one-liner Runtime error: file:line:col: msg; add --explain for the rich report. (For measuring exit codes in a shell, don't pipe before echo $? — redirect instead.) Since engine v0.6.19 an output nobody reads is not a failure: when stdout or stderr's reader is gone (synsema run x.syn | head -1, a parent that closed the pipe, PowerShell capturing a GUI program it does not wait for) the process ends quietly with 0 — the SIGPIPE convention — instead of a Rust panic.

v0.6.14, breaking on purpose. Three things a program that ran before may now stop on, each to keep the ceiling honest: an unknown flag is an error (above); under a host ceiling, stdout is a real capability, so --cap-set without stdout denies output at the first print (--sandbox still includes it); and a render of a disk template needs `require file.read("…") like any other file read (bundled templates and nested include/layout` don't).