yass

Yet Another Spec Syntax — a YAML-based specification language for AI-agent-driven development. Specs capture desired behavior and are consumed to drive TDD and implementation generation.

Early work in progress.

Yass is for people and coding agents who need an explicit, reviewable contract before implementing or changing behavior. The language describes inputs, returns, failures, side effects, and invariants in YAML. The CLI finds and serves those specifications, resolves references, and checks structure and graph hygiene. It reads spec sets without changing them; writing specs and implementing code are tasks for you and your agent.

Start with installation and the quick start, then use the CLI guide to navigate a project. The chapter on developing with agents shows how to elicit and review a new spec, implement an approved one, and adopt Yass in an existing codebase.

The language reference, authoring guidance, and brainstorming flow publish the authoritative source documents that the CLI embeds and serves with yass docs. The JSON Schema supports editor validation. Tooling checks do not establish that a contract captures the intended behavior or that its implementation is correct.

Installation and updates

Released binaries run without Go. Choose Homebrew, the release installer, or a manual download. Releases support macOS and Linux on arm64 and amd64, and Windows on amd64. Check the installed version with:

yass --version

Homebrew

brew tap shakefu/yass https://github.com/shakefu/yass
brew install yass

Update a Homebrew installation through its package manager:

brew update
brew upgrade yass

Release installer (macOS and Linux)

The installer needs curl, tar, and either sha256sum or shasum, plus standard shell utilities. It detects the host OS and architecture, downloads the newest stable release, and checks the archive against that release’s checksums.txt before installing the binary.

curl -fsSL https://raw.githubusercontent.com/shakefu/yass/main/install.sh | sh

By default, it uses /usr/local/bin when that directory is writable. Otherwise it creates and uses $HOME/.local/bin. It reports the installed path and prints a PATH setup suggestion if that directory is absent from PATH.

Pass options after sh -s -- when piping the script:

curl -fsSL https://raw.githubusercontent.com/shakefu/yass/main/install.sh |
  sh -s -- --version v0.4.3 --bin-dir "$HOME/bin"

Both options accept a separate value or an equals sign:

OptionAccepted spellingsEffect
Version--version TAG, --version=TAGSelect a release; v0.4.3 and 0.4.3 select the same tag.
Directory--bin-dir DIR, --bin-dir=DIRInstall yass into the specified directory, creating it if needed.
Help--help, -hDisplay the script header and exit.

For example, the equivalent equals form is:

curl -fsSL https://raw.githubusercontent.com/shakefu/yass/main/install.sh |
  sh -s -- --version=0.4.3 --bin-dir="$HOME/bin"

The installer also accepts environment variables:

VariableDefaultEffect
YASS_VERSIONNewest stable releaseSelect a release, as with --version.
YASS_BIN_DIRWritable /usr/local/bin, otherwise $HOME/.local/binSelect the installation directory.
YASS_REPO_URLhttps://github.com/shakefu/yassOverride the repository base URL for a mirror or tests. The mirror must provide the same release URL and asset layout.

An option overrides its matching environment variable. If an option appears more than once, the last value wins. An empty version selects the newest stable release, and an empty directory selects the default directory. There is no command-line counterpart to YASS_REPO_URL.

Apply environment variables to the sh process that runs the installer:

curl -fsSL https://raw.githubusercontent.com/shakefu/yass/main/install.sh |
  YASS_VERSION=0.4.3 YASS_BIN_DIR="$HOME/bin" sh

To add the default user directory to your current shell’s PATH:

export PATH="$HOME/.local/bin:$PATH"

Put that line in your shell’s startup configuration, such as ~/.zshrc or ~/.bashrc, to keep it for future sessions. Substitute your chosen directory when using --bin-dir or YASS_BIN_DIR.

Manual release downloads

Open the releases page and choose a release. Download its checksums.txt and the archive matching your platform:

PlatformArchive name
macOS, Apple Siliconyass_<version>_darwin_arm64.tar.gz
macOS, Intelyass_<version>_darwin_amd64.tar.gz
Linux, ARM64yass_<version>_linux_arm64.tar.gz
Linux, x86-64yass_<version>_linux_amd64.tar.gz
Windows, x86-64yass_<version>_windows_amd64.zip

The filename’s version omits the tag’s leading v. Compare the archive’s SHA256 digest with its entry in checksums.txt before extracting it. On macOS:

shasum -a 256 yass_0.4.3_darwin_arm64.tar.gz

On Linux, use sha256sum with your archive name. After confirming the matching checksum, extract the binary and place it on PATH. For example, on Apple Silicon:

tar -xzf yass_0.4.3_darwin_arm64.tar.gz yass
mkdir -p "$HOME/.local/bin"
install -m 0755 yass "$HOME/.local/bin/yass"
export PATH="$HOME/.local/bin:$PATH"

On Windows, the shell installer is unsupported. Download the zip and check its digest in PowerShell:

Get-FileHash .\yass_0.4.3_windows_amd64.zip -Algorithm SHA256

Compare the reported hash with the corresponding checksums.txt entry, then extract yass.exe into a directory such as %USERPROFILE%\bin. Add that directory to your user PATH through Windows environment variable settings and open a new terminal before running yass --version.

Use the version you selected in place of 0.4.3 in these examples.

Updating an installed binary

For an installer-managed or manually placed binary:

yass update

This command takes no options or operands and works outside a Yass project. It resolves the newest stable release, verifies the matching archive’s checksum, and replaces the running binary at its resolved path. It needs network access and permission to write the binary’s directory. A failed update preserves the working installation; diagnostics go to standard error with exit status 5.

On success it exits with status 0 and writes one tab-separated record:

updated	0.4.1	0.4.3

The fields are action, installed version, and newest stable version. The action is current when already current, updated after replacement, or homebrew when an older Homebrew-managed binary is detected. For homebrew, run brew update and brew upgrade yass.

yass update selects the newest stable release, even when you previously installed a pinned version. To keep or change a pin, rerun the installer with --version or manually install the selected release instead.

Building from source

A source build needs Go 1.24 or newer and network access on the first build to fetch module dependencies. From a repository checkout:

cd cli
go build -o yass .
./yass --version

To build directly into Go’s binary directory instead:

go build -o "$(go env GOPATH)/bin/yass" .
export PATH="$(go env GOPATH)/bin:$PATH"

Persist the PATH setting in your shell configuration if necessary. For more build and test instructions, see cli/HOWTORUN.txt.

Quick Start

Use yass to describe behavior, then give an agent the relevant obligations. The CLI reads and checks specs; your agent writes tests and implementation code.

Install and orient yourself

Install the latest release with the installer:

curl -fsSL https://raw.githubusercontent.com/shakefu/yass/main/install.sh | sh

The default destination is /usr/local/bin when writable, otherwise ~/.local/bin; add the destination to your shell’s PATH if needed. For Homebrew, platform support, custom destinations, or building from source, see Installation.

yass --version
yass

--version reports the CLI and language versions. Running yass gives an overview of the notation and commands. It also identifies the current project, when one exists; an overview outside a project still introduces the tool.

Use version 0.4.3 or newer for these examples. Older versions such as 0.4.2 expect the previous schema URL and report a modeline lint warning for the current Pages URL. See Updating if your installed version is older.

Create a small project

Create a new directory and its root.yass.yaml:

mkdir greeting-spec
cd greeting-spec
cat > root.yass.yaml <<'YAML'
# yaml-language-server: $schema=https://shakefu.github.io/yass/v1.schema.json
---
description: A greeting function that returns a message for a supplied name.
version: v1
---
spec: Greeting
INPUT:
- MUST: accept a nonempty UTF-8 name string
RETURN:
- MUST: return Hello followed by one space, the supplied name, and an exclamation mark
SIDE-EFFECT:
- MUST-NOT: write to standard output or modify a file
YAML

The first YAML document describes the file. The second names a spec and groups its obligations into slots. root.yass.yaml defines the project root and contains exactly one spec. This example specifies a function with a stated input contract; it does not yet define behavior for calls outside that contract.

Read and check the spec

Run these commands from greeting-spec:

yass root
yass list
yass query root@Greeting
yass query root@Greeting::RETURN
yass find 'supplied name'
yass validate
yass lint

root prints the absolute path of greeting-spec. list prints two tab-separated records: the file root and the spec root@Greeting. query emits YAML, and the second query emits only the RETURN slot. find locates the return obligation with the target root@Greeting::RETURN.

Both validate and lint should produce no output and exit with status 0. Validation checks the language’s structural rules; lint checks project hygiene. Neither proves that the prose is complete or that an implementation satisfies it.

Targets omit the .yass.yaml suffix. yass query root reads the file’s preamble; yass query root@Greeting reads the named spec. See the CLI Guide for target paths, references, filtering, and exit statuses.

Give the agent a bounded task

For an existing spec, a useful starting instruction is:

Use yass to read this project’s specs. Start with yass and yass list, then read root@Greeting. Implement the greeting function with tests for its stated input contract and return value. Raise any missing behavior before extending the contract.

For a new feature, use yass docs brainstorming to elicit behavior before writing the spec. Use yass docs reference and yass docs guidance when authoring or changing it. These documents are embedded in the binary and work outside a project too.

Continue with the worked example in Developing with agents to grow the spec into a project with references and shared contracts.

CLI Guide

The yass CLI helps you locate, retrieve, and check a project’s specs. It reads spec files without changing them. update is the exception: it can replace the installed CLI binary.

yass [-C DIR] [--version] [--help] [SUBCOMMAND [ARGS...]]

Global options go before the subcommand; command options go after it. -C DIR sets the starting directory for paths and root discovery. --help (-h) prints usage; --version (-V) prints the CLI and language versions. Use -- before an operand that starts with -.

yass -C greeting-spec list
yass find -- '-'

The following examples use the project from Quick Start, unless a target is described as an example from a larger project.

Projects, paths, and targets

The nearest root.yass.yaml at or above the starting directory defines the project. A nested root defines a separate project; ordinary project traversal does not include that subtree. Project traversal collects .yass.yaml files regardless of version-control ignore rules. It does not descend through directory symlinks.

Commands accepting PATH take filesystem paths, including the suffix when naming a file. Commands accepting TARGET use the notation below and omit that suffix:

TargetWhat it addresses
rootPreamble of root.yass.yaml
root@GreetingThe spec named Greeting in that file
root@Greeting::RETURNThe spec’s declared RETURN slot
format@GreetingTemplateA design named GreetingTemplate in format.yass.yaml
./format@GreetingTemplateThe same form, relative to the starting directory

A target path without a leading dot resolves from the project root. ./ and ../ paths on the command line resolve from the starting directory. Inside spec files, those paths resolve from the referencing file’s directory, and a bare document name such as GreetingTemplate refers to a document in the same file. On the command line, a target with no @ addresses a file, not a same-file spec. Use root@Greeting to select that spec explicitly.

Document names and slots are case-sensitive. Slots are INPUT, RETURN, ERROR, SIDE-EFFECT, and INVARIANT; a target must name a slot the spec actually declares. Designs have no slots.

Orient: overview and root

yass
yass overview
yass root
yass root root.yass.yaml

With no subcommand, yass runs overview. It introduces the CLI, reports the project when one is available, summarizes the notation, and lists reading commands. It also works outside a project.

root [PATH] prints only the absolute directory path of the project governing PATH, or the starting directory when omitted. Use it to check which project a command will inspect. Missing a project root is an error for root.

Map: list

yass list
yass list root.yass.yaml
yass list --filter 'root*'
yass list --filter root
yass list --filter '*@Greeting'
yass list --filter '*@*Handler*' --filter '*@*Policy*'

list [--filter GLOB] [PATH...] indexes the project, or the files and directories you select. Each tab-separated record has REF, KIND, DETAIL, and DESCRIPTION fields. File detail is the number of documents; spec detail is its declared slots; design detail is its type. File descriptions are shortened to 100 Unicode characters, and document descriptions are absent (-).

Filters match the whole REF string: * matches any run of characters, including / and @, and ? matches one character. All other characters are literal; there are no regexes or bracket classes. Quote patterns so your shell does not expand them. Multiple filters are combined with OR. If a document matches, its parent file record is retained even when that record does not match.

root* matches the file REF root and document REFs such as root@Greeting; the wildcard crosses the @ separator. The exact filter root matches only the file record. *@Greeting selects that document name across files, together with each matching document’s parent file record.

An empty selection prints nothing and succeeds. An unparseable file remains in the index with unavailable fields and causes status 1; use validate to diagnose it.

Read: query

yass query root
yass query root@Greeting
yass query root@Greeting::INPUT root@Greeting::RETURN
yass query --raw root@Greeting

query [--raw] TARGET... emits a YAML document stream in target order. A file target emits its preamble, a spec target emits its declared slots, a slot target emits that slot under the spec name, and a design target emits its design block.

By default, query resolves one level of references:

  • Slot-targeted CONFORMS inserts the referenced obligations before the carrier, with provenance comments. Guarded carriers and guarded inserted obligations have their guards conjoined.
  • USES appends the referenced design once per fragment, with provenance.
  • Whole-spec CONFORMS and SEE remain as authored references.

References inside inserted obligations or appended designs are not recursively expanded. --raw emits the addressed content as authored, with no resolution or provenance comments. It can retrieve a parsed document whose references are broken. All requested targets must resolve before query emits anything; an unresolved target or a failed default resolution leaves standard output empty.

Locate prose: find

yass find 'supplied name'
yass find --slot RETURN 'hello'

find [--slot SLOT] PATTERN performs a case-insensitive literal substring search, not a regex, glob, or word-boundary search. It searches obligation prose, WHEN guards, and individual lines of design content. It does not search names, preamble descriptions, or reference targets.

Records contain REF, WHAT, and TEXT. WHAT is the normativity keyword, WHEN, or content; a matching field appears once with its full text. The REF can be passed directly to query. --slot restricts obligations, while design content is still searched because designs have no slots. No matches is a successful empty result. Parse failures allow matches from the remaining files and produce status 1 with diagnostics on standard error.

Follow dependencies: refs

yass refs root@Greeting
yass refs --out root@Greeting
yass refs --in root@Greeting::RETURN

refs [--in] [--out] TARGET reports direct reference edges for one document or slot. Without flags, or with both flags, it reports both directions. --out shows what the target references; --in shows what references it. Records contain DIR, RELATION, and the REF at the other end. Outgoing records precede incoming records. The quick-start spec has no references, so these examples produce no output and succeed.

A whole-spec target includes incoming references to any of its slots; a slot target selects outgoing references carried in that slot and incoming references that name that slot. File-only targets are not accepted. Incoming edges are searched within the project, and unresolved references are omitted; use validate to diagnose them. References are not followed transitively.

Check: validate and lint

yass validate
yass lint
yass validate root.yass.yaml
yass lint root.yass.yaml

validate [PATH...] checks the addressed files against the language definition: YAML structure, preambles, names, slots, obligations, reference syntax, target existence, and relation/target kinds. It reports errors without repairing files.

lint [PATH...] checks project hygiene: unreachable documents, unbound designs, missing schema modelines, empty slots, and specs without slots. It builds the whole project graph even when paths restrict which files’ advisories it reports. Lint does not run validation first; it remains usable during authoring.

Both commands are silent and exit 0 when clean. Findings go to standard output and exit 1. They check structure and graph properties; they do not assess prose correctness, completeness, or whether code meets the spec.

Read authoring documents: docs

yass docs
yass docs brainstorming
yass docs reference
yass docs guidance
yass docs language

docs indexes the embedded language documents with the task that warrants reading each. docs NAME writes one document whole. Use brainstorming to elicit a new spec, reference and guidance to author specs, and language to change yass itself. These documents travel with the installed version and work without a project root. For implementing an existing spec, start with overview and the project’s own query results.

Keep current: update

yass --version
yass update

