Skip to content

Languages

Cascade ships with eight built-in language profiles. Each profile bundles the conventions, tooling, and LLM guidance for that language. The pipeline reads the profile and adapts: it puts new files in the right directories, names test files the right way, runs the right test command, and passes language-specific guidance into the LLM prompts.

Adding a ninth language is a single registry entry. There is no plugin system to learn; it is one file in one Python module.

LanguageDetectionTest commandSource layout
Pythonpyproject.toml, setup.py, requirements.txtpytestsrc/, tests in tests/
TypeScripttsconfig.jsonnpx vitest runsrc/, tests next to source or in tests/
JavaScriptpackage.jsonnpx vitest runsrc/, tests next to source
Gogo.modgo test ./...flat, tests in same package as *_test.go
RustCargo.tomlcargo testsrc/, tests in tests/ or inline #[cfg(test)]
Javapom.xml, build.gradlemvn testsrc/main/java, tests in src/test/java
RubyGemfile, Rakefilebundle exec rspeclib/, tests in spec/
C#*.csproj, *.slndotnet testsrc/, tests in separate *.Tests project

When you run a Cascade command, the language is resolved in this order, first match wins:

  1. Explicit override in cascade.yaml:
    language: go
  2. CLI flag on the build command:
    Terminal window
    cascade build stories/sprint.yaml --language rust
  3. Auto-detection from marker files in the repo root.

For polyglot repos, the language with the highest detection priority wins. Current priorities (higher beats lower):

PriorityLanguageMarker files
100RustCargo.toml
95Gogo.mod
90Javapom.xml or build.gradle
80C#*.csproj or *.sln
70RubyGemfile or Rakefile
65TypeScripttsconfig.json
60Pythonpyproject.toml, setup.py, requirements.txt
50JavaScriptpackage.json (without tsconfig.json)

So a repo with both pyproject.toml (Python, 60) and go.mod (Go, 95) is detected as Go. If that is wrong for the work you are doing, set language explicitly in cascade.yaml or pass --language on the command line.

Every language profile is a LanguageProfile dataclass with these fields:

FieldWhat it does
nameInternal identifier (lowercase, used in flags and config)
display_nameHuman-friendly name (used in CLI output)
file_extensionsTuple of extensions Cascade considers part of the language. Drives repo scanning and code grouping.
source_dir_defaultWhere new source files go by default (src/ for Python, . for Go)
test_dir_defaultWhere tests go (tests/ for Python, same dir as source for Go)
test_file_globHow test files are named (test_*.py, *_test.go, *Test.kt)
test_commandWhat runs to validate generated code (pytest, go test ./..., etc.)
install_commandHow to install dependencies (pip install -e ., npm install, cargo build)
type_check_commandOptional type check before running tests (mypy ., tsc --noEmit, none for Go)
formatter_commandOptional formatter run before commit (black ., prettier --write, cargo fmt)
detection_filesMarker filenames for auto-detection
detection_priorityTiebreaker for polyglot repos (higher wins)
notes_for_llmLanguage-specific guidance passed into the LLM prompt at plan and code stages

This is the field that makes the generated code feel native rather than generic. Examples from the built-in profiles:

Python:

Use type hints on all public functions. Prefer dataclass or Pydantic models over dicts. Tests use pytest, not unittest. Import order: stdlib, third-party, local. Use pathlib.Path over os.path. Use from __future__ import annotations at the top of every file.

Go:

Error handling is explicit; never _ an error. Use errors.Is and errors.As for unwrapping. Tests live next to source in *_test.go and use the standard testing package; reach for testify only for assertions. Prefer interfaces defined where they are consumed, not where they are implemented.

Rust:

Prefer Result<T, E> over panicking. Use ? for propagation. Tests at the bottom of the file in a #[cfg(test)] mod tests { ... } block; integration tests in tests/. Use cargo clippy conventions. Avoid unwrap() in production code paths.

These notes are LLM-grade guidance: short, opinionated, specific. The profile-bundled notes apply to everyone. Add team-specific overrides in team-memory/conventions.md.

Cascade’s repo scanner uses file_extensions to know which files are “yours” vs vendor/build artifacts. So in a Rust project, it lists *.rs files in the planning prompt but skips target/** automatically.

If a story does not specify file paths, the planner uses source_dir_default and test_dir_default to place new files in the conventional location for the language. For Python: new code lands in src/, new tests in tests/. For Go: new code and tests live side by side in the package directory.

Generated test files follow the language’s test_file_glob pattern, so they get picked up by the test runner automatically. This means:

  • Python: tests/test_users_pagination.py (matches pytest’s default discovery)
  • Go: users_pagination_test.go next to users.go
  • Rust: tests added inline as #[cfg(test)] mod tests { ... } or in tests/users_pagination.rs
  • Java: UsersPaginationTest.java in src/test/java/...

The install command runs after apply and before test. It is best-effort: failure here does not abort the pipeline (Cascade just logs a warning and moves on to tests). This is intentional, because many repos do not have install commands that work cleanly in CI without setup.

The test command is the one place where a non-zero exit code matters for the PR body but does not abort the pipeline. Cascade always commits, pushes, and opens the PR even if tests fail; the failure is recorded in the PR body so a human can see it. Reasoning: a failed test PR is much more useful than no PR at all, because the human can apply a small fix and merge.

You can override the test command in cascade.yaml:

test_command: pytest tests/unit -x --tb=short

This wins over the profile default.

A new language is a single entry in src/cascade/languages.py. Here is what adding Kotlin would look like:

KOTLIN = LanguageProfile(
name="kotlin",
display_name="Kotlin",
file_extensions=(".kt", ".kts"),
source_dir_default="src/main/kotlin",
test_dir_default="src/test/kotlin",
test_file_glob="*Test.kt",
test_command=("gradle", "test"),
install_command=("gradle", "build", "-x", "test"),
type_check_command=None,
formatter_command=("ktlint", "--format"),
detection_files=("build.gradle.kts", "settings.gradle.kts"),
detection_priority=75,
notes_for_llm=(
"Idiomatic Kotlin: prefer val over var, use data classes for "
"value types, expressions over statements, scope functions "
"(let, also, apply) over manual null checks. Tests with JUnit 5 "
"and MockK. Use coroutines for async; never block the main thread."
),
)

Then add it to the PROFILES registry at the bottom of the file:

PROFILES = {
"python": PYTHON,
"typescript": TYPESCRIPT,
# ...
"kotlin": KOTLIN,
}

Submit a PR with that change plus a test in tests/test_languages.py. That is the whole process. No plugin system, no separate package, no registration ceremony.

Cascade is single-language-per-run by default. If your repo has both a Python backend and a TypeScript frontend, and you want Cascade to work on either, you have two options:

  1. Per-command override. Run cascade build stories/backend.yaml --language python for backend stories and --language typescript for frontend ones.
  2. Per-repo split. Treat the two languages as separate Cascade projects with two cascade.yaml files in different subdirectories. Run Cascade from the appropriate subdirectory.

We may add per-story language detection in a future release. Right now, simpler-is-better.