Contributing To Protostar¶
Architecture & Implementation Rules¶
Protostar has one job: save you time on setup you would have done anyway. When evaluating a new feature, ask:
- Would most users want this, or just some?
- Would you plausibly revert this manually after running the tool?
If the answer to either of the first two is "maybe not", the feature probably doesn't belong in the tool.
1. Manifest-first, side-effects-last¶
Modules declare intent into the manifest during build(). The orchestrator executes all side effects afterward in a single, ordered phase. Never call subprocess.run or write to disk inside a module's build() method.
2. The Engine Bulkhead (State vs. Execution)¶
Protostar's core engine (Orchestrator, SystemExecutor, BootstrapModule) is strictly headless and deterministic:
- Decoupled Lifecycle (
planvs.execute): The orchestrator separates state calculation (plan() -> EnvironmentManifest) from disk mutation (execute(manifest) -> ExecutionResult).plan()must remain mathematically pure and side-effect free—never mutating disk state or executing subprocesses. - Strict UI Separation: Engine modules must never import or instantiate terminal UI libraries (
rich.console.Console,questionary), invoke interactive prompts, or make terminal-interactive decisions. All interactive wizards, collision strategy prompts (Merge,Overwrite,Abort), remote template trust dialogs, and progress spinners belong exclusively in the CLI layer (cli.py). - Structured Boundary Communication: The CLI passes caller intent into the engine via immutable
InitRequestobjects and receives execution outcomes viaExecutionResult. Collision states are signaled by raisingWorkspaceCollisionError(paths=...), allowing caller interfaces to decide how to handle conflicts (e.g., interactive prompt vs. CI failure). - Headless Logging: Engine components emit progress updates via
logging.getLogger("protostar").info(...). The CLI layer captures these messages via custom handlers (such asSpinnerHandler) to render rich terminal spinners.
3. Fail loud, fail early¶
All system dependency checks happen in pre_flight(), before the manifest is built and before anything is written. If a preflight check fails, the environment is untouched. This is a guarantee, not a coincidence.
4. Non-destructive by default¶
Protostar never overwrites existing work. .gitignore entries are appended and deduplicated. IDE settings are merged. It must be safe to run against a repo that is already partially configured.
5. Modules are composable, not coupled¶
A module only interacts with the manifest interface. It must not inspect what other modules are loaded, assume a particular run order, or conditionally change behaviour based on the presence of sibling modules.
6. Presets are Independent Pipeline Injections¶
Presets inherit from the PresetModule abstract base class and evaluate independently during the manifest aggregation phase. They do not override language modules; they strictly append domain-specific dependencies and directory scaffolding to the EnvironmentManifest.
7. Structural Error Handling Paradigm¶
To guarantee that the workspace remains deterministic, error management follows a strict type verification structure:
- Never Raise Coarse Exceptions: Do not raise bare
RuntimeError,ValueError, orOSErrorinstances inside pipeline operations. Always throw a specific, domain-modeled subclass ofProtostarErrordefined inprotostar.errors: ConfigurationError: For invalid/malformed configuration files or invalid CLI configuration options.NetworkFetchError: For remote template downloads, network timeouts, or insecure protocol violations.TemplateResolutionError: For template archive extraction failures, unsupported formats, or missing template variables.WorkspaceCollisionError: For detected collisions with existing workspace configuration markers duringplan().MissingDependencyError: For pre-flight binary checks when required system tools are absent.CommandExecutionError: For non-zero return codes from managed subprocesses.CommandTimeoutError: For subprocesses exceeding allocated runtime limits.FileSystemError: For local disk I/O, file writing, or directory creation failures.SecurityViolationError: For unauthorized path traversal attempts (e.g. Zip Slip).ExecutionAbortedError: For explicit cancellations during interactive wizard prompts.PartialExecutionAbortedError: For interruptions occurring mid-execution after disk mutations have begun (stores immutablefrozenset[str]of touched paths).- Respect POSIX Exit Code Mappings:
| Exit Code | POSIX Name | Exception Class | Trigger Condition |
|---|---|---|---|
0 |
EX_OK |
None | Successful execution |
1 |
Generic Exit | CommandExecutionErrorCommandTimeoutError |
Subprocess failure or command timeout |
64 |
os.EX_USAGE |
InvalidUsageError |
Invalid CLI arguments or command usage syntax |
65 |
os.EX_DATAERR |
TemplateResolutionError |
Template resolution error (corrupted archive, missing variables) |
69 |
os.EX_UNAVAILABLE |
MissingDependencyError |
Missing required system binary (uv, git, etc.) |
70 |
os.EX_SOFTWARE |
(Unhandled exception) | Unhandled internal Python bug (prompts automated bug report) |
74 |
os.EX_IOERR |
FileSystemError |
Local filesystem read/write or permission failure |
75 |
os.EX_TEMPFAIL |
NetworkFetchError |
Transient network failure during remote template download |
77 |
os.EX_NOPERM |
SecurityViolationError |
Security violation (e.g., path traversal Zip Slip) |
78 |
os.EX_CONFIG |
ConfigurationError |
Invalid TOML syntax or conflicting CLI configuration |
130 |
Shell Signal | ExecutionAbortedError |
You aborted interactive wizard prompt (Ctrl+C) |
- Enforce Cause Chains: When wrapping secondary background subprocess tracking or physical system calls, always retain stack telemetry history using the
raise NewException(...) from esyntax. - Isolate Actionable Hints: Keep description fields focused on what broke. Place direct system installation fix guidelines or instructions inside the decoupled
hintkeyword configuration parameter so they can be parsed and formatted cleanly on their own visual tier in the terminal.
8. Machine & Agent Interface Invariants (--json & --dry-run)¶
Protostar exposes an experimental machine-readable CLI interface for AI agents, automation pipelines, and external developer tools. Any new subcommands, flags, or error paths must preserve the following operational invariants:
- Strict
stdoutPurity:stdoutis strictly reserved for the machine-readable JSON payload. All human-readable logging, diagnostic summaries, progress spinners, and Rich tracebacks must route exclusively tostderr(e.g., via_stderr_consoleor logging handlers). An automated consumer must always be able to parsestdoutdirectly as valid JSON. - Position-Independent Flag Evaluation: The
--jsonflag is position-independent and must be recognized globally across all commands, subparsers, and bare invocations. - Zero Interactive Trapping in Machine Mode: When
is_json_modeis active, the CLI must never block on terminal-interactive prompts (such asquestionarywizards, collision resolution prompts, or external template trust dialogs). Instead, the CLI must either bypass the prompt deterministically (if explicit override flags like--force-mergeare present) or raise a domain exception immediately so that a structured JSON error envelope is returned. - Deterministic State Serialization (
.to_dict()): All manifest domain slices and execution models exposed to agents must implement deterministic.to_dict()methods: - Mathematical sets (such as
directories,vcs_ignores,workspace_hides) must serialize to alphabetically sorted lists. - Insertion-ordered lists (such as
dependencies,dev_dependencies,system_tasks) must preserve their exact declaration order. - Enums (such as
CollisionStrategy) must serialize as their string.value. - File system paths must be normalized to POSIX string format.
- Protocol Envelopes & API Versioning: All JSON outputs must be wrapped in standard envelopes (
planned,success, orerror) and include the top-level"api_version"key (CLI_API_VERSION = 0during experimental phase) to maintain forward-compatible schema evolution.
Coding Standards¶
-
Type Hinting: All new application functions and methods must include strict Python 3.12 type hints. We use
mypyto statically enforce this (strict = true). The test suite (tests/*) is granted an exemption from strict untyped definition checks. -
Docstrings: Use Google-style docstrings for public functions, classes, and methods. Module-level, package-level, and
__init__docstrings are exempt from linting checks. -
Formatting & Linting: Code is formatted and linted using
ruff.- Use 4-space indentation and double quotes.
- The formatter enforces an 88-character line length.
- Do not bypass the prek hooks, as they will automatically apply the required
isortblock ordering and formatting rules.
Testing Guidelines¶
Because Protostar is a scaffolding tool, its execution inherently interacts with the host filesystem and shell. To maintain a deterministic and isolated test suite:
-
Relaxed Linting: The test suite (
tests/*) is exempt from docstring requirements andprintstatement linting restrictions (T201). -
Disk I/O: Never write to the actual host filesystem during tests. Always use the
pytesttmp_pathfixture to sandbox generated artifacts. -
Subprocesses: Use
pytest-mockto patchsubprocess.run. Do not allow the test suite to execute unmocked shell commands (e.g.,uv initorcargo init) on the host machine. -
Coverage: Ensure new modules or generators maintain or improve the current test coverage metrics (measured via
pytest-cov).
How to Contribute¶
Reporting Bugs¶
- Check if the issue has already been reported.
- Open a new issue with a clear title and description.
- Include the command that caused the error and the resulting traceback.
Development Setup¶
To contribute to this project, you will need the following system-level dependencies installed:
(If you are on macOS with Homebrew: brew install uv just)
-
Fork & Clone
Fork the repo and clone it locally:
-
Environment Setup
-
Install Hooks
Set up prek hooks to handle linting and type checking automatically.
Running Tests & Tooling¶
We use just as our command runner to standardize test execution, linting, and formatting.
To see all available commands and their descriptions, run just in the repository root:
To execute the standard test matrix:
Isolated Manual Testing (Sandboxes)¶
To manually test Protostar in an isolated workspace without modifying your global ~/.config/protostar or host git configuration:
-
macOS Sandbox: Drops into an ephemeral sub-shell in
/tmpwhereprotostaris built fresh from the working tree and$HOMEis sandboxed: -
Linux Sandbox (OrbStack / Docker): Runs inside a clean, disposable Debian container pre-loaded with required system binaries (
direnv,markdownlint-cli2) and inspection tools (eza,bat,ripgrep):
Pull Requests¶
-
Create a Branch
-
Make Changes
Write your code. Ensure your changes are tightly scoped to a single feature, preset, or bug fix. Avoid monolithic pull requests that mix refactoring with new logic.
-
Verify
Ensure your code passes the linter, type checker, and test suite locally. We provide a single command that emulates the GitHub Actions CI pipeline. Run this before pushing:
(Prek will also run
ruffandmypywhen you commit). -
Commit & Push
Use clear, descriptive commit messages.
-
Open a Pull Request
Submit your PR against the
mainbranch.