Rust workspace, edition 2024, stable toolchain. Crates live in crates/ and are
prefixed aster- (the exception is symbol-extractor). The crates/aster-cli
crate is the binary; everything else is a library.
Human-facing setup and PR etiquette live in CONTRIBUTING.md. This file is the
working agreement for agents editing this repo.
- No useless comments in code.
- Match the project's existing style and patterns before writing anything new.
- No em dashes (—) in any writing.
- Never add
Co-Authored-By: Claude/Cursortrailers to commits or PRs, and strip them if found. - Use Conventional Commits for every message.
Every string a user sees (TUI cells, panel cards, error boxes, status lines) is written in plain, relatable language, never harness vocabulary. "Turns", "verdict", "judge", "budget exhausted", "rate limited", "context window" are internal words; users get "tries", "check", "too many requests", "stopped after 20 tries". Errors always say what happened and what to do next in one plain sentence, with the raw provider detail demoted below. Protocol field names, config keys, env vars, and docs stay technical; only rendered copy is covered by this rule.
- Read the crate you are touching and follow its existing patterns before introducing a new one.
- Inline variables into
format!braces:format!("{name}"), notformat!("{}", name). - Collapse nested
ifstatements. - Prefer method references over closures:
.map(str::trim), not.map(|s| s.trim()). - Avoid bare
booland ambiguousOptionparameters that force callers to writefoo(false)orbar(None). Prefer enums, newtypes, or named methods so the callsite reads on its own.aster_policy::ModeandPermissionModeArgare the shape to copy. - Make
matchstatements exhaustive. Avoid wildcard arms over enums we own, so adding a variant produces a compile error instead of silent fallthrough. - New traits get a doc comment explaining their role and what implementors are
expected to do.
McpInvokerincrates/aster-mcp/src/lib.rsis the example. - Prefer private modules with an explicit
pubcrate API. Keep the exported surface as small as the callers need. - Do not create helper functions that are called exactly once.
- Keep the core dependency-light. No vector DB, no external services in the review path. A new heavy dependency needs a written justification in the PR.
- Target modules under 500 LoC, excluding tests.
- Past roughly 800 LoC, add new functionality in a new module rather than extending the file, unless there is a documented reason not to.
- This applies hardest to the files that already attract unrelated changes:
crates/aster-cli/src/chat.rscrates/aster-cli/src/skills.rscrates/aster-harness/src/lib.rscrates/aster-cli/src/review.rscrates/aster-mcp/src/lib.rs
- When extracting from a large module, move the related tests and type docs with the code so the invariants stay next to what owns them.
chat.rsis orchestration. Prefer new modules undercrates/aster-cli/src/over new standalone functions there.
aster-cli is the largest crate, so it is always the path of least resistance:
the imports are there, the helpers are there, and nothing forces you out. That is
how it got large.
Before adding a new concept, feature, or API to aster-cli, consider whether:
- An existing library crate is the right home. Tool execution, context assembly, session state, and policy decisions are library concerns, not CLI concerns.
- It is time to add a crate to the workspace. Refactor as needed to make that happen.
aster-cli should be argument parsing, terminal I/O, and wiring. If a change
would still be correct with no terminal attached, it probably belongs in a
library crate.
The same applies in review: push back on changes that grow aster-cli without
needing to.
Aster builds a context that is sent to the model on every inference request. Every regression here is expensive and hard to see, so these are invariants, not preferences.
- No history rewrite. Context is built up incrementally. The only
permitted rewrite is the summarize-and-replace path in
compact_if_needed, and it must record arecord_summaryevent so the transcript stays honest about what was dropped. - Avoid changes that cause cache misses. Anything prepended or inserted near the head of the request invalidates the provider's prompt cache for the whole conversation. Append instead. If a change moves or reorders early context, say so in the PR.
- Nothing unbounded. Everything injected into context has a bounded size
and a hard cap. Existing caps live as
constat the top of their module:MAX_TOOL_RESULT_CHARS,MAX_SEARCH_HITS,MAX_LIST_ENTRIES,COMPACT_BUDGET_CHARSinchat.rs;inventory_budget_tokensinaster-mcp. New injections declare their own. - No single item over ~10K tokens.
- Flag any new individual item that can exceed ~1K tokens in the PR description. Those need a human to look at them.
- Growth with the repo is unbounded growth.
SkillSet::render_index, agent registries, and memory blocks all render one line per discovered item. Anything that scales with the number of skills, agents, servers, or memory blocks in a user's project needs a cap and a documented truncation rule.
The progressive injection in aster-mcp is the pattern to follow for anything
new that is potentially large: an inventory under a token budget, a search tool
to expand it on demand, and estimated_tokens to measure rather than guess.
Check these surfaces explicitly. Breaking one is a user-visible break even when the code compiles:
- CLI commands, subcommands, and flags (
crates/aster-cli/src/main.rsand each command's*Args) aster.yamlloading and defaults, andaster.yaml.example- The NDJSON event stream emitted by
--stream(the desktop app and the VS Code extension both consume it) - Transcript and session formats in
aster-persist, including resuming an existing session - Policy decisions and permission modes in
aster-policy - The MCP bridge tool schema and its routing contract
ASTER_*environment variables
Unless the change is mechanical, keep it under 800 changed lines. For complex logic changes, aim for 500.
If it is larger, work out whether it splits into reviewable stages and land the smallest coherent one first. Base that on the actual diff and its call sites, not a guess.
-
Behavior changes get tests. Prefer integration tests over unit tests for anything that changes agent behavior:
crates/aster-harness/tests/e2e.rsruns the pipeline end to end against a mock provider, andcrates/aster-persist/tests/roundtrip.rscovers session durability. Extend those rather than reaching for a unit test on an internal helper. -
Compare whole objects with
assert_eq!rather than checking fields one at a time. -
Do not add tests for statically defined values.
-
Do not add negative tests for logic that was removed.
-
Avoid test-only functions in the main implementation. If a test needs a seam, the seam should be justified by the production code.
-
Avoid mutating process environment in tests. Aster reads a lot of
ASTER_*env, and tests run in parallel in one process. Pass the resolved config down instead. -
New test modules go in a sibling file, referenced explicitly, so the filename says what it covers:
#[cfg(test)] #[path = "decision_tests.rs"] mod tests;
This applies to new test modules only. Do not rewrite existing inline
#[cfg(test)] mod tests { ... }blocks just to follow it. -
Tests must pass on Linux, macOS, and Windows unless the feature is explicitly OS-specific. Shelling out to
git,rg,semgrep, orast-grepis the usual way this breaks.
TUI code is crates/aster-cli/src/tui/, built on ratatui 0.29.
- Use
Stylizehelpers:"text".dim(),.bold(),.cyan(),.underlined(). Prefer them over constructingSpan::styledwith an explicitStyle. Span::styledis fine when the style is computed at runtime.- Do not use
.white(). Use the default foreground so the output works on light and dark terminals. - Prefer
"text".into()for spans andvec![...].into()for lines when the target type is obvious. UseLine::from/Span::fromwhen it is not. - Do not refactor between equivalent forms without a readability gain. Follow the conventions already in the file.
- Wrap with
Paragraph::wrap(Wrap { trim: false })rather than splitting text by hand. For single-line elision useconsole::truncate_strwith an…suffix, the wayskills.rsdoes. - Shared drawing lives in
crates/aster-cli/src/tui/helpers.rs. Check it before writing a new banner, input box, chip, or dim line. - Terminal state is owned by
tui/guard.rs. Anything that can leave the terminal in raw mode on a panic or an early return goes through the guard.
The Makefile is the interface. Do not invent cargo invocations.
make fmtafter finishing code changes anywhere in the repo. Do not ask for approval to run it.make lintruns clippy with-D warnings, workspace-wide.make testruns the workspace test suite. For a scoped run while iterating,cargo test -p aster-cliand friends are fine.make checkis fmt-check plus lint plus test, and is exactly what CI gates on. Run it before saying a change is done. Ask before running it if the user is mid-iteration, since it is a full workspace build.make desktop-checktypechecks the Tauri frontend. Run it if you toucheddesktop/. The Rust CI job does not cover it.make installrebuilds and overwrites theasteron PATH. The desktop app and the VS Code extension both shell out to it, so a stale binary makes your changes invisible to them.
Rust builds here are slow and the cargo lock makes them look stuck. Be patient
with make check and make test. Do not kill them by PID.
Do not re-run tests after make fmt.
docs/ holds architecture and design notes: ARCHITECTURE.md, ALGORITHM.md,
ANALYZERS.md, MCP.md, MEMORY.md, HARNESS.md, HARNESS-FINDINGS.md,
LIVING-HARNESS.md, and COMPUTER-USE.md. Update the relevant one when you
change the thing it describes. User-facing product documentation lives on the
site, not in docs/.
Prompts are content, not code. They live in crates/aster-cli/prompts/ and are
pulled in with include_str!. Do not inline prompt text into Rust source.
Conventional Commits: type(scope): summary, imperative mood, lowercase, no
trailing period. Types: feat, fix, perf, refactor, docs, test, chore, build, ci,
style, revert.