| title | CLI | ||
|---|---|---|---|
| permalink | /reference/cli | ||
| diataxis | reference | ||
| redirect_from |
|
This page is the complete inventory of the jaiph CLI: every subcommand, every flag, and every behavior that affects the exit code. It does not explain how to choose between commands — see Why Jaiph for context and the how-to pages for recipes.
The published jaiph bin is node dist/src/cli.js (npm) or the standalone dist/jaiph (Bun-compiled). Both dispatch through src/cli/index.ts.
| Form | Effect |
|---|---|
jaiph |
Print the overview and exit 0. |
jaiph --help / -h |
Print the overview and exit 0. |
jaiph --version / -v |
Print the CLI version and exit 0. |
jaiph <subcommand> [-h | --help] |
Print the subcommand's usage (flags + one example) and exit 0. Recognised anywhere in the arg list before --. (compile scans help inline with no -- cutoff, so it is recognised at any position — see jaiph compile.) |
jaiph <path> |
File shorthand. Paths ending in *.test.jh route to jaiph test; other *.jh paths route to jaiph run. Non-existent paths fall through to normal command parsing. |
jaiph --mcp <file.jh> |
Alias for jaiph mcp <file.jh>, dispatched alongside the subcommand. |
jaiph <unknown> |
Print Unknown command: <name>, repeat the overview, exit 1. |
The reserved internal marker __workflow-runner is excluded from --help/usage and from the file-shorthand path; it is used by process.execPath self-spawn (see Architecture — Distribution: Node vs Bun standalone).
| Subcommand | Purpose |
|---|---|
run |
Compile, launch, and observe one run on the host. |
test |
Execute *.test.jh blocks in-process with mocks. |
compile |
Multi-error validation pass — no scripts/ emission, no runtime spawn. |
format |
Rewrite .jh / .test.jh files into canonical style. |
init |
Initialize .jaiph/ directory layout in a workspace. |
install |
Install project-scoped libraries from the registry or git URLs. |
use |
Reinstall jaiph globally with a selected version or channel. |
mcp |
Serve a file's exported defs as MCP tools over stdio (newline-delimited JSON-RPC). |
serve |
Serve a file's exported defs as an HTTP API, with an OpenAPI document and an embedded Swagger UI. |
{: #jaiph-run}
Compile and execute export def main in the input file.
jaiph run [--target <dir>] [--raw] [--workspace <dir>] [--env KEY[=VALUE]]... <file.jh> [--] [args...]
Every run executes on the host. Isolation is an outer concern: wrap jaiph in a container, a pod, or a CI runner if wanted. Shared flags (--workspace, --env) mean the same thing on jaiph run, jaiph serve, and jaiph mcp (precedence: CLI flags > JAIPH_* env vars > module config metadata > defaults) — see Configuration — Precedence and Environment variables — Precedence. Flags belonging to another command (--host, --port) and unknown flags are usage errors, never positionals.
| Flag | Argument | Effect |
|---|---|---|
--target |
<dir> |
Keep emitted script files and run metadata under <dir> instead of a temp directory. |
--raw |
— | Skip the banner, live progress tree, hooks, and PASS/FAIL footer. The runner child inherits stdio; __JAIPH_EVENT__ JSON lines go to stderr unchanged. |
--workspace |
<dir> |
Override the workspace root used for library resolution. A missing value, missing path, or non-directory aborts with a specific message. There is no JAIPH_WORKSPACE env equivalent input — that name is reserved for the runner. |
--env |
KEY=VALUE or KEY |
Repeatable per-key flag, and the only grant for use clauses on scripts and named prompts: a script runs in a sterile env, a named prompt's agent runs with the prompt env scrub, and each receives a host key iff its declaration uses it and --env names it (see Environment variables — Script subprocess environment). The granted value is injected only into a subprocess whose declaration uses the key; it is not placed on the runner (workflow-leader) process environment, which Jaiph builds from an allowlist (process basics, JAIPH_* control keys, backend credentials) rather than a copy of the host environment, so an ungranted host key is absent from it. Pre-flight collects every use key in the import graph and aborts with E_ENV_MISSING if one was not granted; extra keys nothing uses are fine. --env KEY=VALUE defines KEY with that exact value (first = splits; the value may contain =; empty is allowed). --env KEY forwards the host's current value, aborting with E_ENV_MISSING before spawning if KEY is unset on the host. KEY must match [A-Za-z_][A-Za-z0-9_]* (else E_ENV_INVALID). Runtime-managed keys (JAIPH_WORKSPACE, JAIPH_RUNS_DIR, JAIPH_RUN_ID, JAIPH_SCRIPTS, JAIPH_MODULE_GRAPH_FILE, JAIPH_SOURCE_ABS, JAIPH_META_FILE, JAIPH_ENV_GRANT, JAIPH_ENV_GRANT_FILE, JAIPH_AGENT_TRUSTED_WORKSPACE, JAIPH_TRUST_PROJECT_HOOKS, JAIPH_CHAIN_KEY, JAIPH_RUN_SUMMARY_FILE) are rejected with E_ENV_RESERVED. Values are never path-remapped. jaiph run --raw applies the same grant but skips the graph-wide pre-flight. |
-- |
— | End of Jaiph flags; remaining tokens are forwarded to export def main. |
After module-graph load, before the runner is spawned, the host CLI runs a credential pre-flight (src/cli/run/preflight-credentials.ts). Missing credentials produce either E_AGENT_CREDENTIALS (hard error) or a warning depending on backend — see Authenticate agent backends and Configuration — Credential pre-flight. jaiph run --raw does not run the pre-flight.
| Marker | Meaning |
|---|---|
▸ |
Step started. |
✓ |
Step completed successfully (with elapsed time). |
✗ |
Step failed (with elapsed time). |
ℹ |
log message (blue marker and text; the tree prefix is dim; no elapsed time). |
! |
logerr message (red; rendered on stdout with the progress tree). |
⚠ |
logwarn message and automatic leaf-step idle warnings (yellow; rendered on stdout with the progress tree). |
· |
Continuation marker (heartbeat lines in non-TTY mode). |
₁, ₂, … |
Subscript prefix for run async branch numbering. |
PASS line: ✓ PASS def main (0.2s). TTY runs append a transient ▸ RUNNING def <name> (X.Xs) line that is replaced by the PASS/FAIL line on exit. --raw and non-TTY modes skip both. Disable color globally with NO_COLOR=1.
Non-TTY heartbeat cadence is controlled by JAIPH_NON_TTY_HEARTBEAT_FIRST_SEC (default 60) and JAIPH_NON_TTY_HEARTBEAT_INTERVAL_MS (default 30000, floor 250). Leaf script and prompt steps emit a yellow ⚠ idle warning when they produce no stdout/stderr for JAIPH_STEP_IDLE_WARN_SEC (default 180; 0 disables).
A leaf script step whose subprocess produces no stdout/stderr for JAIPH_STEP_IDLE_KILL_SEC (default 3600, one hour; 0 disables) is terminated and fails. The runtime emits a red LOGERR line naming the step and how long it was silent, then kills the step's subprocess, so a stuck script cannot hold an overnight run open indefinitely. New output resets both the warn clock and the kill clock. The kill applies to script steps only; prompt steps get idle warnings but are not killed.
Step lines include the kind (def, prompt, script) and name. Parameterised invocations append key="value" pairs in parentheses (positional params use 1=… / 2=…); whitespace is collapsed; values are truncated to 32 characters. Prompt step lines additionally show the backend name (or custom command basename), the effective model, and the first 24 characters of the prompt body in quotes: ▸ prompt claude sonnet "Classify this task…" on start and ✓ prompt claude sonnet (5s) on completion. Any trailing key="value" parameter pairs shown after the preview are capped at 96 characters in total. The model is the value passed to the backend (agent.model / JAIPH_AGENT_MODEL / a --model flag); it is a bare token between the backend and the quoted preview. When a built-in backend (cursor, claude, codex) auto-selects its own model, the token shows the literal default (▸ prompt cursor default "…"); it is omitted only for custom agent commands, which have no model concept (▸ prompt my-agent "…").
When export def main returns a value (success only), the runtime writes return_value.txt under the run directory. Interactive jaiph run prints that value on stdout after the PASS line, separated by a blank line. jaiph run --raw never prints it to stdout; the file alone is the contract.
Each run directory is <JAIPH_RUNS_DIR>/<YYYY-MM-DD>/<HH-MM-SS>-<source>/, UTC. <source> is JAIPH_SOURCE_FILE if set, otherwise the entry-file basename. A same-second collision against the same source (e.g. two concurrent jaiph mcp/jaiph serve calls) appends a short -<run id prefix> suffix instead of reusing the first run's directory. Layout pinned in Architecture — Durable artifact layout.
Step .out files are written incrementally; consumers may tail -f them. .out / .err pairs are allocated at STEP_START with monotonic per-run sequence numbers (%06d-<safe_name>.out|.err).
Interactive jaiph run only (--raw omits this block). On non-zero exit, the CLI emits a stderr footer with Logs:, Summary:, out: / err: paths, and an Output of failed step: excerpt. The fields are resolved from the last STEP_END object with non-zero status in run_summary.jsonl; out_content / err_content are preferred over out_file / err_file.
Hooks load from ~/.jaiph/hooks.json (global) and <project>/.jaiph/hooks.json (project-local; project overrides global per event). Hooks run on the host CLI process. The project-local file runs only when the operator trusts the workspace with JAIPH_TRUST_PROJECT_HOOKS=1; absent the opt-in it is ignored with a stderr notice while the global file still runs. See Add a hook and JAIPH_TRUST_PROJECT_HOOKS.
Execute *.test.jh blocks using the same NodeWorkflowRuntime as jaiph run, in-process, with mock support.
jaiph test # discover all *.test.jh under the workspace root
jaiph test <dir> # discover all *.test.jh recursively under <dir>
jaiph test <file.test.jh> # run a single test file
| Invocation | Workspace root detection |
|---|---|
jaiph test |
Walk up from process.cwd() until .jaiph or .git; falls back to process.cwd(). |
jaiph test <dir> |
Walk up from the resolved <dir>. |
jaiph test <file> |
Walk up from the test file's directory. |
Zero matches with no arguments (or with a directory containing no *.test.jh files) writes jaiph test: no *.test.jh files found (nothing to do) to stderr and exits 0. An explicit file path that does not exist or is not *.test.jh exits 1. Plain def files(*.jh without .test) are not supported as test entries. Extra positional tokens after the path are accepted but ignored.
--env KEY[=VALUE] (repeatable) grants keys to matching script and named-prompt use clauses, the same grant as jaiph run --env. Unlike jaiph run, there is no use pre-flight: a use key that was not granted does not fail the test — the key is simply absent in the subprocess env (mock the script or grant the key when the value matters). A bare --env KEY unset on the host still aborts with E_ENV_MISSING.
Assertions: expect_contain, expect_equal, expect_not_contain — see Write & run tests.
{: #jaiph-compile}
Parse modules and run collectDiagnostics(graph) — the same per-module validator as jaiph run, but collecting every recoverable error instead of stopping at the first — without writing scripts/, without calling buildRuntimeGraph(), and without spawning the run.
jaiph compile [--json] [--workspace <dir>] <file.jh | directory> ...
At least one path is required. Unlike the other subcommands (which stop scanning for help at --), compile parses -h / --help inline in its argument loop, so a help flag is recognised at any position — before or after a path.
| Argument shape | Behaviour |
|---|---|
File path (*.jh or *.test.jh) |
Expanded to the transitive import closure. Each module in the union is parsed and validated once. |
| Directory path | Tree scanned for *.jh files; *.test.jh is skipped (use an explicit file path to validate a test module). Each non-test *.jh is treated as an entrypoint and its closure merged into the validation set. |
| Flag | Effect |
|---|---|
--json |
On success, print [] to stdout. On failure, print one JSON array of { file, line, col, code, message } diagnostics to stdout and exit 1. |
--workspace <dir> |
Override library resolution root for all reached modules. Without it, the workspace is auto-detected per path. |
Within each entry's import closure, diagnostics are sorted by (file, line, col); when multiple entry points are supplied, those batches are appended in discovery order (not re-sorted globally). Without --json, the same set is written to stderr as path:line:col CODE message lines. Any non-empty diagnostic set exits 1. Parser/loader failures abort the affected entry's closure with a single diagnostic for that entry; siblings continue.
Reformat .jh / .test.jh files into canonical style.
jaiph format [--check] [--indent <n>] <path.jh ...>
Paths must end with .jh. Formatting is idempotent. Comments and shebangs are preserved. Triple-quoted bodies and prompt blocks emit verbatim (author margin preserved via trivia). Fenced script bodies are stored dedented in the AST; the formatter re-indents inner lines by one level relative to the surrounding scope.
| Flag | Argument | Default | Effect |
|---|---|---|---|
--indent |
<n> |
2 |
Spaces per indent level. |
--check |
— | — | Verify without writing. Exit 0 when files match canonical form, 1 when any file would change. |
Top-level ordering: the formatter hoists import, config, and channel declarations to the top (in that order, preserving relative source order within each group). Other top-level definitions (const, script, def, test) keep their relative source order. Comments before a hoisted construct move with it; comments before non-hoisted definitions stay in place.
Top-level const quoting: the source delimiter is preserved per binding. Bare tokens stay bare, """…""" values emit verbatim, and a double-quoted value stays double-quoted. The one exception is a double-quoted value whose content contains a " or a \: the formatter emits it as a """…""" block so the text needs no escaping. The formatter never rewrites a quoted value as bare, or a bare token as quoted, based on the value's content (for example, whether it contains a space).
Blank-line preservation: a single blank line between steps inside a def body is preserved. Multiple consecutive blank lines collapse to one. Trailing blank lines before } are removed.
jaiph init [workspace-path]
Creates the following under the target workspace:
| File | Content |
|---|---|
.jaiph/.gitignore |
Two-line file listing runs and tmp. If the file exists and does not match, the command exits non-zero. |
.jaiph/bootstrap.jh |
Canonical bootstrap file, made executable. The body is a triple-quoted multiline prompt that asks the agent to scaffold .jh files. Like .gitignore, if the file already exists and does not match the canonical template, the command exits non-zero. |
.jaiph/SKILL.md |
Copy of the skill markdown shipped with this jaiph build (see JAIPH_SKILL_PATH). |
SKILL.md resolution order: JAIPH_SKILL_PATH (if set and the path exists) → install-relative paths (jaiph-skill.md next to the package tree, then docs/jaiph-skill.md next to the package) → docs/jaiph-skill.md under the current working directory → the embedded copy baked into the binary. There is no "skip and warn" path; the file is always written.
Install project-scoped libraries into .jaiph/libs/<name>/ under the workspace root. The workspace root is detected from process.cwd() (detectWorkspaceRoot — walks up until .jaiph or .git, with temp-directory guards).
jaiph install [--force] [<name[@version]> | <repo-url[@version]> ...]
jaiph install [--force] # restore from lockfile
| Flag | Effect |
|---|---|
--force |
Delete and re-clone existing libraries. Accepted anywhere in the argument list. |
--allow-unpinned |
Install a registry entry that has no pinned commit. Without it, an unpinned registry entry is refused before any clone; with it, the install proceeds after a stderr warning. Does not affect git-URL installs. Accepted anywhere in the argument list. |
| Argument shape | Resolution |
|---|---|
Bare registry name matching ^[A-Za-z0-9_-]+(@[A-Za-z0-9._+/-]+)?$ (no /, no :) |
Looked up in the registry index. Examples: jaiphlang, mylib@v1.2. |
| Anything else | Parsed as a git URL with optional trailing @<version>. Examples: https://github.com/you/queue-lib.git, git@github.com:org/repo.git@main. |
Remote registry and library URLs must use an allowed scheme. A value with an explicit URL scheme is accepted only for https://, ssh://, or file://; scheme-less paths and scp-style git@host:path remotes (which are SSH) are treated as local/SSH and accepted. http://, git://, and any other scheme are rejected before any fetch or clone with ... "<url>" uses disallowed scheme "<scheme>://" — only https:// and ssh:// are permitted for remote sources.
Each successful clone runs these checks before the lib counts as installed:
.jhmodule check — at least one*.jhfile must exist under the clone (recursive,.gitskipped). Failure removes the directory and aborts withlib "<name>" contains no .jh modules — not a jaiph library?. No lock entry written.- Commit capture — when the clone has a
.gitdirectory,git rev-parse HEADis recorded as the 40-charcommiton the lock entry. A clone with no usable git checkout leavescommitunset. .gitstrip —<libDir>/.gitis removed recursively, right after the commit is captured and before the two checks below.- Pinned-commit check — when the registry entry (or lock entry) carries a
commit, the captured HEAD must equal it, or the directory is removed and the install fails with the locked vs cloned SHAs and the remedy. This makes the first install from the registry authenticated, not just restore. - Detached signature check — when the registry entry carries a
signature(a detached minisign signature over the ASCII commit SHA), it is verified against the embeddedjaiph.pubproject key only, never a key supplied by the entry itself (a self-supplied key attests nothing an attacker controlling the entry could not forge). An invalid or unverifiable signature removes the directory and fails the install closed withlib "<name>" signature verification failed for commit <sha>.
jaiph install with no positional args reads .jaiph/libs.lock and clones each entry. The registry is never contacted. If a lock entry carries a commit, the cloned HEAD must match it; on mismatch the directory is removed and the run fails with the locked vs cloned SHAs and the remedy. Lock entries without commit (older lockfiles) restore without the check.
Missing libraries are cloned with bounded concurrency (default 4 in flight). The warm-skip pass runs before any clone. Independent clone failures still propagate; failed libraries are not added to the lockfile.
| Aspect | Value |
|---|---|
| Source | JAIPH_REGISTRY (default https://jaiph.org/registry). Remote sources must satisfy the scheme allowlist. |
| Loading | Loaded once per invocation when at least one positional argument is a bare name. URL-form installs and restore-from-lock never read the registry. |
| Disk paths | Values without a :// scheme, or starting with file://, are read from disk (trusted-local, no signature check). Everything else is fetched via global fetch. |
| Signature verification | A remotely fetched index is verified against a detached <source>.minisig (minisign, jaiph.pub embedded as the trust anchor) before use. A missing, unsigned, or tampered index is rejected — the fetch fails closed. jaiph.org therefore serves registry.minisig alongside registry; see Contributing. |
| Index format | { "libs": { "<name>": { "url": "<git-url>", "description": "<string>", "commit"?: "<40-hex>", "signature"?: "<minisig>" } } }. Each key must match ^[A-Za-z0-9_-]+$. commit (when present) pins the install; signature adds a per-library detached-signature check verified against the embedded jaiph.pub only. Other per-entry keys (including any publicKey) are accepted and ignored. |
| Pin requirement | Installing a bare registry name whose entry has no commit is refused before any clone (lib "<name>" has no pinned commit in registry <source> — ...; re-run with --allow-unpinned to override). This closes the supply-chain gap where a moved ref would run arbitrary code. Pass --allow-unpinned to install anyway after a stderr warning. The shipped docs/registry index pins every entry, and npm run registry:build (strict requireCommit validation) refuses to write an index whose entries are not all pinned. |
| Lookup errors | lib "<name>" not found in registry <source>, failed to read registry <source>: <cause>, failed to fetch registry <source>: HTTP <status>, failed to fetch registry signature <source>.minisig: <cause>, failed to verify registry <source>: signature check failed against <source>.minisig, failed to parse registry <source>: <cause>, ... uses disallowed scheme "<scheme>://" .... |
.jaiph/libs.lock shape:
{
"libs": [
{
"name": "jaiphlang",
"url": "https://github.com/jaiphlang/jaiphlang.git",
"commit": "1a2b3c4d5e6f7890abcdef1234567890abcdef12"
},
{
"name": "queue-lib",
"url": "https://github.com/you/queue-lib.git",
"version": "v1.0",
"commit": "fedcba9876543210fedcba9876543210fedcba98"
}
]
}The lock entry stores the resolved clone URL so restore works without the registry. commit is written automatically after each successful clone.
Reinstall jaiph globally with the selected channel or version.
jaiph use <version|nightly>
| Argument | Effect |
|---|---|
nightly |
Reinstalls from the rolling nightly prerelease. |
<version> (e.g. 0.13.0) |
Reinstalls the release binary for tag v<version>. |
Implementation: with no JAIPH_INSTALL_COMMAND override, jaiph use downloads the install script from ${JAIPH_SITE}/install (default https://jaiph.org), verifies it against the published ${JAIPH_SITE}/install.sha256, and only then runs it with JAIPH_REPO_REF set to nightly or v<version>. A mismatched or missing checksum fails closed rather than piping an unverified script to bash. Setting JAIPH_INSTALL_COMMAND overrides this with a verbatim command (forks, offline bundles, local scripts). The installer then downloads the matching per-platform binary plus SHA256SUMS (and its signature), verifies them, and replaces ~/.local/bin/jaiph (or JAIPH_BIN_DIR).
{: #jaiph-mcp}
Serve a file's defs as MCP tools over stdio. See MCP server in 30 seconds for the recipe and client-registration steps.
jaiph mcp [--workspace <dir>] [--env KEY[=VALUE]]... <file.jh>
jaiph --mcp <file.jh> is an equivalent alias, dispatched after compile in src/cli/index.ts.
| Flag | Argument | Effect |
|---|---|---|
--workspace |
<dir> |
Workspace root for import resolution (default: auto-detected from the file's directory). A missing value or non-directory path aborts with a specific message. |
--env |
KEY=VALUE or KEY |
Same per-key passthrough as jaiph run --env (same forms, validation, and reserved-key rejection), resolved once at startup and applied to every tool call for the server's lifetime. A bare --env KEY unset on the host aborts server startup with E_ENV_MISSING. |
-h, --help |
— | Print the subcommand usage and exit 0. |
Flags that belong to another command (for example --raw or --port) are usage errors naming the owning command — never silently ignored. Precedence across layers is the shared execution-policy order: CLI flags > JAIPH_* env vars > module config metadata > defaults (see Environment variables — Precedence).
- Loads the module graph and runs
collectDiagnostics(the same compile-time pass asjaiph compile). Any diagnostic printsfile:line:col CODE messagelines to stderr and exits1. - A missing path, a non-
.jhpath, or a path that is not a file exits1with a message on stderr. - On success the server runs until stdin closes or it receives
SIGINT/SIGTERM. Shutdown is drain-then-cancel: stdin closing (or the first signal) stops accepting input and waits for in-flight calls to finish before cleaning up and exiting0— a draining call keeps its scripts until it settles. A second signal cancels the in-flight calls instead of waiting: each run's child process tree is terminated (SIGINT, thenSIGKILLafter a grace period), so no child process outlives the server; the killed calls settle with error results and the server still exits0.
From the moment the server starts, stdout carries only newline-delimited JSON-RPC. Every banner, warning, exclusion notice, reload message, and credential-pre-flight warning goes to stderr. Each outbound protocol message is a single atomic write of JSON.stringify(msg) + "\n".
jaiph mcp and jaiph serve write an operator log to stderr only. They never write it to the protocol channel, so MCP stdout stays JSON-RPC and HTTP response bodies stay API payloads. The operator log is not a logging framework, and Jaiph adds no winston, pino, or bunyan for it. It is a thin labelled writer that prints one line at a time to stderr and reuses the same level and color formatting as the jaiph run progress tree. Colors are used only when the stderr sink is a terminal and NO_COLOR is unset.
On every tool call or run the operator log writes two lines. The start line names the def and the run id, for example jaiph mcp: Running <def> run_id=…. The end line reports the terminal status, the exit code, the elapsed time, and the run dir when it is known, for example jaiph mcp: Finished <def> status=ok exit=0 elapsed_ms=… rundir=…. On jaiph serve both lines also carry principal= and correlation=.
Two environment variables change how much the operator log prints, both documented in Environment variables:
JAIPH_SERVER_LOG=debugprints the servers' extradebugdiagnostic lines.JAIPH_SERVER_LOG_RUNS=1mirrors eachlog,logwarn, andlogerrevent to the operator log. Each mirrored line is colored by level and carriesrun_id=and the same depth and async-branch subscript indent as the run tree. Mirroring is off by default, so an MCP host is not flooded and the tool-result text is not repeated. Mirrored lines go through the same credential redaction as the durable run journal, so a secret is never printed to stderr.
Newline-delimited JSON-RPC 2.0. Requests are handled concurrently (a long tools/call never stalls ping or further calls).
| Method | Behaviour |
|---|---|
initialize |
Replies with protocolVersion, capabilities: {tools: {listChanged: true}}, and serverInfo: {name: "jaiph", title: "Jaiph", version}. Echoes the client's protocolVersion if it is one of 2024-11-05, 2025-03-26, 2025-06-18; otherwise replies with the newest of that set. |
ping |
Empty result. |
tools/list |
{tools: [{name, description, inputSchema}]} from the current tool set (re-read per request, so hot reload needs no cache invalidation). |
tools/call |
Runs the def on the host. Result: {content: [{type: "text", text}], isError}. When params._meta.progressToken is present, the run's STEP_START / STEP_END events stream as notifications/progress until the response is sent (see below). |
notifications/cancelled |
Cancels the matching in-flight tools/call (params.requestId): terminates the run's child process tree (SIGINT, then SIGKILL after a grace period); sends no response for that id, and keeps the server serving. A cancellation for an unknown or already-finished id is a no-op. |
| other notifications | Ignored (notifications/initialized, …); no response. |
| unknown request | JSON-RPC error -32601. |
The server emits notifications/tools/list_changed after a successful hot reload (only once initialize has happened).
When a tools/call carries a progressToken, the server also emits notifications/progress ({progressToken, progress, message}) for that call — one per step event, with a monotonically increasing progress counter and a message of "<kind> <name>" (no total, since a def's step count is not known up front). Notifications stop the instant the call's response is sent; a call without a progressToken emits none. See MCP server in 30 seconds — Stream progress and cancel a long call.
| Condition | Code |
|---|---|
| Invalid JSON | -32700 (with id: null) |
| Non-object message | -32600 |
| Unknown method | -32601 |
| Unknown tool, missing/non-string required argument, or unexpected argument key | -32602 (the call never starts) |
| Infrastructure crash while running a call | -32603 (also logged to stderr) |
| Def failure | not a protocol error — a normal result with isError: true and a run dir: pointer |
The tool surface is derived from the entry file only (imports are never exposed):
| Rule | Behaviour |
|---|---|
export def … present |
Exactly the exported defs are exposed. |
| No exports | No tools, plus a warning. |
main |
Exposed only when it is the sole export, named after the sanitized file basename (.jh stripped, non-[A-Za-z0-9_-] → _, truncated to 128; an empty result falls back to the literal def). Skipped when it is not the sole export, or when the sanitized name would collide with an already-exposed def. |
Tool descriptions come from the # comment lines directly above each def (shebang lines dropped, # prefix stripped); the fallback is Run the "<name>" def from <basename>. Every parameter is a required string in the input schema.
- Tool calls execute on the host, the same as
jaiph run. Run artifacts land under.jaiph/runs/exactly as forjaiph run. Concurrent calls each get their own run id and run directory. Two calls that change the same files can race. - Source files in the module graph are watched (polling, ~750 ms). A valid edit re-derives tools and emits
notifications/tools/list_changed; an edit that fails to compile keeps the previous tool set serving and logs diagnostics to stderr. - Calls bind to the generation (emitted scripts + serialized graph) live when they start; a superseded generation's scripts dir survives until its last in-flight call settles, so a call spanning a reload still runs its remaining steps — the same lease model
jaiph serveuses for HTTP runs.
{: #jaiph-serve}
Serve a file's defs as an HTTP API with a generated OpenAPI 3.1 document and an embedded Swagger UI. Same exposure rules and execution layer as jaiph mcp, over HTTP instead of stdio. See Serve defs over HTTP for the recipe.
jaiph serve [--host <addr>] [--port <n>] [--workspace <dir>] [--allow-anonymous] [--env KEY[=VALUE]]... <file.jh>
| Flag | Argument | Effect |
|---|---|---|
--host |
<addr> |
Listen address (default 127.0.0.1). Binding a non-loopback host with no authentication (neither JAIPH_SERVE_TOKEN nor OIDC configured) aborts startup, even with --allow-anonymous. |
--port |
<n> |
Listen port (default 5247). 0 picks a free port. |
--allow-anonymous |
— | Explicit opt-in to run open with no authentication on loopback. Without it, a loopback bind with no JAIPH_SERVE_TOKEN and no OIDC aborts startup, because anonymous mode authorizes every local principal with all capabilities over all runs (loopback guards the network, not other local users — finding M-2). For a single-user workstation only; shared hosts must set JAIPH_SERVE_TOKEN or configure OIDC. When passed, the server prints a startup warning that it is open to all local principals. Ignored (no-op) when a token or OIDC is configured, and it never permits a non-loopback bind. |
--workspace |
<dir> |
Workspace root for import resolution (default: auto-detected). |
--env |
KEY=VALUE or KEY |
Same per-key passthrough as jaiph run --env, resolved once at startup and applied to every run for the server's lifetime. |
-h, --help |
— | Print the subcommand usage and exit 0. |
Flags that belong to another command (for example --raw or --target) are usage errors naming the owning command — never silently ignored. Precedence across layers is the shared execution-policy order: CLI flags > JAIPH_* env vars > module config metadata > defaults (see Environment variables — Precedence).
Startup mirrors jaiph mcp: graph load + collectDiagnostics (diagnostics to stderr, exit 1), credential pre-flight as warnings, and a host-execution notice. All logs go to stderr. Startup prints a line with the listen URL, the /docs and /mcp URLs, and the exposed-def count, followed by an authentication-mode line, a memory-bounds line, and, when terminal runs were rebuilt from disk, a line reporting how many were reconstructed. Per-run operator lines (a start line Running … run_id= and an end line Finished … status=… elapsed_ms=…) and the optional log mirror follow the same stderr-only operator-log contract as jaiph mcp, and HTTP response bodies stay API payloads. See Operator log (stderr) above, and JAIPH_SERVER_LOG and JAIPH_SERVER_LOG_RUNS in Environment variables.
The Cap. column names the capability an authenticated principal must hold to reach each endpoint. In static-token and open (loopback) mode the single principal holds every capability; in OIDC mode capabilities come from the token's OAuth scopes (jaiph:invoke / jaiph:inspect / jaiph:cancel) — see Auth and limits. POST /mcp (MCP Streamable HTTP) shares the same boundary: tools/call needs invoke, tools/list needs inspect.
| Method & path | Cap. | Behaviour |
|---|---|---|
GET / |
none | 302 → /docs. |
GET /healthz |
none | 200 {status, version, tools, in_flight}. Always open and credential-free. |
GET /openapi.json |
none | OpenAPI 3.1 document, regenerated per request (hot reload needs no cache invalidation). 404 when JAIPH_SERVE_EXPOSE_DOCS=false. |
GET /docs |
none | Self-contained Swagger UI shell. The pinned swagger-ui-dist assets are embedded in the binary and served from same-origin /docs/* paths, so it needs no browser internet access. 404 when JAIPH_SERVE_EXPOSE_DOCS=false. |
GET /docs/swagger-ui-bundle.js, GET /docs/swagger-ui.css |
none | The embedded Swagger UI assets, each stamped with a sha384 Subresource Integrity hash over the served bytes. 404 when JAIPH_SERVE_EXPOSE_DOCS=false. |
GET /v1/defs |
inspect |
{defs: [{name, description, params}]}. |
POST /v1/defs/{name}/runs |
invoke |
Start a run. Default 202 + Location: /v1/runs/{id}; ?wait=true blocks for the terminal 200. Send an Idempotency-Key header (scoped to the authenticated principal + def) to make retries safe: an identical repeat returns the original run (200, no second spawn); a reused key with different arguments is 409 E_IDEMPOTENCY_CONFLICT and spawns nothing. |
GET /v1/runs |
inspect |
Runs started by this process plus runs reconstructed from disk on restart, newest first, scoped to the caller's own runs (all runs for a static/open principal). Paginated: ?limit (default 100, clamped to 1000), ?offset (default 0). Response is {runs, total, limit, offset} and never unbounded. |
GET /v1/runs/{id} |
inspect |
The run object. 404 unknown (a run the principal does not own is indistinguishable from nonexistent). |
GET /v1/runs/{id}/events |
inspect |
The run's run_summary.jsonl. Default application/x-ndjson snapshot, streamed from disk (never buffered whole); Accept: text/event-stream replays then follows it live, closing with event: end when terminal. The snapshot mode first verifies the journal's keyed integrity chain and returns 409 E_TAMPERED when the chain does not verify (see Architecture — Keyed hash chain). Served verbatim (already credential-redacted); raw capture files are never exposed. 404 unknown. |
GET /v1/runs/{id}/artifacts |
inspect |
{artifacts: [{path, size, mtime}]} for files published under the run's artifacts/ (empty when none). 404 unknown. |
GET /v1/runs/{id}/artifacts/{path} |
inspect |
Download one published file (application/octet-stream), streamed with backpressure — never buffered whole, so an arbitrarily large file costs no server memory and a client disconnect closes the file. Traversal-proof — .., absolute paths, and escaping symlinks are 404. 413 E_ARTIFACT_TOO_LARGE when the file exceeds JAIPH_SERVE_MAX_ARTIFACT_BYTES. |
POST /v1/runs/{id}/cancel |
cancel |
202; the run reaches cancelled. 409 if already terminal. |
The run object is {run_id, def, status, started_at, ended_at, exit_status, signal, result_text, run_dir, principal, correlation_id} where status is running | succeeded | failed | cancelled | interrupted. principal is the audit subject that created the run (anonymous/operator in open/static mode, the token sub or client_id in OIDC mode — never a token) and correlation_id is the request id attached at create time; both are null when unset. interrupted is the terminal state a run is reconciled to after a process death caught it mid-flight — its outcome is unknown, so it is neither succeeded nor failed, but it is never reported as permanently running. A def failure is not an HTTP error — the run object reports status: "failed" with the same failure narrative jaiph mcp returns, over HTTP 200/202. Errors use {error: {code, message}} with 400 E_BAD_ARGS, 401 E_UNAUTHORIZED (missing or invalid static token; in OIDC mode, a request with no bearer token, or a verified token that carries neither sub nor client_id), 401 E_TOKEN_EXPIRED / 401 E_TOKEN_INVALID (OIDC token expired, or bad audience/issuer/key/signature/algorithm), 403 E_FORBIDDEN (principal lacks the required capability), 404 E_NOT_FOUND, 409 E_RUN_TERMINAL, 409 E_IDEMPOTENCY_CONFLICT (idempotency key reused with different arguments), 409 E_TAMPERED (the run's journal failed its keyed integrity chain), 413 E_BODY_TOO_LARGE (1 MiB request-body cap), 413 E_ARTIFACT_TOO_LARGE (artifact download over JAIPH_SERVE_MAX_ARTIFACT_BYTES), 415 (non-application/json body), 429 E_TOO_MANY_RUNS, and 503 E_AUTH_UNAVAILABLE (OIDC identity provider / JWKS unreachable).
Each run's public record is persisted beside its journal as run.json when it finishes, and reconstructed into the registry on startup — so GET /v1/runs, /v1/runs/{id}, /events, and /artifacts keep working for pre-restart terminal runs, and idempotency keys survive a restart. jaiph serve is a single-replica service: the run registry, concurrency cap, and idempotency index are per-process and not shared across replicas — run two behind one load balancer and each has its own view. See Serve — deployment topology.
- Authentication has two production modes (credentials come from the environment, never argv) plus an anonymous mode that is an explicit opt-in for a single-user workstation (
--allow-anonymous). Static single-operator token:JAIPH_SERVE_TOKENis a shared secret required on every/v1/*and/mcprequest (Authorization: Bearer <token>, constant-time compared). It is a fail-closed gate for one operator — no per-user identity, revocation, or per-action authorization; the operator holds every capability and sees every run — not multi-tenant authentication. OIDC/JWT (multi-tenant): setJAIPH_SERVE_OIDC_ISSUER+JAIPH_SERVE_OIDC_AUDIENCE(takes precedence over the static token; setting only one is a startup error) to verify bearer JWTs against the issuer's JWKS (discovered from<issuer>/.well-known/openid-configuration, or setJAIPH_SERVE_OIDC_JWKS_URI) with a maintained JWT library — signature,exp/nbf,aud,iss,kid, and an explicit allowlist of asymmetric signing algorithms (RSA, ECDSA, and EdDSA families; symmetric algorithms,alg: none, andES256Kare rejected). Each token is authorized by OAuth scopes:jaiph:invoke(run),jaiph:inspect(read defs/runs/events/artifacts, MCPtools/list),jaiph:cancel(cancel a run); a missing capability is403 E_FORBIDDEN, and a principal (the tokensub, orclient_idforsub-less machine tokens; a verified token with neither is401 E_UNAUTHORIZED) may inspect or cancel only the runs it created. The authenticated subject and the request's correlation id (X-Correlation-Id/X-Request-Id, else a generated UUID) attach to run metadata, the invoke/cancel audit log lines, OTLP resource attributes, and Sentry tags — never a token or a claim value. - Binding a non-loopback
--hostwith no authentication is a startup error, and--allow-anonymousdoes not lift it. On loopback with no token or OIDC, startup is also refused unless you pass--allow-anonymous— anonymous mode makes every caller theanonymousprincipal with all capabilities over all runs, so it is for a single-user workstation only and prints a startup warning when enabled. JAIPH_SERVE_EXPOSE_DOCS(defaulttrue) controls whether/docsand/openapi.jsonare served; setfalse(or0) to return404for both and hide the API surface./healthzis always open and credential-free (liveness/readiness only — no tokens or sensitive detail).JAIPH_SERVE_MAX_CONCURRENT(default4) caps simultaneous runs; requests beyond it get429.JAIPH_SERVE_MAX_ARTIFACT_BYTES(default0= no cap) refuses artifact downloads larger than the limit with413. Downloads stream with backpressure regardless, so the default keeps server memory bounded no matter the file size; set a finite cap only to reject oversized downloads outright.- Memory bounds keep a long-lived server from growing without limit:
JAIPH_SERVE_MAX_OUTPUT_BYTES(default 1 MiB) caps collected stdout, stderr, log output, and the residentresult_textper run (overflow dropped with a truncation marker);JAIPH_SERVE_RETAIN_RUNS(default500) andJAIPH_SERVE_RETAIN_AGE_SEC(default86400,0disables) bound how many completed runs stay in the in-memory registry, evicting the oldest terminal records first. Active runs are never evicted, and eviction drops only the in-memory record — durable.jaiph/runsjournals and artifacts persist on disk and are the operator's to prune. See Serve defs over HTTP. - Execution and hot reload are identical to
jaiph mcp; a superseded generation's scripts dir survives until its in-flight HTTP runs finish.
See Environment variables for the complete inventory. The variables most relevant to CLI behavior:
JAIPH_RUN_TIMEOUT— parent-enforced wall-clock cap for a run.JAIPH_NON_TTY_HEARTBEAT_FIRST_SEC,JAIPH_NON_TTY_HEARTBEAT_INTERVAL_MS— non-TTY progress cadence.JAIPH_RUNS_DIR,JAIPH_WORKSPACE,JAIPH_SOURCE_FILE— run-layout inputs.JAIPH_INSTALL_COMMAND,JAIPH_REGISTRY,JAIPH_SKILL_PATH— install / init inputs.NO_COLOR— disable ANSI colour output.
- Live contract (runtime → CLI):
__JAIPH_EVENT__JSON lines on stderr only. Hooks and the interactive progress tree consume this stream. Stdout carries plain script output forwarded as-is. - Durable contract:
.jaiph/runs/...+run_summary.jsonl+.out/.errstep artifacts + optionalreturn_value.txt. See Architecture — Durable artifact layout.
run_summary.jsonl event types: RUN_START, RUN_END, STEP_START, STEP_END, LOG, LOGERR, LOGWARN, INBOX_ENQUEUE, INBOX_DISPATCH_START, INBOX_DISPATCH_COMPLETE, PROMPT_START, PROMPT_END. Every object carries type, ts (UTC), run_id, and event_version (currently 1). Step events also carry id, parent_id, seq, depth. See Architecture — Contracts.
.jh is the file extension for Jaiph source. Import resolution appends .jh when the path omits the extension. *.test.jh is the test-module convention recognised by jaiph test and file shorthand.
- Configuration — config keys, precedence, scoping.
- Grammar — syntax and validation catalog.
- Language — step semantics and step-output contract.
- Environment variables — every variable Jaiph reads.
- MCP server in 30 seconds — exposing a file's exported defs to MCP clients via
jaiph mcp.