Frontmatter • Manifests • Changelogs
Part of the LVNACY Apparatus for Obsidian
Archivist is a system-wide CLI tool for bulk-managing YAML frontmatter and generating structured archive documents — manifests and changelogs — across Obsidian vaults. It is built for the LVNACY Apparatus, a modular, git-backed Obsidian architecture for managing stories, publications, research libraries, and related creative projects.
Archivist automatically scopes every command to the current git repo or submodule root via git rev-parse --show-toplevel. It does not matter where you are in a vault hierarchy — run it from anywhere inside the repo and it finds its footing.
As of the Apparatus Platform (Phase 1), Archivist also knows about every module you've ever pointed it at. A machine-wide registry at ~/.archivist/ tracks modules, the apparati they belong to, and how they're nested inside one another — so archivist add, archivist deinit, and archivist sync can clone, register, deregister, and reconcile modules without you hand-editing config files or crossing your fingers about what's linked to what. See The Apparatus Registry below.
The ARCHIVE/ directory in the LVNACY Apparatus serves as the living example and output target for this tool.
Archivist is built around three conventions.
A hidden directory, .archivist/ at the root of any project Archivist manages, where config.yaml and any customizations exist.
config.yaml:
# .archivist
uuid: 3f9a2b7e-4c1d-4e8a-9b2f-6d5c8e1a0f3b # always first — this module's identity, registry-wide
module-type: story # story | publication | library | vault | general
apparati:
- writinguuid is the module's actual identity in the Apparatus registry (see below) — not its path, which can be renamed or moved out from under it without warning. module-type drives changelog routing and git hook behavior. apparati is the list of Apparati this module belongs to; it is a list because a module is allowed to belong to more than one, and it is a list of plain strings, not a true/false/name-string field — that older format is gone, full stop. Projects without a .archivist/ directory are ignored by the hooks entirely — Archivist never touches repos it has not been asked to manage.
Additional optional fields:
# For library modules — where archivist scans for catalogued works
works-dir: works
# Override the default changelog output directory (relative to repo root)
# Defaults: story/publication → ARCHIVE/CHANGELOG/, everything else → ARCHIVE/
changelog-output-dir: ARCHIVE/LOGS
# Templater expression handling. Set by archivist init — can also be edited directly.
# resolve — Archivist resolves tp.date.*, tp.file.*, tp.frontmatter.* at write time
# preserve — Archivist safely round-trips <% %> expressions without touching them
# false — Treat <% %> as plain strings. No handling at all.
templater: preserve
# This module's remote — written automatically by `archivist init` or `archivist add`.
# git-remote is the clone URL; git-remote-name is the local remote label (origin, upstream,
# whatever). Don't hand-edit these unless you know exactly what you're doing.
git-remote: git@github.com:lvnacy-notes/some-story.git
git-remote-name: origin
# Which vault module(s) currently contain this one, by name — human-readable, not UUIDs.
# Written by `archivist add` (or init's own containment check) when this module gets
# linked into a vault. Don't populate this by hand; it's a side effect, not a setting.
vaults:
- main-vault
# Files and directories to exclude from frontmatter and reclassify operations.
# Standard .gitignore pattern syntax — leading slashes, double-star globs,
# negation with !. See https://git-scm.com/docs/gitignore#_pattern_format.
# Has no effect on changelog or manifest commands — scope those via git staging.
ignores:
- "ARCHIVE/**"
- "templates/"
- "*.draft.md"sample-changelog.py
Archivist ships with a sample module complete with instructions on customizing the generated changelogs. Currently this is scoped to modifying changelogs in Library modules. Greater extensibility is planned for future release.
Each Apparatus module maintains an ARCHIVE/ directory at its root. Archivist writes all generated changelogs directly into this directory (or ARCHIVE/CHANGELOG/ for story and publication modules by default). For publication modules, ARCHIVE/ also serves as the search root for MANIFEST_TEMPLATE.md — the template that drives edition manifest output.
MANIFEST_TEMPLATE.md— drives edition manifest output (publication modules only)archive.db— SQLite database shared bymanifestandchangelog publication
Field order in generated manifests is determined entirely by the template. To add, remove, or reorder a field, edit the template — no code changes required. Changelog frontmatter fields are defined in each subcommand module directly.
Archivist installs two git hooks:
pre-commit— checks whether a manifest or changelog is staged. If not, prompts you to generate one before the commit proceeds.post-commit— always prints commit details (SHA, message, branch). In Archivist-managed repos, also backfills the commit SHA into any manifest included in the commit, and delegates changelog sealing toarchivist changelog seal.
Hooks are installed globally into ~/.git-templates/hooks/ and copied automatically into new clones. Existing repos can be synced with archivist hooks sync.
[!warning] Existing Git Hooks Will Be Overwritten! Running
archivist initwill overwrite any existing Git Hooks. Before running this command, backup your hooks and add back the functionality you desire after Archivist has written the hooks needed. Subsequent re-runs ofarchivist initwill continue to overwrite hooks, so always maintain a backup.
[!warning] Upgraded from before the Apparatus Platform? Re-sync your hooks. The pre-commit hook grew a registry-sync step in Phase 1. Existing installs are running the old hook content and Archivist will not silently rewrite it out from under you — that's not how git hooks work and it's not how this tool works either. Run
archivist hooks sync(per-repo) orarchivist hooks install(global) after upgrading, or the new module simply never gets itslast_synced_attimestamp bumped. Nothing breaks. It just quietly does less than it could.
A single machine-wide SQLite database at ~/.archivist/registry.db, plus one additional SQLite file per Apparatus (~/.archivist/[name].db). This is where Archivist keeps track of every module it has ever registered, what Apparatus (or Apparati) each one belongs to, and which modules contain which — independent of where anything currently lives on disk.
~/.archivist/ is itself a git repo (archivist init runs git init on it the first time it sees your machine), so you can back the whole thing up like anything else you care about.
Why a registry at all? Directories get renamed. Vaults get restructured. A module's path is a cache, not its identity — its uuid is. Every registry lookup that matters resolves by UUID first and refreshes the cached path as a side effect, rather than the other way around. Rename a submodule's folder and the registry does not blink.
registry.db schema:
-- One Apparatus per row. A module can belong to more than one.
CREATE TABLE apparati (
uuid TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
db_path TEXT NOT NULL,
created_at TEXT NOT NULL,
git_remote TEXT
);
-- One module per row, regardless of type or how many Apparati it's in.
CREATE TABLE modules (
uuid TEXT PRIMARY KEY,
name TEXT NOT NULL,
module_type TEXT NOT NULL CHECK(module_type IN ('story','publication','library','vault','general')),
path TEXT NOT NULL,
git_remote TEXT,
git_remote_name TEXT,
decimated_at TEXT, -- deregistered but not forgotten; NULL if still active
last_synced_at TEXT
);
-- Containment: which module lives inside which. Not limited to vault containers.
CREATE TABLE module_bays (
container_id TEXT NOT NULL REFERENCES modules(uuid),
contained_id TEXT NOT NULL REFERENCES modules(uuid),
PRIMARY KEY (container_id, contained_id)
);
-- Membership: which module belongs to which Apparatus. Many-to-many, on purpose.
CREATE TABLE module_apparatus (
module_uuid TEXT NOT NULL REFERENCES modules(uuid),
apparatus_uuid TEXT NOT NULL REFERENCES apparati(uuid),
PRIMARY KEY (module_uuid, apparatus_uuid)
);Modules are never hard-deleted. archivist deinit (standalone mode) stamps decimated_at rather than dropping the row — history sticks around, and a decimated module can be brought back with reactivate_module() if it turns out you deregistered the wrong thing at 1am. Foreign-key enforcement is turned on for every connection this codebase opens; if you're poking at the DB by hand, do the same or SQLite will happily let you write garbage.
Each Apparatus also gets its own DB (~/.archivist/[name].db) for cross-module aggregation — changelogs, works, authors, and a works_authors junction table. This is not the same as the per-project ARCHIVE/archive.db covered below; they're peers serving different layers and neither touches the other.
Archivist is designed to live in a permanent location outside any vault and be installed as an editable package via pip.
curl -fsSL https://raw.githubusercontent.com/lvnacy-notes/archivist-cli/main/install.sh | bashThe script checks for Python 3.12+ and git, clones the repo to ~/tools/archivist-cli, installs the package, and runs archivist hooks install automatically. The manual steps below are available if you prefer to inspect and run each step yourself:
# Clone somewhere permanent
mkdir -p ~/tools && cd ~/tools
git clone https://github.com/lvnacy-notes/archivist-cli.git
# Install with your pyenv-managed Python
cd archivist-cli
$(pyenv which pip) install -e .The -e flag installs in editable mode — edits to source files take effect immediately without reinstalling.
Requirements: Python 3.12+, git in your $PATH. We use PEP 701's relaxed f-string grammar throughout, so 3.10 and 3.11 are dead to us — don't bother trying, it won't import. Windows folks: the interactive Apparatus checkbox menu (init, add, sync's handoffs) uses stdlib curses, which Windows doesn't ship — run pip install windows-curses or you'll get bumped to a plainer numbered-prompt fallback automatically. Either way it works; one's just prettier.
Dependencies: argcomplete, pathspec, and pyyaml, installed automatically. The frontmatter commands are stdlib only; pyyaml is required by manifest and changelog; pathspec is required by init, add, sync, and migrate. The Apparatus registry itself is pure sqlite3 — stdlib, no new dependency.
After installing, run these two commands:
# Install hooks globally — applies to all future clones automatically
archivist hooks install
# Inside each existing project you want Archivist to manage
cd path/to/your/project
archivist init[!note] Archivist Init Running
archivist initwill also install hooks globally.archivist hooks installandarchivist hooks syncis available should you choose to skiparchivist initand create the config directory yourself.
Interactive project setup. Run once per project — or once per machine after cloning an existing Apparatus module.
archivist initIf it can't find a git repo where you're standing, it offers to run git init for you first — decline, and it exits cleanly rather than dying on you with a stack trace.
If no .git repo exists yet, or no .archivist/ config directory is found, it walks you through setup:
- First time on this machine? If
~/.archivist/doesn't exist yet, it's created now — directory,git init, schema, the works. You'll be asked for a registry remote so~/.archivist/itself can be backed up and restored on a new machine later. Optional; skip it and set it yourself whenever withgit -C ~/.archivist remote add origin <url>. - Is this module part of an Apparatus? Yes / No — a module doesn't need one to use frontmatter or changelog features.
- If yes: select any number of existing Apparati, and/or conjure brand-new ones, from an interactive checkbox menu (arrow keys, spacebar, enter). A module belonging to more than one Apparatus is a real, supported case, not an edge case.
- Select a module type from the available list (
story,publication,library,vault,general). - For
librarymodules: set theworks-dirpath (where Archivist scans for catalogued works). - Optionally set a custom
changelog-output-dirto override the default output location. - Prompts for Templater expression handling mode (
resolve,preserve, orfalse) and writes it to.archivist— see Templater support below. - For Apparatus modules: resolves this module's git remote (auto-detected if there's exactly one, prompted for if there are several or none) and writes
git-remote/git-remote-name. - Registers the module in the Apparatus registry (upserting
modules,module_apparatus, and — if this module is itself nested inside a registered container —module_bays), and writes.archivist/config.yamlto the repo root withuuidas the first field and an emptyignoreslist — open the file afterward and fill that in. - Copies
sample-changelog.pyto.archivist/forlibrarymodules. - Stages
.archivist/(and, if this project just moved off a legacy flat.archivistfile, stages that deletion in the same breath). - Installs git hooks locally.
If .archivist already exists, it displays the current config and offers to update it or reinstall hooks. Confirming an update rebuilds the config from fresh prompts rather than patching it in place — the only thing carried forward from the old config is the existing uuid. This is also how a legacy flat .archivist file, or an old apparatus: "true"/"<name>" field from before the registry existed, gets cleaned up: there's no dedicated migration logic for either, because a confirmed re-run of init simply never writes the old shape back out.
⚠️ Warning:archivist initwill overwrite any existing git hooks in the repo's.git/hooks/directory without a backup. If you have custom hooks, read every prompt carefully before confirming. Review your existing hooks first withls .git/hooks/and preserve anything you need before proceeding.
# Preview without writing anything
archivist init --dry-runarchivist <command> [subcommand] [options]
Bulk-manage YAML frontmatter properties across every .md file in the repo. All subcommands recurse from the git root and support --dry-run.
All subcommands share the same set of optional selection flags for scoping which files are processed:
| Flag | Short | Description |
|---|---|---|
--file |
-f |
Operate on exactly one file. Mutually exclusive with all other selection flags. |
--path |
Limit the walk to this directory subtree (relative to repo root). | |
--class |
-c |
Only process notes whose class frontmatter value matches (case-insensitive). |
--class-property |
Frontmatter key used as the class discriminator (default: class). |
|
--tag |
Only process notes carrying this tag in their frontmatter. |
All provided filters combine with AND logic. With no selection flags, the command operates on every .md file in the repo.
Files and directories listed in ignores in .archivist are excluded automatically from all frontmatter commands. Explicitly targeting an ignored file via --file is an error.
Adds a property to the frontmatter of every note in scope. Creates a frontmatter block if one does not exist. Skips notes that already have the property unless --overwrite is passed.
# Add a bare key with no value
archivist frontmatter add -p reviewed
# Add a key with a value
archivist frontmatter add -p status -v draft
# Overwrite if already exists
archivist frontmatter add -p status -v published --overwrite
# Scope to a directory or class
archivist frontmatter add -p status -v draft --path content/characters
archivist frontmatter add -p status -v draft --class character
# Preview without writing
archivist frontmatter add -p status -v draft --dry-run| Flag | Short | Required | Description |
|---|---|---|---|
--property |
-p |
✅ | Property name to add |
--value |
-v |
❌ | Value to pair with the property |
--overwrite |
❌ | Overwrite if the property already exists | |
--dry-run |
❌ | Preview without writing to disk |
Plus all shared selection flags above.
Removes a property and its value from every note in scope. If the removal leaves the frontmatter block empty, the block is dropped entirely.
archivist frontmatter remove -p status
archivist frontmatter remove -p status --class character
archivist frontmatter remove -p status --dry-run| Flag | Short | Required | Description |
|---|---|---|---|
--property |
-p |
✅ | Property name to remove |
--dry-run |
❌ | Preview without writing to disk |
Plus all shared selection flags above.
Renames a property key across all notes in scope, preserving its value exactly. Handles scalar values, inline lists, and multi-line block sequences.
archivist frontmatter rename -p status -n state
archivist frontmatter rename -p tags -n keywords --path content/
archivist frontmatter rename -p tags -n keywords --dry-run| Flag | Short | Required | Description |
|---|---|---|---|
--property |
-p |
✅ | Current property name |
--new-name |
-n |
✅ | New property name |
--dry-run |
❌ | Preview without writing to disk |
Plus all shared selection flags above.
Applies a frontmatter template to all notes matching a set of filter criteria. For each matching note:
- Adds properties present in the template but missing from the note (using template defaults)
- Removes properties present in the note but absent from the template
- Reorders properties to match the template order
- Preserves existing values for retained properties
The template is the authority. The template is the law.
Filter by any combination of --class, --path, and --tag. All provided filters must match (AND logic). At least one filter is required — Archivist will not rewrite your entire vault on a hunch.
archivist frontmatter apply-template -t templates/character.md -c character
archivist frontmatter apply-template -t templates/article.md --tag draft
archivist frontmatter apply-template -t templates/location.md -c location --class-property type
archivist frontmatter apply-template -t templates/character.md -c character --path content/characters
archivist frontmatter apply-template -t templates/character.md -c character --tag hero --dry-run| Flag | Short | Required | Description |
|---|---|---|---|
--template |
-t |
✅ | Path to the template markdown file |
--class |
-c |
❌ | Class value to match (e.g. character, location) |
--class-property |
❌ | Frontmatter property used as the class discriminator (default: class) |
|
--path |
❌ | Limit search to this directory (relative to repo root) | |
--tag |
❌ | Match notes that carry this tag in their frontmatter | |
--dry-run |
❌ | Preview without writing to disk |
At least one of --class, --path, or --tag is required.
If your notes use the Obsidian Templater plugin, Archivist handles <% %> expressions in frontmatter values without corrupting them. Behavior is controlled by the templater key in .archivist, set during archivist init:
| Mode | Behavior |
|---|---|
preserve |
Detects <% %> expressions and round-trips them safely. Archivist masks them before any frontmatter manipulation and restores them verbatim afterward — no resolution, no corruption. Open the file in Obsidian and run Templater yourself. |
resolve |
Resolves a static subset of Templater expressions at write time using a Python reimplementation of the relevant tp.* API surface. No Obsidian required, no Node.js required. Unresolvable expressions are preserved verbatim with a warning. |
false |
Treats <% %> as plain strings. Zero overhead. Use this if your project has no Templater expressions. |
What resolve mode handles:
# tp.date — all of these resolve correctly
created: <% tp.date.now("YYYY-MM-DD") %>
due: <% tp.date.now("YYYY-MM-DD", 7) %>
tomorrow: <% tp.date.tomorrow() %>
# tp.file — resolved against the target note, not the template file
title: <% tp.file.title %>
folder: <% tp.file.folder() %>
modified: <% tp.file.last_modified_date("YYYY-MM-DD") %>
# tp.frontmatter — cross-property references within the same note
slug: <% tp.frontmatter["title"] %>What resolve mode does not handle:
tp.system, tp.user, tp.obsidian, and any expression that requires a running Obsidian instance or user interaction. These are left verbatim with a ⚠️ warning. Switch to preserve if your templates rely on them.
Important: In resolve mode, apply-template resolves template default values against the target note's context — tp.file.title gives you the target note's title, not the template file's. This is the correct behavior.
Clone a module and register it with the Apparatus, in one shot. Git runs first. If git fails, nothing is written to the registry — the module doesn't exist as far as Archivist is concerned until git says it does.
archivist add git@github.com:user/repo.git
archivist add git@github.com:user/repo.git modules/repo
archivist add --depth 1 git@github.com:user/repo.git modules/repoNo .git in your current directory → plain git clone. Standing inside a git repo already → git submodule add, and the new module gets wired into the current repo's bay if the current repo is itself a registered module.
Any flag Archivist doesn't recognize gets forwarded straight to git, written exactly as you'd type it for git directly — no -- or = gymnastics required. Archivist finds the URL/path by shape (scheme://, user@host:, or a leading ./, ../, /, ~ — the same convention git itself uses to disambiguate a repository source from a local path), and treats everything before it as git's problem, not archivist's:
archivist add --name custom git@github.com:user/repo.git --dry-runPrefer to draw the line yourself? A bare -- right before the URL works too, same as git's own convention:
archivist add --depth 1 -- git@github.com:user/repo.git modules/repoRegistration resolves the module's UUID in priority order: reactivates a decimated module if the config already declares one, links an already-active module into the current bay, registers a config-declared-but-unregistered UUID using the config as source of truth, or — if there's no config at all yet — walks you through the same interactive Apparatus registration archivist init uses. git-remote is stored from the URL you gave it; git-remote-name is resolved afterward by querying git remote -v inside the freshly-cloned module.
| Flag | Description |
|---|---|
--dry-run |
Print the git command and registration plan without executing either |
Plus any git flags you throw at it, forwarded verbatim.
Deregister a module and remove it from git. Operation order is non-negotiable: Apparatus cleanup first, git second, always. Reverse it and a successful git deletion leaves the registry with no way to recover — config.yaml is gone, and there's nothing left to reactivate.
archivist deinit modules/repo
archivist deinit --force modules/repo --retain
archivist deinit modules/repo --dry-runThe confirmation prompt fires even under --dry-run — a preview that skips the prompt tells you nothing true about what would actually happen, since the prompt itself is part of what happens.
Cleanup behavior depends on context, not on how many bay rows happen to be left over:
- Vault-context removal (you're standing inside a registered container of the target): the bay row is removed; the module itself stays active and its other Apparatus memberships are untouched.
- Standalone removal (no superproject context): every bay row and Apparatus membership for the module is stripped, and the module is decimated (
decimated_atstamped, not deleted — see The Apparatus Registry).
Any flag Archivist doesn't recognize is forwarded to git submodule deinit, same shape-detection dance as add:
archivist deinit --force -- modules/repo| Flag | Description |
|---|---|
--retain |
Registry cleanup only — skip the git operation. For when git already ran (by hand, or in a previous failed attempt) and only the registry needs fixing. |
--dry-run |
Preview the Apparatus changes and git command without executing either. Confirmation prompt still fires. |
Plus any git flags, forwarded verbatim.
Non-interactive registry backfill. Walks the submodule tree recursively from wherever you're standing — any module type nests inside any other, this isn't a vaults-only privilege — and makes sure every module already carrying a valid .archivist/config.yaml with a uuid is registered and correctly linked to its direct container.
archivist sync
archivist sync --dry-runReach for this instead of add/init when:
- You
git submodule added something by hand, bypassingarchivist addentirely - You inherited nesting that predates the registry's containment logic
- A directory got renamed or moved and its cached registry path has gone stale
sync is deliberately not init — it never asks you a single fucking question. Anything it can't resolve non-interactively from what's already committed to disk gets reported and skipped:
- No config at all, config found at the legacy flat
.archivistpath, or a directory-form config missing itsuuid→ hands off toarchivist initfor that node - Config found with a
uuidbut no declaredapparati→ reported as skipped.syncwill not guess an Apparatus assignment for you; that decision belongs to a human runninginit.
Reports (linked_count, skipped_count) for the whole subtree when it's done.
| Flag | Description |
|---|---|
--dry-run |
Preview what would be registered and linked without touching the registry |
One-shot structural migration: legacy flat .archivist file → .archivist/ directory form. Content is not touched — this moves the file, it doesn't rewrite what's in it. (Field-format cleanup, like an old apparatus string turning into a proper apparati list, is handled separately by re-running archivist init and confirming the update — see that section.)
archivist migrate --dry-run
archivist migrateRequires explicit confirmation before deleting the flat file — no backup, no undo outside git. Already on the directory form, or nothing to migrate at all? It says so and exits.
git add .archivist/
git rm --cached .archivist
git commit -m 'chore: migrate .archivist to directory form'| Flag | Description |
|---|---|
--dry-run |
Preview the migration plan without writing or deleting anything |
Find every .md file whose frontmatter class field matches a given value and rewrite it to a new value. Surgical — only the class: line is touched. Nothing else in the frontmatter moves.
Matching is case-insensitive. The --to value is written verbatim. Scope with --path to limit the search.
archivist reclassify --from article --to column
archivist reclassify --from article --to column --path content/
archivist reclassify --from article --to column --dry-run| Flag | Required | Description |
|---|---|---|
--from |
✅ | Current class value to match (case-insensitive) |
--to |
✅ | New class value to write |
--path |
❌ | Limit search to this file or directory |
--dry-run |
❌ | Preview changes without writing to disk |
Generates a {edition-name}-manifest.md for a specified edition directory, written to the edition's parent directory. Specific to publication modules.
Operates in two modes: manifest generation and SHA registration.
Frontmatter structure is driven entirely by MANIFEST_TEMPLATE.md, searched recursively under ARCHIVE/ — shallowest match wins. Expected path: ARCHIVE/EDITIONS/MANIFEST_TEMPLATE.md.
Scopes all git diff tracking to the edition directory only — staging the entire project is safe. Classifies each changed file by reading its class frontmatter property directly, sorting columns, edition files, and assets into the correct sections:
class: column— editorial columns and articlesclass: edition— the edition's primary file (drivespublish-dateextraction)- Everything else — assets and supporting files
Renames detected by git mv appear as old → new in a dedicated Moved section.
If the edition's files are not yet staged, Archivist stages them automatically before diffing.
Re-running archivist manifest for the same edition updates the existing manifest in place. User content below the <!-- archivist:auto-end --> sentinel and any file descriptions you've written are preserved across re-runs — only the auto-generated block is regenerated.
Auto-populated frontmatter fields:
| Field | Source |
|---|---|
columns-published |
Count of class: column + class: edition files |
assets-included |
Count of files in the edition directory not classified as columns or edition files |
files-created / files-modified / files-archived |
Scoped git diff counts |
edition |
Quoted wikilink from directory name — VOL-II-NO-27 → "[[VOL II NO 27]]" |
publish-date |
Pulled from the class: edition file's frontmatter if present |
class, category, log-scope, modified, commit-sha |
Auto-set |
# From the edition's parent directory
archivist manifest "./VOL II NO 27"
# With a volume number
archivist manifest "./VOL II NO 27" -v 2
# Diff against a specific commit
archivist manifest "./VOL II NO 27" a1b2c3d -v 2
# Preview without writing
archivist manifest "./VOL II NO 27" --dry-run| Argument | Required | Description |
|---|---|---|
edition-dir |
✅ | Path to the edition directory (relative or absolute) |
commit-sha |
❌ | Diff against a specific commit instead of staged changes |
-v / --volume |
❌ | Volume number for the volume frontmatter field |
--dry-run |
❌ | Print to stdout without writing to disk |
Registers an edition's commit SHA in the archive DB after committing. Verifies the SHA is a valid commit before inserting and stores the commit message alongside it.
archivist manifest --register a1b2c3d
archivist manifest --register a1b2c3d --dry-runReports one of four outcomes: inserted, already registered, already claimed by a changelog, or invalid SHA.
| Argument | Required | Description |
|---|---|---|
--register SHA |
✅ | Verify and register a commit SHA in the archive DB |
--dry-run |
❌ | Preview without writing to the DB |
Archive DB: ARCHIVE/archive.db — SQLite, created automatically on first use.
Generates a CHANGELOG-{date}.md capturing project changes. Output is written to ARCHIVE/ or ARCHIVE/CHANGELOG/ depending on module type (or your changelog-output-dir config). Frontmatter fields are defined per subcommand — no template file required.
Running archivist changelog bare — no subcommand — reads module-type from .archivist and dispatches to the appropriate subcommand automatically.
.archivist module-type |
Runs |
|---|---|
general |
archivist changelog general |
story |
archivist changelog story |
publication |
archivist changelog publication |
library |
archivist changelog library |
vault |
archivist changelog vault |
No .archivist config, or an unrecognized module type? Falls back to general. It'll manage.
--dry-run, --commit-sha, and --path are all accepted by the bare invocation and passed through to whichever subcommand gets invoked:
archivist changelog --dry-run
archivist changelog --commit-sha a1b2c3d
archivist changelog --path ./chapters
⚠️ --helpdoes not auto-route. Argparse handles it before any routing logic runs, soarchivist changelog --helpalways shows the bare command help regardless of your.archivistconfig. For subcommand-specific help, be explicit:archivist changelog publication --help archivist changelog story --help
If files are not staged, Archivist stages them automatically. Pass --path to scope staging and diffing to a specific file or directory; omit it to operate repo-wide.
Re-running a changelog command pre-commit updates the existing file rather than creating a new one. Archivist preserves two things across re-runs:
- User content — everything after the
<!-- archivist:auto-end -->sentinel (your notes, checklist, any edits you've made below the auto-generated block) is carried forward untouched. - Descriptions — any file descriptions you've filled in (single-line or sub-bullet format) are extracted from the existing changelog and reinjected against the same filenames in the new output. Descriptions still showing
[description]are not preserved — only ones you've actually written.
The auto-generated block above the sentinel is always fully regenerated from the current staged state, so file counts, SHA, and the file list stay accurate no matter how many times you re-run.
When --path is active, Archivist stages and diffs only the specified directory. Two interactive prompts exist to keep you from shooting yourself in the foot:
Out-of-scope prompt (Step 3): If there are unstaged changes — modified tracked files or untracked files — sitting outside your scope, Archivist lists them and asks if you want to stage them too. Say y and they get staged. Say n and they're left alone. Either way, the run continues.
Save-before-overwrite prompt (Step 6): If an existing changelog has working-tree edits that haven't been staged yet, Archivist warns you before overwriting it. Say y and it stages the file first. Say n and the rerun proceeds at your own risk. Both prompts are completely suppressed during --dry-run.
# Auto-routes based on .archivist
archivist changelog
archivist changelog --dry-run
archivist changelog --commit-sha a1b2c3d
# Explicit subcommands — always available, always route directly
archivist changelog general
archivist changelog library
archivist changelog publication
archivist changelog story
archivist changelog vault
# Scope to a specific path
archivist changelog story --path ./chapters
# Diff against a specific commit (subcommand required for explicit routing)
archivist changelog general a1b2c3dShared flags:
| Argument | Required | Description |
|---|---|---|
commit-sha |
❌ | Diff against a specific commit |
--path |
❌ | File or directory to stage and scope the diff to |
--dry-run |
❌ | Print to stdout without writing to disk or DB |
Archivist runs a three-pass rename detection pipeline on every changelog run, recovering renames that git's similarity threshold missed:
- Pass 0 (git):
git diff -M— renames git detected natively (>50% content similarity, any directory). - Pass 1 (filename): Pairs unmatched deleted/added files by identical filename across directories. Ambiguous matches (same filename added in two places) are left alone.
- Pass 2 (content): Compares file content against HEAD for remaining unmatched pairs using sequence similarity. Catches the worst case: file renamed AND moved simultaneously.
Same-directory renames are annotated as renamed from old-name.md. Cross-directory moves are annotated as moved from old/path/file.md — because "renamed from note.md" is a useless hint when there are forty files called note.md in the vault. Suspicious renames (unrelated stems, or crossing directories) get a
A clean, minimal changelog with no project-type-specific sections. Suitable for any project. Also invoked by bare archivist changelog when no .archivist config is present.
Output goes to ARCHIVE/.
For library modules. Reads frontmatter from every changed .md file and routes it into named class buckets:
- Works — files carrying a
work-stagefield (regardless ofclassvalue), bucketed by current stage - Author Cards —
class: authorfiles - Publication Cards —
class: collectionfiles - Definitions —
class: entryfiles (word + aliases surfaced) - Other File Changes — everything that didn't claim a named bucket
Auto-populated frontmatter counters: works-added, works-updated, works-removed, authors-added, authors-updated, publications-added, definitions-added.
Catalog Snapshot — the crown jewel of the library changelog. Generated at run time from the full works directory and frozen as static Mermaid charts:
- Stage Distribution — pie chart of work counts per
work-stageacross the entire catalog - Throughput — table of
work-stagetransitions detected in this commit (e.g.,raw→active) - Author Landscape — pie chart of work counts per author (top 8, rest bucketed as "Others")
- Reading Velocity — bar chart of
date-consumedentries over the rolling 12 months - Placeholder Debt — count of works stuck at
placeholderwith nodate-consumed
The works directory defaults to works/ and is configurable via works-dir in .archivist.
Output goes to ARCHIVE/.
For newsletter and publication modules. Queries the archive DB for edition commit SHAs not yet recorded in any changelog, includes them in editions-sha frontmatter, and marks them as claimed in a single transaction — each SHA appears in exactly one changelog, never duplicated, never silently dropped.
The UUID lifecycle: When a publication changelog is first generated, it receives a UUID written into its UUID frontmatter field. Edition SHAs are claimed against this UUID (included_in = UUID) rather than a file path — stable across renames and post-commit hook renaming. At seal time, archivist changelog seal transitions included_in from UUID to the commit SHA, completing the handoff. Iterative re-runs before sealing correctly re-surface SHAs already claimed by the current changelog's UUID.
Auto-populated frontmatter fields:
| Field | Source |
|---|---|
editions-sha |
SHAs from the archive DB with included_in = NULL or included_in = current UUID |
files-created / files-modified / files-archived |
Repo-wide git diff counts |
class, category, log-scope, modified, UUID, commit-sha, tags |
Auto-set |
Output goes to ARCHIVE/CHANGELOG/.
Archive DB: ARCHIVE/archive.db — shared with archivist manifest.
For story and creative writing modules. Includes writing-specific sections: scene development, character arcs, plot advancement, creative considerations, and next steps structured around narrative milestones.
Output goes to ARCHIVE/CHANGELOG/.
For vault-level commits. In addition to standard file change tracking (with routing by template, scaffold, script, and .archivist keywords into named sections), captures:
- Updated in This Commit — which submodule paths were touched in the staged changes or given commit
- Status Overview — a full status table for all registered submodules: current short SHA, whether uncommitted changes exist, whether there are unpushed commits
| Module | SHA | Uncommitted | Unpushed |
|--------|-----|-------------|----------|
| `stories/my-story` | a1b2c3d | clean | pushed |
| `publications/the-bsp` | deadbee | ⚠️ yes | ⚠️ yes |
Useful for knowing exactly where everything stands before it matters.
Output goes to ARCHIVE/.
Backfills a commit SHA into any unsealed changelogs included in the given commit, renames them to mark them as sealed, and updates the archive DB where a UUID is present in frontmatter.
Called automatically by the post-commit hook. You shouldn't need to run this by hand — but if the hook misfired, a seal got missed, or you're just that kind of person, here it is.
What "sealed" means in practice:
- Frontmatter:
commit-sha:is backfilled with the short SHA. - Body table: The
| Commit SHA | [fill in after commit] |placeholder is replaced with the full SHA. - Filename: The changelog is renamed from
CHANGELOG-YYYY-MM-DD.mdtoCHANGELOG-YYYY-MM-DD-{short_sha}.md. This is the lock — sealed files are excluded fromfind_active_changelog()and will never be picked up as an existing changelog on future runs. - Archive DB: If the changelog has a
UUIDin frontmatter, thechangelogstable is upserted with the commit SHA and seal timestamp, andedition_shas.included_intransitions from UUID to short SHA for all claimed edition SHAs.
A commit with no unsealed changelogs exits cleanly. Running seal twice against the same commit is idempotent — the second pass sees the unsealed filename is gone from disk, warns, and exits without touching the sealed file.
archivist changelog seal abc123def456...| Argument | Required | Description |
|---|---|---|
commit-sha |
✅ | Full commit SHA to seal against |
Manage Archivist's git hooks.
Writes hook scripts into ~/.git-templates/hooks/ and ensures git is configured to use that directory as its template source. All future git clone and git init operations will automatically include the hooks.
archivist hooks install
archivist hooks install --dry-runCopies hooks directly into the current repo's .git/hooks/. Use this for repos that existed before hooks install was run. Detects submodules and offers to sync into each one as well.
archivist hooks sync
archivist hooks sync --dry-runpre-commit: Checks whether a manifest or changelog is staged. Only unsealed changelogs count — sealed files (carrying a SHA suffix) are explicitly excluded, because they're closed records from a past commit and have nothing to do with what you're staging right now. If nothing is found, prompts:
📋 archivist: No manifest or changelog found in staged files.
Generate one now?
1. no — proceed with commit as-is
2. manifest — generate an edition manifest
3. changelog — generate a changelog
4. stage existing — add an existing file to staging
5. cancel — abort the commit
Selecting manifest prompts for an edition directory path, runs archivist manifest, and stages the result. Selecting changelog runs the appropriate changelog command for the module type and stages the result. Selecting stage existing prompts for a file path and stages it directly.
post-commit: Runs in every repo — Archivist-managed or not — and prints commit details:
🚀 Commit Details:
✅ SHA: abc123def456...
📝 Short: abc123d
💬 Message: your commit message
🌿 Branch: main
📋 For PR creation, use:
"Create PR for commit abc123d from main to main"
or
"Create PR from main to main"
In Archivist-managed repos, it additionally:
- Backfills the commit SHA into any manifests included in the commit that have an empty
commit-shafrontmatter field (bash-native, runs unconditionally). - Delegates changelog sealing to
archivist changelog seal <full-sha>— which handles backfill, rename, and DB update for changelogs atomically from Python.
The updated files are left unstaged — commit them deliberately, typically alongside the next edition or changelog.
archivist manifest and archivist changelog publication share a SQLite database at ARCHIVE/archive.db, created automatically on first use.
-- Tracks edition commit SHAs from registration through changelog inclusion
CREATE TABLE edition_shas (
sha TEXT PRIMARY KEY,
commit_message TEXT,
manifest_file TEXT,
discovered_at TEXT,
included_in TEXT -- NULL until claimed; holds UUID until sealed, then short_sha
);
-- Registry of all generated changelogs, keyed by UUID. Populated at seal time.
CREATE TABLE changelogs (
uuid TEXT PRIMARY KEY,
commit_sha TEXT,
log_scope TEXT,
created_at TEXT,
sealed_at TEXT,
file_path TEXT
);The included_in lifecycle for edition_shas:
NULL— registered byarchivist manifest --register, not yet in any changelog- UUID — claimed by
archivist changelog publicationon first (or any iterative) run - Short SHA — transitioned by
archivist changelog sealat commit time
This chain is what makes iterative publication changelog re-runs correct (UUID re-surfaces the same SHAs) and makes it impossible for a SHA to appear in two changelogs (once it's been sealed to a commit SHA, the publication query never returns it again).
To inspect or correct entries directly:
sqlite3 ARCHIVE/archive.db
SELECT * FROM edition_shas;
SELECT * FROM changelogs;
-- Fix a wrong SHA
UPDATE edition_shas SET sha = 'correctsha' WHERE sha = 'wrongsha';
-- Delete and re-register a bad entry
DELETE FROM edition_shas WHERE sha = 'wrongsha';
.quit
archivist manifest --register correctshaArchivist is opinionated — built around the conventions of the LVNACY Apparatus. If you want to adapt it to a different project structure, the pattern is intentionally straightforward.
-
Create
archivist/commands/changelog/yourtype.pywith arun(args)function. Use any existing changelog module as a reference — they all follow the same structure: callrun_changelog()fromchangelog_base.pywith yourbuild_frontmatter,build_body, and optionallypost_changes,post_write, andprint_summarycallables. Frontmatter fields are defined directly in theautodict inside your frontmatter builder. -
Add the subcommand parser to
build_parser()incli.py:
yourtype_p = cl_sub.add_parser("yourtype", help="...")
yourtype_p.add_argument("commit_sha", nargs="?", default=None, metavar="COMMIT-SHA")
yourtype_p.add_argument("--path", default=None, metavar="PATH")
yourtype_p.add_argument("--dry-run", action="store_true")- Add the routing branch to
main()incli.py:
elif cl_command == "yourtype":
from archivist.commands.changelog.yourtype import run- Add your module type to
APPARATUS_MODULE_TYPESandMODULE_CHANGELOG_COMMANDinutils/config.py.
No reinstall needed — editable installs pick up changes immediately.
- Create
archivist/commands/yourcommand.pywith arun(args)function. - Add the parser to
build_parser()incli.py. - Add the routing branch to
main()incli.py.
Archivist finds the manifest template by recursively searching ARCHIVE/ for MANIFEST_TEMPLATE.md. To use a different directory structure or template name, update _find_manifest_template() in manifest.py. Template field order is always respected — frontmatter is rendered by iterating template keys in order.
Changelog frontmatter fields are not template-driven. To add, remove, or reorder fields for a changelog subcommand, edit the auto dict inside its _build_frontmatter() function directly.
All five changelog subcommands delegate to run_changelog() in changelog_base.py. If you're adding a new subcommand, you don't reimplement the pipeline — you provide callables:
| Parameter | Signature | Purpose |
|---|---|---|
build_frontmatter |
(ctx: ChangelogContext) -> str |
Build the YAML frontmatter block |
build_body |
(ctx: ChangelogContext) -> str |
Build the markdown body |
post_changes |
(ctx: ChangelogContext) -> None |
Analyse the diff; mutate ctx.data |
get_extra_paths |
(git_root: Path) -> list[Path] |
Extra paths to stage and diff |
print_summary |
(ctx: ChangelogContext) -> None |
Custom summary output |
post_write |
(ctx: ChangelogContext) -> None |
Side-effects after write (DB, etc.) |
ChangelogContext carries everything your callables could want: git_root, output_dir, raw and processed git changes, the full rename lookup, extracted descriptions, preserved user content, the changelog UUID, and a data dict for module-specific state.
This project is not accepting unsolicited PRs. Archivist is purpose-built for the LVNACY Apparatus, and its feature roadmap reflects that specific use case.
That said, discussion is welcome. If you have a suggestion, open an issue. If a discussion produces a viable feature request aligned with the Apparatus workflow, a PR may be invited.
If you want to adapt this for your own use — which is actively encouraged — fork it, modify freely, and build something useful. The adapting section above is a good starting point.
Archivist uses Archivist. Generated changelogs live in ARCHIVE/.
This CLI was developed in collaboration with Mad Alex, the driving force behind the LVNACY Apparatus. It was built to compile comprehensive changelogs and track the progress of their stories and the evolution of their workflows.
"Archivist" is inspired by a concept of the same name being written by Mad Alex, who has been kind enough to allow software and plugins that do not contain proprietary content to be made available as open source. Please follow and subscribe:
- Newsletter: The Backstage Pass
- GitHub: madalexxx
This software is available under the MIT License. See LICENSE for details.
