Skip to main content

Hierarchical prompt resolver CLI

Project description

stemmata

PyPI Python Version License

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

  • Hierarchical composition: prompts declare ancestors as 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stemmata-0.0.1.tar.gz (56.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stemmata-0.0.1-py3-none-any.whl (46.3 kB view details)

Uploaded Python 3

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

Hashes for stemmata-0.0.1.tar.gz
Algorithm Hash digest
SHA256 906e7b2c85d40a786cead45a45f26170286722b6f4643ed7c45386572ef1de89
MD5 ca451acd27253957741e97e91bc9ab95
BLAKE2b-256 ab8699d2ee2a1451f1e4d29e76bd415e5edb6622dfacbe132864afdf931bb646

See more details on using hashes here.

Provenance

The following attestation bundles were made for stemmata-0.0.1.tar.gz:

Publisher: publish.yml on pjmartos/stemmata

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for stemmata-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 782514588aa935f9ac7f46a3c39c0011ee340653f4aeacdea83ebfbb86b89e8a
MD5 15bd7d49be413f8e16f9c5467dbb9a6d
BLAKE2b-256 a868a127153f2f786c55a65ee7b5e11b924e79eac2c604746acc0cb55bb28b5b

See more details on using hashes here.

Provenance

The following attestation bundles were made for stemmata-0.0.1-py3-none-any.whl:

Publisher: publish.yml on pjmartos/stemmata

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page