Ossature

A build system for specs and architecture.

You write the spec. An LLM generates the code, verified task by task. You review the plan before it runs.

$ uv tool install ossature
v… · Open source · MIT License
Works with Anthropic · OpenAI · Mistral · Google · Ollama

The pipeline is three commands.

$ ossature validate   # structural check, no LLM
$ ossature audit      # review the specs, write a plan
$ ossature build      # generate the code, verify each task

Building yep, end to end.

yep is a Rust take on the yes command: print a line forever until the pipe closes. It is built from three files, one for what it does, one for how it fits together, and one for the checks you write yourself.

01

Say what it does

The behavior, the inputs and outputs, and every error case. No code, no types.

$ ossature validate
specs/YEP.smd
---
id: YEP
status: draft
priority: medium
---

# Yep

## Overview

yep writes a line to stdout, over and over, until the
process is killed or the pipe closes.

## Requirements

### Output Repeated String

**Accepts:** Zero or more string arguments. Defaults to
"y". Multiple arguments join with a single space.

**Returns:** The string plus a newline, forever.

**Errors:**

- Broken pipe -> exit silently with code 0
- Other write error (ENOSPC, EIO) -> exit code 1
- Invalid UTF-8 argument -> error to stderr, exit 1
02

Lay out the pieces

The architecture is optional. It names the components and the contracts each one has to hold.

specs/YEP.amd
specs/YEP.amd
# Architecture: Yep

## Components

### OutputLoop

@path: src/output.rs

Handles buffered writing to stdout in a tight loop.

**Interface:**

    fn run(output: &str) -> Result<(), WriteError>

**Contracts:**

- run returns Ok(()) when stdout closes (broken pipe)
- run pre-allocates a buffer and reuses it each iteration
- fill_buffer packs as many copies of output+newline
  as fit in the buffer
03

Write the checks yourself

You write the cases with the exact outcome you expect. The model never sees the expected values, so it cannot write the code to fit them.

specs/YEP.vmd
specs/YEP.vmd
@spec YEP

# yep streams forever, so only invocations that stop on
# their own can be pinned here. Invalid UTF-8 is rejected
# at argument parsing and exits right away, so it has an
# outcome you can write down: exit 1 and an error on
# stderr that names the problem.

@covers "Output Repeated String"
scenario rejects bytes that are never valid utf-8:
when $ yep \xff\xfe
then exit 1
then stderr has "UTF-8"

@covers "Output Repeated String"
scenario rejects invalid utf-8 in any argument:
when $ yep hello \xc3\x28
then exit 1
then stderr has "UTF-8"
04

Audit plans the work

Audit turns the three files into a plan you can read and edit before anything runs.

$ ossature audit
.ossature/plan.toml
[[task]]
id = "006"
spec = "YEP"
title = "Verify: scenarios (YEP)"
outputs = [
    "checks/scenarios.yep.YEP.cases.json",
    "checks/test_checks_scenarios_yep_YEP.py",
]
depends_on = ["005"]
status = "done"
verify = ["python -m pytest checks/test_checks_scenarios_yep_YEP.py -q"]
kind = "verify"
vmd_file = "specs/YEP.vmd"
vmd_group = "@scenarios"
covers = ["Output Repeated String"]
05

Build generates the code

Build runs the plan. The output loop it writes matches the contract from the architecture: return Ok(()) when stdout closes.

$ ossature build
output/src/output.rs
use std::io::{self, Write};

/// Buffered writing to stdout in a tight loop.
/// Returns Ok(()) if stdout closes due to a broken pipe.
pub fn run(output: &str) -> Result<(), WriteError> {
    let mut buffer = Vec::with_capacity(65536);
    fill_buffer(output, &mut buffer);

    let mut stdout = io::stdout().lock();
    loop {
        if let Err(e) = stdout.write_all(&buffer) {
            if e.kind() == io::ErrorKind::BrokenPipe {
                return Ok(());
            } else {
                return Err(WriteError::Io(e));
            }
        }
    }
}
06

Your checks run against it

Your cases become a real test suite that runs against the built binary. It has to pass.

output/checks/
output/checks/test_checks_scenarios_yep_YEP.py
"""Generated verification harness for YEP. Do not edit.

The scenarios come from a generated fixture, produced
from the author-written verification spec. The expected
values are author-owned; changing this file does not
change what correct means.
"""

_EXPECTED_CASE_COUNT = 3

@pytest.mark.parametrize("case", _CASES, ids=_name)
def test_YEP_scenarios(case, tmp_path):
    if case["kind"] == "command":
        _run_command_scenario(case, tmp_path)

def test_YEP_scenario_count():
    assert len(_CASES) == _EXPECTED_CASE_COUNT

The whole project is on GitHub

yep ships with its specs, the generated plan, every prompt, and the code that came out, so you can read the full path from spec to binary. The other examples do too.

Read the full yep example on GitHub →

The specs are the source. The code is output.

01

The spec is the source

Specs describe what to build, architecture describes how it fits together. The code is the output, not the source.

02

Compiles incrementally

Every input is checksummed, so a change recompiles only what depends on it, never the whole project.

03

Scoped per task

Each task is compiled against only the spec sections, types, and files it needs.

04

Reproducible on disk

Specs, the plan, every prompt, and the generated code live in the repo, checksummed.