update checks the newest stable GitHub release. If already current it reports current; otherwise it downloads the platform archive, verifies its checksum, and replaces the installed binary. A Homebrew-managed binary is left in place with a homebrew result; upgrade it with brew update && brew upgrade yass. It needs network access and permission to replace the binary, but no project root. See Installation for installation and upgrade options.

Output and exit statuses

Indexes and search results use tab-separated records, with whitespace collapsed inside fields and - for an absent field. Query output is YAML; named docs are plain text. Diagnostics have five fields: SEVERITY, CODE, LOCATION, LINE, and MESSAGE.

Requested answers go to standard output. Failures to serve a request go to standard error. Validation findings and lint advisories are their commands’ requested answers, so they go to standard output.

StatusMeaning
0Success, including empty search/edge results and clean checks
1Findings: validation errors, lint advisories, or partial results due to parse failures
2Usage error, such as an unknown option or malformed target
3Unresolved path, document, slot, reference, or docs name
4No governing project root
5Environment failure, such as unreadable files or a failed update

For scripts and CI, check the exit status as well as capturing output. validate and lint complement each other, so run both when checking a spec change.

Developing with agents

Yass lets an authoring conversation produce a contract that a fresh agent can implement without that conversation. The CLI reads and checks the contract; it does not call a model, write specs, or generate product code. Use the prompts below with an agent that can run commands in your project directory.

Replace bracketed text before copying a prompt. Give each agent the repository location and access to the CLI. Start a fresh session for implementation and independent review: the spec set must carry the decisions those agents need.

The brainstorming flow owns the authoring process and its approval gates. The authoring guidance owns coverage and structure; the reference owns language syntax. The prompts below ask agents to read those sources rather than substitute a second authoring procedure.

1. Explore an idea

Provide: the project directory, the problem, intended users, and any fixed constraints. A new project may have no root.yass.yaml yet.

Work in [project directory]. I want to explore [problem or idea] for [users].
The fixed constraints are [constraints, or none yet].

Run yass to orient yourself and read yass docs brainstorming. Classify this
request out loud using that flow. Read existing code and use yass list, find,
refs, and query when an existing spec project governs the work.

Establish purpose, users, and observable success. Ask one unresolved decision
per message, proposing marked defaults and alternatives with trade-offs where
the flow requires them. Write back what you understood, separating my stated
requirements from assumptions. Stop for my confirmation before writing specs.
Do not write product code, scaffold a project, or install dependencies.

Expected artifact: a short written-back understanding and any open decisions, in the conversation. Human decision: confirm or correct that understanding. This confirmation permits drafting specs, not implementation.

2. Specify the behavior

Provide: the confirmed understanding and fixed constraints. If this is a new session, paste the understanding and record that you have confirmed it. For an existing feature, name the behavior to change and any compatibility requirements.

Work in [project directory]. I confirmed this understanding:
[confirmed purpose, users, observable success, scope, and constraints].

Run yass and read yass docs brainstorming, guidance, and reference. Follow
the brainstorming flow from this confirmed understanding. Check existing
specs through the CLI before extending them. Resolve remaining decisions
with me; do not invent missing requirements.

Draft a spec set with a root that explains the project and follow the
guidance's Granularity rules for code-file pairing and public-symbol coverage. Keep shared concerns
in one home and bind nonbehavioral constraints through USES design blocks.
Apply Case coverage and the Implementer view rehearsal, following references
that query does not inline. Run yass validate and yass lint and fix findings.
Show every spec document for my review. Stop for approval of the complete
spec set; do not implement here.

Expected artifact: reviewable .yass.yaml files, successful validation and lint results, and the root location. Human decision: approve the set or request corrections. Corrections are checked again before approval. The flow requires its own yes at each gate, even for a small request.

3. Review a spec critically

Provide: the proposed spec set, its root directory, the requested scope, and any requirements it is meant to satisfy. Use an independent session so the reviewer has to locate behavior in the contract.

Review the proposed spec set in [project directory] against [requirements].
This is a review request; do not edit files or implement code.

Run yass, read yass docs guidance and reference, and discover the set with
list, find, refs, and query. Run validate and lint. Apply all five Case
coverage checks, then rehearse implementation from each queried fragment
and the whole-spec CONFORMS and SEE references it leaves visible.

Look for conflicting obligations, two reasonable readings, unstated output
shapes, missing defaults, unspecified boundaries, and failures that cannot
occur within their owning component's input contract. Distinguish dead
residual errors from failures shadowed by another component's guarantee.

Return concise findings with the affected spec or slot, file location,
the concrete case exposing the gap, and the decision needed to resolve it.
Separate tool findings from semantic findings. If there are no findings,
state what you checked and any limits of the review.

Expected artifact: actionable findings or a bounded clean review. Validation and lint check language and graph hygiene; they do not establish that the contract expresses the intended behavior. Human decision: choose the behavior for each gap and approve the revised set after it is rechecked.

4. Implement an approved set

Provide: the directory containing the approved root, approval status, the implementation scope, and repository development instructions. Do not supply the authoring transcript as an extra source of behavior.

Implement the approved spec set in [project directory], scoped to [scope].
The set has been approved; this is the request to implement it.

Start by running yass. Discover the project with list, find, and refs, then
read behavior through query. Follow whole-spec CONFORMS and SEE references
and the USES constraints. Treat the set as the source of requirements, not
earlier conversation or plausible defaults.

Follow repository development instructions. Plan implementation in the
specs' dependency order, implement the scoped behavior, and verify it with
tests tied to observable obligations, including applicable boundary and
failure cases. If implementation requires guessing, report the affected
spec and stop that work for a spec decision rather than filling the gap.
Report changed files, verification results, and any remaining limitations.

Expected artifact: implementation and verification evidence traceable to the approved obligations. Human decision: resolve any spec defect before dependent work proceeds, then review the implementation. Implementing code against an existing set needs the notation summary from yass; the authoring documents are needed when the contract itself must change.

5. Maintain behavior and specs together

Provide: the bug or change request, compatibility constraints, and known reproduction steps. Distinguish a bug against the current contract from a request to change the contract.

Investigate [bug or requested change] in [project directory]. Reproduction
steps and compatibility constraints are [details].

Run yass. Locate the governing specs with list, find, refs, and query, and
compare the observed code behavior with their obligations. Report whether
this is an implementation defect, a requested contract change, or a spec
gap. Check incoming references for consumers affected by the change.

If the approved contract already determines the fix, implement and test
the correction under the repository instructions. If requirements change
or are missing, read yass docs brainstorming and guidance and use the
feature authoring flow: present the understanding, draft after confirmation,
check and review the set, and stop for spec approval and handoff. Do not
silently rewrite the contract to match a bug or implement an unapproved
contract change.

Expected artifact: either a tested fix against the existing contract or a reviewable spec change with its affected consumers. Human decision: settle new behavior and approve revised specs; request implementation as the next step after the authoring handoff.

6. Adopt Yass in an existing codebase

Start with a bounded component whose behavior matters: one formatter, parser, public function, or endpoint. A project root can govern that component’s directory; it need not coincide with the repository root. A nested root takes over its subtree, so choose that boundary deliberately.

Provide: the component boundary, existing code and tests, and known compatibility expectations. Existing code supplies answers for a retrofit; the agent asks you where it is ambiguous, contradictory, silent, or apparently accidental.

Adopt Yass for [bounded component] in [repository directory]. Its code and
tests are [paths]. Preserve [compatibility expectations]. The scope is this
component; do not claim to specify the rest of the repository.

Run yass and read yass docs brainstorming, guidance, and reference. Treat
this as a retrofit. Determine the appropriate project root and inspect
existing specs through the CLI. Use code and tests to establish intent and
edge behavior. Ask me only where the evidence is ambiguous, contradictory,
silent, or apparently accidental, including whether to pin or correct it.

Draft the scoped contract from that evidence; for a retrofit, the draft is
the written-back understanding. Follow the remaining brainstorming stages,
including validate, lint, Case coverage, Implementer view, and review of
every document. Identify behavior the draft preserves and decisions still
needed. Stop for spec approval and handoff without changing product code.

