Universal local-first experiment lifecycle manager inspired by Git
Project description
A universal, local-first experiment lifecycle manager inspired by Git.
English · Русская документация
Git tracks how code changes. expctl tracks how experiments happen.
expctl records the complete lifecycle of an experiment:
experiment
├── run → failed
├── change/fix → Git diff or commit
├── retry → completed
├── metrics and artifacts
└── conclusion
It is not tied to machine learning. It can register Python scripts, C++ benchmarks, shell commands, ROS launches, Docker containers, simulations, network tests, hardware measurements, and manual physical experiments.
Why expctl?
A Git commit tells you which code existed. It does not tell you:
- which command was executed;
- whether the process failed or was interrupted;
- what changed before the retry;
- which metrics and artifacts were produced;
- whether expected outputs were actually created;
- what conclusion was drawn from the result.
expctl stores that missing experiment history while using Git when it is available.
Core principles
- Universal: the core does not know what YOLO, PyTorch, ROS, or LiDAR are.
- Local-first: SQLite and human-readable JSON/Markdown records work offline.
- Git-aware, not Git-dependent: Git snapshots are captured when available.
- Immutable run history: retries and resumes create new linked runs.
- Validated success: exit code
0is not sufficient when required outputs are configured. - Portable: the first version needs no server or cloud account.
Installation
From PyPI
After the package is published:
pipx install expctl
or:
python3 -m pip install expctl
Directly from GitHub
pipx install git+https://github.com/CactysFedya/expctl.git
Development installation
git clone https://github.com/CactysFedya/expctl.git
cd expctl
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -e ".[dev]"
pytest
expctl --version
Expected:
6 passed
expctl 0.2.0
Quick start
Initialize expctl inside any project:
expctl init --name my-project
Create an experiment:
expctl new \
--name generic-baseline \
--goal "Check the baseline implementation" \
--hypothesis "The command produces a valid result" \
--command "python3 examples/dummy_experiment.py --steps 5 --fail-at 3 --output demo_output" \
--metrics "demo_output/metrics.json" \
--required-file "demo_output/result.artifact" \
--required-metric score \
--tag demo
Run the current experiment:
expctl run
After fixing code or configuration:
expctl fix RUN-... \
-m "Removed the intentional failure" \
--commit
Retry with a corrected command:
expctl retry RUN-... -- \
python3 examples/dummy_experiment.py \
--steps 5 \
--output demo_output
Validate, record an artifact, and conclude:
expctl validate RUN-...
expctl result RUN-... \
--metric duration=2.4:s \
--artifact demo_output/result.artifact \
--copy
expctl conclude EXP-... \
-m "The corrected implementation passed validation" \
--complete \
--outcome accepted
Manual physical experiment
Git and a child command are optional:
mkdir lidar-range-test && cd lidar-range-test
expctl init --name lidar-range-test
expctl new \
--name outdoor-range \
--goal "Measure maximum stable range" \
--required-metric max_range \
--tag hardware
expctl run --manual
Record the result later:
expctl result RUN-... \
--mark-completed \
--metric max_range=42.7:m \
--metric packet_loss=0.8:percent \
--artifact measurement.txt \
--copy
Validation model
exit code != 0
→ failed
exit code = 0, but a required file or metric is missing
→ incomplete
exit code = 0 and all configured checks pass
→ completed
Commands in 0.2.0
| Area | Commands |
|---|---|
| Project | init, config, doctor, status |
| Experiments | new, use, list, show, log, derive, archive |
| Runs | run, retry, resume, cancel, recover |
| Changes | change, fix, diff |
| Results | result, validate, conclude |
Generic change types
fix, code, config, parameter, data,
environment, hardware, procedure, other
Examples:
expctl change RUN-... --type parameter \
-m "Changed input size from 512 to 640"
expctl change RUN-... --type hardware \
-m "Moved the test from Raspberry Pi to workstation"
expctl fix RUN-... \
-m "Fixed an incorrect buffer boundary" \
--commit
Generated data
project/
├── .expctl.toml
├── .expctl/
│ ├── expctl.db
│ └── current
└── experiments/
├── README.md
└── 20260723-example-001/
├── README.md
├── experiment.json
├── changes/
└── runs/
└── RUN-.../
├── run.json
├── command.sh
├── environment.json
├── git.diff
├── stdout.log
├── stderr.log
└── artifacts/
Demo
Run the complete failure/fix/retry workflow:
make demo
More details: docs/demo.md.
Security note
expctl executes commands supplied by the user and may copy artifacts. Do not run experiment definitions from untrusted repositories without reviewing the command and file paths first. See SECURITY.md.
Project status
0.2.0 is an alpha release intended for local experimentation and feedback. Database compatibility is not guaranteed until the first stable release.
Contributing
Bug reports, design proposals, documentation improvements, and pull requests are welcome. Read CONTRIBUTING.md before submitting changes.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file expctl-0.2.0.tar.gz.
File metadata
- Download URL: expctl-0.2.0.tar.gz
- Upload date:
- Size: 42.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
15c9e0e5822d643b2d8e02eace298b408c18c28e2925a07930a90fa951c5f499
|
|
| MD5 |
360da4e4beb40da78bb5cc1f7a413ffa
|
|
| BLAKE2b-256 |
e57fa275df0f00f7ed863a9b73774493b42d3a0edcf92e5aca704aadbad8d593
|
Provenance
The following attestation bundles were made for expctl-0.2.0.tar.gz:
Publisher:
publish-pypi.yml on CactysFedya/expctl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
expctl-0.2.0.tar.gz -
Subject digest:
15c9e0e5822d643b2d8e02eace298b408c18c28e2925a07930a90fa951c5f499 - Sigstore transparency entry: 2225090265
- Sigstore integration time:
-
Permalink:
CactysFedya/expctl@7a14a1d3dd52fe97a571d6413f2cd9034793e864 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/CactysFedya
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@7a14a1d3dd52fe97a571d6413f2cd9034793e864 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file expctl-0.2.0-py3-none-any.whl.
File metadata
- Download URL: expctl-0.2.0-py3-none-any.whl
- Upload date:
- Size: 35.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
178cce58b58b821458c41aae615e6946ffdf208e893612f7deae7b3fa815101d
|
|
| MD5 |
a01e8614e79a85cbf614d5509205bb5a
|
|
| BLAKE2b-256 |
4fb4c4a122295676a009d6967bfe11d18156d56d17b0aa1ce2d573217b81b69a
|
Provenance
The following attestation bundles were made for expctl-0.2.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on CactysFedya/expctl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
expctl-0.2.0-py3-none-any.whl -
Subject digest:
178cce58b58b821458c41aae615e6946ffdf208e893612f7deae7b3fa815101d - Sigstore transparency entry: 2225090577
- Sigstore integration time:
-
Permalink:
CactysFedya/expctl@7a14a1d3dd52fe97a571d6413f2cd9034793e864 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/CactysFedya
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@7a14a1d3dd52fe97a571d6413f2cd9034793e864 -
Trigger Event:
workflow_dispatch
-
Statement type: