The geml command does three kinds of thing: convert a document (between json, html, md and geml), read and write by block (list, find, get, set, add, delete, rename, revert), and validate (check, plus each profile's own style check, history verify, codemap verify). Core verbs never carry a profile name. Writes have a guard: a result that does not parse is refused. This page runs every verb, the address forms it accepts and the exit codes against one small document.
Settled fixed by geml --help or the spec. Observed behaviour as run. GEP draft address forms that come from drafts.
| Rule | Source | Status | |
|---|---|---|---|
| General | |||
| 1 | The file position may be - to read stdin. Write verbs (set / add / delete / rename) rewrite the file in place, or print to stdout for -; -o f redirects, -o - is stdout too. Exit 0 success, 1 a document or operation error. | --help | Settled |
| 2 | --root d widens cross-document resolution to directory d, and read and write verbs alike take it. A write is refused when the result fails to parse, so a document that needs the repo root to resolve ../x.md is simply uneditable without --root; otherwise the guard would read its own blind spot as breakage. | --help | Settled |
| 3 | Call list first: the addresses it prints are what every other verb accepts. get without a #id also lists all ids. | --help | Settled |
| Reading | |||
| 4 | list <file> [--json]: one line per addressable block: address, kind, line range; anonymous blocks show as === type or type@hash with a content hash; prose between blocks appears under derived addresses. | --help · §4 | Settled |
| 5 | find <pattern> [file|dir …]: searches block content and answers file<TAB>address, an address rather than a line number, so it pastes straight into get / set. A named file is searched whatever its extension (.md included); a directory walks *.geml only. No match → exit 1. | --help | Settled |
| 6 | get <file> [#id] [--json] [--head|--intro|--body]: a heading id = the whole section; --head the heading line only, --body everything under it, --intro up to the first sub-heading; --json gives the model node. The selector may also be a position, L27 / L27-58: the smallest block containing those lines, which turns a grep hit or a stack-trace line into an address. | --help | Settled |
| Writing | |||
| 7 | set <file> #id [--head|--intro|--body] [--in f[#src]|-]: replaces one block. --in F takes F's block #id, F#src takes #src, otherwise stdin verbatim; default the whole block, --head only the heading line, --body only the body. | --help | Settled |
| 8 | The guard: the result of a write is re-parsed and, if it fails, not written, exit 1, original untouched. Observed: feeding an unterminated code block to set yields "replacement would break the document … not written". | --help · observed | Settled |
| 9 | add (--append | --before #id | --after #id) --in f|-: inserts a fragment, one or more blocks and prose; the content keeps its own ids, collisions are refused. | --help | Settled |
| 10 | delete #id [#id2…]: missing ids are skipped; a reference left dangling is only a warning, not a refusal, with a hint that check will call it an error. Observed: deleting #cmd gives one warning, writes, and check then exits 1. | --help · observed | Settled |
| 11 | rename #old #new: changes the id and every reference, matching at id boundaries (it will not turn #cmd inside #cmdline). Observed: the block and [[#c]] become cmd together. | --help · observed | Settled |
| 12 | replace <old> <new> [--within sel]: an EXPERIMENTAL literal replacement, checked and reported. revert <file> #id [--rev sel]: returns one block to a past version from the .gemlhistory (splice back, resurrect or delete); sel is 0 | -N | id prefix | changed, default -1. | --help | Settled |
| Conversion and validation | |||
| 13 | geml <file> [--to json|html|md|geml] [--from geml|md|json]: the default output is the document-model JSON; --to md is lossy (heading ids and attributes drop, with a note); --to html is self-contained, --fragment gives body markup only; --to geml is a canonical reflow; --from md reads Markdown as GEML, inferred from the extension when possible. | --help · observed | Settled |
| 14 | check [--root d] [--json]: validation only, diagnostics plus an exit code; --json gives the diagnostics array. This is where every right-hand output in this illustrated series comes from. | --help | Settled |
| 15 | Profiles bring their own verbs: style check (exit 0/1/2), history save|get|restore|verify, codemap build|verify|render|serve|refresh|find; also mcp --root (11 tools, every write validated before touching disk) and skill install. Core verbs never carry a profile name. | --help · profiles README | Observed |
| 16 | Address forms: #id, '## Heading' (whole section), L27-58 (position), #fy[2]["Q1"] / #meta["key"] (GEP-0011 coordinates, implemented on this branch), #form#field (GEP-0008 draft, not implemented). | --help · GEP-0011 · GEP-0008 | GEP draft |
A seven-line document. Left, the document; right, the real output of each step.
=== meta title = "cli probe" === # Intro {#intro} Hello world paragraph. === code {#c lang=sh} echo hi === ## Next {#next} See [[#c]].
$ printf '=== code {#c lang=sh}\nunterminated\n' | geml set cli.geml '#c' --in - error: replacement would break the document: unterminated `code` block (no matching === or `=== #c`) (line 6); not written exit 1 · cli.geml untouched
$ geml list cli.geml === meta meta anon L1-3 #intro heading h1 L4-11 Intro #intro-before-c prose L5-5 ← derived address: inside #intro, before #c #c code L6-8 #next heading h2 L9-11 Next $ geml find "echo" cli.geml cli.geml #c exit 0 $ geml find "nothing-here" cli.geml exit 1 $ geml get cli.geml 'L6' position → smallest containing block === code {#c lang=sh} echo hi === $ geml get cli.geml '#intro' --head # Intro {#intro} $ geml get cli.geml '#intro' --body everything under the heading, sub-section #next included Hello world paragraph. === code {#c lang=sh} echo hi === ## Next {#next} See [[#c]].
get 'L6' turns a line number into an address: the smallest block containing it.#intro is the H1 and its section runs to the end of the file, so --body includes ## Next. To change only the heading line, use --head.Three writes followed by one check.
$ geml rename cli.geml '#c' '#cmd' wrote cli.geml $ grep -n cmd cli.geml 6:=== code {#cmd lang=sh} 10:See [[#cmd]]. ← the reference changed with it $ printf '=== note {#n}\nadded\n===\n' | geml add cli.geml --after '#cmd' --in - wrote cli.geml $ geml list cli.geml | tail -4 #intro-before-cmd prose L5-5 #cmd code L6-8 #n note L10-12 #next heading h2 L14-16 Next $ geml delete cli.geml '#cmd' warning: unresolved reference `#cmd` (line 12) — left dangling by delete; run 'geml check' to see it as an error wrote cli.geml exit 0: the delete goes through $ geml check cli.geml error: unresolved reference `#cmd` (line 12) 1 error(s), 0 warning(s) exit 1
| Verb | Stance on references |
|---|---|
| rename | changes the id and every reference, id-boundary safe |
| add | the fragment keeps its own ids; a collision with an existing id is refused |
| delete | a dangling reference is a warning, not a refusal: deleting is the author's explicit intent, check makes it an error afterwards |
| set | a result that does not parse is not written |
The default output is the document-model JSON; Markdown is the lossy side.
$ geml cli.geml --to md note: heading id/attributes dropped (Markdown has no attribute syntax) error: unresolved reference `#cmd` (line 12) diagnostics still reported, output still given --- title: cli probe --- # Intro Hello world paragraph. > added ## Next $ printf '# T\n\nsome md\n' | geml - --from md --to geml # T some md
| Flag | You get | Notes |
|---|---|---|
| --to json | the document model | default; the shape of the §8.4 conformance surface |
| --to html | self-contained HTML | --fragment gives body markup only, assets via pageAssets; the right-hand labels in this series come from it |
| --to md | Markdown | lossy: heading ids and attributes drop, note blocks become blockquotes, data becomes code blocks; a note says so |
| --to geml | canonical reflow | data's json reflowed to two-space indent |
| --from md | reads Markdown as GEML | inferred when the extension is .md; geml notes.md just works |
| --from json | reads a --to json result back | round trip |
--from md inferred); nothing is converted, nothing written. That is the "read long Markdown by block" usage in the geml skill.All verbs share the same selectors.
| Spelling | Names | Source · status |
|---|---|---|
| #id | a block with a declared id; a heading id is the whole section | §4 · settled |
| '## Heading' | by heading text, the whole section likewise | --help · settled |
| L27 · L27-58 | the smallest block containing those lines | --help · settled |
| #a-between-b · #c-before-n · #c-after-p | derived addresses for prose between two blocks; list prints them | §4 · settled |
| === type · type@hash | how an anonymous block appears in list; type@hash pastes back into get | observed |
| #fy[2]["Q1"] · #fy[summary] · #meta["title"] · #cfg["tags"][1] | a table row/cell/column, a summary cell, a meta value, a data value-tree node | GEP-0011 · draft, implemented on this branch (pages 2, 3) |
| #vendor#contacts#email | a field inside a form | GEP-0008 · draft, not implemented (page 6) |
| other.geml#id · other.geml#fy[2] | cross-document, any of the forms above | §5.2 · GEP-0011 |
cli.geml was run in the session scratchpad against the current in-repo build, 1.9.2.
| Command | Result | Maps to |
|---|---|---|
| list · find (hit / miss) · get L6 · get --head / --body | as above; find miss exits 1 | read/write, board 4–6 |
| set with an unterminated block | exit 1 not written, file untouched | board 8 |
| rename · add --after · delete · check | reference rewritten; fragment inserted; dangling warning then check exit 1 | mutate, board 9–11 |
| --to md · - --from md --to geml | lossy note; stdin Markdown read as GEML | conversion |
| geml --help · geml history · geml codemap | verbs and usage text | board 1–3, 12–15 |