Skip to content

Repository files navigation

Lith

Lith renders a short brief into a style-locked image-generation prompt, calls xAI, OpenAI, or MiniMax through one inspectable provider layer, then validates and publishes the image that comes back.

What lith is. Seven fixed visual style families in styles.json, each a prompt template with a few substitution slots. A JSON recipe format that makes a brief re-runnable. A poster spec — headline, subtitle, sections, diagram, footer — serialized into the prompt as a literal copy block the model is ordered to reproduce character for character, so no shipped word was invented by the model. Fifteen named layouts and a resolution chain that picks one from the shape of your content and the shape of the frame. Three console scripts, eight root-level convenience functions, and a typed lith.call API. Standard library only, no vendor SDKs or external binaries.

What lith is not. It is not a vendor SDK or an autonomous publisher. lith-press performs generation, but it does not score or rank candidates, post to any platform, do video, or turn an existing post into a brief. Candidate selection and publication remain explicit handoffs; lith-print with no image source prints its plan and exits 0.

New here? Work through Tutorial: your first announcement image — a brief to a finished image in six steps, with a path that needs no API key.

Where the pipeline draws its lines, and why it now makes the call itself: About the pipeline. How the same spec renders seven ways: About output styles. How panels get arranged: About layouts. The palette and composition rules underneath: About the design language.

Driving lith from a Hermes session: skills/lith/SKILL.md, and how to install it.


Contents


Why & use cases

Plain prompting gets you an image. It does not reliably get you that image again, in your frame, saying the words you wrote.

Prompting a model directlylith
The model paraphrases your headlineThe poster copy is serialized into the prompt as a literal block the model is ordered to reproduce character for character
"same style as last time," from memorySeven fixed families in styles.json — template, negative prompt, palette, default aspect, all data
You get whatever ratio the provider felt likeAspect resolves explicit → content shape → family default → per-model clamp, and lith-print --strict measures the delivered file and exits nonzero when it drifted
The prompt lives in chat scrollbackA JSON recipe re-renders byte-identically forever, to a deterministic output path
One vendor's SDK and payload shapeOne ImageRequest across xAI, OpenAI and MiniMax, with the exact body inspectable before you spend

That last row is not hypothetical. Two sweeps of the 34-recipe testbed once ran through a bridge that took prompt and silently dropped model, n, and aspect_ratio. Every image came back, every frame was wrong, and nothing in the returned bytes said so — the full story.

Ways to leverage it:

  • From an agent or harness. Three commands, stable exit codes, and --emit-json on both planning steps. lith-press --check verifies the route before spending; lith-print --strict turns a silently substituted ratio into a nonzero exit a sweep script can actually catch.
  • From a Hermes session.skills/lith/SKILL.md ships in the repo — the session picks a family, generates, and hands you candidates without improvising a prompt.
  • From Python. Eight root-level functions plus the typed lith.call API, for wiring image generation into an existing app. The prompt-side modules import no network code at all, so you can render and diff prompts in a unit test.
  • As a prompt compiler for a provider lith doesn't support.lith-plate --recipe R --press --emit-json gives you the envelope; take it to any API you like and skip lith-press entirely.
  • In batch and in CI. Recipes are files, so a content calendar is a directory. Every preview mode — --press, --dry-run, --check, --auth, and lith-print with no image — is offline and free.

Not what you want if you need one-off concept art, or a tool that picks the best candidate and posts it for you. Selection and publication stay yours.


Install

Requirements:

Python3.10 or newer (the library uses PEP 604 union syntax)
uvbrew install uv on macOS

As a tool:

uv tool install git+https://github.com/funsaized/lith
lith-plate --help

As a library in another project:

uv add git+https://github.com/funsaized/lith

As a contributor:

git clone https://github.com/funsaized/lith.git
cd lith
uv sync --extra test

uv sync --extra test creates .venv/, resolves pyproject.toml, and installs the project editable, which places lith-plate, lith-press, and lith-print in the venv.

Verify:

uv run lith-plate \
--topic "test" --style B --aspect 16:9 --headline "32 LANGS" --icon "globe"
uv run lith-press --help
uv run python -c "from lith import render_prompt"

All three print and exit 0.


Install the Hermes skill

skills/lith/SKILL.md lets a Hermes session drive the three CLIs on your behalf. It is a workflow wrapper only — every deterministic behavior lives in the lith package, which the skill assumes is already installed. Install the package first; the skill is useless without it.

Hermes discovers skills at ~/.hermes/skills/<name>/SKILL.md. Put lith's there.

Symlink — the right choice from a checkout, since edits to the repo copy take effect on the next Hermes restart. Run it from the repository root:

mkdir -p ~/.hermes/skills
rm -rf ~/.hermes/skills/lith
ln -s "$PWD/skills/lith"~/.hermes/skills/lith

The rm -rf is load-bearing. If a real directory is already at that path — a copy install, or a version from before the skill shipped in this repository — ln -s puts the link inside it at ~/.hermes/skills/lith/lith and exits 0. Nothing errors, and Hermes goes on loading the stale SKILL.md. Adding -fn does not help; macOS cannot unlink a real directory that way.

Copy — for a checkout you intend to delete:

mkdir -p ~/.hermes/skills/lith
cp skills/lith/SKILL.md ~/.hermes/skills/lith/SKILL.md

If you installed lith with uv tool install and have no checkout, clone the repository for the skill file alone:

git clone --depth 1 https://github.com/funsaized/lith.git /tmp/lith
mkdir -p ~/.hermes/skills/lith
cp /tmp/lith/skills/lith/SKILL.md ~/.hermes/skills/lith/SKILL.md

Restart Hermes. Its session-start loader reads the skills directory once at startup, so a skill added or edited mid-session is not picked up.

Verify:

test -f ~/.hermes/skills/lith/SKILL.md &&echo"skill resolves"test! -e ~/.hermes/skills/lith/lith &&echo"not nested"
lith-plate --help >/dev/null && lith-press --help >/dev/null \
&& lith-print --help >/dev/null &&echo"cli ok"

All three lines must print. The second is what catches the nesting trap above — the first passes either way.

Then ask the session for something the skill covers — "generate a family B announcement image for X" — and confirm it checks the route before generating rather than improvising a prompt.

To update the skill after pulling: symlink installs need nothing but a Hermes restart; copy installs need the cp re-run. To uninstall, rm -rf ~/.hermes/skills/lith.

What the skill instructs

Worth knowing before you hand a session the keys — the file is short and worth reading in full:

  • Run lith-press --check, generate with the selected route, then finish through lith-print --strict.
  • Use absolute paths for recipes and images.
  • Ask the user to pick when candidate selection is subjective.
  • Never publish, post, or upload without separate authorization.
  • Write the full poster spec into the brief, because every word in it is printed into the image verbatim.

The skill declares platforms: [linux, macos] and needs nothing beyond a Python install on either.


CLI reference

lith-plate

Renders a brief into a prompt. Prints a human-readable summary by default; with --press, prints a press envelope instead.

lith-plate --recipe PATH [options]
lith-plate --topic TEXT --style {A..G} --headline TEXT [options]
FlagTypeDefaultDescription
--recipepathRecipe file. Supplies the brief, model, and n.
--topicstrOne-sentence brief. Required without --recipe.
--styleAGStyle family. Required without --recipe.
--headlinestrIn-image headline. Required without --recipe.
--aspectprovider ratio unionfamily defaultOne of 1:13:44:39:1616:92:33:29:19.519.5:99:2020:91:22:121:9auto. Use a concrete ratio for lith-press.
--iconstrgearMotif substituted into {icon}.
--nint4Candidate count recorded in the envelope.
--seedintNoneSeed recorded in the envelope.
--modelsee Aspect ratiosgrok-imagine-image-2.0Model recorded in the envelope for a later lith-press.
--outpathderived stemOutput path recorded in the envelope, verbatim. Without it, the derived value carries no extension.
--pressflagoffEmit the envelope instead of the summary.
--emit-jsonflagoffWith --press, emit JSON rather than key=value lines.