Expected artifact: a small spec project describing the chosen component, with explicit unresolved decisions where evidence was insufficient. Human decision: approve what the contract preserves and any intentional corrections. A subsequent implementation request can then align code with the approved contract.

A small end-to-end workflow

Suppose you want a library function formatLabel that formats an item count. Use the idea prompt with: “Return 1 item for one and <count> items otherwise; accept nonnegative integers; do not perform I/O.” The author still needs to settle the accepted integer range, the exact failure outcome for invalid inputs, and any technology constraints that the request does not answer.

After you confirm the understanding, the author drafts root.yass.yaml describing the library and referring to format-label.yass.yaml@formatLabel. That file describes the public function in its own spec. Its contract pins the exact strings, the zero case, invalid inputs, and absence of side effects. The author validates, lints, and rehearses both queried specs before presenting them. An independent reviewer uses the critical-review prompt to check that the cases can be implemented without guessing.

For example, suppose you explicitly approve these remaining choices: the function accepts integers, accepts the range zero through 2,147,483,647 as valid counts, and returns a two-field result rather than throwing an error. Inputs outside the integer contract have explicitly unspecified behavior. These are hypothetical user decisions, not defaults for an agent to invent. The reviewable artifact could be:

root.yass.yaml:

# yaml-language-server: $schema=https://shakefu.github.io/yass/v1.schema.json
description: A library for formatting integer item counts.
version: v1
---
spec: LabelLibrary
INPUT:
  - MUST: accept calls to the public formatLabel function
RETURN:
  - MUST: provide formatLabel with the behavior defined by its function spec
    SEE: format-label@formatLabel
SIDE-EFFECT:
  - MUST-NOT: perform I/O or change external state

format-label.yass.yaml, paired with the code file for formatLabel:

# yaml-language-server: $schema=https://shakefu.github.io/yass/v1.schema.json
description: The public item-count formatting function.
version: v1
---
spec: formatLabel
INPUT:
  - MUST: accept one integer count
  - WHEN: the supplied value is not an integer or the argument is missing
    MAY: behave in an unspecified manner outside the input contract
RETURN:
  - MUST: return a result with exactly the fields label and error
  - WHEN: count is an integer from 0 through 2147483647 inclusive
    MUST: return a string label and a null error
  - WHEN: count equals 1
    MUST: set label to the exact string "1 item"
  - WHEN: count is 0 or an integer from 2 through 2147483647 inclusive
    MUST: set label to count's base-10 digits without leading zeros followed by " items"
ERROR:
  - WHEN: count is an integer less than 0 or greater than 2147483647
    MUST: return a null label and the exact error string "invalid count"
SIDE-EFFECT:
  - MUST-NOT: perform I/O or change external state
INVARIANT:
  - MUST: treat the valid-count and invalid-count cases as exhaustive for integer inputs

The reviewer can now check these concrete cases against the files:

InputExact result
0{label: "0 items", error: null}
1{label: "1 item", error: null}
2{label: "2 items", error: null}
2147483647{label: "2147483647 items", error: null}
-1 or 2147483648{label: null, error: "invalid count"}
Missing argument or nonintegerExplicitly unspecified; no expected result

The integer cases are exhaustive, so the spec carries no guard-less residual ERROR. The unspecified cases are outside this function’s integer input contract; another component calling it would need to state its own handling of that boundary.

Approve the final set, then start a fresh implementation session with the implementation prompt. The implementation agent queries the set and tests zero, one, multiple items, and the specified invalid-input cases. Acceptance rests on those obligations and verification results, not on remembering the example conversation.

An incremental adoption example

A service already has a formatLabel utility and tests for one and several items, but no test for zero. Scope the adoption prompt to that utility’s directory. If no root governs it, the author drafts a root describing that small library and a paired function spec, using the implementation to answer what zero currently does. If the code accepts negative counts accidentally, the author presents that behavior for your decision: preserve it for compatibility, or specify a correction.

After approval, ask an implementation agent to add contract-based coverage and make any approved corrections. Expand adoption to the next component only when its boundary is clear. If an existing root already governs the utility, extend that project’s reachable spec graph instead of introducing a nested project without considering which files it would take over.

yass — Language Reference

yass — Yet Another Spec Syntax. A YAML-based specification language for AI-agent development: agents (or humans) write specs that capture desired behavior, and tooling consumes them to drive test (TDD) and implementation generation.

File

  • A yass file uses the .yass.yaml extension — with at least one character before it, so a file named exactly .yass.yaml is not a yass file — and is a YAML multi-document stream (----separated).
  • Read as UTF-8.
  • SHOULD carry the schema modeline as its first line, so editors validate against the JSON Schema with no per-repo config: # yaml-language-server: $schema=https://shakefu.github.io/yass/v1.schema.json

Project root (root.yass.yaml)

  • A file named exactly root.yass.yaml is required, and its presence defines the project root: the project root is the directory that contains it. Every root-relative ref path — a path token with no leading dot — resolves from there.
  • A project root is not a repository root. It is defined by the presence of the file and by nothing else: the anchor is deliberately decoupled from the repository, so nothing ties it to a VCS checkout, a package manifest, or a build directory. A project root may sit at the top of a checkout, or nowhere near it.
  • Discovery is by name alone. There is no .git search and no deepest-.yass.yaml-ancestor fallback; a project root is never inferred from layout. Absence is an error.
  • Layouts are arbitrary. A repository MAY hold several projects, each with its own root.yass.yaml, and the one that governs a given file is the nearest at or above it — a nested root takes over its own subtree, and the outer root does not govern within it.
  • Contents: exactly one spec: document, describing the project as a whole — what it is and what it must do — written with the ordinary five slots. There is no new document type; the project is a normal spec. A project need not produce an invocable program: it may produce a library, a data format, or a language (this repository’s project is yass itself, which has no entry point), so read the slots structurally when the subject is not function-shaped (see Slots). The file MAY also carry design: documents. More than one spec, or none, is a validation error. (How to write that spec: see GUIDANCE, The root file.)
  • Membership is reachability. Every spec and design under a project root MUST be reachable from the root spec through a chain of references, and CONFORMS, USES, and SEE all count as edges — reachability is a property of the reference graph, not of any one relation. A spec that no chain reaches is either dead or evidence that the root file is missing an obligation.
  • Encoded in the self-definition as the Root construct, which CONFORMS: Document — a root file is an ordinary yass file (preamble first, ----separated stream) that has been given a fixed name and a constrained payload.

Preamble

  • Required, exactly one, and MUST be the first document of every file. Making it mandatory simplifies parsing and validation.
  • Identified structurally by the absence of a spec: or design: key.
  • The file name is the identity of the whole unit; the preamble does not repeat it.
  • Keys:
    • description — required; a string file-level summary (block scalar > allowed). Surfaced by tooling that summarizes files.
    • version — required; the yass language version the file targets. Pinned at v1 in the current revision.
    • related — optional; a list of freeform external resource locations (paths, URLs, issue links). A distinct channel from the SEE relation; never resolved by tooling.

Spec document

  • A spec: key naming the spec (may be namespaced, e.g. pkg.Symbol) plus zero or more slots. A document carries exactly one of spec: or design: — carrying both is a validation error.
  • Name charset: ASCII letters and digits, ., _, and - (so a slot-keyword name like Slot.SIDE-EFFECT is legal and addressable). The composition regex ^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$ also rejects a leading dot, a trailing dot, and consecutive dots.
  • Names MUST NOT case-insensitively match any yass keyword — a slot name, a normativity keyword, WHEN, or a relation (CONFORMS, USES, SEE).
  • Names MUST be unique within a file, so refs resolve unambiguously.
  • Granularity: one public symbol / endpoint → one spec. Also the unit of retrieval.
  • The five slots are themselves defined in yass.yass.yaml as Slot.INPUT, Slot.RETURN, Slot.ERROR, Slot.SIDE-EFFECT, and Slot.INVARIANT.

Design document

  • A design: key naming the block, plus required type and content keys — and no slots. A peer of spec: in the YAML stream:

    ---
    design: StartupSequence
    type: ordered-steps
    content: |
      1. Warm the cache
      2. Connect the database
      3. Mark the service live
    
  • Names share the file-wide uniqueness namespace with spec names (same name grammar), so a ref target stays unambiguous — a bare name or path@Name resolves to exactly one document, spec or design.

  • type — required; a brief freeform interpretation hint, one or two words (pseudocode, ordered-steps, constraint, stack, …), opaque to tooling. Deliberately not a closed set: tooling never branches on the value, so there is no out-of-set case to pin.

  • content — required; a string holding the freeform body, written as a literal block scalar (|) by convention — the rule fixes the value’s type, not its scalar style, since a YAML parser cannot distinguish styles after loading. The body is opaque: no refs are recognized inside it.

  • Normative force: an implementation bound to a design block via reference MUST follow it. The block is not commentary; it binds through the reference graph (the USES relation — see References).

  • The home for normative content that is not an obligation on observable behavior: algorithms / pseudocode, technology or stack constraints from outside forces, and multi-spec lifecycle procedures that have no owning symbol.

  • An unreferenced design block is dead weight — declared normative content nothing binds to — and is flagged by lint.

Slots

The recognized set (each is a YAML list of obligations), written UPPERCASE (see Casing convention below):

  • INPUT
  • RETURN
  • ERROR
  • SIDE-EFFECT
  • INVARIANT

The first four are function-shaped and map onto Design by Contract: INPUT = precondition (what it accepts), RETURN = postcondition (what it yields), ERROR = failure modes, SIDE-EFFECT = observable effects beyond the return. INVARIANT completes the contract: constraints that always hold, independent of any call.

INVARIANT is universal (any spec may use it) but is a slot of last resort: an obligation SHOULD be placed in INVARIANT only when none of INPUT, RETURN, ERROR, or SIDE-EFFECT is suitable. It exists so always-true constraints are not forced into SIDE-EFFECT (which means effects) — not as a general catch-all.

In the ERROR slot specifically, a guarded obligation (with WHEN) names one specific failure mode, and a guard-less obligation is the residual — the policy for any failure not matched by a guarded obligation in the same slot. A residual is meaningful only when the guards leave inputs unmatched: if the guarded obligations already account for every input, the residual set is empty and a guard-less catch-all is dead — it can never fire, and MUST-NOT be carried. State that exhaustiveness in the spec’s INVARIANT instead: a guard-less ERROR obligation is itself read as the residual, so exhaustiveness cannot live there.

ERROR is also where rejections live, and only there: a rule phrased as be rejected sits in an ERROR slot, and validation enforces exactly those rules — a MUST or MUST-NOT in any other slot binds authoring without being a validation rejection (reachability and the modeline SHOULD are lint’s advisory domain).

The residual discipline generalizes beyond ERROR: whenever a slot branches on a closed set of values — most commonly an INPUT that dispatches on a subcommand, mode, or enum — it must state the behavior for a value outside that set, or one that is missing. The residual is to a dispatch what the guard-less catch-all is to the error table. (The draft-time pass that walks every such case — closed sets, guards, segmentation boundaries, failures, trusted inputs: see GUIDANCE, Case coverage.)

When a spec describes something that is not a function (e.g. the language defining itself), read the function-shaped slots structurally: INPUT = the form a thing takes, RETURN = what a well-formed thing denotes, ERROR = malformed forms that must be rejected, SIDE-EFFECT = real effects of processing it.

Obligation

An obligation is a YAML mapping (a list item under a slot):

  • Normativity — exactly one key, unless ref-only. RFC 2119 / RFC 8174 vocabulary, negatives hyphenated: MUST, MUST-NOT, SHOULD, SHOULD-NOT, MAY. Value = the obligation prose.
  • Guard (optional) — WHEN, value = condition prose. Expresses a sufficient condition. If WHEN is present it MUST be accompanied by a normativity keyword.
  • References (optional) — CONFORMS / USES / SEE, each at most once per obligation, value = a single ref-target string.
  • Closed key set — a normativity keyword, WHEN, and the relation keys are the only keys an obligation mapping may carry; any other key is a validation error.
  • Prose values — a normativity keyword’s or WHEN’s value must be a string: mappings, sequences, core-schema nulls, and non-string scalars (numbers, booleans) are rejected. An absent value and unquoted null, Null, NULL, or ~ resolve to null; quoted empty strings and quoted "null" or "~" remain strings. Resolve YAML tags before checking this rule rather than testing only the scalar text.
  • Ref-only — a mapping with one or more relation keys and no normativity keyword and no WHEN. Allowed; it adds no obligation of its own and resolves per its relation (a slot-targeted CONFORMS transcludes; a whole-spec CONFORMS stays in place as a declaration that the enclosing spec conforms to the referenced spec as a whole; a ref-only USES — a bare - USES: Name list item — transcludes too, appending its design block’s content).

