# SC-Compose Requirements > Status: Active Release Baseline > Product: `sc-composer` (library) and `sc-compose` (CLI) > Document role: Normative release requirements for both crates This document supersedes the prior high-level placeholder. It is the normative release requirements baseline for `sc-compose` v1.4.1. ## 1. Intent This document defines the required behavior of `sc-composer` and `sc-compose`. It is the design authority for release work. If the implementation diverges from this document, the implementation is wrong unless the document is explicitly amended. ## 2. Problem Statement Teams need one deterministic composition engine for prompt profiles, instruction templates, and composed prompt output across multiple AI runtimes. Without a shared implementation, include handling, variable validation, discovery conventions, and diagnostics drift across callers. `sc-composer` exists to provide one reusable implementation for: - prompt and profile file resolution, - Jinja2-style template rendering, - include expansion, - variable declaration and validation, - deterministic composition output, - machine-readable diagnostics. ## 3. Product Scope The product has two deliverables: - Library crate: `sc-composer` - CLI binary crate: `sc-compose` The library is the semantic source of truth. The CLI is a thin interface over the library. ### 3.1a Phase P Go module distribution The Phase P generated-Go strategy is governed by [ADR-0020: Generated Go Binding Strategy](adrs/0020-generated-go-binding-strategy.md), which is **Accepted**. The native libraries are distributed as the target-specific release bundles documented by the Go integration guide; a Go module download alone does not include those native libraries. The repository MUST publish a generated Go module for the canonical `sc-sha` hash operations so external consumers can use the shared hash contract without copying its implementation. The module MUST use the documented submodule path `github.com/randlee/sc-compose/bindings/sc-sha-go`, generated UniFFI source, and the matching packaged native artifact for each supported target (Linux/amd64, macOS/arm64, and Windows/amd64). Release validation MUST cover generated source drift, native artifact selection, conformance vectors, and an independent Go consumer. The adapter MUST depend only on `sc-sha` and MUST NOT add ATM, resolver, filesystem, or duplicate hash logic. ### 3.1 ATM Independence This repository is intentionally independent from ATM and any other orchestration runtime. - No `ATM_HOME` environment variable may be referenced anywhere in this repo. - No `agent-team-mail-*` crate may appear in any `Cargo.toml` in this repo. - No ATM spool, socket, mailbox, or runtime path convention may be assumed. - No `use atm_*::...` or `use agent_team_mail::...` imports may appear in the library or CLI crates. - Any ATM integration belongs in adapters outside this repository rather than in `sc-composer` or `sc-compose`. ### 3.2 Boundary Rules - `sc-composer` must remain runtime-agnostic. - `sc-composer` must not depend on mailbox formats, daemon lifecycle behavior, team coordination state, or runtime-specific home-directory conventions. - `sc-compose` must be usable as a standalone tool without any external orchestration runtime. - If an external system needs integration-specific behavior, that adaptation must live outside this repository rather than inside the core composition semantics. ### 3.3 Non-Goals The initial product explicitly does not provide: - daemon control or process management, - mailbox handling or message routing, - team configuration or ATM runtime management, - network I/O or remote template fetching, - ATM-specific file path conventions or runtime lookup behavior. ## 4. Functional Requirements ### FR-1: Template Inputs - The engine must support plain text and markup source files, including `.txt`, `.md`, and `.xml`. - The engine must support template files ending in `.j2`, including typed variants such as `.md.j2`, `.txt.j2`, and `.xml.j2`. - Any filename ending in `.j2` must be treated as a template. - Files may begin with YAML frontmatter. - Frontmatter is optional. ### FR-1a: Frontmatter Schema Frontmatter must support this schema: ```yaml pass: 1 required_variables: - variable_name variables: variable_name: required: true defaults: variable_name: value input_defaults: variable_name: fallback metadata: key: value json_escape_mode: auto ``` Schema rules: - `required_variables` is optional. - `pass` is optional and identifies an explicit pass number for stacked frontmatter blocks. - `defaults` is optional. - `input_defaults` is accepted as an alias for `defaults` in frontmatter. - For compatibility with existing template metadata, a frontmatter `variables` map with `{ required: true }` declarations is accepted as an equivalent spelling of `required_variables`. - `metadata` is optional. - `json_escape_mode` is optional for JSON templates and accepts only `auto` or `legacy`. `auto` is the default; `legacy` is an explicit compatibility mode for manually quoted string placeholders and emits a migration warning. - The recognized top-level frontmatter keys are `pass`, `required_variables`, `variables`, `defaults`, `input_defaults`, `metadata`, and `json_escape_mode`. When scanning stacked frontmatter, a later block containing an unrecognized top-level key is treated as template body content. - If a frontmatter block exists and a field is omitted, it defaults to: - `required_variables: []` - `defaults: {}` - `metadata: {}` - If no frontmatter block exists at all, the file is treated as having no declarations and no defaults. - `required_variables` values must be unique variable names. - `defaults` supplies optional values that become part of the render context unless overridden by environment-derived or explicit input values. - If both `defaults` and `input_defaults` appear in the same frontmatter block, `input_defaults` wins for overlapping keys and validation emits a `WARN_VAL_CONFLICTING_DEFAULT_SECTIONS` warning diagnostic. - `metadata` is descriptive only. It must not directly change render semantics unless a future requirement explicitly assigns meaning to a metadata key. ### FR-1b: Value Types ### FR-1b-json: JSON interpolation contract - JSON templates use complete-value `auto` interpolation by default. Bare placeholders own their JSON quoting and preserve scalar, object, array, and null types. - An explicit `legacy` mode safely escapes string contents without adding surrounding quotes already present in manually quoted source. - CLI mode selection takes precedence over root frontmatter, followed by the default `auto` mode. - `validate` and `validate --lint` emit the migration-directed warning for legacy mode or quoted placeholders detected in JSON context: `Template uses legacy JSON escape mode. Migrate to bare placeholders (auto mode) to avoid double-quoting issues. See docs/migration/json-escape-mode.md` - A JSON render must not emit output until the complete body parses successfully. - The parser gate applies to ordinary `render` as well as `render --check-render` and runs before stdout, file, dry-run preview, or JSON-envelope emission. - `validate` remains static-only and must report that state explicitly. The `validate --check-render` variant renders in memory with the supplied exact context, reports the checked-render state, and emits no body or file. - A malformed rendered JSON body must fail closed with the stable `ERR_RENDER_JSON_MALFORMED` diagnostic. The diagnostic includes template, line, column, and byte offset, but never echoes rendered values. - Source lint uses the canonical `ERR_JSON_MODE_CONTRACT` diagnostic for an auto-mode quoted scalar placeholder. Ambiguous quoted expressions use `WARN_JSON_QUOTED_PLACEHOLDER` as a conservative finding instead of being silently treated as safe. The existing `WARN_JSON_LEGACY_ESCAPE_MODE` migration warning remains emitted by static validation for legacy mode or detected quoted placeholders. The render-context value model accepts any finite JSON/YAML-compatible tree that the existing `serde_json::Value` and Minijinja context can represent. The six Phase O.4 repository assignment templates are explicit `auto`-mode consumers of this contract. Their scalar interpolation slots and scalar loop elements are bare; `carry_forward_findings_json` is the only reviewed raw-JSON fragment and must contain a validated JSON array. The O.4 semantic fixture corpus is the acceptance evidence for quotes, backslashes, Unicode, newlines, empty and optional values, arrays, objects, null branches, and injection-safe control characters. The migration matrix and legacy exception are maintained in `docs/migration/json-escape-mode.md`. The Phase O.5 release gate extends this contract across consumer repositories: - every release-candidate campaign reads a source-of-truth inventory of repository roots and pinned commits, verifies each commit before scanning, and reports the actual JSON-template count and every path; - an unavailable or unpinned root blocks an unconditional release claim; - a successful JSON render is a valid campaign PASS only after the complete emitted body is parsed as one JSON document; parser failure, partial output, timeout, or success-status/body mismatch is fail-closed evidence; - the original 1.4.0 quoted-placeholder shape remains a permanent negative fixture: auto mode rejects it before emission, while explicit legacy mode produces one safely escaped string and one deprecation diagnostic; - external findings are owned by the external repository and require a separately merged migration/fix before the release gate can become green. - Variables used by template rendering must be one of: - string - number - boolean - null - an object/map with string keys, recursively containing supported values - a sequence recursively containing supported values, including arrays, objects, scalars, `null`, and jagged shapes - The top-level `--var-file` document remains a JSON/YAML object and YAML map keys remain strings; these ingress boundaries are independent of nesting depth. - `metadata` may contain arbitrary YAML values because it is descriptive only and does not participate in rendering semantics. HTML-Report follow-on design track: - FR-12 through FR-15 are implemented by Phase HTML-Report. - The remaining design exploration in [docs/html-sprint-report-plan.md](html-sprint-report-plan.md) is limited to H5-and-later work such as multi-panel HTML/XHTML composition, wrapper-level output viewing behavior, and possible post-render-hook design that stays outside the core `sc-compose` contract unless explicitly accepted later. ### FR-1c: File Extension and Discovery Conventions - Profile and prompt assets must support both plain files and template files. - Within a candidate directory, resolver probe order for agent and command files must be: 1. `.md.j2` 2. `.md` 3. `.j2` - Skill probe order must be: 1. `/SKILL.md.j2` 2. `/SKILL.md` 3. `/SKILL.j2` - CLI `render` and `validate` must accept explicit template paths anywhere under the configured root, including nested skill templates. ### FR-1d: Template Pack Layout - Bundled examples and user templates use different on-disk layouts. - Bundled examples are stored on disk as flat `*.j2` files directly under the examples root. - Example names are derived from the template filename by removing the trailing `.j2` suffix and then one remaining source extension when present. Examples: - `hello.md.j2` -> `hello` - `service-config.yaml.j2` -> `service-config` - Derived bundled example names must be unique. If two flat example files normalize to the same name, the examples root is invalid until the collision is removed. - User templates are stored as one subdirectory per template under the user templates root. - A user template directory name is the template name. - A user template directory may contain one or more files, including one or more `.j2` templates and supporting assets. - `template.json` is optional for user template directories. If present, it is user-facing metadata and may contain only: - `description` - `version` - `input_defaults` - `input_defaults` may provide default render inputs using supported render-context value types. - `template.json` must not introduce alternate render semantics, hook execution, or manifest-owned entrypoint selection in the initial release. - The CLI treats each normalized bundled example entry as a single-template example pack even though the on-disk layout is a flat file. - Named render from `sc-compose examples ` resolves the matching flat example-pack file under the examples root. - Named render from `sc-compose templates ` is defined only when the template directory contains exactly one root-level `*.j2` file. - Template directories with zero or multiple root-level `*.j2` files remain listable and addable, but they are not implicitly renderable by name in the initial release. ### FR-2: Variable Resolution and Precedence - Final render context precedence must be: 1. explicit input variables, 2. environment-derived variables, 3. built-in render-context variables, 4. user-template `input_defaults`, 5. frontmatter defaults. - Frontmatter-declared `required_variables` must be evaluated after the merge. - Variables present only in `defaults` are optional by default. - A variable may appear in both `required_variables` and `defaults`; in that case the default value satisfies the requirement unless overridden. - An empty sequence value such as `[]` is valid input and satisfies a required variable when provided explicitly or by defaults. - `validate` and `render --dry-run` must emit an informational diagnostic when a referenced or required variable is satisfied by a default value rather than explicit caller input. - Explicit CLI `--var key=value` inputs are always strings. - This string-only behavior is intentional: CLI text inputs are not coerced based on their spelling. Callers that need numeric, boolean, null, object, or sequence values must use `--var-file` or template-owned defaults. - Variables loaded through `--var-file` may be any supported render-context value type. - Variables loaded through `--env-prefix` are always strings. - After the final context merge, every referenced variable path must either have a value binding or be handled by the unbound-variable policy described in FR-2a. A missing binding is distinct from an undeclared token and from an extra caller-provided variable. - `ComposePolicy.unbound_variable_policy` controls referenced-but-unbound paths with `error`, `warn`, or `ignore` severity. When unset, it inherits `unknown_variable_policy` for compatibility; an explicit value keeps the two policy axes independent. - If frontmatter is absent: - the engine must discover referenced variables from the template and include graph, - `validate` must emit a generated-frontmatter recommendation, - diagnostics must include a direct fix command: `sc-compose frontmatter-init .j2`. ### FR-2c: Built-In Render-Context Variables - Every render context must inject these built-in variables when the caller does not supply them: - `TEMPLATE_NAME` - `HOSTNAME` - `USERNAME` - `RENDER_DATE` - `RENDER_TIMESTAMP` - Built-ins sit below explicit caller inputs and environment-derived values, and above template-owned defaults. - `TEMPLATE_NAME` must reflect the resolved template filename actually rendered, not a caller alias. - Caller-provided values always win: - explicit input values override built-ins, - environment-derived values override built-ins, - template-owned defaults do not override built-ins. ### FR-2a: Tokens Not Declared in Frontmatter Referenced tokens that are not declared in frontmatter must follow these rules: - Default behavior: - they remain preserved in rendered output, - they do not become implicitly required variables, - they produce diagnostics in both `render` and `validate`. - Strict behavior: - validation fails, - rendering fails, - diagnostics identify the undeclared referenced tokens. This behavior is distinct from missing required variables. A token that is undeclared is not automatically treated as required unless it is explicitly listed in `required_variables`. Referenced-but-unbound tokens are a separate axis from undeclared tokens: - A referenced token with no binding after the final context/default merge emits `ERR_VAL_UNBOUND_VARIABLE` according to `unbound_variable_policy`. - A referenced token may be declared in frontmatter and still be unbound; the unbound-variable policy applies independently of `strict_undeclared_variables`. - A referenced token may be undeclared but bound by caller input; the strict undeclared-token policy applies independently and the value is not reported as unbound. - Loop locals, `{% set %}` locals, built-in render-context variables, and nested paths satisfied by a merged object value count as bindings. ### FR-2b: Missing and Extra Variables - Missing frontmatter-declared required variables must fail rendering. - Undefined-variable render failures and undeclared-token diagnostics must use distinct stable diagnostic codes. - Missing-variable diagnostics must include: - the full set of missing variable names, - the file in which each variable became required, - line and column when available, - the include chain when applicable. - Extra input variables not declared by the template or frontmatter must be policy-controlled with `error`, `warn`, or `ignore`. ### FR-3: Include Expansion - The engine must support inline include directives in the form `@`. - Include resolution order must be: 1. path relative to the containing file, 2. path relative to the configured root. - Nested includes must support: - cycle detection, - bounded maximum depth, - deterministic expansion order. - Included templates must be evaluated under the same context and validation policy as their parent template. - Include expansion must be applied consistently whether rendering to stdout or to a file. - Include failures must produce actionable diagnostics with include-chain context. ### FR-3a: Frontmatter Across Includes - A file's own frontmatter applies to that file. - Required-variable declarations discovered from included files participate in validation of the overall composition result. - Defaults declared in included files participate in context construction unless overridden by parent-file defaults, environment-derived variables, or explicit input variables. - If multiple files declare a default for the same variable, precedence must be: 1. explicit input variables, 2. environment-derived variables, 3. including file defaults, 4. included file defaults discovered deeper in the include graph. - `metadata` from included files must be preserved in trace data only if the library exposes include metadata in a future API. Metadata must not affect current render semantics. ### FR-4: Safety Constraints - File reads must be confined to a configured root by default. - Path traversal outside the allowed root set must fail. - Callers may optionally provide additional allowed roots. - Template rendering must not execute arbitrary host code. ### FR-5: Prompt Resolution Conventions The resolver must support `file` mode and `profile` mode. In `file` mode: - the caller provides an explicit path, - no precedence search is performed. In `profile` mode: - the caller provides a profile kind and name, - the caller may provide a runtime or omit it, - the resolver searches runtime-specific and shared locations according to a configured path policy. Runtime-specific directories: - `.claude/agents/` - `.claude/commands/` - `.claude/skills/` - `.hermes/agents/` - `.hermes/commands/` - `.hermes/skills/` - `.codex/agents/` - `.codex/commands/` - `.codex/skills/` - `.gemini/agents/` - `.gemini/commands/` - `.gemini/skills/` - `.opencode/agents/` - `.opencode/commands/` - `.opencode/skills/` Shared directories: - `.agents/agents/` - `.agents/commands/` - `.agents/skills/` Default runtime search order for agents: - `claude`: `.claude/agents/` -> `.agents/agents/` - `hermes`: `.hermes/agents/` -> `.agents/agents/` -> `.claude/agents/` - `codex`: `.codex/agents/` -> `.agents/agents/` -> `.claude/agents/` - `gemini`: `.gemini/agents/` -> `.agents/agents/` -> `.claude/agents/` - `opencode`: `.opencode/agents/` -> `.agents/agents/` -> `.claude/agents/` Default runtime search order for commands: - `claude`: `.claude/commands/` -> `.agents/commands/` - `hermes`: `.hermes/commands/` -> `.agents/commands/` -> `.claude/commands/` - `codex`: `.codex/commands/` -> `.agents/commands/` -> `.claude/commands/` - `gemini`: `.gemini/commands/` -> `.agents/commands/` -> `.claude/commands/` - `opencode`: `.opencode/commands/` -> `.agents/commands/` -> `.claude/commands/` Default runtime search order for skills: - `claude`: `.claude/skills//` -> `.agents/skills//` - `hermes`: `.hermes/skills//` -> `.agents/skills//` -> `.claude/skills//` - `codex`: `.codex/skills//` -> `.agents/skills//` -> `.claude/skills//` - `gemini`: `.gemini/skills//` -> `.agents/skills//` -> `.claude/skills//` - `opencode`: `.opencode/skills//` -> `.agents/skills//` -> `.claude/skills//` Ambiguity contract: - If a runtime is explicitly provided, only that runtime path chain is used. - If a runtime is omitted, the resolver must evaluate all configured runtime and shared roots. - If multiple candidates match, resolution must fail with an actionable ambiguity error requiring an explicit runtime selector. - If exactly one candidate matches, the resolver may select it without an explicit runtime. There is no flat shared fallback such as `.agents/`. Shared prompts live only under `.agents/agents/`, `.agents/commands/`, and `.agents/skills/`. The resolver path policy must be configurable by callers and must not be hardcoded into downstream integrations. ### FR-6: Composition Pipeline Final composed output must concatenate blocks in this fixed order: 1. resolved profile body, 2. guidance block, 3. user prompt block. Each block may be empty. Ordering is never caller-defined. ### FR-7: CLI Surface `sc-compose` must provide these commands: - `render` - `resolve` - `validate` - `frontmatter-init` - `template-init` - `init` - `verify` - `extract` - `observability-health` - `examples` - `templates` - `reports` - `help` (see FR-22) The CLI must support: - `--mode ` - `--kind ` - `--agent ` - `--agent-type ` as an alias for `--agent` - `--runtime ` as an optional runtime selector - `--ai ` as an alias for `--runtime` - `--var key=value` repeatably - `--var-file ` - `--env-prefix ` - `--strict` - `--unknown-var-mode ` - controls both extra caller-provided variables and referenced-but-unbound variables; the CLI maps the selected mode to both policy axes. - `--root ` - `--file ` - `--output ` where applicable - `--guidance ` - `--guidance-file ` - `--prompt ` - `--prompt-file ` - `--json` - `--dry-run` - `--force` where applicable - `--pass N` repeatably where applicable - `--against ` where applicable - `--quiet` where applicable - `--builtin-var KEY=VALUE` repeatably where applicable Command behavior: - `render` - renders one resolved template or profile, - writes to stdout by default, - may write to a file when requested, - must honor validation and strictness policy, - accepts optional guidance and user prompt blocks. - `resolve` - is defined for `profile` mode, - prints the selected profile path, - reports attempted search paths, - fails in `file` mode. - `validate` - performs full include expansion and variable analysis, - does not write output files, - exits non-zero on validation failure. - `frontmatter-init` - discovers referenced variables, - prepends minimal frontmatter, - fails if frontmatter already exists unless `--force` is provided. - `template-init` - converts a concrete file into a templated file using one or more pass groups declared with `--pass N`, - accepts `--var key=value` replacements within each pass group, - accepts `--force` to overwrite an existing frontmatter/template header, - supports `--dry-run` without writing the rewritten file, - exits with code `3` when declared literal values are not found in the source file because that outcome is a usage/configuration failure rather than a successful drift result. - `init` - creates `.prompts/`, - ensures `.prompts/` is ignored by Git, - scans repository templates, - validates discovered templates, - fails if invalid templates are found, - prints recommendations for missing or weak frontmatter. - `observability-health` - reads the current CLI logger health state without mutating composition or log configuration, - prints a human-readable health summary by default, - emits the documented JSON schema when `--json` is provided. - `examples` - supports: - `examples list` - `examples ` for implicit named render - resolves example packs from the bundled examples root, - uses the same render flags and output semantics as `render` for implicit named render. - `templates` - supports: - `templates list` - `templates add [name]` - `templates ` for implicit named render - resolves template packs from the user templates root, - uses the same render flags and output semantics as `render` for implicit named render, - allows `add` from either a single file or a directory source, - stores a file source as `//`, - stores a directory source as `//...`. - `reports` - supports: - `reports init` - `reports smoke` - `reports finalize` - `reports render-spec` - `reports index` - `reports verify` - `reports publish-manifest` - owns the shared reporting runtime surface rather than repo-specific producer command bodies, - keeps publish upload and browser-open behavior outside the command family. - `verify` - compares a deployed file against the rendered output of `--against