With --recipe, the recipe's model and n win and --model / --n are ignored; --seed, --out, --press, and --emit-json still apply.

Envelope fields, in order: prompt, negative_prompt, aspect_ratio, model, n, seed, output_path, style, aspect_note, copy_note, limit_notes. aspect_note is null unless the model forced a different ratio. copy_note is null unless the copy block is too thin for the template around it — a brief with no sections renders about sixteen characters of copy against fifteen hundred of instructions, and the model starts lettering the instructions instead. limit_notes names model-specific n or prompt-length violations without changing the prompt.

uv run lith-plate --recipe recipes/live_test_recipe.json --press --emit-json

Exit codes: 0 on success, 2 on an argparse error (including a missing --topic/--style/--headline when --recipe is absent).

lith-press

Turns a rendered recipe into real candidate bytes through xAI, OpenAI, or MiniMax. Inspect the route and exact provider payload before spending money.

lith-press --recipe PATH [--out DIR] [--n N] [--resolution {1k,2k}]
[--quality {low,medium,high}] [--seed N]
[--dry-run | --check | --auth] [--emit-json]
FlagTypeDefaultDescription
--recipepathRecipe to render and call. Required except with --auth.
--outdirectoryrecipe's output directoryCandidate directory. Files are {family_key}_{slug}-c{index}.{ext}.
--nintrecipe nOverrides the candidate count for this call. Provider maxima are xAI/OpenAI 10 and MiniMax 9.
--resolution1k2kxAI resolution. Other adapters report it in unsupported.
--qualitylowmediumhighOpenAI quality. Other adapters report it in unsupported.
--seedintMiniMax seed. Other adapters report it in unsupported.
--dry-runflagoffPrint the exact provider URL, redacted headers, request body, and unsupported fields; make no call.
--checkflagoffPrint whether Hermes image_generate or lith-press preserves the recipe's model and aspect; make no call.
--authflagoffReport each provider's resolving credential tier and short fingerprint, never its value. --recipe is optional.
--emit-jsonflagoffEmit JSON for --auth, --check, or a live result. --dry-run is always JSON.

--dry-run, --check, and --auth are mutually exclusive. A live call writes one file per CallResult.candidates entry, choosing the extension from the bytes rather than the model id. Human output also prints any reported model, aspect, revised prompt, cost, and every unsupported field; JSON output carries the same metadata plus the provider's raw response.

Credentials resolve in this order: shell environment, the recipe repository's .env, ~/.hermes/.env, then compatible OAuth entries in ~/.hermes/auth.json. The strict variable names are XAI_API_KEY, OPENAI_API_KEY, and MINIMAX_API_KEY; copy .env.example for a repo-local setup. A repo .env is ignored by Git. lith-press --auth shows which tier won.

MiniMax is implemented but its 1500-character prompt cap is lower than every current integration recipe. The adapter raises before any network call and names the measured length and cap; compact templates are a separate design task.

uv run lith-press --check --recipe recipes/integration/24-aspect-ultrawide.json
uv run lith-press --dry-run --recipe recipes/integration/01-stack-A.json
uv run lith-press --recipe recipes/integration/01-stack-A.json --n 2 --emit-json

Exit codes: 0 on success and 2 on an argparse error. Credential, request, transport, or provider failures terminate nonzero without writing candidates.

lith-print

Validates a generated image and publishes it under the recipe's deterministic path. With no image source, it prints its plan and exits.

lith-print --recipe PATH [--image-url URL | --image-file PATH] [options]
FlagTypeDefaultDescription
--recipepathrequiredRecipe file.
--image-urlurlHTTP(S) URL of the generated image. Mutually exclusive with --image-file.
--image-filepathLocal generated image. Mutually exclusive with --image-url.
--output-dirpathbeside the recipeDirectory for the published file. Defaults to the recipe's sibling outputs/, not the cwd.
--strictflagoffExit 1 when the delivered frame does not match the request. The file is still published.

Two modes:

ConditionBehavior
No --image-url and no --image-filePrints recipe, family, style, aspect, model, prompt, and output path; exits 0. Nothing is written.
Image sourceWrites the image to the recipe's output path, extension sniffed from the bytes; exits 0, or 1 under --strict if the frame drifted.

lith-print is the only step that compares the delivered frame against the one the prompt was composed for, so it is the only place a silently substituted ratio becomes visible. Without --strict that comparison is a [warn] line on stdout and the command still exits 0 — fine when a person is reading the output, useless to a sweep script scraping exit codes. Pass --strict in batches. The image is published either way, because the bytes are the evidence you need to diagnose the substitution.

--image-url fetches under four guards: HTTP(S) schemes only, re-checked after redirects; 30-second timeout; 25 MB ceiling enforced while streaming; and a structural validation for JPEG, PNG, or WebP before any write. --image-file skips the network guards, keeps structural validation, and no-ops the copy if source and destination resolve to the same path.

Bytes are staged as <stem>.part and renamed once the format is known, because the extension cannot be chosen before the bytes are inspected. The published extension follows the image, not the recipe: Grok returns JPEG, gpt-image-1 returns PNG. Nothing is re-encoded.

uv run lith-print \
--recipe recipes/live_test_recipe.json \
--image-file outputs/B_brutalist_32_langs_raw.jpg

Exit codes: 0 on success, 2 on an argparse error. A failed download or a non-image body raises and terminates with a traceback.


Python API

from lith import ... exposes eight names. Full signatures, return shapes, exception tables, and internals for every module — Python API and implementation reference.

NameSignaturePurpose
render_prompt(style, brief=None) -> dict[str, str]Substitute a brief into a family template. Returns prompt, negative_prompt, aspect_ratio, style.
load_recipe(path) -> RecipeRead and validate a recipe file.
recipe_from_brief(brief, *, style, model, n, ...) -> RecipeValidate generated data and construct a recipe.
validate_brief(brief) -> dictValidate brief shape and authored field types.
expand_brief(topic, llm_cmd, ...) -> dictExpand a topic into a brief using an LLM command you supply.
parse_brief_response(text) -> dictFirst decodable JSON object in an LLM reply.
output_path(out_dir, family_key, headline, ext) -> PathDerive an artifact path.
slug(text) -> strFilename-safe slug; "untitled" when empty.
fromlithimportload_recipe, render_promptrecipe=load_recipe("recipes/live_test_recipe.json")
rendered=render_prompt(recipe)

Recipe, FAMILY_KEYS, REQUIRED_BRIEF_KEYS, load_styles, get_family, and DEFAULT_PROMPT are not in __all__ but are importable from their modules and used by the console scripts.

Provider calls live in the separate lith.call API:

fromlithimportload_recipe, render_promptfromlith.callimportImageRequest, generaterecipe=load_recipe("recipes/integration/01-stack-A.json")
rendered=render_prompt(recipe)
result=generate(ImageRequest(
prompt=rendered["prompt"],
model=recipe.model,
aspect=rendered["aspect_ratio"],
n=recipe.n,
negative_prompt=rendered["negative_prompt"],
))

result.candidates always contains decoded bytes. result.unsupported makes every supplied field the selected provider could not accept visible; no adapter appends such a field to prompt. See the full lith.call reference.


Recipe format

A recipe is a JSON object. See recipes/live_test_recipe.json.

KeyTypeRequiredDefaultDescription
style"A""G"yesStyle family letter.
briefobjectyesSubstitution values; see below.
namestringnofile stemRecipe identifier.
descriptionstringnonullFree text; not used at runtime.
modelstringnogrok-imagine-image-2.0Must be a model in the capability table below.
nintno4Candidate count; must be within the selected model's provider limit.

brief keys:

