sc-compose
Compose once. Render deterministically. Ship everywhere.
Project website
sc-compose is a standalone CLI and library for teams whose templates have
outgrown copy-paste. Compose templates from shared, version-controlled
fragments, declare inputs up front, and render deterministic output across any
runtime — AI agent profiles, pytest fixtures, .NET harnesses, HTML reports, and
service configs.
One engine, everywhere. A single Rust library (sc-composer) drives the
CLI (sc-compose), Python bindings, and any embedded host. Templates are
Jinja2 with YAML frontmatter. Shared fragments use @-include. Required inputs
fail loudly at render time — no guessing, no silent defaulting.
For AI agent workflows, one profile resolves across Claude Code, Codex, Gemini,
and OpenCode through each runtime's native search chain, with a shared
.agents/ fallback so you override only the runtimes that genuinely need it.
Quickstart
# Install
brew install randlee/tap/sc-compose # macOS
winget install randlee.sc-compose # Windows
scoop bucket add randlee https://github.com/randlee/scoop-bucket
scoop install sc-compose # Windows (Scoop)
cargo install sc-compose # from source
pip install sc-compose # Python
# Render your first template
echo 'Hello {{ name }}!' > hello.txt.j2
sc-compose render --file hello.txt.j2 --var name=World
# → Hello World!
Feature Highlights
Compose Templates from Shared Fragments
Place shared conventions in one file. Reference them from everywhere. Edit
once, every downstream template picks up the change.
@<_includes/house-style.md>
Includes nest, cycles are detected, paths are confined to the workspace root.
Declare Inputs Up Front
YAML frontmatter makes required inputs explicit:
---
required_variables:
- task_id
- branch
defaults:
pr_target: develop
---
Missing a required variable? sc-compose fails with an actionable diagnostic
that names the missing variable, the file that declared it, and the include
chain.
One Profile Across Four Runtimes
Author an agent profile once under .agents/agents/. Override only the
runtimes that need specialization:
your-repo/
├── .agents/agents/reviewer.md ← works for Claude, Codex, Gemini, OpenCode
├── .claude/agents/ ← Claude-only overrides (optional)
├── .codex/agents/ ← Codex-only overrides (optional)
sc-compose render --mode profile --kind agent --agent reviewer --runtime claude
Multi-Pass Nested Templates (v1.3.0)
Progressive resolution: deploy-time → install-time → invocation-time variables
in one file. Outer passes use more braces; inner passes use fewer. Shared
fragments (@-include) work at every pass.
# Render all three passes
sc-compose render config.yaml.2.j2 --all \
--pass 3 --var-file deploy.json \
--pass 2 --var-file install.json \
--pass 1 --var-file invoke.json
# Verify deployed config hasn't drifted
sc-compose verify deployed.yaml --against config.yaml.2.j2 --all --pass ...
Bundled Examples & Personal Templates
sc-compose examples list # discover starter templates
sc-compose examples pytest-fixture # generate test stubs
sc-compose templates add my-template.md.j2 # save for reuse
sc-compose templates my-template # render by name
Reporting Subsystem
Produce compliance evidence from declarative specs:
sc-compose reports init # scaffold report catalog
sc-compose reports smoke # render smoke report fixture
sc-compose reports render-spec spec # render from semantic spec
sc-compose reports finalize # materialize metadata
sc-compose reports publish-manifest # CI handoff manifest
Python Bindings
from sc_compose import compose, render_template, Renderer
result = render_template("Hello {{ name }}", {"name": "world"})
# Multi-pass rendering:
# compose(request) — full pipeline with ComposePolicy.passes
Pre-built wheels for macOS, Linux, Windows (Python 3.11+).
Go Bindings
The generated sc-sha-go module exposes typed Go bindings for the canonical
sc-sha file and composition hash operations:
go get github.com/randlee/sc-compose/bindings/sc-sha-go@bindings/sc-sha-go/v1.5.0
Import the generated package from
github.com/randlee/sc-compose/bindings/sc-sha-go/go/sc_sha_go. See
docs/integrations/sc-sha-go.md for the
native target matrix, release layout, conformance vectors, and consumer
handoff requirements.
Install Matrix
| Platform | Method | Command |
|---|
| macOS | Homebrew | brew install randlee/tap/sc-compose |
| Windows | Winget | winget install randlee.sc-compose |
| Windows | Scoop | scoop bucket add randlee https://github.com/randlee/scoop-bucket``scoop install sc-compose |
| Any (Rust) | crates.io | cargo install sc-compose |
| Any (Python) | PyPI | pip install sc-compose |
| Any (source) | cargo | cargo build --release -p sc-compose |
| Rust lib | Cargo.toml | sc-composer = "1.5.0" |
Bundled examples are guaranteed in Homebrew, Scoop, Winget, and GitHub Release
installs. cargo install ships the binary only — set SC_COMPOSE_DATA_DIR for
examples.
Status
| |
|---|
| Version | 1.5.0 |
| MSRV | Rust 1.94.1 |
| Rust edition | 2024 |
| Platforms | macOS, Linux, Windows |
| Stability | stable 1.5 release line |
Documentation
Why sc-compose?
Prompt files drift across repos, tasks, and runtimes. Teams end up with several
copies of the same prompt: .claude/agents/foo.md, .codex/agents/foo.md, a
Slack paste, a gist, and a shell-history version. Those copies diverge. Agent
behavior diverges with them. Debugging turns into prompt diffing.
sc-compose treats prompts as source code you compose, not text you copy.
Compose once. Render deterministically. Keep shared fragments in one place and
include them by reference. Pass task context as variables. Validate required
inputs at render time so missing data fails fast instead of being guessed.
The workspace provides three packages:
- sc-composer — a Rust library with the render, include-expansion, validation, and diagnostics pipeline
- sc-compose — a CLI wrapper over the library for scripts, shells, and agent-invocable workflows
- sc-compose (PyPI) — Python native extension for
pip install
All three are standalone. None is coupled to any particular orchestration
system.
CLI Reference
| Command | What it does |
|---|
render | Render a template or resolved profile to stdout or a file |
resolve | Print the resolved profile path and search trace |
validate | Expand includes and analyze variables without writing output |
frontmatter-init | Discover referenced variables and prepend minimal frontmatter |
init | Create .prompts/, add it to .gitignore, and scan templates |
observability-health | Report process-local structured logging health |
examples list | List bundled starter templates |
examples | Render a bundled example with --var / --var-file |
templates list | List your saved personal templates |
templates add [name] | Save a file or directory to your local template store |
templates | Render a saved template with --var / --var-file |
template-init | Convert a concrete file into a multi-pass stacked template |
verify | Verify a deployed file matches its multi-pass template source |
reports init | Create the shared report scaffold and starter catalog |
reports smoke | Render the built-in smoke report fixture |
reports finalize | Materialize metadata and archives for producer-owned outputs |
reports render-spec | Render a semantic report spec into shared artifacts |
reports index | Summarize current latest report artifacts |
reports verify | Verify required report evidence is present |
reports publish-manifest | Write machine-readable publish handoff manifest |
Key flags:
| Flag | Purpose |
|---|
--mode | Template lookup mode (default: file) |
--kind | Profile kind in profile mode |
--agent | Profile name in profile mode |
--runtime | Runtime selector |
--file | Template path in file mode |
--var key=value | Input variable (repeatable) |
--var-file | JSON/YAML variable file (- for stdin) |
--env-prefix | Absorb env vars matching prefix |
--guidance / --guidance-file | Append guidance block |
--prompt / --prompt-file | Append user prompt block |
--output | Write rendered output to file |
--dry-run | Report without modifying files |
--json | Machine-readable output with diagnostics envelope |
--strict | Fail on undeclared referenced variables |
--all | Render all passes (multi-pass templates) |
--pass N --var ... | Per-pass variable inputs |
Run sc-compose --help for the full flag surface.
Resolver Search Chains
| Runtime | Agents | Commands | Skills |
|---|
| Claude | .claude/agents, .agents/agents | .claude/commands, .agents/commands | .claude/skills, .agents/skills |
| Hermes | .hermes/agents, .agents/agents, .claude/agents | .hermes/commands, .agents/commands, .claude/commands | .hermes/skills, .agents/skills, .claude/skills |
| Codex | .codex/agents, .agents/agents, .claude/agents | .codex/commands, .agents/commands, .claude/commands | .codex/skills, .agents/skills, .claude/skills |
| Gemini | .gemini/agents, .agents/agents, .claude/agents | .gemini/commands, .agents/commands, .claude/commands | .gemini/skills, .agents/skills, .claude/skills |
| OpenCode | .opencode/agents, .agents/agents, .claude/agents | same pattern | same pattern |
Claude is the universal fallback because it is the most common author target in
practice.
Contributing
main is protected. Create feature branches from develop and follow
docs/git-workflows.md for branching and review rules.
Adhere to the Pragmatic Rust Guidelines for code style.
License
MIT. See LICENSE.