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:
| Option | Accepted spellings | Effect |
|---|---|---|
| Version | --version TAG, --version=TAG | Select a release; v0.4.3 and 0.4.3 select the same tag. |
| Directory | --bin-dir DIR, --bin-dir=DIR | Install yass into the specified directory, creating it if needed. |
| Help | --help, -h | Display 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:
| Variable | Default | Effect |
|---|---|---|
YASS_VERSION | Newest stable release | Select a release, as with --version. |
YASS_BIN_DIR | Writable /usr/local/bin, otherwise $HOME/.local/bin | Select the installation directory. |
YASS_REPO_URL | https://github.com/shakefu/yass | Override 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:
| Platform | Archive name |
|---|---|
| macOS, Apple Silicon | yass_<version>_darwin_arm64.tar.gz |
| macOS, Intel | yass_<version>_darwin_amd64.tar.gz |
| Linux, ARM64 | yass_<version>_linux_arm64.tar.gz |
| Linux, x86-64 | yass_<version>_linux_amd64.tar.gz |
| Windows, x86-64 | yass_<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
yassto read this project’s specs. Start withyassandyass list, then readroot@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:
| Target | What it addresses |
|---|---|
root | Preamble of root.yass.yaml |
root@Greeting | The spec named Greeting in that file |
root@Greeting::RETURN | The spec’s declared RETURN slot |
format@GreetingTemplate | A design named GreetingTemplate in format.yass.yaml |
./format@GreetingTemplate | The 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
CONFORMSinserts the referenced obligations before the carrier, with provenance comments. Guarded carriers and guarded inserted obligations have their guards conjoined. USESappends the referenced design once per fragment, with provenance.- Whole-spec
CONFORMSandSEEremain 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.
| Status | Meaning |
|---|---|
0 | Success, including empty search/edge results and clean checks |
1 | Findings: validation errors, lint advisories, or partial results due to parse failures |
2 | Usage error, such as an unknown option or malformed target |
3 | Unresolved path, document, slot, reference, or docs name |
4 | No governing project root |
5 | Environment 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:
| Input | Exact 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 noninteger | Explicitly 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.yamlextension — with at least one character before it, so a file named exactly.yass.yamlis 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.yamlis 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
.gitsearch 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 carrydesign: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, andSEEall 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
Rootconstruct, whichCONFORMS: 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:ordesign: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 atv1in the current revision.related— optional; a list of freeform external resource locations (paths, URLs, issue links). A distinct channel from theSEErelation; 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 ofspec:ordesign:— carrying both is a validation error. - Name charset: ASCII letters and digits,
.,_, and-(so a slot-keyword name likeSlot.SIDE-EFFECTis 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.yamlasSlot.INPUT,Slot.RETURN,Slot.ERROR,Slot.SIDE-EFFECT, andSlot.INVARIANT.
Design document
-
A
design:key naming the block, plus requiredtypeandcontentkeys — and no slots. A peer ofspec: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@Nameresolves 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
USESrelation — 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):
INPUTRETURNERRORSIDE-EFFECTINVARIANT
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. IfWHENis 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 unquotednull,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-targetedCONFORMStranscludes; a whole-specCONFORMSstays in place as a declaration that the enclosing spec conforms to the referenced spec as a whole; a ref-onlyUSES— a bare- USES: Namelist 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. TheSLOTtoken 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.yamlextension is omitted. - The slot is the finest addressable unit. Named anchors, never line numbers.
Slots belong to specs only:
::SLOTon 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.)
- A bare name (
-
Relations:
Relation Target Resolution Meaning CONFORMSspec or slot slot: inlined; whole-spec: not hard requirement — must match the spec or slot USESdesign block content appended + provenance binding design the implementation must follow SEEspec, slot, or design pointer, never inlined open-ended pointer — imposes nothing on its own CONFORMSandUSESeach pin a target kind;SEEtakes 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.CONFORMSis 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-targetedCONFORMS(…::SLOT) inlines the referenced slot’s obligations in front of the carrier. A whole-specCONFORMS(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 thatCONFORMSmust “match the referenced spec or slot.”USESbinds 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 aUSES, the guard scopes when the design binds.SEEnames 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.
- 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). - Is the target a design block whose freeform body binds the implementation? →
USES. Design blocks only —USESat a spec is a validation error. - 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;SEEonly says where the named thing is defined. WhatSEEmust never carry is a constraint — if the target’s rules bind this spec, that is whole-specCONFORMS, and reaching forSEEsilently drops the requirement.
- Must this obligation match what the target says? →
-
Guards conjoin when an inlined obligation is itself guarded. When a slot-targeted
CONFORMScarrier has aWHENguard and an inlined obligation carries its ownWHEN, 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
CONFORMScarries the dataflow reading. When one spec’sINPUTconsumes the data another spec’s slot produces — characteristically anINPUTthat points at a producer’sRETURN,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’sRETURNguarantees characterize it here, and inlining puts those guarantees in front of the consumer’sINPUT. 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 guardWHEN, 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
RETURNslot”), 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.yamlfile. 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 severalspec:documents. - Every public symbol or endpoint SHOULD have a definition in the
.yass.yamlfile. - The
.yass.yamlfile MUST be structured parseably. - The
.yass.yamlfile MUST-NOT require special syntax to be read by an AI agent. - The
.yass.yamlfile MUST conform to the format when written, and MUST be validatable by tooling.
Resolution and scope
- Resolution: a slot-targeted
CONFORMSis inlined into the referencing spec (one level in v1); a whole-specCONFORMS(no::SLOT) is not inlined — it is a conformance reference to the entire spec.SEEis a pure pointer, never inlined.USESappends 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
COMPATIBLErelation was deliberately excluded. WHENstates a sufficient condition only.- Extension:
.yass.yaml, chosen over bare.specafter a collision review. Bare.specis owned by RPM spec files / PyInstaller / Vim+Neovim filetype / GitHub Linguist / freedesktop MIME, and — decisively — a bare.specnever gets theyamllanguage id, so it earns neither free YAML highlighting nor JSON-Schema association. A trailing.yamlfixes that; the.yassinfix 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.yamlis 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 severalspec: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
INPUTconsumes the data another spec’sRETURNproduces (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’sRETURNguarantees characterize the data crossing the boundary — and inlining puts them in front of the consumer’sINPUT. 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, neverSEE. 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-specCONFORMS, which means must match the referenced spec.SEEis 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. ReserveSEEfor 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.
-
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 withharvest, 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. -
Conditional behavior (any slot). For each
WHENguard, 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-guardedMUST-NOT, or an unguarded baseline the guarded obligation overrides. Do not encode necessity in the guard’s prose —WHENcannot 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. -
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.
- the exact separator and its character class (e.g. one ASCII space
-
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: inERROR, an obligation with noWHENis the policy for any failure no guarded obligation matched (the reading is fixed inyass.yass.yamlunderSlot.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’sINVARIANTand omit the residual fromERROR. A residual over an empty remainder is dead and can never fire, asserting one alongside an exhaustive partition is a contradiction, and a guard-lessERRORobligation 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 (
CONFORMSif that contract binds this component,SEEto 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-lessERROR— 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. -
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
yasscommands 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:
yassreportsproject 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 withyass findandyass refs, andyass queryonly 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 guidancefirst: 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 withUSES(The root file).
Stage 4 — Draft
- MUST lay files out as guidance’s Granularity, The root file, and Ordering say;
yass docs referencesettles a construct in doubt. - MUST write only obligations: nothing from the conversation survives except inside a
design:block somethingUSES. - MUST run
yass validateandyass lintand 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
| Thought | Reality |
|---|---|
| “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.