KeyRequiredUsed for
topicyesValidation and human context; not substituted into any template.
headlineyesThe spec's TITLE: line and the output filename.
iconyes{icon} slot.
aspectnoPins the ratio. Omit to derive it from content shape, then the family default.
volumeno{volume} slot; family C only. Defaults to "1".
titlenoOverrides headline in the spec's TITLE: line only; the filename still uses headline.
subtitlenoSpec SUBTITLE: line, and a subtitle zone in {layout}.
sectionsnoList of {heading, lines} objects — the section panels. heading is required on each; lines is 2–4 strings.
diagramnoOne sentence naming every label in a simple drawing; adds a drawing zone. Described, not lettered — only the labels it names appear as text.
diagram_positionnobelow (default) · above · beside · center. radial forces center.
layoutnoArrangement for the section panels; see below. Omit to derive it from section count and frame shape.
footernoOne short line under a horizontal rule.
base_colornoOverrides the family palette's background in {base_color}.
accentnoOverrides the family palette's accent in {accent}.

All seven families carry {spec} and {layout}, so spec keys reach every one of them. A brief with no sections degrades to a title-only spec, which is what every pre-spec recipe produces.

Recipes are validated before rendering: style and model must be known, n must fit the model limit, authored fields must be non-empty strings, sections must contain headings and string lists, layout keys must be known, and aspect must be auto or a positive W:H ratio. validate_brief and recipe_from_brief expose the same boundary for data returned by expand_brief.

{
"name": "live_test_recipe",
"style": "B",
"brief": {
"topic": "Dill Pickles and all things great about them",
"headline": "DILL PICKLES",
"icon": "lightning",
"aspect": "1:1",
"sections": [
{"heading": "01 - COLD CRUNCH", "lines": ["Firm cucumbers snap with every bite"]}
]
},
"model": "grok-imagine-image-2.0",
"n": 1
}

Style families

Seven families, defined in src/lith/data/styles.json. That file is authoritative for prompt text; this table is the index.

Every family carries {spec} and {layout}. The copy path is identical across all seven — the brief supplies every word, the template supplies only how those words are drawn. "Extra slots" below lists what a family uses beyond those two.

LetterKeyNameDefault aspectExtra slotsBest for
AA_stickerSticker / whisper-joke infographic16:9{accent}Quick reactions, ship announcements, POV jokes
BB_brutalistSci-fi brutalist UI16:9{icon}Feature flagships, capability reveals
CC_patentVintage technical manual / patent diagram2:3{icon}{volume}How-it-works posts, educational threads
DD_mangaManga tape-insert / risograph2:3{base_color}Release announcements, chapter framing
EE_screenshotEditorial screenshot polish16:9UI demos, product launches
FF_woodcutWoodcut / analog engraving2:3{icon}Sponsor announcements, team memos
GG_logRole-log / status dashboard16:9{icon}Operational posts, build-in-public stats

Slot resolution, per render_prompt:

SlotSourceFallback
{headline}brief["headline"]"NEW"
{icon}brief["icon"]"gear"
{volume}brief["volume"]"1"
{base_color}brief["base_color"], else palette["background"]"#000000"
{accent}brief["accent"], else palette["accent"]"#00E5FF"
{spec}the brief's copy fields, serializedtitle-only block
{layout}the zones the brief has copy fortitle zone alone

A palette field holding a list is joined with " | " — for example A_sticker's four accents render as #FF2E88 | #00E5FF | #F2FF00 | #FF6B35. An empty list falls back to the default. The brief wins over the palette, so a family listing three backgrounds needs the recipe to name one, or the prompt asks for a "single flat background" and then lists three colors.

Layouts

brief.layout selects how section panels are arranged. Omit it and lith derives one from the panel count and the frame's orientation.

KeyArrangement
stackOne full-width column
two-column · three-columnBalanced columns
grid-2x2 · grid-2x3 · grid-3x2 · grid-3x3Strict grids
heroFirst panel full width at double height, rest in a grid beneath
sidebarFirst panel a tall left rail, rest stacked to its right
timelineVertical sequence on a spine, each panel stepped right
radialPanels ringed around a centred drawing on leader lines
masonryTwo columns of unequal height, no two tops aligned
zigzagAlternating left/right, offset and rotated a degree or two
splitTwo facing groups either side of one strong rule
diagonalStepping upper-left to lower-right, corners overlapping

Derived when layout is absent:

PanelsPortraitLandscape
1stackstack
2two-columntwo-column
3herothree-column
4grid-2x2grid-2x2
5herohero
6grid-2x3grid-3x2
7–9two-columngrid-3x3

Column counts cap at two in portrait: three narrow columns of body copy in a tall frame is where legibility goes first.

Why the families exist, what they have in common, and how to rotate them: About the design language.


Aspect ratios

brief.aspect pins a ratio. Omit it and lith resolves one, in this order:

  1. brief.aspect, when set
  2. content shape — 3+ sections resolve portrait 2:3, 1–2 resolve 1:1
  3. the family's default_aspect
  4. 16:9

Whatever those choose is then clamped to what the recipe's model can actually produce. A model does not reject a ratio it lacks; it silently substitutes one, so lith substitutes first and says so.

ModelCapability variantLimit
grok-imagine-image-2.0 (default)ratio enum1:13:44:39:1616:92:33:29:19.519.5:99:2020:91:22:1 (auto is recorded but never sent) · n ≤ 10
grok-imagine-image-quality, grok-imagine-imageratio enum1:116:99:164:33:43:22:32:11:2 · n ≤ 10
gpt-image-2, gpt-image-2-2026-04-21constrained pixel rangeedges divisible by 16 and ≤3840, ratio 1:33:1, 655,360–8,294,400 pixels · n ≤ 10
gpt-image-1.5, gpt-image-1, gpt-image-1-minifixed pixel sizes1024x10241536x10241024x1536 (auto is recorded but never sent) · n ≤ 10
image-01ratio enum1:116:94:33:22:33:49:1621:9 · n ≤ 9 · prompt ≤1500 characters

MODEL_ASPECTS stores a ModelCapability for each row rather than a set. Exactly one of ratio_enum, pixel_sizes, or pixel_range is populated; n_max and optional prompt_max_chars travel with it. For OpenAI models, pixel_size(model, aspect) translates a ratio to WIDTHxHEIGHT: the 1.x line uses its fixed lookup, while gpt-image-2 searches the constrained range.

Not every listed model can render a dense spec

The table above is about frames. It says nothing about whether a model can letter forty lines of authored copy correctly, and they differ sharply. Measured across the integration testbed on 2026-08-16, one candidate per recipe:

ModelDense spec copy
grok-imagine-image-2.0, grok-imagine-image-quality, grok-imagine-imagereliable — 16 of 17 rows letter-perfect
gpt-image-2, gpt-image-2-2026-04-21reliable
gpt-image-1.5reliable
gpt-image-1, gpt-image-1-mininot suitable

The 1.0 tier fails structurally rather than cosmetically: whole sections vanish, headings desync from the bodies beneath them, and panels duplicate. One row rendered 01 - THE MESH above section 01's content and dropped section 02 entirely; another printed MagicONS for MagicDNS and lost 04 - INGRESS. The frames were exact and lith-print --strict exited 0 for every one of them — this is a copy-fidelity property, and no exit code detects it.

Use gpt-image-1 and gpt-image-1-mini for a title-and-subtitle poster, or not at all. For anything with sections, prefer gpt-image-2, gpt-image-1.5, or the Grok line.

When a clamp happens, lith-plate prints warning: ... on stderr and sets aspect_note in the envelope; lith-print prints it as a [warn] line. lith-press prints the same warnings on stderr and carries aspect_note, copy_note and limit_notes in its --emit-json payload, including under --check and --dry-run. lith-print also compares the published image's real dimensions against the request and warns when they differ by more than 2%, which catches a provider that ignored the field entirely.


styles.json schema

version string schema version ("1.0.0")
description string free text
families object family key -> family object
rules object authoring constraints; advisory, not enforced at runtime

