This file provides guidance to AI coding agents when working with code in this repository.
csspin is the core Python package shipping spin, a task runner and CLI that
provisions development environments (Python, Node, etc.) and standardizes
project workflows via a plugin system. This repo is the core/bootstrap layer
only; most actual functionality (Python tooling, Node/frontend, Java, docs,
workflows) lives in separate sibling repos named csspin-* (e.g.
csspin-python, csspin-frontend, csspin-docs, csspin-workflows), all
living flat under the same GitLab group as this repo, one clone per package.
csspin is self-hosted: this repo has its own spinfile.yaml and uses spin
to provision, test, and build itself.
Everything runs through spin once provisioned (spinfile.yaml declares
csspin_python.pytest, csspin_python.uv_provisioner, csspin_docs.sphinx,
csspin_workflows.stdworkflows):
spin provision— set up the dev environment (installs Python, plugin packages, and project dependencies into.spin/venv). Re-run after changingplugin_packages,plugin_paths, orpluginsinspinfile.yaml.spin test— run the full test suite via thetestworkflow hook (equivalent tospin pytestundercsspin_workflows.stdworkflows). CI runsspin test -vv --coverage --with-test-report.spin pytest <args>— invoke pytest directly with arbitrary args, e.g. a single test:spin pytest tests/test_cli.py::test_name.spin docs— build the Sphinx documentation (doc/), including regeneratingdoc/schemaref.rstfromsrc/csspin/schema.yamlvia theschemadoctask (seebuild_rulesinspinfile.yaml).spin cleanup(--purgeto also remove{spin.data}) — remove provisioned, project-local resources.spin --dump— print the fully resolved configuration tree along with the file/line each value came from; the primary tool for debugging configuration and plugin-loading issues.prek run(orpre-commit run) — black, isort, prettier, flake8, pylint, mypy, and misc hygiene hooks (.pre-commit-config.yaml). pylint/mypy excludetests/andplugins/.
Pytest markers: slow (excluded by default via pytest.opts: [-m, "not slow"] in spinfile.yaml) and wip.
spin's entire behavior is driven by one nested, ordered-dict-like structure:
the configuration tree (csspin.tree.ConfigTree, in src/csspin/tree.py).
Every plugin's settings, spinfile.yaml contents, CLI overrides, and even
spin's own bootstrap state (cfg.spin.*) live in this single tree, accessed
both as cfg["key"] and cfg.key. ConfigTree tracks, per key, the source
file/line the value came from — this is what powers spin --dump.
Startup sequence (src/csspin/cli.py, function cli):
find_spinfilewalks up from cwd to locatespinfile.yaml.load_minimal_treebuilds the tree from spin's ownschema.yaml, merges inspinfile.yaml, then the optional user global ($XDG_CONFIG_HOME/spin/global.yaml, disable viaSPIN_DISABLE_GLOBAL_YAML), then loadscsspin.builtinand appliesplugins/plugin_paths/plugin_packagesdirectives.- For
provision/cleanup/system-provision, control goes straight tocsspin.builtintasks (src/csspin/builtin/__init__.py), since plugin packages may not be installed yet. - Otherwise
load_plugins_into_treeimports each declared plugin (load_plugininsrc/csspin/cli.py, recursive over each plugin'srequires.spindependency list) and topologically sorts them (reverse_toposort) intocfg.spin.topo_plugins— dependencies configure and initialize before dependents. finalize_cfg_treeruns every loaded plugin'sconfigure(cfg)hook in topological order (toporun), then interpolates and type-sanitizes the whole tree (tree.tree_sanitize).- The click command group
commands(built up by plugins callingtask()) dispatches to the requested subcommand.
Plugins optionally ship a <plugin_name>_schema.yaml next to their module,
declaring the type, default, and help text for each setting
(src/csspin/schema.py). Each schema node becomes a BaseDescriptor
subclass (path, str, secret, int, float, bool, list, object
— registered via @descriptor(tag) into DESCRIPTOR_REGISTRY), responsible
for coercing values and producing defaults. secret-typed values get
collected into csspin.secrets and are masked in all log output
(obfuscate). Spin's own top-level schema lives in src/csspin/schema.yaml.
src/csspin/__init__.py is the public API plugins import from (__all__
lists the supported surface: task, group, config, sh, echo/info/
debug/warn/error, interpolate/interpolate1, invoke, toporun,
Memoizer, etc.). src/csspin/cli.py is spin's own bootstrap/CLI machinery
(click integration, plugin loading) and is imported lazily from __init__.py
functions to avoid an import cycle. src/csspin/builtin/ holds the tasks
that are always available without any plugin packages (provision,
cleanup, system-provision, run, schemadoc, distro).
Key plugin-facing concepts:
task(): decorator wrappingclick.command, introspecting the function signature for special parameter names (cfg→ config tree,ctx→ click context,args→ passthrough args) andoption()/argument()annotations for the rest.when="<hook>"registers the task to run as part ofinvoke(hook)(e.g. howcsspin_python.pytesthooks intospin test).noenv=Truemarks a task as runnable without a provisioned environment.invoke(hook, ...): runs every task registered under a given hook name in registration order, filtering kwargs to what each task actually accepts — this is how workflow commands liketest/build/lint(defined incsspin_workflows.stdworkflows) fan out to plugin-provided tasks.interpolate1/interpolate:str.format_map-style templating against the config tree plusos.environ, used pervasively (e.g."{python.version}","{spin.project_root}"). Do not call at plugin module import time — the tree isn't populated yet; only call insideconfigure()/task bodies. Nearly everycsspinI/O helper (sh,cd,readtext, ...) interpolates its path/string arguments automatically.sh(): the shell-out primitive. Commands run with the environment variables of spin's process; environment-providing plugins (e.g.csspin_pythonfor its venv) activate their environment once, ininit, by setting environment variables only. Plugin code must never import from a project environment, and activation must never touch spin'ssys.path(see "Plugin code and the project environment" indoc/howtoplugins.rst).spin.subprocess_environmentanduse_subprocess_environmentare deprecated;sh()still honours them with a warning until they're removed.build_target/build_rules: a tiny make-like system driven by thebuild_rulessection ofspinfile.yaml(see thedoc/schemaref.rstandrequirements.txtrules in this repo's ownspinfile.yaml), checking source mtimes viais_up_to_datebefore rebuilding.
tests/test_*.py— unit tests for the modules insrc/csspin/(tree, schema, cli, yaml handling, public API).tests/schema/— schema-loading/coercion tests, including a standalonetestpluginfixture package with its own*_schema.yaml.tests/schema/test_schema_secrets.yamland friends cover the descriptor edge cases (secrets, internal-only overrides, environment coercion).tests/schema/test_schema_extended.pycovers schema features not exercised by the YAML-file-driven tests.tests/integration/— exercises plugin loading and provisioning end to end (test_provisioning.py), and directive handling (directives/test_directives.py) against fixture plugins/spinfiles underfixtures/.tests/data/csspin_dummyandtests/data/csspin_depend— fake, installable plugin packages used to test plugin-package discovery/dependency loading without hitting a real index.- Fixtures (
tests/conftest.py) build config trees viaload_minimal_tree(andcfg/cfg_spin_dummyfor pre-built trees), rather than constructingConfigTreeobjects by hand.
See CONTRIBUTING.md for the full process (issue-first, draft MR/PR, GitLab
is canonical with a GitHub push mirror for external contributors). Release
notes go into doc/relnotes.rst, and only when preparing a release. Do not
add or change entries there as part of a feature or fix. There is no separate
changelog file.