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 experiments, immutable runs, changes, failures, retries, arbitrary results, artifacts, observations, and conclusions. It works for software, hardware, scientific, and manual experiments without embedding assumptions about a particular domain.
EXP-... experiment
├── RUN-001 failed
│ └── CHG-001 fix
│ └── RUN-002 retry completed
├── RUN-003 completed · BEST
├── notes and next actions
└── derived experiments
Why expctl?
A Git commit tells you which code existed. It does not tell you:
- which command or physical procedure was executed;
- whether the run failed, was interrupted, or produced incomplete outputs;
- which change led to a retry;
- which metrics, typed conditions, and artifacts were recorded;
- which result is currently considered best;
- what should happen next;
- whether the experiment can be transferred to another project.
expctl stores that missing experiment history while using Git when it is available.
Principles
- Universal: no required ML, robotics, or scientific vocabulary.
- Local-first: SQLite plus human-readable JSON and Markdown records.
- Git-aware, not Git-dependent: software and manual experiments use the same core.
- Immutable runs: retry and resume create linked records rather than rewriting history.
- Validated success: exit code
0can still becomeincompletewhen required outputs are missing. - Extensible metadata: free-form tags, arbitrary metrics, and typed custom fields.
- Portable: experiments can be exported and imported as ZIP bundles.
Installation
For command-line applications, pipx is recommended:
pipx install expctl
Using pip is also supported:
python3 -m pip install expctl
Check the installation:
expctl --version
# expctl 0.3.0
Quick start
mkdir my-experiment && cd my-experiment
expctl init --name my-project
expctl new \
--name baseline \
--goal "Check the current implementation" \
--command "python3 task.py" \
--metrics "out/metrics.json" \
--required-file "out/result.bin" \
--required-metric score \
--tag baseline
expctl run
When code changes after a failed run:
expctl fix FAILED \
-m "Fixed incorrect boundary handling" \
--commit
expctl retry FAILED
Navigation and history
Built-in references are resolved inside the current experiment:
CURRENT_EXP current selected experiment
LATEST latest run of CURRENT_EXP
FAILED latest unsuccessful run of CURRENT_EXP
BEST selected best run of CURRENT_EXP
Custom aliases are supported:
expctl alias set baseline LATEST
expctl show baseline
Display a Git-inspired tree:
expctl log CURRENT_EXP --graph --metric score
Example:
EXP-20260723-001 baseline [completed] · BEST=RUN-20260723-002
└── RUN-20260723-001 run [failed]
└── CHG-20260723-001 fix Fixed boundary handling
└── RUN-20260723-002 retry [completed] · score=0.91
Compare and select results
expctl compare RUN-001 RUN-002 RUN-003 \
--metric score \
--metric latency \
--field device
Choose the best run manually:
expctl best set RUN-003 -m "Best balance of quality and latency"
Or use one explicit numeric rule:
expctl best select CURRENT_EXP --metric latency --minimize
Tags and typed fields
Tags remain free-form:
expctl tag add LATEST verified
expctl tag add CURRENT_EXP outdoor
Typed custom fields avoid domain-specific columns in the core:
expctl field set LATEST device "Raspberry Pi 5" --type string
expctl field set LATEST temperature 71.5 --type number --unit C
expctl field set CURRENT_EXP controlled true --type boolean
expctl field set CURRENT_EXP started 2026-07-23T14:30:00 --type datetime
Supported field types:
string, number, integer, boolean, datetime, path, json
Observations and next actions
Timestamped observations do not alter run status:
expctl note LATEST "Vibration appeared after five minutes" --kind observation
Minimal next actions stay intentionally smaller than a task manager:
expctl next add LATEST "Repeat outdoors"
expctl next list CURRENT_EXP
expctl next done ACT-...
Export and import
Export metadata, logs, patches, and records:
expctl export CURRENT_EXP --output experiment.zip
Include copied artifact contents:
expctl export CURRENT_EXP \
--output experiment-with-artifacts.zip \
--include-artifacts
Import into another initialized project:
expctl init --name target-project
expctl import experiment-with-artifacts.zip
Imports preserve experiment, run, and change IDs and therefore reject collisions.
Manual physical experiment
Git and a child command are optional:
expctl new \
--name outdoor-range \
--goal "Measure maximum stable range" \
--required-metric max_range \
--tag hardware
expctl run --manual
expctl result LATEST \
--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.3.0
| Area | Commands |
|---|---|
| Project | init, config, doctor, status |
| Experiments | new, use, list, show, log, derive, archive |
| Runs | run, retry, resume, cancel, recover, compare |
| Changes | change, fix, diff |
| Results | result, validate, conclude, best |
| Metadata | tag, field, alias, note, next |
| Portability | export, import |
Development
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
make demo
Expected for 0.3.0:
9 passed
expctl 0.3.0
Database upgrade
Opening a 0.2.0 project with 0.3.0 automatically migrates the local database from schema version 2 to 3. Back up .expctl/expctl.db before upgrading important projects. Downgrading the migrated database to 0.2.0 is not supported. See docs/UPGRADING.md.
Security
expctl executes commands supplied by the user and can copy files into experiment bundles. Review commands and paths from untrusted projects before running or importing them. See SECURITY.md.
Project status
0.3.0 is alpha software. The data model and CLI may evolve before 1.0.0.
Contributing
Bug reports, design proposals, documentation improvements, and pull requests are welcome. Read CONTRIBUTING.md.
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.3.0.tar.gz.
File metadata
- Download URL: expctl-0.3.0.tar.gz
- Upload date:
- Size: 57.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bdbb697c6c4024dd13a7d71e04ff0a2ce64cb47ff465d21df14596734cf0d451
|
|
| MD5 |
a2237181d0eb0e1b31e1c3d14972e94c
|
|
| BLAKE2b-256 |
b0634d64ae981fc97dd161919f33598c2139319e4b9120ec45ef70d1253191b9
|
Provenance
The following attestation bundles were made for expctl-0.3.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.3.0.tar.gz -
Subject digest:
bdbb697c6c4024dd13a7d71e04ff0a2ce64cb47ff465d21df14596734cf0d451 - Sigstore transparency entry: 2225496817
- Sigstore integration time:
-
Permalink:
CactysFedya/expctl@5214d6a545cc6b3e3604fc10395c5a4a4cec35e3 -
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@5214d6a545cc6b3e3604fc10395c5a4a4cec35e3 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file expctl-0.3.0-py3-none-any.whl.
File metadata
- Download URL: expctl-0.3.0-py3-none-any.whl
- Upload date:
- Size: 47.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 |
f4b5e0eaf4e73fbf2cc3836dd850dce2edfbb2ed2e72592ebf16dde298f17ced
|
|
| MD5 |
d32f9e5718bf860d2259b3cea049d8d8
|
|
| BLAKE2b-256 |
d9dc58104f05762c97e1fca48d47546640830a7c5aa6d9ef15c1f6d843c4a10d
|
Provenance
The following attestation bundles were made for expctl-0.3.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.3.0-py3-none-any.whl -
Subject digest:
f4b5e0eaf4e73fbf2cc3836dd850dce2edfbb2ed2e72592ebf16dde298f17ced - Sigstore transparency entry: 2225496980
- Sigstore integration time:
-
Permalink:
CactysFedya/expctl@5214d6a545cc6b3e3604fc10395c5a4a4cec35e3 -
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@5214d6a545cc6b3e3604fc10395c5a4a4cec35e3 -
Trigger Event:
workflow_dispatch
-
Statement type: