Skip to content
ochat
Search documentation

Use quotes for an exact phrase.

Search by topic, command, or code identifier.

    GitHub ↗

    ChatMD language reference

    The complete declaration syntax for agent definitions.

    View Markdown source ↗

    For current native/daemon hosting, see host modes and agent-host orchestration. Daemon work belongs to the session actor, not a connected UI. The existing language/tool APIs remain shared; file-backed session/controller descriptions should be read in that host context. Instruction helper compatibility names emit developer-role messages.

    ChatMarkdown (ChatMD) is a small, closed XML vocabulary embedded in Markdown for authoring LLM conversations as plain files.

    The core idea is simple: a ChatMD file is both:

    • a prompt (model config, tool permissions, instructions, context), and
    • an auditable transcript (tool calls + tool outputs + assistant replies).

    This makes workflows reproducible, diffable, and easy to review.

    “What the model sees” (and the explicit exceptions)

    Section titled ““What the model sees” (and the explicit exceptions)”

    Ochat tries hard to ensure the model sees exactly what’s in your ChatMD document, but there are a few explicit, documented mechanisms that transform the file:

    • HTML comments are stripped: <!-- ... --> is removed before parsing and never reaches the model.
    • <import/> expands: selected <import src="..."/> directives are replaced with the contents of the referenced file at parse time.
    • Runtime declarations stay host-managed: top-level <script>, <shell_access>, and <moderator_runtime> declarations are parsed, validated, and executed by the host. They are not converted into model-visible request history.
    • RAW blocks disable parsing: RAW| ... |RAW is treated as literal text (no tag parsing inside).
    • Optional meta-refine preprocessing: if enabled, the prompt may be rewritten before parsing (see “Meta-refine” below).

    Everything else is intentionally boring: there are no hidden templates, implicit tool permissions, or “magic” side channels.


    ChatMD is not general HTML/XML. It recognises a closed set of lowercase tag names.

    These are the tag names ChatMD recognises (lowercase, case-sensitive):

    • Message / transcript structure: msg, user, assistant, system, developer
    • Host-managed runtime declarations: script, shell_access, moderator_runtime
    • Tools / tool trace: tool, tool_call, tool_response
    • Inline helpers: doc, img, agent, import
    • Reasoning: reasoning, summary
    • Configuration: config

    Inside <shell_access> and <tool type="shell">, the lexer additionally recognizes a strict nested shell vocabulary for capabilities, resolver, environment, limits, backends, policy/matchers, approvals/reviewers, interceptors, effect analysis, secrets, audit, and fixed-command children. These nodes are built by the normal ChatMD parser and validated by the shell declaration layer. Unknown shell children/attributes are errors rather than literal message text.

    A ChatMD document must be a sequence of recognised ChatMD elements at the top level.

    • ✅ Allowed at top-level: <user>...</user>, <tool .../>, <config .../>, etc.
    • ✅ Whitespace between top-level elements is allowed.
    • ❌ Plain text at top-level is an error.
    • ❌ Unknown tags at top-level are an error (because unknown tags are treated as literal text).

    Example:

    <!-- ✅ valid -->
    <user>Hello</user>
    <!-- ❌ invalid: top-level text -->
    Hello

    Unknown markup is preserved as literal text inside recognised elements. For example:

    <user>Hello <b>world</b></user>

    <b> is not a ChatMD tag, so it is preserved as literal text and passed through to the model.

    Tags are case-sensitive. Use lowercase:

    • <user>...</user>
    • <User>...</User> (treated as unknown text; may break top-level validity)
    • Flag attributes are supported and mean “present = true”, e.g. <doc src="x" local/>.
    • Attributes can be quoted with single or double quotes.
    • Quoted values support backslash-escaped quotes.
    • A small set of HTML entities are decoded in attribute values: &amp;, &lt;, &gt;, &quot;, &apos;.

    RAW blocks let you embed arbitrary text without the ChatMD lexer interpreting <tags> inside it.

    Syntax:

    • opener: RAW|
    • terminator: |RAW

    Everything between them is treated as literal text.

    This is the #1 way to embed:

    • JSON tool arguments / results
    • code containing < / > or XML-like snippets
    • “literal ChatMD” examples inside a message

    Example:

    <user>
    Here is JSON (no escaping needed):
    RAW|
    { "path": "README.md", "offset": 0 }
    |RAW
    </user>

    These elements form the “program” and the transcript.

    The smallest useful ChatMD file is:

    <config model="gpt-4o" temperature="0"/>
    <user>
    Hello.
    </user>
    ElementPurposeNotes / key attributes
    <config .../>Model and generation parametersOptional. If multiple appear, the first <config/> wins. Flag attribute: show_tool_call.
    <tool .../>Declare tools available to the assistantBuiltin, long/compact shell, agent-backed, or MCP-backed.
    <user>...</user>User messageThe most common input block.
    <assistant>...</assistant>Assistant messageUsually written by ochat into the transcript. Often uses RAW blocks for faithful round-tripping.
    <system>...</system>System messageHigh-priority instructions.
    <developer>...</developer>Developer messageMid-priority instructions (below system).
    <msg role="...">...</msg>Generic message formEscape hatch for less common roles/legacy. Roles supported by the runtime: user, assistant, system, developer, tool.
    <tool_call ...>...</tool_call>Tool invocation recordTypically written by ochat; see “Tool calls & tool responses”.
    <tool_response ...>...</tool_response>Tool output recordTypically written by ochat; see “Tool calls & tool responses”.
    <reasoning ...>...</reasoning>Reasoning recordTypically written by reasoning-capable models; requires id if authored manually.
    <script ...>...</script> / <script ... src="..." />Host-managed ChatML scriptTop-level only. Supports moderator and shell extension kinds.
    <shell_access ...>...</shell_access>Named shell runtimeStrict host-only configuration; never model history.
    <moderator_runtime shell_runtime="..."/>Moderator process bindingRoutes Process.run through a named shell runtime.

    3.3 Inline content helpers (only inside message bodies)

    Section titled “3.3 Inline content helpers (only inside message bodies)”

    These tags are recognised by the parser, but they are primarily meaningful inside message bodies (e.g. inside <user>...</user>):

    Inline tagWhat it does
    <doc src="..." [local] [strip] [markdown] />Inline document text (local file or remote URL).
    <img src="..." [local] />Inline an image (remote URL or local file encoded as a data URI).
    <agent src="..." [local]> ... </agent>Run another ChatMD prompt as a sub-agent and insert its final answer.
    <import src="..."/>Parse-time include (only expands in certain places; see below).

    <script> declares a host-managed ChatML program. The script is retained in the typed prompt model, but it is not sent to the model as a message.

    Every script uses language="chatml" and a unique ID. Supported kinds are moderator, shell_matcher, shell_reviewer, shell_before_interceptor, shell_after_interceptor, shell_effect_analyzer, and shell_audit_filter. A prompt may contain many shell scripts, but at most one conversation moderator is selected.

    Supported forms:

    <script language="chatml" kind="moderator" id="main">
    ... ChatML source ...
    </script>
    <script language="chatml" kind="moderator" id="main" src="moderator.chatml" />

    Validation rules:

    • src="..." and inline body text are mutually exclusive.
    • src="..." is loaded during prompt parsing, so missing files fail early.
    • Relative src paths resolve against the directory of the source file declaring the script. A script in an imported ChatMD file therefore loads relative to that imported file, not the root prompt directory.
    • Duplicate script IDs are errors even when kinds differ.
    • More than one selected moderator script is an error.
    • Extra attributes are rejected.

    If a script lives in a separate file, the parsed prompt retains both the src path and the loaded source text so later compilation can report the original location clearly.

    When a prompt includes a moderator <script>, ochat keeps the script host-managed:

    • the script is parsed and validated with the rest of the prompt,
    • it is compiled once per prompt load,
    • each chat session gets a fresh runtime instance,
    • resumed sessions restore only the serializable moderator snapshot,
    • the script itself is never turned into a model-visible message.

    Prompts without a <script> keep the baseline behavior. The shared drivers, chat_tui, export flow, and nested run_agent path all fall back to the ordinary non-moderated request assembly.

    The shared moderation vocabulary defines these phase names:

    • session_start
    • session_resume
    • turn_start
    • message_appended
    • pre_tool_call
    • post_tool_response
    • turn_end
    • internal_event

    In ChatML scripts, the event constructor exposed for appended canonical items is Item_appended(item). The host phase name remains message_appended, but script authors should pattern-match on Item_appended.

    Built-in drivers currently emit:

    • session_start for new moderated sessions,
    • session_resume when restoring a persisted moderator snapshot,
    • turn_start before each model request,
    • message_appended after a canonical transcript item is appended during the streamed runtime,
    • pre_tool_call before a tool executes,
    • post_tool_response after a tool output item is produced,
    • turn_end after a streamed turn completes,
    • internal_event while replaying queued moderator events FIFO.

    For moderated chat_tui sessions, this lifecycle is split across three host layers:

    • Moderator_manager owns durable moderator state, overlay state, halted state, and queued internal events.
    • In_memory_stream owns one active model/tool turn and handles explicit safe points such as turn start, post-tool-result, and turn end.
    • chat_tui owns the session controller that reacts to idle wakeups, defers wakeups during active turns, refreshes the visible transcript from effective history at safe points, and starts follow-up turns only when the UI is idle.

    The visible transcript shown by chat_tui is not raw canonical history. It is the current canonical history projected through the moderator overlay. During streaming the UI may still apply token and tool patches directly for responsiveness, but safe-point refreshes reproject the visible transcript from effective history after moderation-visible changes.

    The current safe points are:

    • startup / resume moderation,
    • turn-start request preparation,
    • post-tool-result continuation,
    • turn end,
    • idle/background moderator drains in chat_tui,
    • compaction completion in chat_tui.

    Moderator scripts can emit host-visible runtime requests:

    • Runtime.request_turn() requests that the host run one more ordinary model turn after the current turn completes.
      • It is interpreted after turn_end handling finishes.
      • Multiple requests collapse to a single continuation decision.
      • Runtime.end_session(...) overrides request_turn.
      • In chat_tui, the same request can also schedule an idle follow-up turn after startup work or an idle internal-event drain, as long as no turn is already active.
    • Runtime.request_compaction() requests that the embedding compact history (if supported).
      • This request does not itself force another model turn.
    • Runtime.end_session(reason) requests session termination.

    The shared drivers interpret these requests through an explicit runtime-semantics policy layer.

    3.5.1a Deferred steering notes and safe-point input

    Section titled “3.5.1a Deferred steering notes and safe-point input”

    In the older file-backed chat_tui host, when the user submits steering text while a turn is already streaming, the host does not inject a new canonical user message into the in-flight request. Instead it records a deferred steering note in session-controller state.

    That deferred steering note is applied only at the next safe model-input boundary. Concretely:

    • it remains outside canonical transcript history,
    • it survives until the next safe-point request preparation,
    • it is appended as transient developer input for that request only,
    • it never rewrites tool output history in place.

    This preserves the in-flight reasoning/tool workflow while still letting the user steer the next request. Native-local and daemon hosts instead admit canonical deferred entries through the actor; see the host-specific steering contract.

    3.5.2 Model recipes (Model.call / Model.spawn)

    Section titled “3.5.2 Model recipes (Model.call / Model.spawn)”

    Moderator scripts can call host-registered model recipes:

    • Model.call(recipe_name, payload_json) runs a host-defined recipe and returns structured JSON.
    • Model.spawn(recipe_name, payload_json) starts a background job and returns a stable job id immediately.

    Spawned model jobs deliver completion back to the moderator as internal events, using stable v1 tags:

    • Model_job_succeeded(job_id, recipe_name, result_json)
    • Model_job_failed(job_id, recipe_name, message)

    Note: the initial implementation may track spawned jobs in memory only; in-flight jobs are not durably persisted across process restarts.

    Moderator scripts receive ctx.items, where each item has the structural shape:

    type item =
    { id : string
    ; value : json
    }

    The moderator surface also installs an Item helper module for common structured-item operations:

    • Item.create(id, value)
    • Item.id(item)
    • Item.value(item)
    • Item.kind(item)
    • Item.role(item)
    • Item.text_parts(item)
    • Item.input_text_message(id, role, text)
    • Item.output_text_message(id, text)

    Turn.append_item, Turn.replace_item, and Turn.delete_item are the preferred item-oriented mutation names. The older *_message names remain as aliases.

    3.6 Overlay, tool moderation, and persistence semantics

    Section titled “3.6 Overlay, tool moderation, and persistence semantics”

    Moderator scripts do not rewrite canonical OpenAI history in place. Instead the host keeps a durable overlay that can:

    • prepend synthetic developer messages (the compatibility operation is named prepend_system),
    • append synthetic items,
    • replace projected items by id,
    • delete projected items by id,
    • halt the session with an explicit reason.

    That overlay is applied before the next moderated model turn to produce the effective request history. Export and preview flows materialize synthetic overlay entries explicitly so the saved ChatMD transcript stays human-readable.

    Tool moderation is also host-managed:

    • Tool.approve() keeps the call as-is,
    • Tool.reject("reason") skips execution and synthesizes a tool output item,
    • Tool.rewrite_args(json) changes the tool payload deterministically,
    • Tool.redirect("other_tool", json) rewrites both tool name and args.

    After every successful moderation event, the host replays queued internal events FIFO through phase internal_event, with a safety limit to avoid unbounded loops. Spawned model job completions (Model.spawn) are delivered using this same internal-event replay mechanism.

    In chat_tui, queued internal events can surface while the UI is otherwise idle. Background producers may enqueue work and register a wakeup callback; the session controller drains that queued work at an idle safe point, refreshes the visible transcript if overlay state changed, and may schedule a follow-up turn when the moderator requests one.

    An end-to-end idle async completion currently looks like this:

    1. a moderator script calls Model.spawn(...)
    2. the background job finishes and is converted into Model_job_succeeded(...) or Model_job_failed(...)
    3. the host enqueues that internal event into Moderator_manager
    4. the idle chat_tui reducer receives a moderator wakeup
    5. chat_tui drains queued internal events, refreshes visible transcript state from effective history, and applies any notices or halted state updates
    6. if the moderator emitted Runtime.request_turn(), chat_tui starts one more ordinary turn from the current session state

    An end-to-end non-interrupting steering flow currently looks like this:

    1. the assistant is already in a streamed turn or tool workflow
    2. the user submits steering text
    3. chat_tui records a deferred steering note instead of appending a canonical user message mid-turn
    4. the current turn reaches a safe point and eventually completes
    5. the next request is prepared from moderator-effective history
    6. the deferred steering note is appended as transient developer input for that next request only

    Persisted moderator state is intentionally narrow:

    • current script state,
    • queued internal events,
    • halted flag,
    • host overlay snapshot,
    • script id and source hash used to validate restores.

    Only data-shaped ChatML values cross that persistence boundary. Closures, refs, builtins, modules, and tasks are rejected instead of being serialized.


    <config/> controls model selection and generation parameters that ochat currently wires through.

    Supported attributes:

    • model="..." (string)
    • max_tokens="..." (int)
    • temperature="..." (float)
    • reasoning_effort="..." (string; interpreted by the OpenAI client)
    • id="..." (string label; optional)
    • show_tool_call (flag attribute; presence enables inline tool payloads)

    Example:

    <config model="gpt-4o" temperature="0.2" max_tokens="1024"/>

    4.1 show_tool_call (inline vs externalised tool payloads)

    Section titled “4.1 show_tool_call (inline vs externalised tool payloads)”

    When show_tool_call is present, ochat persists tool arguments/results inline using RAW blocks. When absent (default), large tool payloads are written to ./.chatmd/*.json and referenced via <doc .../>.

    See the “Tool calls & tool responses” section for exact layouts.


    <tool/> declarations define what actions the assistant is allowed to take.

    ChatMD supports five tool “shapes”:

    <tool name="read_file"/>
    <tool name="apply_patch"/>

    read_file also has a structured built-in form for declaring named readable roots:

    <tool name="read_file" description="Read project source and documentation.">
    <read id="source" path="lib" description="Relative to ochat's launch directory"/>
    <read id="docs" path="${workspace}/docs-src" description="Project docs"/>
    </tool>

    The self-closing form defaults to one cwd root at ${tool_dir}. Relative <read path="..."/> values also use ${tool_dir}. The model-visible read_file schema gains an optional root enum containing the declared IDs, and its generated description includes every resolved root path and root description. A custom tool description is appended to that generated usage guidance.

    Calls with root use a path relative to that named root:

    {"root":"docs","file":"overview/tools.md","offset":0,"line_count":100}

    Without root, relative paths resolve from the ochat launch directory. Relative and absolute requests are accepted only when the canonical target is inside a configured root. Targets must be existing regular text files. Explicit <read id="computer" path="/"/> grants host-wide read access subject to operating-system permissions. See the tools reference for complete semantics and security guidance.

    Each nested <read/> has these attributes:

    AttributeRequiredMeaning
    idyesUnique model-visible root selector.
    pathyesExisting directory path or standard path expression.
    descriptionnoUsage guidance included in the tool description sent to the model.

    The parent <tool name="read_file"> accepts an optional description. The compatibility name get_contents accepts the same structure but still exposes the model-visible function name read_file.

    Path variables: ${workspace} and source directories

    Section titled “Path variables: ${workspace} and source directories”

    The standard variables accepted in path are ${workspace}, ${tool_dir}, ${prompt_dir}, ${source_dir}, ${session_dir}, ${cache_dir}, and ${home}. Their values are supplied by the execution host. In native local and daemon hosts, ${workspace} is the selected workspace, while ${tool_dir} retains the host launch directory unless an embedder supplies it explicitly. Captured prompts use materialized source directories for ${prompt_dir} and ${source_dir}. See workspaces and paths for the complete native-host contract, and the batch command guide for file-backed execution context. Unknown variables are fatal. All roots must resolve to existing directories before the first model request.

    read_file calls accept file, optional root, optional non-negative offset, and optional non-negative line_count. Root paths and requested files are canonicalized before confinement checks, so .. and symlinks cannot escape the declared roots. The full runtime behavior is documented in the tools reference.

    <tool name="rg" command="rg" description="ripgrep search"/>

    This compact form is desugared into a fixed shell tool. Full shell tools bind to a named runtime and support fixed, structured, conservative chain, raw, and script-file modes:

    <shell_access id="development" extends="builtin:workspace-development@1"/>
    <tool name="shell" type="shell" mode="structured" runtime="development"/>

    The runtime is compiled and authorized before the tool is exposed. See the shell runtime reference and shell tool reference.

    <tool name="triage" agent="prompts/triage.chatmd" local description="Triage a bug report"/>

    5.5 MCP-backed tools (import tools from an MCP server)

    Section titled “5.5 MCP-backed tools (import tools from an MCP server)”
    <tool mcp_server="https://tools.acme.dev" includes="weather,stock_ticker" strict/>
    • Legacy non-long-form declarations use exactly one of command, agent, or mcp_server. Long-form shell tools use type="shell", mode, runtime, and mode-specific attributes/children.
    • For builtin/shell/agent tools, name="..." must be non-empty.
    • Configured read_file roots require non-empty, unique id values and an existing directory path. Only nested <read/> elements are accepted.
    • For MCP tools:
      • name="..." selects a single tool name, or
      • include="a,b" / includes="a,b" selects a comma-separated list, or
      • neither means “no filter” (implementation-dependent; typically exposes the server’s tool catalog).

    For a deeper tool reference (built-ins, schemas, and examples), see: docs-src/overview/tools.md.


    6) Messages: <user>, <assistant>, <system>, <developer>, <msg>

    Section titled “6) Messages: <user>, <assistant>, <system>, <developer>, <msg>”

    Most prompts use the shorthand message tags:

    <system>You are careful and cite sources.</system>
    <user>
    Read README.md and propose a patch.
    </user>

    <msg role="..."> exists as an escape hatch:

    <msg role="user">Hello</msg>

    Supported roles in the runtime conversion are: user, assistant, system, developer, tool.


    7) Tool calls & tool responses (<tool_call>, <tool_response>)

    Section titled “7) Tool calls & tool responses (<tool_call>, <tool_response>)”

    These elements represent the on-disk execution trace of tool usage.

    They are usually written by ochat, not hand-authored.

    Tool calls:

    <tool_call function_name="read_file" tool_call_id="call_123">
    ...
    </tool_call>

    Tool responses:

    <tool_response tool_call_id="call_123">
    ...
    </tool_response>

    There is also a special round-tripping convention for “custom tool calls”:

    • <tool_call type="custom_tool_call" ...>
    • <tool_response type="custom_tool_call" ...>

    7.2 Persistence mode A: inline payloads (show_tool_call)

    Section titled “7.2 Persistence mode A: inline payloads (show_tool_call)”

    When show_tool_call is enabled, ochat persists payloads inline:

    <tool_call tool_call_id="call_123" function_name="read_file" id="item_456">
    RAW|
    { "path": "README.md" }
    |RAW
    </tool_call>
    <tool_response tool_call_id="call_123">
    RAW|
    ...tool output...
    |RAW
    </tool_response>

    7.3 Persistence mode B: externalised payloads (default)

    Section titled “7.3 Persistence mode B: externalised payloads (default)”

    When show_tool_call is not set, payloads are written to ./.chatmd/ and referenced:

    <tool_call function_name="read_file" tool_call_id="call_123" id="item_456">
    <doc src="./.chatmd/0.tool-call.call_123.json" local/>
    </tool_call>
    <tool_response tool_call_id="call_123">
    <doc src="./.chatmd/0.tool-call-result.call_123.json" local/>
    </tool_response>

    This keeps the main transcript readable even when tools exchange large JSON payloads.


    8) Inline content helpers (the “power tools” inside messages)

    Section titled “8) Inline content helpers (the “power tools” inside messages)”

    8.1 <doc src="..." .../> — inline documents (local or remote)

    Section titled “8.1 <doc src="..." .../> — inline documents (local or remote)”
    <user>
    Summarise this:
    <doc src="README.md" local/>
    </user>

    Attributes:

    • src="..." required
    • local (flag): read from disk instead of HTTP
    • strip (flag): if the doc is HTML, strip markup and collapse whitespace into readable text
    • markdown (flag): convert HTML to Markdown (local file or remote URL)

    Precedence:

    • If strip is present, it takes precedence over markdown.

    Local path resolution:

    • Local docs are resolved against the prompt directory first; if still relative and not found, the process CWD is also consulted.

    8.2 <img src="..." [local]/> — inline images

    Section titled “8.2 <img src="..." [local]/> — inline images”
    <user>
    What’s wrong with this UI?
    <img src="assets/screenshot.png" local/>
    </user>

    If local is present, the image is encoded as a data URI before being sent to the API.

    8.3 <agent src="..." [local]> ... </agent> — call a sub-agent

    Section titled “8.3 <agent src="..." [local]> ... </agent> — call a sub-agent”

    An <agent> runs another ChatMD prompt and substitutes its final answer inline.

    Example (summarise a document using a specialised agent prompt):

    <user>
    Here’s a summary produced by my sub-agent:
    <agent src="prompts/summarise.chatmd" local>
    <doc src="README.md" local strip/>
    </agent>
    </user>

    Notes:

    • Any children of <agent> become the sub-agent’s runtime input.
    • Results are cached (so repeated identical agent calls can be cheap).

    9) <import src="..."/> — parse-time include (modularity)

    Section titled “9) <import src="..."/> — parse-time include (modularity)”

    <import/> keeps prompts maintainable by letting you reuse top-level declarations and shared message text (policies, glossaries, style guides).

    Where imports expand

    Imports are expanded recursively at the document top level and inside:

    • <user>...</user>
    • <system>...</system>
    • <developer>...</developer>
    • <agent>...</agent>
    • <msg role="user|system|developer">...</msg>

    Where imports do not expand

    Inside other elements, <import/> is preserved as literal text (for example inside <assistant>, <tool_call>, <tool_response>, <reasoning>).

    Relative src paths resolve against the importing source file’s directory. Top-level imports can supply tools, scripts, and shell-runtime declarations; those declarations retain the imported source context. The optional namespace attribute qualifies imported declarations; see IDs, references, and namespaces. Import cycles and duplicate sibling namespace aliases are rejected.

    Native-local and daemon hosts additionally pin these sources into an artifact tree. Imports/scripts must remain beneath the root prompt directory; parent traversal outside it and absolute import/script paths are rejected during artifact construction, even if standalone parsing succeeds. Runtime ${prompt_dir}/${source_dir} refer to that captured tree, not the original source directories. See pinning and paths.

    For example, a top-level declaration bundle can be imported with:

    <import src="runtime/common.chatmd" namespace="team"/>

    For shared message text:

    <system>
    <import src="policies/safety.md"/>
    </system>

    If enabled, ochat can run a “meta-refine” pass before parsing ChatMD.

    Enable it via:

    • environment variable OCHAT_META_REFINE (truthy values like 1, true, yes, on), or
    • placing the marker comment <!-- META_REFINE --> anywhere in the prompt.

    • Top-level text is forbidden: wrap content in <user>...</user>, etc.
    • Unknown tags at top-level fail: unknown markup becomes literal text and triggers the top-level text rule.
    • Mismatched tags fail: <user>...</assistant> is an error.
    • Unterminated quoted attribute values fail: e.g. alt="... without closing quote.
    • Unterminated RAW blocks fail: RAW| ... |RAW must be closed.
    • Tag names are lowercase and case-sensitive.
    • Shell configuration is strict: unknown/duplicate runtime sections, unresolved references, import/inheritance cycles, unsupported features or profiles, missing required backends, rejected manifests, and administrative ceiling violations fail before dependent tools are published. There is no fallback to direct execution.

    For the complete shell grammar and diagnostics, see ChatMD shell runtime reference.