ascribe versions
List a project’s release lines, say what differs between two, release a line in one command, export one into a folder of HTML, and retire it from the source.
For AI agents: the documentation index is at llms.txt, and this page is available as Markdown.
The commands for a project’s release lines: what they are, what differs between two, the release that makes a planned line live, the export that keeps a copy of a line once it leaves, and the retirement that takes it out of the source. Publish a site for each version and Export and retire a line are the tasks they’re used in. The options every command accepts are in the command reference, and the JSON each writes with --format json is in JSON reports: ascribe versions.
ascribe versions
Lists a project’s release lines, says what differs between two, releases one, archives one, and retires one. Each reads the lines from ascribe.toml’s [releases], and a line is named as a mark names it: the target, then the version, "self-managed 3.4".
ascribe versions list
Shows each target’s lines, newest first: whether each is current, live, planned, or archived, the output and base path of each live line on each build versioned by the target, with how many pages it publishes, the last day of support from ends, and an archived line’s address and the day it ended. It reads the project and builds nothing. In examples/quill:
self-managed, versioning `site`
3.5 current site at /, 6 pages
3.4 live site@3.4 at /3.4/, 5 pages--format <FORMAT>: How to show the lines.text(the default): Short text, for people and agents to read.json: One JSON document, for tools.
With --format json, it writes the same in the document its schema describes. It exits with 0, and 2 when the project doesn’t load.
The JSON for this example
{
"schema_version": 1,
"ascribe_version": "0.4.4",
"targets": [
{
"target": "self-managed",
"builds": [
"site"
],
"lines": [
{
"line": "3.5",
"status": "current",
"outputs": [
{
"build": "site",
"base": "/",
"pages": 6
}
]
},
{
"line": "3.4",
"status": "live",
"outputs": [
{
"build": "site@3.4",
"base": "/3.4/",
"pages": 5
}
]
}
]
}
]
}ascribe versions changes
Says what differs between two lines of a target, from the marks alone, with nothing built: the pages, sections, blocks, and table rows that are in the second line and not the first (added), in the first and not the second (removed), and in both with a different state, such as deprecated in one (changed). Each is listed with its file and line, the page, the heading it’s under, and its first line. A page whose own mark adds it is one entry, not an entry for each part of it. A page added in place of one removed, which replaces links it with either way, is one entry too: it’s listed as added, and names the page it’s in place of, which isn’t listed as removed. ascribe versions changes "self-managed 3.4" 3.5 in examples/quill:
In 3.5 and not 3.4:
docs/configure.md:5: the page "Configure the agent", in place of "Configure the agent (3.4)" at docs/configure-legacy.md:5
docs/keys.md:25: the section "Sign webhook events"
docs/webhooks.md:5: the page "Webhooks"The second line is a version of the first’s target, and either may come first. Each must be declared in the list, whether live, planned, or archived. A line holds what’s marked for a version in it, not what’s marked for a later one, so a line is compared as it was released, whichever is current.
FROM: The line compared from, named as a mark names it: “self-managed 3.4”.TO: The line compared to, of the same target: 3.5.--format <FORMAT>: How to show what differs.text(the default): What was added, removed, and changed, for people.json: One JSON document, for tools.markdown: A first draft of release notes, with a heading for each page.
--format markdown writes a first draft of release notes: a heading for the line, and one for each page, with what’s new, gone, and changed on it.
## self-managed 3.5, since 3.4
### Configure the agent
In `docs/configure.md`.
- New: the page, in place of "Configure the agent (3.4)" in `docs/configure-legacy.md`
### API keys
In `docs/keys.md`.
- New: the section "Sign webhook events"
### Webhooks
In `docs/webhooks.md`.
- New: the pageWith --format json, it writes the document its schema describes:
The JSON for this example
{
"schema_version": 1,
"ascribe_version": "0.4.4",
"target": "self-managed",
"from": "3.4",
"to": "3.5",
"added": [
{
"file": "docs/configure.md",
"line": 5,
"kind": "page",
"page": "Configure the agent",
"text": "Configure the agent",
"in_place_of": {
"file": "docs/configure-legacy.md",
"line": 5,
"page": "Configure the agent (3.4)"
}
},
{
"file": "docs/keys.md",
"line": 25,
"kind": "section",
"page": "API keys",
"heading": "Sign webhook events",
"text": "Sign webhook events"
},
{
"file": "docs/webhooks.md",
"line": 5,
"kind": "page",
"page": "Webhooks",
"text": "Webhooks"
}
],
"removed": [],
"changed": []
}| Code | Meaning |
|---|---|
0 |
Compared, whether or not anything differs |
2 |
It couldn’t compare: the project doesn’t load, or a line isn’t named as a mark names one or isn’t declared |
ascribe versions release
Releases a line: rewrites each mark’s planned for its target to the line’s version, makes the line live, and archives the old current line where no build is versioned by the target. When a line ships has each step. It edits ascribe.toml in place, keeping its comments and order, and each mark where it’s written. A feature’s spec names its versions, so planned in one is left as it is. In a copy of examples/quill that plans 3.6, with a section marked @available: self-managed planned, ascribe versions release "self-managed 3.6" --next 3.7 --dry-run says what it would do, --next adding 3.7 to planned so that planned names it from then on. Its site is versioned, so 3.5 stays live, with an output of its own, site@3.5:
self-managed 3.6 would be live, and current.
Would rewrite 1 mark from `self-managed planned` to `self-managed 3.6`, in 1 file.
self-managed 3.7 would be planned.
What a reader sees changes in:
the file the lines share: 1 file changed
site's plain output: 6 files changed
site's site output: 2 files changed
site@3.4's plain output: 5 files changed
site@3.4's site output: 1 file changed
site@3.5's plain output: 8 files added
site@3.5's site output: 15 files added
Dry run: nothing written. It would write ascribe.toml and 1 file under docs.When it archives the old current line, the marks that name a version in it say nothing any more: every line left has what they mark. It removes or rewrites each one available-same-in-every-line would report, as that check’s fix does: a mark with nothing left to say goes, and one that still says something, such as a state or another target, keeps that alone, as self-managed beta 3.5 becomes self-managed beta. A feature’s spec is rewritten the same way. The text lists each with its file and line, and the JSON as cleared and cleared_features, so after it ascribe check reports no available-same-in-every-line for the archived line. A mark that said nothing before the release, naming an older line, is left for ascribe check to report. A badge build showed what these marks said, so the pages it drew a badge on are among the outputs the release changes.
It builds the project in memory before and after, and refuses when rewriting the marks or archiving the old line would change what a reader sees: both are meant to change nothing, so a change there is a mark the rewrite got wrong. What the release itself changes, each page whose content was planned and is now live, it lists by build. It writes nothing when it refuses, and --dry-run says what it would do and writes nothing.
It writes only files git can put back as they were: ascribe.toml and each page it rewrites must be tracked, and unchanged since the last commit. So the release is one commit to read, and git checkout undoes it. With --force it writes them anyway, as it must outside a repository.
In a project whose [from] takes the target’s lines from another project’s ascribe.toml, the release is made where they’re written: in that project, its ascribe.toml, its pages’ marks, and its outputs. The text says so first, and the JSON’s from names the file, from the current folder; files are then from that project’s root. The lines move for every project that takes them, from the one release. A mark on a page of a project that takes them isn’t rewritten.
A line that isn’t planned can be released too, as long as it falls between the current line and the oldest planned one: a patch release, 3.5.1 when the lines are written as patches, needs no entry first. planned keeps naming the planned line, so no mark is rewritten.
It refuses a line that’s live or archived already, so running it twice changes nothing the second time, and a planned line with an older one planned before it. A mark it can’t rewrite where it’s written, such as a frontmatter available: that spans lines, is named with its file and line, to write on one line first.
LINE: The line to release, named as a mark names it: “ascribe 0.4.0”.--next <LINE>: The line planned after the released one, added to planned, so thatplannednames it from then on.--dry-run: Say what the release would do, and write nothing.--force: Write the files even when git can’t put them back: untracked or changed files, or no repository at all.--format <FORMAT>: How to show what the release did.text(the default): Short text, for people and agents to read.json: One JSON document, for tools.
With --format json, it writes the document its schema describes:
The JSON for this example
{
"schema_version": 2,
"ascribe_version": "0.4.4",
"written": false,
"target": "self-managed",
"line": "3.6",
"was_planned": true,
"next": "3.7",
"next_added": true,
"archived": null,
"files": [
{
"file": "docs/keys.md",
"marks": [
{
"line": 30,
"from": "self-managed planned",
"to": "self-managed 3.6"
}
]
}
],
"cleared": [],
"cleared_features": [],
"outputs": [
{
"build": "",
"output": "_ascribe",
"added": [],
"removed": [],
"changed": [
"versions.json"
]
},
{
"build": "site",
"output": "plain",
"added": [],
"removed": [],
"changed": [
"configure.md",
"install-agent.md",
"keys.md",
"quickstart.md",
"upgrade.md",
"webhooks.md"
]
},
{
"build": "site",
"output": "site",
"added": [],
"removed": [],
"changed": [
"_ascribe/help.json",
"keys.md"
]
},
{
"build": "site@3.4",
"output": "plain",
"added": [],
"removed": [],
"changed": [
"configure-legacy.md",
"install-agent.md",
"keys.md",
"quickstart.md",
"upgrade.md"
]
},
{
"build": "site@3.4",
"output": "site",
"added": [],
"removed": [],
"changed": [
"upgrade.md"
]
},
{
"build": "site@3.5",
"output": "plain",
"added": [
"_fragments/prerequisites.png",
"configure.md",
"install-agent.md",
"keys.md",
"playground.png",
"quickstart.md",
"upgrade.md",
"webhooks.md"
],
"removed": [],
"changed": []
},
{
"build": "site@3.5",
"output": "site",
"added": [
"_ascribe/headers.json",
"_ascribe/help.json",
"_ascribe/nav.json",
"_ascribe/pages.json",
"_ascribe/permalinks.json",
"_ascribe/redirects.json",
"_ascribe/schema.ts",
"_fragments/prerequisites.png",
"configure.md",
"install-agent.md",
"keys.md",
"playground.png",
"quickstart.md",
"upgrade.md",
"webhooks.md"
],
"removed": [],
"changed": []
}
],
"from": null
}| Code | Meaning |
|---|---|
0 |
Released, or with --dry-run, it would be |
2 |
Nothing was written: the line can’t be released, a mark can’t be rewritten, a step that should change no output changes some, or git can’t put the files back and there’s no --force |
ascribe versions export
Renders a line, live or planned, into a folder of HTML that needs nothing else: every page of the line with a statement of what it is, a sidebar with a search box, a contents page, and a print stylesheet, marked so crawlers leave it alone, working from a disk with no server. Its archive is a copy of the line to host once the line leaves the window; exporting a line doesn’t make it archived, which retire does. Export and retire a line says what’s in it, and the archive contract its layout. In a copy of examples/quill, ascribe versions export "self-managed 3.4" --to archive/3.4 --current https://docs.quill.dev/ writes the archive and says what to add to ascribe.toml once it’s hosted:
Wrote 5 pages to archive/3.4. Open archive/3.4/index.html.
Its search index is 4 KB.
A link is written as text, since the page it leads to isn't in the line:
docs/upgrade.md: "webhooks" led to /webhooks/ in 3.5
Host the folder, then record its address:
[releases.self-managed.archived]
"3.4" = { url = "…" }--to is from the current folder, and is a folder that’s empty or one an archive was written in: writing again replaces the files its manifest lists, and nothing else. It refuses a folder with anything else in it, writing nothing there. It writes nothing into the project, runs the checks for the line’s build first, and refuses when they find an error. A planned line’s build is checked too, though ascribe check checks only the live lines, so the error names the build it found them in. Exporting the current line works, and the text says it’s unusual. A link to a page the line doesn’t have is written as its text, and the text names each one. The text says how large the search index is, and when it’s over 8 MB, that a reader’s first search waits for it to load; it’s written whatever its size.
It was ascribe versions archive until this release, which named the copy for the state the line reaches only once it’s retired. The old name still runs it, with a warning, for one more release, so a script or CI job that names it keeps working until you change it.
LINE: The line to export, named as a mark names it: “self-managed 3.4”. It’s live or planned.--to <DIR>: The folder to write the archive in, from the current folder: an empty one, or one an archive was written in, whose files it replaces.--build <NAME>: The versioned build to export the line of, when two builds are versioned by the line’s target.--current <URL>: The address of the current documentation, which the statement at the top of every page links to.--format <FORMAT>: How to show what was written.text(the default): Short text, for people and agents to read.json: One JSON document, for tools.
With --format json, it writes the document its schema describes:
The JSON for this example
{
"schema_version": 1,
"ascribe_version": "0.4.4",
"target": "self-managed",
"line": "3.4",
"status": "live",
"build": "site",
"folder": "archive/3.4",
"contents": "index.html",
"pages": 5,
"assets": 2,
"removed": 0,
"search_index_bytes": 3195,
"search_index_large": false,
"unlinked": [
{
"file": "docs/upgrade.md",
"text": "webhooks",
"href": "/webhooks/",
"line": "3.5"
}
]
}| Code | Meaning |
|---|---|
0 |
The archive is written |
2 |
Nothing was written: the project doesn’t load, the line isn’t declared or is archived, no build or more than one is versioned by its target and --build doesn’t say which, the checks find an error, or the folder isn’t empty and isn’t an archive |
ascribe versions retire
Takes the oldest live line out of the window once its archive is hosted: moves it from live to archived with its address and the day it ended, rewrites each mark for its target to the simplest spec that means the same in every line still live or planned, lists what no line has any more, and says where each of the line’s addresses should go. Export and retire a line has each step. It edits ascribe.toml in place, keeping its comments and order, and each mark where it’s written. In examples/quill, ascribe versions retire "self-managed 3.4" --url https://archive.quill.dev/3.4/ --dry-run --show-unused says what it would do:
self-managed 3.4 would be archived, at https://archive.quill.dev/3.4/.
Would rewrite 6 marks in 5 files. No live line's output would change.
Expected: builds that aren't lines of self-managed change only self-managed's part of their availability lines:
cloud's plain output changes install-agent.md
cloud's site output changes install-agent.md
1 page is in no live line:
docs/configure-legacy.md:5: the page "Configure the agent (3.4)"
Delete it with: ascribe versions retire "self-managed 3.4" --url https://archive.quill.dev/3.4/ --remove-unused
1 page was replaced by another. Its old address, and where it should go:
/3.4/configure-legacy/ → /configure/
4 other addresses of 3.4 go to the same page in the current line, or in the archive: --format json lists each.
Dry run: nothing written.The line’s ended is its date in ends, which goes, or, when it had none, the day the checks run as. Its address is --url, the one its archive is read at.
It builds the project in memory before and after, and refuses when a live line’s output changes: a line’s output never showed a version from before the window, so the rewrite is meant to change none. A build that isn’t a line of the target can change what it shows: a badge build shows those versions, and a filter build for an older version gains the content a mark no longer limits to later ones. So it names each such build’s pages, as those it adds, removes, and changes, and the rewrite needs --force to go ahead.
A page whose only change is the target’s part of its availability lines, such as a cloud build’s self-managed (preview, 3.4+) becoming self-managed (preview), is the retirement doing its job: it’s listed under its own heading, as expected in the JSON, and needs no --force. Any other change does, another target’s part of the same line included. It writes only files git can put back as they were, as release does, or anything with --force.
It refuses a line that isn’t live, which is why running it twice changes nothing the second time; the current line, and a live line that isn’t the oldest; a line with no --url, since after the clean-up the source no longer holds what the line needs, so ascribe versions export comes first; and a target no build is versioned by, whose list release already keeps to one live line. Like release, it’s made in the project that writes the lines, when [from] takes them from there.
It deletes nothing unless it’s told to. A page, section, block, or table row that no live or planned line has is listed with its file, line, and first line, and so is a mark that says nothing once rewritten: one for every target its scope allows, generally available, with nothing inside it relying on it. Such a mark still draws a badge, so the rewrite keeps it. --show-unused lists each, and --remove-unused deletes them, along with the replaces line of a page that replaced one it deletes. Both work on a line retired already.
Each address the line published goes to the same page in the current line, to the page that replaces it there, or to its page in the archive when the current line has neither. The text names the replaced pages, and the JSON lists every address. In a project with ascribe.addresses, each address that goes to a page in the current line is recorded there as a [moved] entry keyed by the address, so the build writes its redirect, and the text says how many; the JSON’s recorded says whether they were. The archive’s addresses aren’t recorded, since an entry leads to a page of the project, and without the file nothing is: those are yours to add to the host’s redirects.
LINE: The line to retire, named as a mark names it: “self-managed 3.4”. It’s the oldest live line.--url <URL>: The address the line’s archive is read at, recorded as itsurlin [releases].--dry-run: Say what the retirement would do, and write nothing.--force: Write the files even when git can’t put them back, and rewrite the marks even when a build that isn’t a line of the target changes.--show-unused: List each block, row, section, and page no live or planned line has, and each mark that says nothing once rewritten, with its page and first line. Works on a line retired already.--remove-unused: Delete what –show-unused lists. Works on a line retired already.--format <FORMAT>: How to show what the retirement did.text(the default): Short text, for people and agents to read.json: One JSON document, for tools.
With --format json, it writes the document its schema describes, here as of 16 October 2026:
The JSON for this example
{
"schema_version": 1,
"ascribe_version": "0.4.4",
"written": false,
"retired": true,
"target": "self-managed",
"line": "3.4",
"url": "https://archive.quill.dev/3.4/",
"ended": "2026-10-16",
"features": [],
"files": [
{
"file": "docs/configure.md",
"marks": [
{
"line": 5,
"from": "self-managed 3.5",
"to": "self-managed"
}
]
},
{
"file": "docs/install-agent.md",
"marks": [
{
"line": 5,
"from": "cloud, self-managed preview 3.3",
"to": "cloud, self-managed preview"
},
{
"line": 80,
"from": "cloud, self-managed preview 3.4",
"to": "cloud, self-managed preview"
}
]
},
{
"file": "docs/keys.md",
"marks": [
{
"line": 25,
"from": "self-managed 3.5",
"to": "self-managed"
}
]
},
{
"file": "docs/upgrade.md",
"marks": [
{
"line": 5,
"from": "self-managed 3.4",
"to": "self-managed"
}
]
},
{
"file": "docs/webhooks.md",
"marks": [
{
"line": 5,
"from": "self-managed 3.5",
"to": "self-managed"
}
]
}
],
"unused": [
{
"file": "docs/configure-legacy.md",
"line": 5,
"kind": "page",
"text": "Configure the agent (3.4)"
}
],
"removed": false,
"redirects": [
{
"from": "/3.4/configure-legacy/",
"to": "/configure/"
},
{
"from": "/3.4/install-agent/",
"to": "/install-agent/"
},
{
"from": "/3.4/keys/",
"to": "/keys/"
},
{
"from": "/3.4/quickstart/",
"to": "/quickstart/"
},
{
"from": "/3.4/upgrade/",
"to": "/upgrade/"
}
],
"recorded": false,
"outputs": [],
"expected": [
{
"build": "cloud",
"output": "plain",
"added": [],
"removed": [],
"changed": [
"install-agent.md"
]
},
{
"build": "cloud",
"output": "site",
"added": [],
"removed": [],
"changed": [
"install-agent.md"
]
}
],
"from": null
}| Code | Meaning |
|---|---|
0 |
Retired, or with --dry-run, it would be; or, on a line retired already, what it left is listed or deleted |
2 |
Nothing was written: the line can’t be retired, it has no address, a mark can’t be rewritten, a live line’s output changes, a build that isn’t a line changes and there’s no --force, or git can’t put the files back and there’s no --force |