livt - Living Text
Collaborate on board. Make it living in text.
Outcome
Collaborative outcomes evolve alongside the product as living text.
Opportunities
- Stale Discovery: Discovery-phase artifacts are not persisted after synchronous collaboration sessions
- Discovery-Development Gap: Persisted discovery artifacts are not leveraged in the development process
Solutions
- Provide a CLI tool that captures collaborative outcomes as text files (YAML, Markdown)
- Track consistency across artifacts via ID-based references with automated checks
- Treat Formulation artifacts (Gherkin scenarios) as generated output, not the source of truth — that lives in the Discovery artifacts
Live Demo
livt dogfoods itself: its own discovery artifacts — stories, story maps, example mappings, and ubiquitous language — are published with livt build as a live demo. It shows exactly what the guides below describe.
Installation
livt ships as a single static binary. Every release is built by the release workflow on GitHub Actions, and every artifact is:
- listed in
checksums.txt(SHA-256) - signed with a GitHub artifact attestation (Sigstore build provenance) tying it to the exact source commit and workflow run
- built from the module as served by the Go module proxy and verified against the Go checksum database, so the binaries can be reproduced from the public source
Pick the method that matches how much of that verification you want automated.
mise (recommended)
The mise github backend
downloads the release binary and verifies its build provenance attestation by
default — a tampered or foreign-built asset fails to install.
mise use "github:boykush/livt@<version>"
Or in mise.toml:
[settings]
lockfile = true # pin checksum + provenance in mise.lock
[tools]
"github:boykush/livt" = "<version>"
With lockfile = true, the
asset checksum and attestation provenance are recorded in mise.lock, so
later installs — on CI or a teammate’s machine — must match bit-for-bit.
Manual download
Download the archive for your platform and checksums.txt from
GitHub Releases, then verify
before running:
# Provenance: proves the artifact was built by this repository's release
# workflow on GitHub Actions (requires the GitHub CLI)
gh attestation verify livt_<version>_<os>_<arch>.tar.gz --repo boykush/livt
# Integrity: check the SHA-256 checksum (macOS: shasum -a 256 -c)
sha256sum --check --ignore-missing checksums.txt
tar -xzf livt_<version>_<os>_<arch>.tar.gz
install livt ~/.local/bin/ # or anywhere on your PATH
Build from source
Source installs are verified by the Go checksum database: everyone gets
byte-identical source for a given version, and a silently re-pushed tag is
rejected. Pin a release version rather than latest so installs stay
reproducible.
go install github.com/boykush/livt@<version>
Or with the mise go backend
(requires Go on your PATH):
mise use "go:github.com/boykush/livt@<version>"
Getting Started
Installation
Install with mise, which verifies the release binary’s build provenance attestation by default:
mise use "github:boykush/livt@<version>"
See Installation for all methods and how release artifacts are verified.
Quick Start
- Create the required directories:
mkdir -p stories discoveries/usm discoveries/example-mappings ubiquitous
- Create your first story in
stories/my-first-story.md:
---
name: My first story
---
As a user
I want to do something
So that I get value
- Build and serve:
livt serve
- Open http://localhost:3000 in your browser. Every page has a sidebar to switch between Example Mappings, Story Maps, Stories, and Ubiquitous Language. Open Stories to find the story you just created:
Stories
Stories are Markdown files with YAML frontmatter, stored in the stories/ directory.
The stories/ directory is the committed story registry: once a story candidate is committed for detailed discovery, its key is owned by stories/{story-key}.md.
Format
---
name: Story display name
---
Story body in Markdown.
The name field in frontmatter is required. The story key is derived from the filename (without .md).
Story keys must be kebab-case, using lowercase letters, numbers, and hyphens.
Key uniqueness is enforced by the filesystem: two committed stories cannot use the same stories/{story-key}.md path.
Example
stories/confirm-story-map.md:
---
name: Confirm story map
---
As a team member
I want to view the story map as a board
So that I can visually confirm the discovery outcomes maintained in text
This story has key confirm-story-map, which is used to reference it from story maps and example mappings.
Commit from a story map
Committing a story candidate from a story map into the registry is handled by the
/story-commit skill (from the discovery-facilitator plugin), rather than a CLI command.
Given the story map and the candidate, it creates stories/{story-key}.md first and then writes
the same key back to the matching candidate in the story map.
Story Maps
Story maps are YAML files stored in discoveries/usm/. They define the structure of a User Story Map with activities, steps, stories, and release slices.
Format
name: Map Name
activities:
- key: activity-key
name: Activity Name
steps:
- key: step-key
name: Step Name
stories:
- key: story-key
name: Story Card Name
release: release-id
- name: Lightweight Story Card
releases:
- id: release-id
name: Release Name
ubiquitous:
- term-key
Ubiquitous Language
ubiquitousis optional: each entry is a ubiquitous language term key- Referenced terms render as pink stickies below the board, linking to
ubiquitous.html#{term-key} - A key with no matching term file renders as a plain pink card
Releases
- Each release defines a horizontal divider on the board
- Each release has an
idthat story cards reference withrelease - Stories with a
releaseappear above that release’s divider - A release without
namedefaults to “Release N” based on position - Stories not in any release appear below all dividers
- Story cards without
keyappear as plain cards and can still belong to a release - A story can reference only one release
Example
discoveries/usm/collaborative-discovery.yaml:
name: Collaborative Discovery
activities:
- key: story-mapping
name: Story Mapping
steps:
- key: discover-stories
name: Discover stories
stories:
- key: confirm-story-context
name: Confirm story context
release: walking-skeleton
- key: confirm-story-map
name: Confirm story map
release: walking-skeleton
- name: Draft session outcomes
- key: slice-releases
name: Slice into releases
stories:
- key: split-release-scope
name: Split release scope
release: release-2
- key: discovery
name: Discovery
steps:
- key: discover-rules
name: Discover rules
stories:
- key: confirm-discovery-outcomes
name: Confirm discovery outcomes
release: walking-skeleton
releases:
- id: walking-skeleton
name: Walking Skeleton
- id: release-2
Example Mappings
Example mappings are YAML files stored in discoveries/example-mappings/. They capture the rules, examples, and questions discovered during an Example Mapping session for a story.
Format
story: story-key
rules:
- id: R-01
name: Rule description
examples:
- id: EX-01
name: Example description
issues:
- https://github.com/owner/repo/issues/1
automated: true
- id: R-02
name: Rule the spec no longer asks for
retired: true
questions:
- id: Q-01
text: Question text
ubiquitous:
- term-key
storyis optional (links to the corresponding story detail page)- IDs must be unique within their rule or question list
ubiquitousis optional: each entry is a ubiquitous language term key, rendered as a pink sticky linking toubiquitous.html#{term-key}. A key with no matching term file renders as a plain pink card.issuesis optional: the rule’s automation Issue URLs on implementation repos (Issue URLs only). The livt repository records the links; their state lives at the URL target. A rule withoutissuesis unlinked.automatedis optional: records the judgment that the rule is actually automated by tests, which is independent of Issues being filed or closed. Absent means not automated. Set it when the rule’s automation lands; unset it when the rule changes.retiredis optional and applies to a rule, an example, or a question: it records that the item is no longer part of the spec. Absent means live.
Retiring an item
An item that no longer holds is marked retired: true — never deleted, and never commented out:
- Deleting frees the ID. With
R-01/R-02/R-03on file, deletingR-03makesR-02the highest, so the next rule takesR-03back. Alivt://mapping/{story-key}/rule/R-03reference already quoted in an Issue or a test comment then resolves to a different rule instead of failing — the quietest way for a reference to break. Retired items keep their IDs taken: new IDs are numbered from the max including them. - Commenting out loses the record. A comment is not part of the YAML structure, so any tool that rewrites the file drops it.
retired: trueis a field and survives.
A retired item stays readable in the file and still resolves by its livt URI, carrying retired: true so the reader can tell. It leaves the board and the Tasks page: a retired question is not an open question, and a retired rule is not waiting for a test.
Visual Layout
The board renders cards in the Example Mapping format:
- Yellow card: Story (top)
- Blue cards: Rules (row below story)
- Green cards: Examples (stacked under their rule)
- Red cards: Questions (separate column)
- Pink cards: Ubiquitous language terms (referenced via
ubiquitous, below the board)
Example
discoveries/example-mappings/confirm-discovery-outcomes.yaml:
rules:
- id: R-01
name: An example mapping can be rendered as a sticky view with only a story reference
examples:
- id: EX-01
name: A YAML with only a story reference displays a single yellow Story card
- id: R-02
name: Cards are laid out following the Example Mapping format
examples:
- id: EX-01
name: Rules are displayed as blue cards in a row below the Story card
- id: EX-02
name: Examples are displayed as green cards stacked under their Rule
- id: EX-03
name: Questions are displayed as red cards in a separate column
questions: []
Ubiquitous Language
Ubiquitous language terms are Markdown files with YAML frontmatter, stored in the
ubiquitous/ directory. Each file is a single term, and livt renders them as a
table you can browse like a database.
Format
---
name: Term display name
---
Definition in Markdown.
The name field in frontmatter is the display term. The term key is derived
from the filename (without .md) and must be kebab-case, using lowercase
letters, numbers, and hyphens. The body is the term’s definition.
Example
ubiquitous/story-map.md:
---
name: Story Map
---
A board to overview activities, steps, and stories alongside release slices.
Contexts
A term can be scoped to a context by putting it in a directory:
ubiquitous/
story-map.md # holds across contexts
billing/
invoice.md # what "invoice" means in billing
shipping/
invoice.md # what "invoice" means in shipping
A context is optional. Cut one when a word means different things to different parts of the domain — the DDD bounded context — and leave a term at the root when its meaning holds everywhere. Most livt repositories start with every term at the root and grow contexts only where the language actually diverges.
The directory is what makes a term unique, so invoice above is three separate
terms, not one with three labels. Each has its own definition, its own row, and
its own address. A term reference is therefore {ctx}/{term-key} for a scoped
term and {term-key} for a context-free one, and the two never resolve to each
other: a bare invoice names the root term even when billing/invoice exists.
Contexts are one directory deep. ubiquitous/billing/eu/invoice.md is not a
term livt can address, and is left out of the glossary.
Visual Layout
livt build renders every term as a row on a single page at ubiquitous.html,
with Term, Key, and Definition columns. A scoped term shows its
context beneath its key. Each row carries an id={term-ref} anchor, so a term is
linkable as ubiquitous.html#{term-key} or ubiquitous.html#{ctx}/{term-key}.
When a livt repository cuts at least one context, the page carries a filter bar above the
table. Selecting a context narrows the table to that context’s terms, and the
selection is mirrored in the ?context= query parameter so a filtered view is
shareable. Context-free terms are not part of any single context, so they drop
out while a filter is on rather than being repeated under every chip.
Referencing terms from boards
Story maps and example mappings can declare the terms they use with a top-level
ubiquitous list of term references:
ubiquitous:
- story-map
- billing/invoice
Referenced terms render as pink stickies below the board, each linking to its glossary row, with the context shown on the sticky when it has one. A reference with no matching file renders as a plain pink card, so references degrade gracefully. See Story Maps and Example Mappings.
Commands
livt serve
Build artifacts and start a local server.
While the server is running, livt watches the input directories
(discoveries/example-mappings, stories, discoveries/usm, and ubiquitous).
When a file changes, livt rebuilds and reloads the page in the browser
automatically, so you can preview refinements while editing.
livt serve [flags]
| Flag | Short | Default | Description |
|---|---|---|---|
--port | -p | 3000 | Port to listen on |
--out | -o | dist | Output directory |
livt build
Build static HTML from artifacts without starting a server.
livt build [flags]
| Flag | Short | Default | Description |
|---|---|---|---|
--out | -o | dist | Output directory |
livt mcp
Run an MCP (Model Context Protocol) server that exposes the livt repository (story maps, stories, example mappings, and the ubiquitous language). An implementation repo’s coding agent can then fetch the spec for a story or rule without reading livt’s source.
The livt repository usually lives in a separate checkout from the consumer, so point at
it with --root or the LIVT_ROOT environment variable. The flag takes
precedence; both default to the current directory.
livt mcp [flags]
| Flag | Default | Description |
|---|---|---|
--root | $LIVT_ROOT, then . | Path to the root of the livt repository |
--http | (off; stdio) | Serve over Streamable HTTP at this address (e.g. localhost:5488) instead of stdio; the MCP endpoint is <addr>/mcp |
Transports
By default the server runs over stdio, spawned per consumer — the client
launches livt mcp as a subprocess. stdio is the recommended form: the
binding to the livt repository is read at spawn time from the consumer’s own
environment, so each workspace resolves against what it declares, and a
session never outlives the declaration it was started with.
Reach for --http when reachability is the problem — a client that
cannot spawn processes, or several repos sharing one machine-wide server
without each holding a checkout of the livt repository. It serves Streamable
HTTP from one long-running process:
livt mcp --http localhost:5488
Each consumer points its MCP client at http://localhost:5488/mcp. The server
is stateless and read-only, so one process backs many clients; keep git pull
current on its checkout and the served spec (and spec_version) updates live.
The binding trades the other way from stdio: the server fixes its repository
once, at start, for every client — not per workspace.
For distributing this client configuration to implementation repos, see the
livt-mcp plugin.
This mode assumes local use with no authentication — the server is meant to bind
to localhost, not a public network.
Tools
| Tool | Arguments | Returns |
|---|---|---|
list_stories | opportunity (optional) — an opportunity name, matched exactly against a story map display name; keeps only the stories on that map, and an unknown name yields an empty list | Every story with its key and name. Each entry links to its story resource (uri); stories that have an example mapping also include example_mapping_uri, and stories on a story map carry opportunities — one map name plus story map resource URI per map they sit on. |
list_story_maps | — | Every story map with its name and its story map resource URI (uri). |
list_terms | — | Every ubiquitous language term with its key, display name, and term resource URI (uri) — including terms no board references. A term scoped to a context also carries ctx, which is part of what identifies it. |
Resources
The spec itself is exposed as resources, addressable by URI (story map → story → mapping → rule → example, with questions and ubiquitous terms linked from mappings and story maps):
| URI | Returns |
|---|---|
livt://story-map/{map_name} | A story map: activities, steps, story cards, and releases. Committed story cards link to their story resource. {map_name} is the map’s display name (percent-encoded) — the same identifier the build output uses for story-map/{name}.html. |
livt://story/{story_key} | The story’s name, body, and frontmatter meta (e.g. issue), plus example_mapping_uri when a mapping exists and opportunities — the story maps the story sits on, as map name plus story map resource URI. |
livt://mapping/{story_key} | The story’s example mapping (rules, examples, questions, ubiquitous terms). Each rule, example, and question carries its own uri, and ubiquitous_terms resolves each referenced term to its resource URI. Retired entries are listed too, flagged — the mapping is the structural record their ids are numbered from. |
livt://mapping/{story_key}/rule/{rule_id} | A single rule and its examples, plus its recorded automation: issues (automation Issue URLs) and automated (whether the rule is automated by tests). Rules inside livt://mapping/{story_key} carry the same fields. |
livt://mapping/{story_key}/rule/{rule_id}/example/{example_id} | A single example of a rule. Example ids are numbered within their rule, so the address carries {rule_id} — EX-01 alone does not identify an example. |
livt://mapping/{story_key}/question/{question_id} | A single question. Questions hang off the mapping rather than off a rule, so the address stops at {story_key}. |
livt://ubiquitous/{term_key} | A ubiquitous language term’s name and definition. This shape addresses a term whose meaning holds across contexts. |
livt://ubiquitous/{ctx}/{term_key} | A term scoped to one context, carrying ctx alongside its key. A context is optional and part of the address, so the same {term_key} can name one term at the root and another inside a context; the two never resolve to each other. |
A retired rule, example, or question keeps resolving by its URI and carries
retired: true, so a reference to it reads as retired rather than failing (or,
worse, landing on whatever reused its id). Live items omit the field.
Read them with resources/read; all appear in resources/templates/list. The
server advertises templates only — there is no concrete resource list and no
change notification (subscribe); every read is served fresh from disk.
Every tool and resource payload also includes a spec_version field – the
short git revision of the livt repository – so consumers can tell which version of the
spec they are reading and detect drift.
Citing the livt repository
The handshake carries instructions, so every session tells the consuming agent
how to reference the livt repository in what it produces: copy the uri from the result
verbatim, and never write a bare id. Ids are unique only within one mapping
file, so R-02 exists in every mapping and identifies nothing on its own.
// livt://mapping/place-order-with-saved-card/rule/R-13/example/EX-01
That applies wherever a reference leaves the livt repository – a test comment, an issue body, a commit message, a PR description. A published living-document URL is a convenience link for humans, not the citation form: it depends on where the site is deployed, and the livt URI does not.
A URI cited this way is read back with livt resolve, which
needs no MCP client – so the citation stays followable for CI, an editor, or a
reader who only has a checkout.
livt resolve
Resolve a livt URI against the livt repository, without running an MCP client. A rule, example, or question cited in a test comment, an issue body, or a commit message can then be read back by whoever needs it — CI, an editor, or an agent that is not connected to livt over MCP.
livt resolve <uri> [flags]
| Flag | Default | Description |
|---|---|---|
--root | $LIVT_ROOT, then . | Path to the root of the livt repository |
--format | json | Output form: json or url |
--base-url | — | Root of the deployed site; required by --format url |
Every URI shape the MCP server exposes as a resource resolves here: mappings, rules, examples, questions, stories, story maps, and ubiquitous language terms.
Output forms
--format json (the default) prints the same payload an MCP
resources/read of that URI serves, spec_version included — the two surfaces
resolve through one code path, so a consumer sees one shape whichever way it
asked:
livt resolve livt://mapping/trace-test-to-rule/rule/R-04
--format url prints the item’s page on the deployed site, using the same
URI-to-page derivation the site build anchors its
stickies to:
$ livt resolve livt://mapping/trace-test-to-rule/rule/R-04 \
--format url --base-url https://boykush.github.io/livt
https://boykush.github.io/livt/mapping/trace-test-to-rule.html#rule-R-04
The livt repository is still read in this form, even though the page path derives from the URI alone: a link to a page that was never built is worse than an error.
When a URI does not resolve
Both cases exit non-zero and explain themselves on stderr, because they need different fixes:
| Malformed URI | The string is not a livt URI at all. The error lists every shape one can take. Fix the URI. |
| Nothing to resolve | The URI is well formed but the livt repository holds no such item, e.g. rule "R-99" not found in story "trace-test-to-rule". Fix the reference, or add the item. |
livt version
Print the version of livt.
livt version
File Structure
Input
project-root/
stories/
{story-key}.md # Story files
discoveries/
usm/
{map-name}.yaml # Story map files
example-mappings/
{story-key}.yaml # Example mapping files
ubiquitous/
{term-key}.md # Terms holding across contexts
{ctx}/
{term-key}.md # Terms scoped to one context
- Story keys are derived from filenames (without extension)
- Story keys must be kebab-case: lowercase letters, numbers, and hyphens
- The
stories/directory is the committed story registry, andstories/{story-key}.mdprovides story key uniqueness - Example mapping filenames must match story keys to link them
- Term keys are derived from filenames, and a term’s context from the directory holding it. The path is what makes a term unique, so the same key can sit at the root and under a context as two separate terms
- A context is optional and one directory deep; terms nested deeper are not addressable and are left out of the glossary
- A term is anchored as
ubiquitous.html#{term-key}, orubiquitous.html#{ctx}/{term-key}when it is scoped
Output
livt build generates the following structure:
dist/
index.html # Example mappings overview (home)
story-maps.html # Story maps overview
stories.html # Story list
ubiquitous.html # Ubiquitous language table
tasks.html # Open questions and un-automated rules
story/
{story-key}.html # Story detail pages
mapping/
{story-key}.html # Example mapping boards
story-map/
{map-name}.html # Story map boards
Every page shares a left sidebar that links the four resource types (Example Mappings, Story Maps, Stories, Ubiquitous Language) and, below them, Tasks. The overview pages render each example mapping and story map as a preview card.
tasks.html gathers what the livt repository leaves unfinished, so neither kind has to
be hunted for board by board:
- Open Questions — every
questionsentry across the example mappings. These close by a conversation, so they feed the next discovery session. - Un-automated Rules — every rule with no
automated: truerecorded. These close by a test, so they read as the list of behaviour still to build.
Retired items are on neither list, and off the boards as well: nothing can close them, so they would sit here forever.
Each item names the story it came from and links to its own sticky on that
story’s mapping board. Both lists are filtered together by opportunity, and the
selection is mirrored in the ?opportunity= query parameter so a filtered view
is shareable.
Items carry no status beyond being listed. A rule records its automation issue URLs but not their open/closed state, and the build never queries the issue tracker, so an item leaves this page by being resolved — a question becoming a rule, a rule becoming automated — rather than by moving through states.
livt URI
A livt URI names one point in the livt repository — a rule, an example, a question, a story, a story map, or a ubiquitous language term. It is the form to reach for whenever a reference has to survive outside the livt repository: a test comment, an issue body, a commit message.
livt://mapping/{story-key}/rule/{rule-id}
The MCP server addresses its resources by these URIs — see Commands for what each one returns — and the generated site anchors its stickies to them. Neither depends on where the site is deployed.
Why not a bare ID
R-02 on its own is not a reference. IDs are numbered within the file that
holds them, so every example mapping in this repository defines an R-02, and
every rule that has examples defines an EX-01. Even R-13 EX-01 narrows it to
nothing — the reader still cannot tell which mapping was meant, and neither can
a search. The story key is the part that makes it addressable:
livt://mapping/trace-test-to-rule/rule/R-02/example/EX-01
Relative to a repository
The widening stops there: a livt URI never says which livt repository
it belongs to. Its interpretation is uniform — one point in the livt
repository at hand — the same shape http://localhost/ has, where every
reader agrees on the meaning while the referent depends on where it is
resolved. Which repository answers is supplied by the consumer’s own
declaration (--root / LIVT_ROOT), so scope lives in the workspace, not
in the name. That is what keeps a citation short, and what lets the same
reference work in every checkout of the same repository.
The first segment is drawn from a closed set — mapping, story,
story-map, ubiquitous — so a future form that names a repository in
that position stays open without breaking any URI written today.
URI to page
livt build renders the livt repository as a static site, and this is where each URI
lands in it. Paths are relative to the output root.
| livt URI | Page |
|---|---|
livt://mapping/{story-key} | mapping/{story-key}.html |
livt://mapping/{story-key}/rule/{rule-id} | mapping/{story-key}.html#rule-{rule-id} |
livt://mapping/{story-key}/rule/{rule-id}/example/{example-id} | mapping/{story-key}.html#rule-{rule-id}-example-{example-id} |
livt://mapping/{story-key}/question/{question-id} | mapping/{story-key}.html#question-{question-id} |
livt://story/{story-key} | story/{story-key}.html |
livt://story-map/{map-name} | story-map/{map-name}.html |
livt://ubiquitous/{term-key} | ubiquitous.html#{term-key} |
livt://ubiquitous/{ctx}/{term-key} | ubiquitous.html#{ctx}/{term-key} |
An example’s anchor repeats its rule for the same reason its URI does: EX-01
recurs under every rule of a board, so #example-EX-01 would land on whichever
one happened to be first. The sticky’s own badge still shows the local EX-01 —
that is the ID the livt repository numbers — while the link behind it carries the rule.
A term takes a context the same
way, and for the same reason: invoice can mean one thing in billing and
another in shipping, so the context is part of the address rather than a label
on it. The context is optional — a term whose meaning holds everywhere keeps the
one-segment form — and the two shapes never resolve to each other.
A retired item has no sticky,
so its URI lands on the board with nothing to scroll to. The URI still resolves:
ask the tooling, which answers with the item and retired: true. Deriving a
page is how a reference is shown, not how it is resolved.
Store the URI, render the URL
Nothing in the livt repository records where the site is deployed. The deployment URL is
prefixed onto the paths above only while a page is being rendered, which is why
the same livt repository can be served from a local livt serve, from GitHub Pages, and
from an internal host at once:
livt://mapping/trace-test-to-rule/rule/R-02
-> mapping/trace-test-to-rule.html#rule-R-02
-> https://boykush.github.io/livt/demo/mapping/trace-test-to-rule.html#rule-R-02
A deployed URL is therefore a fine thing to paste into a chat and a poor thing to commit: it goes stale the moment the site moves, and it buries the identity it was meant to carry. Keep the URI; let the URL be derived.
Where each form comes from
- A URL, to send to a person — take it from the board. Every sticky shows its own ID in the bottom-right corner; clicking it copies the deployed URL of that sticky, so what you paste already points at the exact card.
- A livt URI, to leave in an artifact — take it from the tooling rather than
the board. Every rule, example, and question the MCP server returns carries
its own
uri, so an agent writing a test can quote the point it is proving without composing the URI by hand.
Keeping the two apart is what stops the site from becoming a second source of identity. The board is for reading and sharing; the URI is what the livt repository actually knows about itself.
Reading one back
A URI left in a test comment is only worth as much as the ability to follow it,
and that has to work for a reader who is not running an MCP client — CI, an
editor, or someone with just a checkout. livt resolve
turns any of the shapes above back into the point it names:
$ livt resolve livt://mapping/trace-test-to-rule/rule/R-02
{ "spec_version": "...", "rule": { "id": "R-02", ... } }
$ livt resolve livt://mapping/trace-test-to-rule/rule/R-02 \
--format url --base-url https://boykush.github.io/livt
https://boykush.github.io/livt/mapping/trace-test-to-rule.html#rule-R-02
The URL form is derived from the table above rather than restated, so a link the CLI hands out and the anchor the build writes cannot drift apart.