Skip to main content

A terminal tool for managing experimental Python projects

Project description

novo

novo

Spin up Python experiments in seconds, from anywhere in your terminal.

Install once with uv, then use novo from any directory to scaffold isolated, git-tracked Python experiments from reusable seed templates — drive it from the command line or a built-in TUI.

PyPI Python 3.11+ Built with uv License: MIT Changelog


Install

novo is distributed as a uv tool — installed once into an isolated environment and exposed on your PATH.

Prerequisite: uv and git.

# from PyPI
uv tool install novo

# or from this repo
uv tool install git+https://github.com/fmeiraf/novo

If novo isn't found after install, run uv tool update-shell once to add ~/.local/bin to your PATH.

novo --version

That's it — the novo source tree is no longer needed. Use novo from any directory.


First steps

1. Pick a workspace (optional)

novo stores experiments in a workspace — a directory marked with .novo/ (same pattern as git's .git/). You don't have to set one up to get started: novo new works from any directory.

When you run any novo command, it resolves the active workspace in this order:

  1. cwd discovery — walks up from the current directory looking for a .novo/ marker. First hit wins.
  2. configured home — if nothing is found, falls back to workspace.path in ~/.config/novo/config.toml (only if you've set it).
  3. auto-detached — if neither resolves, novo runs in detached mode against the current directory. novo new drops a self-contained experiment in cwd; read-only commands refuse with a novo init tip.

So novo new churn-analysis in a brand-new terminal just works — the experiment lands right next to you, as its own self-contained project. novo prints a one-line hint the first time so you know what happened:

ℹ no workspace found here — running in detached mode (run `novo init` to set up a workspace).

Want experiments to live in a specific tree (e.g. one per project)? Turn any directory into a workspace:

cd ~/code/experiments
novo init

novo init writes the .novo/ marker and initializes the directory as a git repo. From then on, every novo command run inside that tree (or any subdirectory) operates on that workspace.

You can also point at a workspace explicitly with --workspace <path> or NOVO_WORKSPACE=<path>, or force detached mode with --detached (see Detached mode).

2. Create your first experiment

novo new image-classifier --tag ml --desc "ResNet experiments"
novo new quick-spike --no-date                # skip the YYYY-MM-DD- prefix
novo new pinned-run --python 3.12 --seed user:data-science

novo creates a date-prefixed directory inside your workspace, sets up a uv project, applies the default seed, and commits the result. Pass --no-date (per experiment) or novo config set naming.date_prefix false (globally) to drop the date prefix; pass --python <ver> to override defaults.python.

3. Set up a seed (optional)

Seeds are reusable project templates — your favorite stack, scripts, and config files copied into every new experiment.

novo seed list                                          # see what's available
novo seed init data-science                             # scaffold a new empty seed
novo seed init data-science --desc "Numpy + pandas"     # with a description
novo seed init data-science --scope user                # force user scope
novo seed init data-science --path ./my-registry/etl    # write the seed to a custom dir

By default seed init writes to the workspace's .novo/seeds/ when cwd is inside a workspace, otherwise to ~/.local/share/novo/seeds/. Use --scope local/--scope user to force one, or --path <dir> to drop the seed anywhere on disk — that last form is what you want when authoring a remote registry.

Without --path, seed init data-science creates ~/.local/share/novo/seeds/data-science/ with a seed.toml manifest and a template/ directory. Edit the manifest to declare dependencies and post-create hooks:

[seed]
name = "data-science"
description = "Numpy + pandas + jupyter starter"

[seed.dependencies]
packages = ["numpy", "pandas", "jupyter"]

[seed.post_create]
commands = ["mkdir notebooks"]

Drop starter files into template/ — they'll be copied into every new experiment that uses the seed:

novo new churn-analysis --seed data-science

4. Use seeds from a team registry (optional)

A remote seed registry is a git repo whose top-level subdirectories each contain a seed. One repo can hold many seeds:

team-seeds/
├── etl/
│   ├── seed.toml
│   └── template/
├── ml-experiment/
│   ├── seed.toml
│   └── template/
└── ...

Link it once with a local name:

novo seed link git@github.com:team/novo-seeds.git --name team

Every seed in the registry is now available as remote:team/<seed-name>:

novo new pricing-model --seed remote:team/ml-experiment
novo seed sync team        # pull updates later
novo seed unlink team      # drop the link

Authoring your own registry

A registry is just a git repo whose top-level subdirectories each contain a seed (one seed.toml per subdir). Use seed init --path to scaffold seeds directly into the repo — no need to bounce through user or local scope first:

git clone git@github.com:team/novo-seeds.git
cd novo-seeds

novo seed init etl            --path ./etl            --desc "Standard ETL stack"
novo seed init ml-experiment  --path ./ml-experiment  --desc "Pytorch + MLflow starter"

# edit the generated seed.toml + template/ in each directory, then commit + push
git add . && git commit -m "add etl + ml-experiment seeds" && git push

Anyone who's already linked the registry can pull your additions with novo seed sync <name> (or r in the TUI's Seeds tab).

See docs/seeds.md for scope resolution, identifier syntax, and the full registry layout.


Configuration

novo works out of the box, but a handful of settings let you tailor the defaults. They live in a single TOML file at ~/.config/novo/config.toml (XDG-compliant; the exact path depends on your OS).

Key Type Default What it does
workspace.path str "" (none) Optional "home" workspace used when cwd discovery finds nothing. Empty = no home workspace; commands auto-detach in cwd. Set explicitly with novo config set workspace.path <path>.
defaults.seed str "default" Seed used when novo new is called without --seed. Accepts scoped forms (e.g. "user:data-science", "remote:team/etl").
defaults.python str "" (system) Python version passed to uv init for new experiments (e.g. "3.12"). Override per-experiment with --python.
defaults.auto_commit bool true Auto-commit the workspace on novo new / novo delete.
defaults.detached_git bool true In detached mode, init a git repo inside each created experiment and commit.
naming.date_prefix bool true Prefix experiment directories with today's date (2026-05-06-foo). Skip per-experiment with --no-date.
[[seeds.remotes]] table-list [] Linked remote seed registries. Managed via novo seed link / unlink. See docs/seeds.md for registry layout.

Inspecting and changing settings

novo config show                          # table of all keys
novo config get defaults.python           # read one key
novo config set defaults.python 3.12      # write one key
novo config set naming.date_prefix false  # accepts true/false, yes/no, 1/0, on/off

You can also edit ~/.config/novo/config.toml directly:

[workspace]
path = "/Users/me/code/experiments"

[defaults]
seed = "user:data-science"
python = "3.12"
auto_commit = true
detached_git = true

[naming]
date_prefix = true

[[seeds.remotes]]
name = "team"
url = "git@github.com:team/novo-seeds.git"
ref = ""   # empty = follow the cloned branch

Pinning a Python version

A seed itself can't declare a Python version today — it's resolved per-experiment in this order:

  1. --python flag on novo new
  2. defaults.python in config.toml
  3. Whatever uv picks as the system default

So to make every new experiment use 3.12 by default:

novo config set defaults.python 3.12
novo new quick-test          # uses 3.12
novo new legacy --python 3.10  # one-off override

The TUI

Run novo with no arguments to launch the interactive terminal UI — a Textual app for browsing, searching, and managing experiments without memorizing flags.

 ┌─ Experiments ─────────────┐ ┌─ Details ──────────────────┐
 │  > 2026-04-21-transformer  │ │  Name: transformer-exp     │
 │    2026-04-20-image-cls    │ │  Seed: ml-stack            │
 │    2026-04-19-data-clean   │ │  Tags: nlp, pytorch        │
 └────────────────────────────┘ └────────────────────────────┘
  n new  d delete  s seeds  / search  Enter open  ? help  q quit
Key Action
n Create new experiment
d Delete selected
s Switch to Seeds tab
e Switch to Experiments tab
/ Search
Enter Open experiment in a new terminal window
j / k Navigate (vim-style)
? Help
q Quit

Extra bindings on the Seeds tab: N scaffold seed, l link remote registry, u unlink, r sync remotes, t focus the template tree.

See docs/tui.md for the full screen and widget breakdown.


Detached mode

Sometimes you don't want a workspace at all — just a one-off, self-contained experiment wherever you happen to be standing. That's what --detached is for.

cd ~/scratch
novo --detached new spike-idea

What you get:

  • A fresh project directory at ~/scratch/spike-idea/ with its own uv env and seed-applied files.
  • Its own git repo + initial commit (when defaults.detached_git = true, the default).
  • No registration anywhere — novo's workspace listing won't track it. novo list / info / search / delete / open deliberately refuse under --detached to keep the boundary clear.

Use --at <path> to drop the experiment somewhere other than cwd:

novo --detached new spike-idea --at ~/tmp

Running novo --detached with no subcommand launches a minimal TUI landing screen with the actions that make sense outside a workspace: new experiment here, link a remote registry, init a workspace here, browse seeds (CLI hint).

Drop --detached (or run novo init once in that directory) when you want to go back to workspace mode.


Everyday commands

novo                                       # launch the interactive TUI
novo new <name>                            # create an experiment
novo list                                  # list experiments
novo info [name]                           # workspace summary, or details for one experiment
novo search <query>                        # fuzzy search across name, description, tags
novo open <name>                           # cd into an experiment (needs shell integration)
novo delete <name>
novo init [path]                           # write a `.novo/` marker at path (default cwd)
novo seed list | init <name>               # browse / scaffold seeds
novo seed link <url>                       # link a remote seed registry (idempotent)
novo seed sync [<name>] | unlink <name>    # refresh or remove a linked remote
novo config show | get <key> | set <key> <value>
novo --workspace <path> <cmd>              # operate on a specific workspace
novo --detached new <name>                 # create a self-contained experiment in cwd

For novo open to actually cd, add this to your ~/.zshrc / ~/.bashrc:

eval "$(novo --shell-init)"

Updating & uninstalling

uv tool upgrade novo
uv tool uninstall novo

Troubleshooting

Common gotchas, especially when upgrading from older versions of novo.

Every novo run lands experiments in the same old workspace

Symptom. You run novo (or novo new …) from a directory that has no .novo/ marker, but every experiment ends up inside one specific old workspace tree.

Cause. Pre-0.2.0 novo init silently wrote the target path into the global config as workspace.path. Newer versions don't touch the config on init (they only write the per-directory .novo/ marker), but they still honor a previously-written workspace.path as the fallback workspace whenever cwd walk-up finds no marker — so an init you ran months ago can still steer every run.

Fix. Clear the pin:

novo config set workspace.path ""

After that, novo from a non-workspace cwd auto-detaches the way 0.2.0+ intends (the TUI lands on the minimal DetachedScreen, and novo new … drops a self-contained experiment in cwd).

I can't find my config file at ~/.config/novo/config.toml

Cause. platformdirs resolves config paths per-OS:

OS Config file path
Linux ~/.config/novo/config.toml
macOS ~/Library/Application Support/novo/config.toml
Windows %APPDATA%\novo\config.toml

Fix. Don't edit the file directly — use novo config show / get / set so you don't have to think about the path. The README's TOML snippets show the schema; the actual file lives wherever your platform stores config.

novo: command not found after install

Fix. Add ~/.local/bin to your PATH and reopen the terminal:

uv tool update-shell

uv tool upgrade novo says nothing changed but I'm still on an old version

Fix. Force a clean reinstall:

uv tool install --reinstall novo

This invalidates uv's wheel cache; --force alone doesn't.

novo open <name> opens a new shell instead of cd-ing me into the experiment

Cause. The shell-integration eval isn't installed.

Fix. Add this to your ~/.zshrc or ~/.bashrc, then reopen the shell:

eval "$(novo --shell-init)"

Documentation

Deeper docs live in docs/:

Document Description
architecture.md Layers, data flow, project structure
cli.md All CLI commands and flags
tui.md Textual app, screens, keybindings
core.md Experiment, seed, config, workspace, remote, git logic
seeds.md Seed scopes, identifier syntax, remote sync workflow
models.md Pydantic schemas
development.md Local dev setup
testing.md Test layout and conventions

Development

Work on novo itself with an editable install so changes are picked up immediately:

git clone https://github.com/fmeiraf/novo
cd novo
uv tool install --editable .
uv run pytest

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

novo-0.2.2.tar.gz (42.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

novo-0.2.2-py3-none-any.whl (62.0 kB view details)

Uploaded Python 3

File details

Details for the file novo-0.2.2.tar.gz.

File metadata

  • Download URL: novo-0.2.2.tar.gz
  • Upload date:
  • Size: 42.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for novo-0.2.2.tar.gz
Algorithm Hash digest
SHA256 a1bdee8fe328de76bc66b9f2a48d00db3735a8a30f0010bf6772416d1e763720
MD5 e82fc10a9703d009e169ed4b9e16367b
BLAKE2b-256 ac5db6b46eba7ba3bdb1c0909241332bb05062d1b9aaaf01fd51393099a81eba

See more details on using hashes here.

Provenance

The following attestation bundles were made for novo-0.2.2.tar.gz:

Publisher: pypi_workflow.yml on fmeiraf/novo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file novo-0.2.2-py3-none-any.whl.

File metadata

  • Download URL: novo-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 62.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for novo-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 ded2658a8f7dab806236aa99c3d7fae23706ce969627a9b8219c2fdfced7d482
MD5 9af7407bfac22236b6f5f715bb4cc500
BLAKE2b-256 70ea0495a9bfe208df7167903e92f826573264693fd31240c3ed966fa37c64ec

See more details on using hashes here.

Provenance

The following attestation bundles were made for novo-0.2.2-py3-none-any.whl:

Publisher: pypi_workflow.yml on fmeiraf/novo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page