Cabin is a pre-1.0, Cargo-inspired (not Cargo-compatible) package manager and
build system for C/C++, implemented in Rust. Reuse Cargo vocabulary only
where the C/C++ semantics really line up. docs/architecture.md is the
canonical architecture and scope document (crate ownership, boundaries, data
flow, scope exclusions); if it disagrees with this file, update both in the
same change and treat the architecture doc as authoritative.
crates/- Rust workspace crates. Readcrates/AGENTS.mdbefore changing anything under it.crates/cabin/- thecabinbinary. Readcrates/cabin/AGENTS.mdbefore changing CLI code.docs/- canonical Markdown docs, rendered by the website. Per-page summaries are in the "Repository shape today" section ofdocs/architecture.md.website/- Astro site forcabinpkg.com; also rendersdocs/. Readwebsite/AGENTS.mdbefore changing website code or docs rendering.examples/- runnable Cabin packages covered by CLI integration tests..cargo/config.toml- the Cargo aliases that expose thextask-*repository tools. See "Repository Automation".RELEASING.md- maintainer release procedure. Do not infer release rules from CI alone, and do not change cargo-dist, binstall, publish, or release workflow behavior as part of unrelated work.
cargo ciruns the CI gate locally, scoping expensive checks to the surfaces changed relative toorigin/main. Agent stop hooks runcargo ci --hook, which blocks one attempt to stop while the gate is red; a second stop is let through with a warning (stop_hook_active).- The exact per-command shapes are in
CONTRIBUTING.md"Required checks". Mirror the flags verbatim:--all-features,--locked,RUSTFLAGS="-D warnings",RUSTDOCFLAGS="-D warnings", and clippy's trailing-- -D warningsare intentional. - Changes under
docs/orwebsite/require, fromwebsite/:npm ci && npm run lint && npm run build && npm test(build runs typecheck, Astro build, CSP checks, and docs-link checks). For docs-only changes, run only the checks matching the touched surface. cargo ciruns the same four website commandswebsite.ymldoes, scoped to changes touchingwebsite/,docs/orports/. Outside that scope it skips them, so a change that reaches the site another way still needs them by hand.- Commit subjects follow Conventional Commits, lower-case, at or under 100 characters (commitlint runs in CI).
- Do not edit
typos.tomlor add allowlist entries unless a reviewer explicitly asks. Fix the spelling instead.
Repository automation belongs in Rust. Each tool is a private
crates/xtask-<name> crate (publish = false, never part of the shipped
cabin binary), reached through a Cargo alias in .cargo/config.toml. The
aliases name root-workspace packages, so run them from the repository root -
registry/ is a separate workspace and resolves against its own manifest.
The migration is complete: registry/scripts/ is gone, and no shell
tooling is left in this repository. Do not reintroduce any.
- New automation becomes a subcommand of the
xtask-*crate that already owns that responsibility, or a new crate when the responsibility and the dependency set are genuinely new. Do not add a shell or Perl script, and do not put substantial logic (loops, conditionals, functions, traps, heredocs, embeddednode -/python3 -/perl -e) in a workflowrun:block - call an alias instead. Plain invocations (cargo build,npm ci, package installation) stay inline. - The rule covers repository tooling only. Product source (
crates/cabin*,registry/src/), website source and its npm scripts, theDockerfile,demo.tape, and devcontainer provisioning are not repository automation and are unaffected. - Workflows trigger on
pushtomainand onpull_request, with no trigger-levelpaths:filter: a filtered-out workflow leaves its required checks pending forever, while a job skipped by anif:satisfies them. Component scoping therefore happens at the job level - each workflow'schangesjob evaluates the shared filter lists in.github/path-filters.ymland the expensive jobs skip when theirs does not match. That file is the ONE copy of every dependency list; keep its lists coarse (whole directories, shared dependencies) and keep.github/**in every list so workflow or filter edits re-run everything. No test polices the file - the previous per-workflow copies grew a YAML-scraping test that broke on an unrelated rename. Two carve-outs: a required matrix job reports through a non-matrix aggregate (build-and-test-required), because skipping a matrix job never expands it, so a required per-leg name would never report and the merge would hang forever; and a failed (not skipped)changesjob skips its dependents to satisfied - the aggregate fails closed on that, and the other required checks accept it with the red gate run as the only signal. - No test may read a file under
.github/or depend on a workflow living at a particular path. Moving or renaming a workflow must never be able to fail the test suite.
Apply these pragmatically, not mechanically. When they conflict, prioritize correctness, readability, simplicity, and ease of change over theoretical purity. Make the smallest coherent change that solves the current problem, and do not add speculative flexibility.
- Keep the implementation simple and focused: KISS and YAGNI.
- Avoid duplicating the same knowledge or business rule: DRY.
- Do not introduce abstractions prematurely: follow AHA and the Rule of Three.
- Separate distinct concerns and keep each component responsible for one coherent purpose. Prefer high cohesion and low coupling.
- Prefer composition over inheritance.
- Keep APIs and behavior unsurprising: follow the Principle of Least Astonishment.
- Limit dependencies on internal object structure: follow the Law of Demeter.
- Validate assumptions early and fail fast with clear errors.
- Use types and data structures that make invalid states unrepresentable where practical.
- Prefer explicit behavior, dependencies, and configuration over hidden or implicit mechanisms.
- Apply SOLID principles where they improve clarity, substitutability, and maintainability, but avoid unnecessary indirection.
- If an external library can solve the same problem, use it, but weigh the full cost first.
- State assumptions before coding when the request is ambiguous. Ask instead of silently picking between incompatible interpretations.
- Make surgical changes: no refactoring adjacent code, reformatting unrelated files, or removing pre-existing dead code unless asked. Prefer simple, direct Rust and existing local patterns; add abstractions only when they remove real duplication or match an established boundary.
- Comments explain constraints and rationale, not mechanics: non-obvious invariants, compatibility requirements, workarounds, external constraints, or why an obvious alternative is incorrect. Do not write comments that restate what the code does. Before keeping or adding a comment, first consider whether clearer naming, extracting a function, introducing a type, or restructuring would make it unnecessary.
- Business logic belongs in the owning crate;
crates/cabinparses flags, calls typed APIs, and renders results. Boundary or scope questions:docs/architecture.md("Scope and limitations" lists what is deliberately deferred - do not implement deferred features). - Do not implement "not implemented" features. Unknown future syntax should
fall through generic
deny_unknown_fieldsor clap unknown-flag diagnostics, not feature-specific rejection arms. - Keep C support first-class: when touching build planning, manifests, flags, toolchains, generated Ninja, packaging, lockfiles, metadata, or docs for those areas, cover C alongside C++ (fixtures included).
- Keep generated and machine-readable output deterministic (sorted or
normalized); the full list is in
docs/architecture.md("Contributor-facing architecture guardrails"). - Add focused tests for behavior changes: unit tests in the owning crate,
plus CLI integration coverage when user-facing. Test portability rules
live in
crates/AGENTS.md. - A scripted or repeated edit must assert, not assume: check that the pattern matched and that the old identifier is gone afterwards. A silently-skipped edit passes every check that does not compile the file.
- Green checks only cover the surfaces they touch. Before calling a change verified, name which check would have failed had the change been wrong; if none would, the change is unverified.
- A port directory under a ports tree is published verbatim: editing
it - a comment included - changes the published archive bytes.
A byte-only correction is a packaging revision; an edit that changes what
resolution consumes (
dependencies,features,standards) is not a revision at all and needs a new upstream version, withlinksstamping the one-way exception - seedocs/foundation-ports.md"Correcting a published port". The preflight dry-run publishes into a fresh temporary registry, so it cannot catch that distinction for you. Never fold such an edit into an unrelated change, and check the published digest either way. --targetis reserved for future platform/toolchain triples; never use it for manifest-target selection.--build-diris the build-output flag;--target-diris not a Cabin alias.
- Detailed behavior belongs in
docs/, not here. If a behavior or architecture change affects users, update the matching docs page in the same change. - New
docs/*.mdpages must be added towebsite/src/lib/docsNav.ts. - If positioning, supported languages/platforms, install instructions, the
top-level command surface, or package-page snippets change, update
website/in the same change or call out the required follow-up. - A change that alters what is true rather than what runs (a migration, a
removal) silently invalidates prose. Sweep every surface that describes the
thing:
docs/,CONTRIBUTING.md, the per-exampleREADME.mds and the aggregateexamples/README.md, the ports treeREADME.md, and website copy that names specific packages or features. Separate claims about a thing's shape ("published from the curated recipe", "reachable asport = true"), which go stale, from durable facts about upstream or policy, which do not - and fix only the former.
- The diff is limited to the requested behavior or documentation change, and new or changed behavior has tests at the right layer.
- Required checks for the touched surface were run, or skipped checks are called out with a reason.
- Docs, examples, website, and
AGENTS.mdpointers are updated when the user-visible surface changes; generated output remains deterministic.
All implementation work must use branches and pull requests. Never implement changes directly on the default branch.
Each pull request must be the smallest cohesive change that can be meaningfully reviewed, tested, and squash-merged.
Default to splitting work into smaller pull requests. Keep changes together only when separating them would make an intermediate pull request:
- broken or non-buildable;
- untestable;
- misleading or semantically incomplete;
- unsafe to merge;
- or not meaningfully reviewable on its own.
Do not combine changes merely because they belong to the same issue, feature, or overall task.
Before making changes:
- Inspect the repository status and preserve all unrelated local changes.
- Update the local default branch from its remote.
- Determine whether the requested work fits into one minimal, cohesive pull request.
- If multiple pull requests are needed, briefly state the proposed sequence and the responsibility of each pull request.
- Create a fresh branch from the latest default branch for the first pull request.
Do not discard, reset, overwrite, commit, or otherwise modify unrelated local changes.
A pull request may include only:
- the code required for its single cohesive change;
- tests required to verify that change;
- documentation required to accurately describe that change;
- and strictly necessary mechanical updates caused by that change.
Do not include:
- unrelated refactoring;
- opportunistic cleanup;
- optional improvements;
- formatting changes outside the affected area;
- preparatory work that is needed only by a later pull request;
- or additional features that can be reviewed and merged separately.
When uncertain whether two changes belong together, default to separate pull requests.
Do not split work into artificial or meaningless fragments. Every merged pull request must leave the repository in a valid, tested, and usable state.
Use one branch per pull request.
The initial implementation should normally be committed as one cohesive commit. During the pull request lifecycle, additional fixup commits are allowed for:
- review feedback;
- CI failures;
- test corrections;
- small omissions;
- and other changes required to make the current pull request acceptable.
Do not add unrelated work through fixup commits.
Use clear branch names and commit messages that describe the specific change.
If the overall task is too large for one minimal, cohesive pull request, split it into an ordered sequence of smaller pull requests.
Later pull requests may depend on earlier pull requests. Dependent work must be processed strictly sequentially:
- Implement, test, commit, push, and open the current pull request.
- Address review feedback with fixup commits as needed.
- Wait until the pull request is approved and all required checks pass.
- Do autosquash and rebase the current branch onto the updated default branch, resolving any conflicts.
- Wait until the pull request is approved and all required checks pass again.
- If you get new review feedback, repeat steps 2–5 until the pull request is ready to merge.
- Squash-merge the pull request into the default branch.
- Update the local default branch from the remote.
- Create a fresh branch from the updated default branch.
- Begin the next dependent pull request.
Do not:
- create stacked branches;
- create a later branch from an unmerged feature branch;
- keep multiple dependent pull requests open simultaneously;
- target one feature branch from another pull request;
- or place several sequential review steps into one multi-commit pull request.
Independent pull requests may be open concurrently only when they do not depend on or overlap with each other.
If a task cannot be decomposed into valid sequential pull requests without leaving broken or unusable intermediate states, keep the inseparable portion together and explain why it cannot be split further.
Before opening or updating a pull request:
- Run the relevant formatting, linting, build, and test commands.
- Fix failures caused by the current change.
- Report which checks were run and their results.
- Clearly disclose any checks that could not be run and why.
Do not claim that a change is complete or tested when the relevant verification has not been performed.
After opening a pull request:
- keep all further changes limited to the current pull request’s stated purpose;
- use fixup commits for review feedback and CI corrections;
- rerun relevant checks after material changes;
Do not begin the next dependent pull request while the current one remains unmerged.
Use squash merge only.
Never use:
- merge commits;
- rebase merge;
- direct pushes to the default branch;
- or manual merging outside the pull request workflow.
Merge only after:
- the pull request has received the required approval;
- all required checks have passed;
- and there are no unresolved review comments or requested changes.
If authorized to perform the merge, use the repository’s squash-merge mechanism and delete the merged branch. Otherwise, stop after the pull request is ready and wait for an authorized maintainer to squash-merge it.
After merging, update the local default branch before starting any subsequent work.
When choosing between one larger pull request and several smaller sequential pull requests, prefer the smaller sequence unless the split would create a broken, untestable, misleading, unsafe, or meaningless intermediate state.