Family object:

FieldTypeRead byDescription
namestringrender_promptHuman-readable name returned as style.
prompt_templatestringrender_promptstr.format template; slots per the table above.
negative_promptstringrender_promptReturned verbatim.
default_aspectstringrender_promptUsed when the brief omits aspect.
paletteobjectrender_promptOnly background and accent are substituted; other keys document the family.
best_forstring[]Documentation.
iconographystring[]Documentation; suggested icon values.

rulesmax_accent_colors, max_words_in_image, always_oversize_headline, always_one_decorative_motif, prefer_asymmetric_composition, always_one_idea_per_image — are an authoring checklist. No code reads them. They describe the sparse families (A, B, C, E, F, G); a spec-driven family carrying {spec} and {layout} deliberately overrides max_words_in_image and always_one_idea_per_image, since a dense poster is many ideas and a hundred-odd words on purpose.

Pass an alternate file with load_styles(path); the CLIs always use the bundled copy.


Output paths

The filename derives from output_path(dir, family_key, headline, ext):

FilePatternExample
Published image{family_key}_{slug(headline)}{ext}outputs/B_brutalist_32_langs.jpg

The directory is --output-dir for lith-print, defaulting to the recipe's sibling outputs/; lith-plate derives the same directory from --recipe, and falls back to ./outputs in flag mode where there is no recipe to anchor to. lith-print sniffs ext from the image bytes — .jpg, .png, or .webp. lith-plate has no bytes yet, so a path it derives is a bare stem and both commands print it the same way, as {stem}.<jpg|png|webp>. An explicit --out is recorded verbatim instead. The file is overwritten without prompting when a recipe is re-run.

outputs/B_brutalist_32_langs_raw.jpg is a committed legacy Grok artifact used as complete JPEG bytes by the tutorial and smoke test. The opt-in provider canaries use the dill-pickle brief in recipes/live_test_recipe.json.


Tests

uv run pytest
386 passed, 3 skipped

No credentials, no network. The three skips are live provider canaries, which run only when you explicitly authorize the spend — a populated API key does not enable them.

Full details, including the markers, the live canaries, and the branch-aware capability gates: Testing reference.

To retain live canary images for viewing, set an output directory and disable pytest capture so each saved path is printed:

LITH_LIVE_OUTPUT_DIR=outputs/canaries \
LITH_RUN_LIVE_PROVIDER_CANARIES=1 \
uv run pytest -m live_provider -s

Status

ComponentState
Prompt rendering from styles.jsondone
Recipe loader and dry-run driverdone
Validate-and-publish driverdone
Spec-driven poster copy ({spec} / {layout})done, all seven families
Layout vocabulary (15 arrangements)done
Aspect resolution and per-model clampingdone
Published-image aspect checkdone
Direct xAI/OpenAI/MiniMax provider layerdone — lith-press / lith.call
Credential inspection and request dry-rundone
Topic expansion (expand_brief)done, library only — no CLI
Hermes SKILL.md wrapperdone, shipped in skills/; installed manually
Candidate scoringnot built
Video augmentationout of scope; rationale
Post → brief ingestionout of scope; rationale
Calendar rotation toolnot built

Documentation

DocumentTypeRead it when
Tutorial: your first announcement imageTutorialLearning the pipeline by running it once
Python API and implementation referenceReferenceCalling the library, or reading the internals
Testing referenceReferenceRunning the suite, markers, live canaries, coverage gates
About the pipelineExplanationUnderstanding what's built, what isn't, and why
About layoutsExplanationChoosing an arrangement, or understanding the one lith derived
About output stylesExplanationChoosing a family, and how one spec renders seven ways
About the design languageExplanationThe palette, typography and composition rules underneath
skills/lith/SKILL.mdAgent instructionsChecking what a Hermes session will do on your behalf
ContributingHow-toChanging the CLI, adding a provider, or authoring a style
This READMEReferenceLooking up a flag, a field, or a signature

MIT licensed. See LICENSE.