Hierarchical prompt resolver CLI
Project description
stemmata
Prompt reuse across repositories is a mess. You copy a YAML prompt into a new project, tweak it, and within a week the original and the copy have diverged. Multiply that by a dozen services and you're maintaining the same boilerplate in twenty places.
stemmata fixes this with hierarchical composition: prompts declare ancestors (by relative path or by registry coordinate), and the CLI resolves the full inheritance chain into a single, deterministic YAML document. Ancestor prompts are distributed as npm packages through any private registry you already run.
Table of Contents
- Features
- Installation
- Quick Start
- CLI Reference
- Prompt Format
- Merge Semantics
- Exit Codes
- Configuration
- Testing
Features
- Hierarchical composition: prompts declare
ancestorsas paths or(package, version, prompt)coordinates; the full transitive closure is resolved eagerly via breadth-first search. - Deterministic merging: nearest-wins for scalars and lists, deep-merge for maps, with breadth-first search distance plus reference occurring-ordering (for breaking ties) so the output is reproducible.
- Placeholder interpolation:
${path}references resolve against the merged namespace, with structural, textual, and list-splat forms. - npm registry transport: speaks the standard npm REST API; credentials read from
~/.npmrc.
Installation
pip install stemmata
Requires Python 3.12+ (for tarfile.data_filter). Sole third-party dependency is PyYAML.
Quick Start
# You have a local prompt that inherits from a base — resolve it:
stemmata resolve ./prompts/onboarding.yaml
# Or resolve a prompt published to your registry by coordinate:
stemmata resolve '@acme/prompts-core@1.2.3#onboarding'
# Need machine-readable output for a script or pipeline:
stemmata --output json resolve ./prompts/onboarding.yaml
# Wipe the local cache (by default stored under ~/.cache/stemmata):
stemmata cache clear
CLI Reference
stemmata [GLOBAL FLAGS] <subcommand> [ARGS]
Global flags
| Flag | Default | Description |
|---|---|---|
--output {yaml,json,text} |
per-subcommand | Output format (yaml for resolve, json for everything else). |
--verbose |
off | Timestamped diagnostics on stderr. |
--offline |
off | Forbid network access; exit 22 if a fetch would be needed. |
--refresh |
off | Re-fetch artifacts even if cached. |
--version |
— | Print version and exit. |
resolve <target>
Resolves a single prompt. Target is either a local path (./prompts/onboarding.yaml) or a registry coordinate (@<scope>/<name>@<version>#<prompt-id>).
Resource limits: --max-prompts (default 1000), --max-depth (default 50), --http-timeout (default 30s), --timeout (default 5m).
On success, stdout carries the resolved YAML (or a JSON envelope with {root, content, ancestors[]}). On failure, stdout carries a JSON error envelope regardless of --output, and stderr gets a one-line human-readable summary.
publish [path]
Builds and uploads the package at path (default .) to the registry routed by ~/.npmrc. Before any bytes leave the machine, every prompt listed in package.json is checked for: (1) ancestor cycles, (2) intra-document type conflicts, (3) placeholder resolvability against the fully resolved namespace, (4) dependencies consistency with the cross-package references found in the prompts, (5) manifest closure under relative-path references — every local ancestors entry must resolve to a path that is itself declared in prompts, since only manifest-listed files are bundled, and (6) $schema validation against the prompt's content contract. All errors discovered in the pass are aggregated into a single envelope; the headline exit code is the most severe one (cycle > schema > reference > merge > placeholder).
Flags: --dry-run (build the tarball but skip upload), --strict-schema (treat unfetchable / unvalidated $schema as an error rather than a warning), --tarball <path> (write the built tarball to path). The tarball is deterministic: identical inputs produce byte-identical output.
$schema enforcement requires pip install stemmata[publish] (adds jsonschema). Without it, publish warns and skips schema validation in default mode, or errors in --strict-schema mode.
cache clear
Evicts every cached entry.
Prompt Format
A prompt is a structured mapping (only YAML is supported as of now) with reserved envelope keys plus arbitrary content:
ancestors:
- ../base.yaml # relative path (within package)
- package: "@acme/common" # cross-package coordinate
version: "1.0.4"
prompt: "defaults"
$schema: "https://schemas.example/foo.v1.json" # optional, enforced at publish time if present
database:
host: "db.internal"
port: 5432
body: |
Region is ${vars.region}; DB is ${database.host}:${database.port}.
ancestors and $schema are stripped from the namespace; every other key is addressable via dotted path.
Package manifest (package.json)
{
"name": "@acme/prompts-core",
"version": "1.2.3",
"license": "UNLICENSED",
"dependencies": { "@acme/common": "1.0.4" },
"prompts": [
{ "id": "base", "path": "base.yaml", "contentType": "yaml" },
{ "id": "onboarding", "path": "extra/onboarding.yaml", "contentType": "yaml" }
]
}
name must be @<scope>/<n>. version is strict SemVer, no ranges. prompts is non-empty; id defaults to basename without extension and must match [a-z0-9][a-z0-9_-]*.
Merge Semantics
Reachable prompts are layered by breadth-first search distance from the root (distance 0 = root, wins everything). Ties at the same distance break by enqueue order.
Maps are deep-merged, with the nearer value winning at each leaf:
# ancestor (distance 1) # root (distance 0)
database: database:
host: "base.internal" host: "override.internal"
port: 5432 ssl: true
Resolved: database.host = "override.internal" (nearer wins), database.port = 5432 (survives from ancestor), database.ssl = true (only root provides it).
Lists replace wholesale — no element-level merge. null at a nearer layer shadows the entire subtree beneath it.
For the full interpolation reference (structural vs. textual placeholders, list splat, non-splat ${=...} form, escaping, version conflict resolution), see docs/interpolation.md.
Exit Codes
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Generic / unexpected failure |
2 |
Usage error |
10 |
Schema validation error |
11 |
Unknown ancestor or prompt id |
12 |
Cycle detected |
14 |
Unresolvable placeholder |
15 |
Merge / interpolation type mismatch |
20 |
Network / registry error |
21 |
Cache error |
22 |
Offline-mode violation |
On failure, stdout always carries a JSON error envelope with {status, exit_code, command, error: {code, category, message, ...}} regardless of --output. Stderr gets a single-line human summary.
Configuration
Registry routing and credentials come from ~/.npmrc for both fetch and publish.
Testing
PYTHONPATH=src python -m pytest tests/ -q
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stemmata-0.0.1.tar.gz.
File metadata
- Download URL: stemmata-0.0.1.tar.gz
- Upload date:
- Size: 56.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
906e7b2c85d40a786cead45a45f26170286722b6f4643ed7c45386572ef1de89
|
|
| MD5 |
ca451acd27253957741e97e91bc9ab95
|
|
| BLAKE2b-256 |
ab8699d2ee2a1451f1e4d29e76bd415e5edb6622dfacbe132864afdf931bb646
|
Provenance
The following attestation bundles were made for stemmata-0.0.1.tar.gz:
Publisher:
publish.yml on pjmartos/stemmata
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stemmata-0.0.1.tar.gz -
Subject digest:
906e7b2c85d40a786cead45a45f26170286722b6f4643ed7c45386572ef1de89 - Sigstore transparency entry: 1301198634
- Sigstore integration time:
-
Permalink:
pjmartos/stemmata@5569c016e707a564aa42d94bc59109247ff1d70b -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/pjmartos
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5569c016e707a564aa42d94bc59109247ff1d70b -
Trigger Event:
push
-
Statement type:
File details
Details for the file stemmata-0.0.1-py3-none-any.whl.
File metadata
- Download URL: stemmata-0.0.1-py3-none-any.whl
- Upload date:
- Size: 46.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
782514588aa935f9ac7f46a3c39c0011ee340653f4aeacdea83ebfbb86b89e8a
|
|
| MD5 |
15bd7d49be413f8e16f9c5467dbb9a6d
|
|
| BLAKE2b-256 |
a868a127153f2f786c55a65ee7b5e11b924e79eac2c604746acc0cb55bb28b5b
|
Provenance
The following attestation bundles were made for stemmata-0.0.1-py3-none-any.whl:
Publisher:
publish.yml on pjmartos/stemmata
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stemmata-0.0.1-py3-none-any.whl -
Subject digest:
782514588aa935f9ac7f46a3c39c0011ee340653f4aeacdea83ebfbb86b89e8a - Sigstore transparency entry: 1301198805
- Sigstore integration time:
-
Permalink:
pjmartos/stemmata@5569c016e707a564aa42d94bc59109247ff1d70b -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/pjmartos
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5569c016e707a564aa42d94bc59109247ff1d70b -
Trigger Event:
push
-
Statement type: