After making code changes, you must always lint using make agent-check.
make agent-check
# If the current system doesn't have the `make` command,
# lookup the "agent-check" target in the Makefile and run the commands one by one (targets fix-unused-imports format lint pyright mypy)This runs multiple code quality tools:
- Pyright: Static type checking
- Ruff: Fix unused imports, lint, format
- Mypy: Static type checker
- plxt: Format and lint TOML, MTHDS, and PLX files
Always fix any issues reported by these tools before proceeding.
Non-subject function parameters across pipelex/ source must be keyword-only (a bare * after the subject). The convention is mechanically enforced and already runs as part of make agent-check, but you can invoke it on its own:
make check-keyword-only # alias: make cko — read-only gate; hard-blocks on any violation
make fix-keyword-only # alias: make fko — auto-insert a bare * for mechanically-fixable violationscheck-keyword-only owns the pass/fail gate; fix-keyword-only rewrites what it can and reports the shapes it can't fix mechanically (resolve those by hand). See docs/contribute/keyword-only-arguments.md for the full convention.
If you need to clean derived files and caches, typically after you erased files or moved tests, the linters can get confused, the pytest collection can be off...
make cleanderivedmake agent-test runs the test suite and is critical at the end of a coding session to verify everything is good before wrapping up.
At intermediate steps during LOCAL development, it's OK to run only the tests relevant to your changes — either by calling pytest directly from the .venv (e.g. .venv/bin/pytest -x -q tests/unit/path/to/test_module.py) or using make t TEST=TestClassName. This applies only to local setups, not cloud agents.
make agent-test
# If the current system doesn't have the `make` command, lookup the "agent-test" target in the Makefile and run the command manually.
# Zero output on success; full output on failure.Use make agent-test-debug (alias: make atd). Same suite, but with stale-process cleanup upfront, an outer wall-clock timeout so fixture-teardown hangs and xdist worker-replace loops can't run forever, direct file redirect for live progress (tail -f /tmp/pytest-agent-test-debug.log), and -v so each test name lands in the log as it runs. On failure or timeout it prints the failed tests, the log path, and a grep hint.
For the full debugging methodology — clean-state protocol, when to bail to the user, how to grep failures by error class name, when xdist failures are flakes vs real bugs — see docs/agents/debugging-hanging-pytest-runs.md.
LOCAL ONLY: The commands below are meant for a human developer running on their local machine. If you are an AI agent (Claude Code, Cursor, Codex, or any other agent running in the cloud or in a sandboxed environment), do NOT use these commands. Use
make agent-testinstead.
If anything went wrong, you can run the tests with prints to see the error:
make test-with-prints
# If the current system doesn't have the `make` command, lookup the "test-with-prints" target in the Makefile and run the command manually.LOCAL ONLY: The commands below are meant for a human developer running on their local machine. If you are an AI agent (Claude Code, Cursor, Codex, or any other agent running in the cloud or in a sandboxed environment), do NOT use these commands. Use
make agent-testinstead.
make tp TEST=TestClassName
# or
make tp TEST=test_function_nameNote: Matches names starting with the provided string.
LOCAL ONLY: The commands below are meant for a human developer running on their local machine. If you are an AI agent (Claude Code, Cursor, Codex, or any other agent running in the cloud or in a sandboxed environment), do NOT use these commands. Use
make agent-testinstead.
To rerun only the tests that failed in the previous run, use:
make tp TEST=LF
# or with any test target
make test TEST=LF
make t TEST=LFNote: TEST=LF (or TEST=lf) will use pytest's --lf flag instead of name filtering.
CRITICAL: Before running any pipelex commands or pytest, you MUST use the appropriate Python virtual environment. The only exceptions are our make commands which already include the env activation.
Call the CLI directly from the virtual environment:
.venv/bin/pytest -s -v -k test_render_jinja2_from_text
.venv/bin/pipelex validate --allFor standard installations, the virtual environment is named .venv. Always check this first. On Windows, the path is .venv\Scripts\ instead of .venv/bin/.
The pipelex-dev CLI provides internal development tools that are not distributed with the package. It is available in the virtual environment.
.venv/bin/pipelex-dev --helpKey commands:
-
generate-mthds-schema: Regenerate the MTHDS JSON Schema (derived/mthds_schema.json). Run this after modifyingmthds_schema_generator.py..venv/bin/pipelex-dev generate-mthds-schema
-
generate-error-pages: Regenerate the per-class error reference pages underdocs/errors/— one Markdown page perPipelexErrorsubclass, which is what each error'stype_uridereferences to. Run after adding or renaming an error class. Pages a maintainer claims with a<!-- pipelex:authored -->marker are preserved across runs. Also available asmake generate-error-pages(aliasmake gep)..venv/bin/pipelex-dev generate-error-pages
-
refresh-graph-ui-sri: Refetch the pinned graph viewer assets from jsDelivr (@pipelex/mthds-uistandalone JS+CSS,elkjs) and rewritepipelex/graph/reactflow/standalone_assets.pywith newsha384Subresource Integrity hashes. Use when bumping the pinned mthds-ui or elkjs version..venv/bin/pipelex-dev refresh-graph-ui-sri --mthds-ui-version 0.6.3 # or rotate elkjs alongside: .venv/bin/pipelex-dev refresh-graph-ui-sri --mthds-ui-version 0.6.3 --elkjs-version 0.11.1
- Blueprints (
pipelex/pipe_operators/,pipelex/pipe_controllers/,pipelex/core/) are the MTHDS language reference — what.mthdsfiles parse into. - Specs (
pipelex/builder/pipe/) are a convenience authoring format for AI agents. Each spec hasto_blueprint()that transforms it into the corresponding blueprint. Spec-level fields may differ from blueprint-level fields.
When adding validation or fields, decide which layer they belong to. Language rules go on blueprints; authoring convenience goes on specs. See pipelex/builder/CLAUDE.md for details.
- The main config model is defined using
ConfigModelclasses, derived frompydantic BaseModel - The model is defined in
pipelex/system/configuration/configs.py, some of the submodels being defined in their respective sub-packages - When adding new configs, place them where it makes most sense, ask the user if you need arbitrage
- As per our python standards, use StrEnum for multiple-value enums. In that case they must not be strict pydantic fields, i.e. add
= Field(strict=False) - Important: NEVER EVER set default values for config attributes in the class definition. All the default values are defined in the main config file
pipelex/pipelex.toml. The only exception si for Optional values which must be set toNonein the class definition. - If (and only if) you add some config that will clearly make sense for client projects to override, for instance if it's a case of user preference, then you can also add a copy of the settings to the project override config file
.pipelex/pipelex.toml. NEVER add them commented out: commented-out TOML is never parsed or validated, so it rots silently when keys are refactored. Instead, write the actual default values (matchingpipelex/pipelex.toml, even empty ones likeactivity_queues = {}) so the override file stays valid and behaves like setting nothing. Plain prose comments explaining the setting are fine — it's commented-out keys/values that are forbidden. - The different
pipelex.tomlfiles and the python modelconfigs.pymust be up to date with each other in terms of structure and attributes, otherwise the loading of teh config fails. To check quickly that you're good, just runmake tbwhich tests the boot sequence, which includes the config loading.
Non-subject function parameters across pipelex/ source must be keyword-only, so call sites are self-documenting: do_thing(retries=3, timeout=30) over the opaque do_thing(3, 30). The compliant shape places a bare * after the subject:
def f(subject, *, opt1, opt2): ...— the first non-self/clsparameter (the subject) may stay positional; everything after it must be keyword-only. Making the subject keyword-only too (def f(*, a, b)) is always allowed and often preferable.- A lone subject (
def render(node)) is compliant; a second bare positional (def f(a, b),def truncate(text, max_length=80)) is a violation.
The rule is mechanically enforced by the check-keyword-only AST guard, which runs in make agent-check, in the make check aggregate, and in CI; the tree is fully compliant, so it hard-blocks on any violation. Carve-outs (dunders, pydantic validators/serializers, Typer/pytest/Jinja2 framework entrypoints, @override impls) are skipped automatically. A genuinely justified one-off uses an inline # kw-only: ignore comment on the def line (place it right after the open paren so ruff format keeps it on the header line). Watch for functions a framework or the interpreter invokes positionally (callbacks, __import__ hooks, route handlers): the type checker is blind to those, so make agent-test is the safety net.
The full specification — the two exceptions, the carve-out list, the symmetric-tuple allowlist, the escape hatch, and worked examples — is in docs/contribute/keyword-only-arguments.md.
We use Material for MkDocs. All markdown in our docs must be compatible with Material for MkDocs and done using best practices to get the best results with Material for MkDocs.
- Always add a blank line before any bullet lists or numbered lists in MkDocs markdown.