References

  • Relation is the YAML key; target is a single string.

  • Target syntax: path@SpecName::SLOT

    • A bare name (SpecName) addresses a spec or design in the same file.
    • @ separates a path from the spec name (path@SpecName) for other files.
    • :: separates the spec name from a slot (SpecName::SLOT); omit it to address the whole spec. The SLOT token must name one of the five recognized slots (see Slots); any other slot token is a validation error.
    • Path resolution: a ./ or ../ path is relative to the referencing file; a path with any other lead resolves from the project root (see Project root above) — including one whose leading dot opens a hidden name, such as .config, which is a root-relative token, not a file-relative one. A path beginning with / is rejected: yass paths are file-relative or root-relative, never absolute. A path may climb with ../ past the project root; the reached file stays governed by its own nearest root. The .yass.yaml extension is omitted.
    • The slot is the finest addressable unit. Named anchors, never line numbers. Slots belong to specs only: ::SLOT on a target that resolves to a design is a validation error — a design carries no slots.
    • (# was rejected as a separator — a leading # is a YAML comment, and @ is a reserved YAML indicator that can’t start a plain scalar either. Making same-file refs bare names means no target ever leads with an indicator, so none need quoting.)
  • Relations:

    RelationTargetResolutionMeaning
    CONFORMSspec or slotslot: inlined; whole-spec: nothard requirement — must match the spec or slot
    USESdesign blockcontent appended + provenancebinding design the implementation must follow
    SEEspec, slot, or designpointer, never inlinedopen-ended pointer — imposes nothing on its own

    CONFORMS and USES each pin a target kind; SEE takes any. A relation/target-kind mismatch — USES → spec, CONFORMS → design — is a validation error, so the CONFORMS-vs-USES distinction is machine-checkable rather than a judgment call.

    CONFORMS is a hard requirement with two aspects of one meaning: the carrier must match the referenced spec or slot, and inlining is how that match is made checkable in place. A slot-targeted CONFORMS (…::SLOT) inlines the referenced slot’s obligations in front of the carrier. A whole-spec CONFORMS (no ::SLOT) is a conformance reference to the entire spec — it is not transcluded (there is no single slot to splice); the conformer must satisfy the referenced spec as a whole. This matches the language meaning that CONFORMS must “match the referenced spec or slot.” USES binds the carrier to a design block: resolution appends the block’s typed content to the emitted fragment, once, with a provenance comment (# USES: StartupSequence), deduplicated when several obligations reference the same block — a design block has no obligations to splice into a slot. When a guarded obligation carries a USES, the guard scopes when the design binds. SEE names a spec or design for the reader: it is never inlined and imposes nothing on the carrier beyond what the obligation’s own prose already states.

  • Choosing a relation — three questions, in order.

    1. Must this obligation match what the target says? → CONFORMS. Slot-targeted (…::SLOT) when one specific slot is the contract; whole-spec when another spec owns cross-cutting rules this one must obey (see GUIDANCE, Composition).
    2. Is the target a design block whose freeform body binds the implementation? → USES. Design blocks only — USES at a spec is a validation error.
    3. Everything else → SEE. It is the open-ended catch-all, and the right key even when a genuine dependency exists: a dispatcher spec naming the handler specs it dispatches to, a stage that must run after another spec, or plain “go read this next.” The dependency is stated in the obligation’s own prose; SEE only says where the named thing is defined. What SEE must never carry is a constraint — if the target’s rules bind this spec, that is whole-spec CONFORMS, and reaching for SEE silently drops the requirement.
  • Guards conjoin when an inlined obligation is itself guarded. When a slot-targeted CONFORMS carrier has a WHEN guard and an inlined obligation carries its own WHEN, the inlined obligation applies only when both guards hold — the carrier’s guard conjoined with the inner guard. (This is the language-level meaning; how a tool renders the combined guard text is the tool’s concern.)

  • Slot-targeted CONFORMS carries the dataflow reading. When one spec’s INPUT consumes the data another spec’s slot produces — characteristically an INPUT that points at a producer’s RETURN, CONFORMS <producer>::RETURN — it means the obligation must match the data that slot produces: the data crossing the boundary is exactly what that slot yields, so the producer’s RETURN guarantees characterize it here, and inlining puts those guarantees in front of the consumer’s INPUT. This is the structural anchor for a pipeline or producer/consumer relationship. It does not by itself decide the trust boundary — which of the producer’s guarantees the consumer relies on versus re-checks is the consuming spec’s own obligation to state (see GUIDANCE, Composition and Case coverage). That obligation includes the residual on violation: for each guarantee the consumer relies on without re-validating, the consuming spec must also state what it does if that guarantee does not hold — even if only to declare the behavior unspecified. Stating the trust without pinning its violation leaves every implementer to invent the out-of-contract behavior.

  • DRY is achieved by transclusion (inlining), not bare pointers.

Casing convention

Fixed-meaning keywords are written UPPERCASE; content-bearing field keys are lowercase.

  • UPPERCASE (the vocabulary — the key is the meaning): slot names (INPUT, RETURN, ERROR, SIDE-EFFECT, INVARIANT), normativity (MUST, MAY, …), the guard WHEN, and the relations (CONFORMS, USES, SEE). Also the slot portion of a ref target (the part after ::).
  • lowercase (labels for values): spec, design, type, content, description, version, related.
  • In prose, write a keyword UPPERCASE when you mean the keyword itself (e.g. “the RETURN slot”), so it cannot be confused with the ordinary word.

Rationale: it mirrors the RFC 2119 uppercase-keyword convention already used for normativity, unifies the structural vocabulary as one visually distinct layer, and disambiguates homographs (RETURN the slot vs. “return” the word).

In the self-definition the rule is encoded once as the Keyword construct; slots, normativity, the WHEN guard, and relations declare CONFORMS: Keyword::INVARIANT rather than restating it — transcluding the invariant instead of conforming to it, because the keyword constructs are the rule’s home, not its conformers.

Provenance (emitted fragments)

  • Inlined obligations carry a YAML comment naming the source, e.g. # CONFORMS: db/user@User::RETURN. Appended design blocks carry the same, e.g. # USES: StartupSequence.
  • An emitted fragment identifies the queried spec; there is no host file: header (redundant and able to contradict — the filesystem is the source of truth).

Footgun

  • Obligation prose containing : (colon-space) mis-parses as a nested YAML mapping and must be quoted.

Meta-rules (the spec system, distinct from in-spec obligations)

  • Every unit of specification MUST be paired with its .yass.yaml file. A unit is the project’s chosen pairing — a command, module, package, or similar cohesive component — not necessarily one source file, and a spec file may hold several spec: documents.
  • Every public symbol or endpoint SHOULD have a definition in the .yass.yaml file.
  • The .yass.yaml file MUST be structured parseably.
  • The .yass.yaml file MUST-NOT require special syntax to be read by an AI agent.
  • The .yass.yaml file MUST conform to the format when written, and MUST be validatable by tooling.

Resolution and scope

  • Resolution: a slot-targeted CONFORMS is inlined into the referencing spec (one level in v1); a whole-spec CONFORMS (no ::SLOT) is not inlined — it is a conformance reference to the entire spec. SEE is a pure pointer, never inlined. USES appends its design block’s typed content to the emitted fragment, once per block, with provenance.
  • Verification is out of scope — tooling routes/retrieves, never verifies obligation content (keeps it language-agnostic). A COMPATIBLE relation was deliberately excluded.
  • WHEN states a sufficient condition only.
  • Extension: .yass.yaml, chosen over bare .spec after a collision review. Bare .spec is owned by RPM spec files / PyInstaller / Vim+Neovim filetype / GitHub Linguist / freedesktop MIME, and — decisively — a bare .spec never gets the yaml language id, so it earns neither free YAML highlighting nor JSON-Schema association. A trailing .yaml fixes that; the .yass infix namespaces it.

yass — Authoring Guidance

Steerage for AI agents (and humans) writing yass specs.

Guiding principle

A specification must be fully intelligible to a reader — human or model — who has never seen this project, with no prior prompting and no other documents open. Tooling can make working with specs faster, but a spec must never require tooling or a companion document to be understood or implemented.

Granularity: keep specs small and located

The core failure mode: an LLM, unguided, will slop a single giant spec file for a whole codebase. To prevent that:

  • One spec file per unit of specification. A .yass.yaml is paired with the code it describes at the project’s chosen unit — a command, module, package, or similar cohesive component — not necessarily one file for one source file. A spec file may hold several spec: documents when they share that unit.
  • One spec definition per public symbol or endpoint. Each public function, method, class, or endpoint gets its own spec: document.

The root file: say what the project is, first

root.yass.yaml is required, and it is the first file a reader opens — the entry point into the spec set, the same way a main is the entry point into a program. Write it so a reader (or an implementing agent) learns what the project is before reading anything else: what it is called, how it is used, what it does with its input, and what it leaves behind. A root file that reads as a list of pointers has failed; the reader should be able to stop after it and still describe the project.

It carries exactly one spec for the project, and that spec is an ordinary one: there is no special document type, and its five slots carry exactly the meaning they carry in any other spec. The only thing that changes is the subject — the project rather than a function — and the reference’s Slots section already says how to read them.

Dispatch is a guarded SEE. One guarded obligation per recognized subcommand, naming the spec that owns it: the guard is the subcommand value, the prose says it dispatches, and SEE names where that behavior is defined. This is the decision procedure in the reference (Choosing a relation) applied to a dispatcher — the handler’s rules do not bind the dispatcher, so CONFORMS would be wrong, and dispatch is exactly the “genuine dependency stated in the obligation’s own prose” case that SEE is for.

A program-wide constraint that belongs to no single spec is a design block. Wire format, log line shape, exit-code policy, a startup sequence — write it once as a design: document and bind it with a reference-only USES in the root spec’s INVARIANT. That gives the constraint one home and normative force, without inventing a prose slot the language does not have.

Deliberate non-goal: no free-prose channel

yass intentionally provides no formal way to attach non-spec prose or commentary to a spec. Authors get structured obligations only. Free text does appear in a few declared fields — the preamble description, the preamble related list of external resource locations, and a design block’s type hint — but each is structural metadata, not a commentary channel.

Rationale: when a language allows free prose (as Allium does), the prose goes out of control — specs bloat with narrative that isn’t behavior. Withholding a prose/comment channel forces authors (especially LLMs) to express intent as obligations, or not at all.

Note: the only comments yass emits are tooling-generated provenance comments (# CONFORMS: ...) on resolved fragments. Those are not an author-facing channel, and should not become one.

One fenced carve-out: a design block (a design: document — see Design document in the reference) carries normative freeform content, but it is not a prose channel on specs. The prose is fenced inside a declared block with a name, a required type, and normative force through the reference graph (USES); it cannot leak into a spec: document, and an unreferenced design block is dead weight that lint flags — declared normative content nothing binds to. The forcing function stays intact for spec documents: express intent as obligations, or not at all. The preamble description and related fields remain the only free-text fields outside a design block.

Ordering: implementation sequence

Spec document fragments (spec: documents) within a file SHOULD be ordered in the sequence they should be implemented. This gives an implementing agent (or human) a natural top-to-bottom work order and makes dependency ordering explicit without extra metadata.

Two levels of ordering are at play:

  • Inter-file ordering — across spec files. Enforceable today with numeric-prefix naming conventions (e.g. 00-init.yass.yaml, 01-core.yass.yaml, 02-api.yass.yaml).
  • Intra-file ordering — within a single spec file. The YAML document stream is ordered; spec documents earlier in the file should be implemented before later ones.

Composition: dataflow and cross-cutting concerns across specs

Specs describe components one at a time, but real programs are wired together. Two gaps recur when a reader has only the specs and no architecture note, and both are closed by obligations, not prose:

  • Name the dataflow and the trust boundary. When one spec’s INPUT consumes the data another spec’s RETURN produces (a pipeline stage, a handler reading a producer’s output), point at the producer with a slot-targeted reference — CONFORMS <producer>::RETURN. That pointer is not decorative: it means the data this input consumes must match exactly what that slot produces, so the producer’s RETURN guarantees characterize the data crossing the boundary — and inlining puts them in front of the consumer’s INPUT. Having named it, state explicitly which of those upstream guarantees the consuming spec relies on (and therefore does NOT re-validate) and which it re-checks. A consumer that silently re-validates, or silently trusts, forces every implementer to guess the boundary; they will guess differently. The consuming spec owns that decision — make it in an obligation. The violation side of that reliance — what the consumer does when a trusted guarantee does not hold — is a coverage question: see Case coverage, Trusted inputs.

  • Give every cross-cutting concern a single home. When a rule spans many specs — a wire format, the shape of an error line, how input is segmented, how a subcommand is dispatched — write it once in one spec that owns it completely, and reference that spec from the others. Do not restate the rule in fragments across the specs it touches. A reader should learn the whole of a concern from one place rather than reconstructing it from scattered, drift-prone mentions.

  • “That spec owns rules I must obey” is whole-spec CONFORMS, never SEE. This is the commonest whole-spec reference there is — a shared wire-protocol spec owning line segmentation, the error-line format, and exit policy for three stage specs that must all obey it. Point at it with whole-spec CONFORMS, which means must match the referenced spec. SEE is the wrong key: it names a spec for the reader and imposes nothing, so an author who reaches for it here silently drops a real constraint and every implementer is free to ignore the owning spec. Reserve SEE for pointers that bind nothing — a dispatch target, an ordering note, related reading.

Case coverage: enumerate, then account for the remainder

Before considering a draft complete, identify every place it selects behavior, divides input into cases, names failures, or relies on another component’s guarantees. Then work through each applicable check below, over the whole spec and its binding references — a single obligation need not carry the entire decision. Slot.INPUT and Slot.ERROR make several of these binding; the checks are the discipline those rules generalize.

  1. Values that select behavior (usually INPUT). For each command, mode, enum, or other closed set the spec branches on, account for every recognized value, an unrecognized value, and absence: if the enumerated set is {tally, grade, pack}, say what the program does with harvest, and with no command at all. Otherwise each implementer invents the out-of-set handling. A reader must never have to deduce the residual case from the enumerated ones.

  2. Conditional behavior (any slot). For each WHEN guard, determine what constrains behavior when the condition is false. A guard states a sufficient condition only — “when exactly one item matches, emit a fragment” does not itself forbid emitting a fragment otherwise — so an existing baseline or another obligation may already cover the false case, and that is fine. But if the intent is “only when,” say so with obligations: a complement-guarded MUST-NOT, or an unguarded baseline the guarded obligation overrides. Do not encode necessity in the guard’s prose — WHEN cannot carry it. Where guards overlap, make the applicable obligations compatible, or state the precedence inside one obligation’s prose: a slot’s obligation order carries no meaning, so list position settles no conflict.

  3. Segmented input (INPUT). When a spec defines how input is broken into units — records, lines, fields, tokens — state the boundary behavior completely, not just the happy path. For each level of segmentation, name:

    • the exact separator and its character class (e.g. one ASCII space 0x20, not the vaguer “whitespace”, which invites splitting on tabs and other Unicode spaces);
    • empty input (zero units);
    • an empty or blank unit in the interior;
    • a leading or repeated separator;
    • the mechanics of an optional trailing terminator — say how many trailing separators are absorbed (characteristically exactly one), and therefore what a second trailing separator, or a blank final unit, denotes. “MAY accept a trailing newline” without the count leaves an implementer to guess whether one or all are stripped;
    • the degenerate input that is only separators — a lone separator with no content, or a run of them — which the cases above otherwise leave under-determined (is a lone separator empty input, or one empty unit?).

    Each of these is a case an implementer will hit and otherwise resolve by guessing. State the intended outcome as an obligation, or declare it out of scope — do not leave it to emerge.

  4. Failures (ERROR). Name each foreseeable failure as a guarded obligation carrying its observable outcome (a specific error code, message, or exit status); a reader must not have to deduce a domain rule from “everything else.” Then check whether any failures remain uncovered. If they do, state the residual policy in a guard-less obligation: in ERROR, an obligation with no WHEN is the policy for any failure no guarded obligation matched (the reading is fixed in yass.yass.yaml under Slot.ERROR). If the guarded cases are instead exhaustive — every input rejected by a named guard or accepted as well-formed — state that exhaustiveness in the spec’s INVARIANT and omit the residual from ERROR. A residual over an empty remainder is dead and can never fire, asserting one alongside an exhaustive partition is a contradiction, and a guard-less ERROR obligation is itself read as the residual so exhaustiveness cannot live there.

    Reachability is judged within the component’s declared input contract, not the composed program. A guarded failure another component prevents in this composition is shadowed, not dead: it stays, its prose states the preventing guarantee or check and names the spec that owns it (CONFORMS if that contract binds this component, SEE to point), and Trusted inputs still applies to the violation side. A dead obligation contributes no behavior within its owning contract — the guard-less residual over an empty remainder above; that prohibition is specific to guard-less ERROR — and is removed or revised. Apparent shadowing may instead reveal redundancy, a contradiction, or a mistaken reading: verify each claim. Obligation order settles nothing — state the shadowing explicitly, never by list position. For review and testing, exercise a component policy at the component boundary; an end-to-end input bypassing upstream validation is not required.

  5. Trusted inputs (INPUT). For each guarantee from a producing spec that the consumer relies on without re-validating — Composition covers naming the producer and the trust boundary — state what the consumer does if that guarantee is violated, even if only to declare the behavior unspecified. Pointing at the producer’s contract does not discharge this: the contract says what should arrive, not what the consumer does when it doesn’t.

The finishing question: can a reader locate the contractual disposition of every case this pass identified? Defined behavior, an explicitly permitted choice, and a stated “unspecified” all count; silence does not.

Implementer view: read the set as its implementer

The implementing agent starts cold and holds the set only through yass query. Before showing a draft and again before handing the set off, the authoring agent MUST rehearse that view: query every spec and implement from the fragment — the queried spec with slot-targeted CONFORMS obligations spliced in front of their carriers (provenance-commented, guards conjoined) and USES design blocks appended. Whole-spec CONFORMS and SEE are not transcluded: they remain references out of the fragment, and following them the way a cold reader would is part of the pass. Wherever the only honest answer is a guess, the set is defective — fix the spec, not the implementer’s imagination.

Case coverage works from the author’s map of decisions; the rehearsal works from the artifact a cold implementer holds, and that vantage has gap classes of its own:

  • A policy named but never stated — “library defaults when the policy is absent,” with no default anywhere in the set. Naming a policy is a promise; state the value.
  • A value used but never defined — a named directory, a content type, a path, a token appearing in several fragments and defined in none.
  • A named seam with no shape — an extension point three specs accept and none constructs: nothing tells the implementer what building it requires.
  • A rule gap at a boundary — a case each side of an interface assumes the other owns, so no spec disposes of it: what validation rejects up front versus what only the call can know, and what happens then.

Case coverage’s own classes — a classification with no vocabulary, an uncovered failure, an unstated trust boundary — still apply as written; the fragment view is where their misses surface.

Read the transclusions as the consumer will hold them: a slot-targeted CONFORMS inherits the producer’s whole slot, so a RETURN mixing data guarantees with assertions hands the consumer misleading obligations under the producer’s provenance. Confirm each guard-less residual the fragment view exposes can fire — Case coverage’s Failures check applies to every one.

The dress rehearsal: what cannot be implemented from the fragment and the contracts it points to cannot be implemented from the set.

yass — Brainstorming a Spec

How an agent turns a user’s request into a spec set that another agent can implement cold. The keywords below bind the agent running this flow, in the RFC 2119 sense.

Why the dialogue exists

The spec set is the only thing the implementing agent will hold: not this conversation, not this document. Every behavior the spec leaves unstated, that agent decides alone, and two agents decide differently. Each question below moves one such decision to the user while it is still cheap.

Gate

  • MUST-NOT write product code, scaffold, install a dependency, or create a project before the user approves the spec set.
  • MUST-NOT write a spec file before the user confirms the written-back understanding (stage 2). WHEN the work is a retrofit, the code pins the intent: the draft is the write-back and stage 5 is its review.
  • Each gate takes its own yes, whatever the size of the request.
  • Skip a question only when the request or the repository already answers it — never because the answer seems obvious.
  • WHEN no user can answer: MUST-NOT pass a gate; report the open questions and stop.
  • MAY read the repository and run yass commands at any stage.

Stage 1 — Classify, out loud

State the class in one sentence before the first question, so the user can override it.

  • Fresh project: yass reports project none. The root spec comes first, then the specs it reaches.
  • Feature: a root governs the directory. MUST read yass list, locate what the feature touches with yass find and yass refs, and yass query only those documents; MUST-NOT read spec files by hand for what those commands serve. New behavior extends existing specs before it adds new ones.
  • Several independent subsystems in one request: decompose first — name the pieces, their order, and the boundaries between them — then run this flow once per piece.
  • Either of the first two is also a retrofit when the code already exists and the spec is written after it. Then the code is the interviewee: MUST answer stages 2 and 3 from it, and ask the user only where it is ambiguous, self-contradictory, silent, or looks accidental — and then whether the spec pins that behavior or corrects it.

When in doubt whether a request is several subsystems, decompose. Complexity found mid-flow means stop and decompose then; nothing shrinks a class once taken.

Stage 2 — Establish intent

  • MUST know, before proposing anything: what the thing is for, who or what uses it, and what “done” observably looks like.
  • MUST put one decision per message to the user, and batch what the request or the repository already answers as proposed defaults for them to correct. SHOULD offer choices with a recommended default marked, so the user corrects rather than composes; WHEN the choice shapes the spec — a wire format, a storage model, a dispatch structure — MUST present two or three options and their trade-offs.
  • MUST then write the understanding back in a few lines, separating what the user said from what was assumed.

Stage 3 — Round the edges

  • MUST read yass docs guidance first: it owns the edge cases this stage asks about, and this document does not restate them. Section names below are its.
  • Interview by slot, for each spec the set will hold: propose every answer with a marked default and let the user correct in one reply, one slot per message where the user must decide and batched where the request, the repository, or the code already answers. Every answer becomes an obligation.
    • INPUT — the forms accepted; for a closed set, the out-of-set and missing cases; for split input, every boundary (Case coverage).
    • RETURN — what is yielded, in its exact shape: format, ordering, numbering, and what an empty result looks like.
    • ERROR — each foreseeable failure with its observable outcome, then the residual or the exhaustiveness that replaces it (Case coverage).
    • SIDE-EFFECT — what changes outside the return, and what MUST-NOT change.
    • INVARIANT — only what fits none of the above.
    • Boundaries — WHEN one spec consumes what another produces: what is trusted, what is re-checked, and what happens on violation (Composition, Case coverage).
    • Constraints that are not behavior — stack, algorithm, startup order: a design: block bound with USES (The root file).

Stage 4 — Draft

  • MUST lay files out as guidance’s Granularity, The root file, and Ordering say; yass docs reference settles a construct in doubt.
  • MUST write only obligations: nothing from the conversation survives except inside a design: block something USES.
  • MUST run yass validate and yass lint and fix every finding before showing a draft.

Stage 5 — Review

  • MUST put every spec document in front of the user and ask whether it is right: in chat, a short file whole and a long one spec by spec; or, WHEN the set is large or the work lives on a branch, as a pushed diff reviewed there.
  • Before showing, MUST apply guidance’s Case coverage pass and its Implementer view rehearsal to the draft and check what the tools cannot: an obligation with two readings, two that contradict. Fix, then show.
  • The user’s corrections go back into the files. Re-run validate and lint after each.

Stage 6 — Hand off

  • WHEN the user approves the spec set: report where the root is and, WHEN there is code to write, how to begin — an implementing agent starts fresh in the project directory, runs yass, and builds what the spec set describes — then MUST stop.
  • Before handing off, MUST re-apply guidance’s Implementer view rehearsal to the approved set and fix and re-show whatever it surfaces.
  • Implementing here is a new request. WHEN the user makes it: MUST work from the spec set through yass query, not from memory of this conversation — a gap that surfaces there is a spec defect to fix, not a gap to fill silently.

Red flags

ThoughtReality
“The request already says what they want”It says the purpose. It rarely says the edges.
“I’ll pick a sensible default”A default the user never saw is a decision the implementer remakes. Propose it, marked.
“It’s a small feature; the gate is overkill”The gate is the yes, not the size.

JSON Schema

yass.v1.schema.json is the per-document editor-validation schema for yass v1 files. Each YAML document in a .yass.yaml stream validates as a preamble, a spec, or a design. Stream-level rules (exactly one leading preamble, name uniqueness, ref resolution, relation target kinds) are out of scope and remain in yass validate.

The schema is published at:

https://shakefu.github.io/yass/v1.schema.json

Point yaml-language-server at it with the modeline every .yass.yaml file carries as its first line:

# yaml-language-server: $schema=https://shakefu.github.io/yass/v1.schema.json

Source: yass.v1.schema.json in the repository root.

Contributing to the documentation

The site is an mdBook built from docs/. Install the version used in .github/workflows/docs.yaml (currently 0.4.40), then run these commands from the repository root:

mdbook build docs
mdbook serve docs --open

The second command starts a local preview and rebuilds when source files change. Stop it with Ctrl-C. Output in docs/book/ is generated and ignored by Git. The workflow also copies yass.v1.schema.json to docs/book/v1.schema.json; to preview that download locally, copy it after building:

cp yass.v1.schema.json docs/book/v1.schema.json

Where to edit

Edit CLI guides and examples in docs/src/, and add new chapters to docs/src/SUMMARY.md. Verify command examples against the current CLI contracts in cli/ and a locally built binary; cli/HOWTORUN.txt explains the build. When documenting a behavior change, update its governing spec and implementation as part of that change rather than silently describing a new contract here.

The language reference, authoring guidance, and brainstorming chapters are {{#include}} stubs. Edit their authoritative sources in context/, then run:

./script/sync-docs
./script/test

This refreshes and verifies the corpus embedded in the CLI. Do not edit cli/internal/yass/docs/ directly.

Before submitting a change, build the book, inspect its rendered pages and navigation, and run prek run --all-files. For installer examples, also run ./script/test-install. Keep the affected AGENTS.md index current when paths or development commands change.

CI and publishing

Pull requests affecting documentation, authoritative language sources, the schema, or the documentation workflow build the book in CI. Pushes to main with those changes build and deploy to GitHub Pages. A manual workflow run on main can also deploy. Pull request builds do not deploy. The site publishes the authoritative language documents through includes, without a second maintained copy.