Skip to content

Blueprints: custom YAML specs, callbacks, and self-contained single-file builds - #56

Merged
jaykv merged 7 commits into
mainfrom
feat/blueprints
Oct 12, 2026
Merged

jaykv merged 7 commits into
mainfrom
feat/blueprints

Conversation

@jaykv

@jaykv jaykv commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Stack 5 of 6 (more to follow). Base: feat/revive-ai-commands. Merge in order; each PR shows only its own changes.

Integration notes (this branch is merged on top of the fixes and AI PRs, and the conflicts were resolved in favour of keeping both): a failed build or a CLI that is not loaded exits 1; Click bool options are --flag/--no-flag switches rather than is_flag=True; callback and group help text goes through the same escaping as the fixes PR; Click command and callback params share one implementation. Where the description below differs, these notes win. Verified after the merge: ruff, mypy --strict and 481 tests pass on Python 3.11 and 3.15.

Summary

Finishes the blueprint feature and redoes it as a typed spec. A blueprint is a pydantic model for your own YAML schema plus a to_manifest() method. Because the result is an ordinary CLIManifest, both commanders, render, test, docs and build work unchanged on files written in a custom spec. cli build turns such a file into one executable that needs neither cliffy, the blueprint nor the YAML.

Stacks on chore/python-3.11-3.15.

What changed

Blueprints (cliffy/blueprint.py, transformer.py, cli.py)

  • Blueprint(BaseModel, ABC): the fields are the spec, to_manifest() returns a CLIManifest or a mapping. Optional hooks: pre_validate(raw), post_manifest(manifest), post_codegen(code). No class-level state and no prints. Unknown keys are rejected by default.
  • Selection: a top-level blueprint: key in the file, or --blueprint/-b on load, run, render, build, dev, test, validate and docs. The option wins. A path in the file is relative to the file; a path on the command line is relative to the working directory.
  • A reference is a .py file (tasks.py, tasks.py:Tasks), a module:Class, or the name of a plugin registered under the cliffy.blueprints entry-point group.
  • Errors are explicit for not found (lists the installed blueprints), ambiguous (lists the providing packages), no blueprint or several in a module, not a blueprint, and import failure. Spec validation errors are printed against the user's spec, one tools[1].parameters[0].type: ... line per problem.
  • The reference is stored in the CLI metadata, so cli update reuses it and cli info shows it.
  • New commands: cli blueprint list (alias cli blueprints) and cli blueprint schema REF [-o file], which prints the JSON schema of a spec for editor completion.

Callbacks (manifest.py, both commanders)

  • New manifest field callbacks: a list of {run, group, params, invoke_without_command}. A callback runs before the commands of the CLI or of one group (db, db.tables), its params become options of the CLI or group, and ctx is available in run.
  • Works for Typer and Click. --version and --aliases are kept (the WIP dropped them).
  • A callback whose group matches no command group is a validation error.
  • The maintainer's WIP examples/greet.yaml (callbacks only, no commands) runs unmodified on this branch.

Builds (builder.py)

  • The blueprint is applied at build time. The zipapp contains the generated module and its dependencies only.
  • Dependencies are installed with the running interpreter's pip in a subprocess. On failure the build prints pip's reason and writes no file.
  • The framework the generated code imports is bundled and pinned to the installed version unless requires names it. Click CLIs now get rich-click; the old code always bundled typer, even for use_click manifests.
  • Builds are reproducible: shiv --reproducible, --no-compile, and the generation timestamp is removed from the bundled module. Two builds of the same input are byte-identical (asserted in a test).
  • Interpreter: --python still sets the shebang. The build reports the supported Python range (>=3.X for pure-Python bundles, ==3.X.* when a compiled extension is bundled), and the bundled CLI exits with a readable message on an unsupported Python.
  • Still shiv zipapps; no native rewrite.

Examples and docs

  • examples/blueprints/tasks.py + tasks.yaml: a small task runner.
  • examples/blueprints/mcp_tools.py + text-tools.yaml: an MCP tool server spec. Each tool becomes a command, tools prints the definitions, and serve speaks MCP over stdio (JSON-RPC, no third-party dependency).
  • *.schema.json for both specs; a test checks they match the blueprints.
  • examples/callbacks.yaml, with manifest tests.
  • docs/blueprints.md and one nav line in mkdocs.yaml. Callbacks are documented at the end of that page, because it is the only docs file this MR may touch.
  • examples/cliffy_schema.json regenerated.

Breaking changes

  1. cliffy.blueprint.Blueprint API replaced. The hook-only base class (validate_hook, pre_process_hook, load_hook, Blueprint.create) is gone. It was never wired into the CLI on main.
  2. build_cli() and build_cli_from_manifest() return a BuildResult (exit_code, output, exception, path, requires_python) instead of a click.testing.Result. exit_code, output and exception keep their meaning.
  3. commands is no longer required in a manifest, so a callbacks-only CLI is valid. A manifest without commands used to fail validation.
  4. Click, explicit params: name: --verbose with short: -v used to render as '----verbose', '--v', which was unusable. It now renders correctly, and an explicit type: bool option is a flag (is_flag=True), as with Typer. Simple params (--flag: bool) are unchanged.
  5. Generated imports are sorted. Output is deterministic; the order differs from before.
  6. Built CLIs pin the framework version to the one installed next to cliffy unless requires names it.

Fixes outside the brief, needed to make the feature work

  • cli test on a use_click manifest crashed with AttributeError: 'RichGroup' object has no attribute '_add_completion', because typer's CliRunner rejects a plain click group. Tester now uses click's runner for click manifests.
  • The click option rendering in item 4 above.

How verified

macOS arm64, uv 0.13.0, one venv per Python, uv run --locked --all-extras --group docs.

Python ruff format --check ruff check mypy cliffy --strict mypy tests pytest
3.11.11 pass pass pass pass 259 passed
3.15.0 pass pass pass pass 259 passed
  • 71 new tests (188 -> 259). None skipped, xfailed or removed.
  • The end-to-end test builds examples/blueprints/text-tools.yaml, checks the archive contains no cliffy, pydantic or blueprint files, then runs the artifact with python -I from another directory with a stripped environment: tool commands, --version, and an MCP initialize / tools/list / tools/call session over stdin.
  • By hand: an artifact built on 3.11 ran on 3.15, and one built on 3.15 ran on 3.11. examples/click_hello.yaml and examples/callbacks.yaml build and run.
  • The callbacks commit (87a8f30) was also tested on its own on 3.11: 201 passed.
  • mkdocs build renders the new page.
  • Not run: Python 3.12-3.14, Linux, Windows, GitHub CI.

🤖 Generated with Claude Code

Summary by Sourcery

Enable custom YAML blueprints, manifest callbacks, and reproducible self-contained CLI builds across the command-line workflow.

New Features:

  • Add typed, extensible blueprints for custom YAML specifications with validation hooks, manifest conversion, plugin discovery, schema generation, and CLI integration.
  • Support manifest callbacks for CLI and command groups across both Typer and Click, including callback parameters and context-aware execution.
  • Build reproducible, self-contained zipapp executables from manifests or blueprint specifications with bundled dependencies and Python-version guards.
  • Provide task-runner and MCP tool-server blueprint examples with generated schemas.

Bug Fixes:

  • Fix Click CLI testing by selecting the appropriate Click runner for Click-generated manifests.
  • Correct Click option rendering for explicit boolean and short options.

Enhancements:

  • Persist blueprint references in loaded CLI metadata so updates and related commands reuse the selected blueprint.
  • Allow callbacks-only manifests and validate callback group references.
  • Return structured build results with dependency installation failures and artifact details instead of Click testing results.

Documentation:

  • Document blueprint authoring, selection, validation, hooks, schema generation, packaging, plugins, examples, and callbacks.
  • Add the blueprints page to the documentation navigation.

Tests:

  • Add comprehensive coverage for blueprint resolution, validation, hooks, plugins, CLI integration, callbacks, reproducible builds, Python compatibility, and self-contained artifacts.

Chores:

  • Regenerate the example manifest schema and add blueprint example specifications and schemas.

jaykv and others added 6 commits October 12, 2026 02:19
A blueprint is now a pydantic model for a custom YAML schema plus
to_manifest(), with pre_validate / post_manifest / post_codegen hooks.
References resolve from a .py file, a module:Class path or a
cliffy.blueprints entry point, with explicit not-found, ambiguous and
invalid-spec errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A callback runs before the commands of the CLI or of one command group,
can declare params that become options of the CLI or group, and gets the
context as `ctx`. The built-in --version and --aliases options are kept.

- `commands` is no longer required, so a callbacks-only CLI is valid.
- click: an explicit param no longer renders as `----name`, and an
  explicit bool option is a flag like it is with typer.
- `cli test` uses click's runner for `use_click` manifests; typer's
  runner rejects a plain click group.
- Generated imports are sorted, so the same manifest gives the same code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blueprints: a file selects its blueprint with a top-level `blueprint:`
key or with --blueprint on load, run, render, build, dev, test, validate
and docs. The reference is stored with a loaded CLI, so `cli update`
reuses it and `cli info` shows it. `cli blueprint list` (alias
`cli blueprints`) lists installed blueprints and `cli blueprint schema`
prints the JSON schema of a spec.

Builds: `cli build` applies the blueprint at build time and writes one
zipapp that needs neither cliffy nor the blueprint.
- Dependencies are installed with the running interpreter's pip in a
  subprocess; a failure reports pip's reason and writes no file.
- The framework the generated code imports (typer or rich-click) is
  pinned to the installed version unless `requires` names it. Click
  CLIs used to get typer bundled instead of rich-click.
- The archive is reproducible: fixed timestamps and no generation time
  in the bundled module.
- The build reports the Python versions the bundle supports, and the
  bundled CLI exits with a readable message on any other version.
- build_cli() returns a BuildResult instead of a click test Result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- examples/blueprints/tasks.py: a small task-runner spec.
- examples/blueprints/mcp_tools.py: an MCP tool server spec. Every tool
  becomes a command, and `serve` speaks MCP over stdio without extra
  dependencies.
- JSON schemas for both specs, checked against the blueprints in tests.
- Tests cover reference resolution (file, module:Class, entry point),
  not-found / ambiguous / invalid-spec errors, hooks, the CLI commands,
  and an end-to-end build that executes the artifact in a subprocess
  from another directory in isolated mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved so that the fixes of fix/audit-bugs-and-security and the
blueprint and callback features both survive:

- cliffy/builder.py: blueprints' single-file build (BuildResult, pinned
  framework, reproducible zipapp, `blueprint=`), reading and writing as
  UTF-8. The script is staged in a TemporaryDirectory, which also covers
  the cleanup-on-failure fix. get_framework_requirements is superseded by
  get_build_requirements.
- cliffy/cli.py: `--blueprint` on every command and the `blueprint` group
  next to `ai`; exit codes of the fixes branch kept (a failed build exits
  1); `cli update` and `cli docs <loaded>` reuse the stored blueprint.
- cliffy/commanders/click.py: callbacks on the root and on groups, with
  help emitted through py_str. parse_param_list and get_param_list_names
  are the one param implementation, shared by commands and callbacks
  (global params, validated names, `--flag/--no-flag` bools).
- cliffy/commanders/typer.py: callbacks plus py_str escaping.
- cliffy/reloader.py, cliffy/tester.py, cliffy/transformer.py:
  `blueprint=` threaded through, UTF-8 and context managers kept, include
  cycle detection next to blueprint resolution.
- tests/test_builder.py: all tests of both sides in one file. The tests of
  the fixes branch patch the pip subprocess instead of shiv's pip and
  expect the pinned framework requirement.
- tests/test_callbacks.py: the click bool option is expected as a
  `--debug/--no-debug` switch.
- cliffy/ai.py: `commands` became optional for callbacks-only CLIs; a
  model reply without `commands` and without callbacks is still rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2d0162f0-6149-40d6-a097-9853407f4791

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This PR replaces the old blueprint hooks with typed custom YAML specifications that are resolved and transformed into ordinary manifests, adds framework-independent callbacks, and produces reproducible self-contained zipapps while wiring blueprint selection, metadata, validation, testing, documentation, and CLI workflows end to end.

Sequence diagram for loading a custom blueprint spec

sequenceDiagram
    participant User
    participant CLI
    participant Transformer
    participant Blueprint as Blueprint module
    participant Manifest as CLIManifest
    participant Commander

    User->>CLI: load or run YAML
    CLI->>Transformer: Transformer(manifest, blueprint)
    Transformer->>Transformer: find_blueprint_reference
    Transformer->>Blueprint: resolve_blueprint
    Blueprint-->>Transformer: Blueprint subclass
    Transformer->>Blueprint: pre_validate(raw)
    Transformer->>Blueprint: model_validate(spec)
    Transformer->>Blueprint: to_manifest()
    Blueprint-->>Transformer: CLIManifest or mapping
    Transformer->>Blueprint: post_manifest(manifest)
    Transformer->>Manifest: validate manifest
    Transformer->>Commander: generate_cli(manifest)
    Commander-->>CLI: generated CLI
Loading

Sequence diagram for building a self-contained blueprint CLI

sequenceDiagram
    participant User
    participant CLI
    participant Transformer
    participant Builder
    participant Pip
    participant Shiv
    participant Zipapp

    User->>CLI: build custom YAML spec
    CLI->>Transformer: build_cli_from_manifest
    Transformer->>Transformer: apply blueprint and generate code
    Transformer->>Builder: build_cli(script, requires)
    Builder->>Pip: install dependencies into staging
    Pip-->>Builder: packages or failure reason
    alt dependencies installed
        Builder->>Shiv: create reproducible zipapp
        Shiv->>Zipapp: write single executable
        Zipapp-->>Builder: path and requires_python
        Builder-->>CLI: BuildResult(exit_code=0)
    else installation fails
        Builder-->>CLI: BuildResult(exit_code=1, output)
    end
Loading

Flow diagram for blueprint-driven CLI generation

flowchart LR
    YAML[Custom YAML spec] --> Resolve[resolve_blueprint]
    Resolve --> Validate[build_manifest]
    Validate --> Manifest[CLIManifest]
    Manifest --> Generate[TyperCommander or ClickCommander]
    Generate --> CLI[Generated CLI]
    CLI --> Workflows[load, run, render, test, docs]
    CLI --> Build[build_cli]
    Build --> Zipapp[Self-contained reproducible zipapp]
Loading

File-Level Changes

Change Details Files
Replace the legacy blueprint hook API with typed, discoverable YAML-spec adapters that transform custom documents into standard CLI manifests.
  • Define the abstract Pydantic Blueprint API, validation/code-generation hooks, strict extra-field handling, and schema generation.
  • Resolve blueprints from file paths, importable modules/classes, or entry-point plugins with detailed diagnostics.
  • Apply file or CLI-selected blueprints throughout transformation, preserve references in metadata, and add list/schema CLI commands.
  • Add task-runner and MCP tool blueprint examples, schemas, documentation, and comprehensive resolution/validation coverage.
cliffy/blueprint.py
cliffy/transformer.py
cliffy/cli.py
cliffy/commander.py
cliffy/homer.py
cliffy/manifest.py
cliffy/reloader.py
docs/blueprints.md
mkdocs.yaml
examples/blueprints/mcp_tools.py
examples/blueprints/mcp_tools.schema.json
examples/blueprints/tasks.py
examples/blueprints/tasks.schema.json
examples/blueprints/tasks.yaml
examples/blueprints/text-tools.yaml
examples/cliffy_schema.json
tests/test_blueprint.py
Add manifest callbacks that execute at the CLI or nested group level and expose callback parameters in both generated frameworks.
  • Model callbacks with run code, target group, parameters, and invoke-without-command behavior; allow callbacks-only manifests and validate group targets.
  • Generate callback decorators, context parameters, options, and run blocks for Typer and Click while retaining built-in version and alias options.
  • Use Click's runner for Click-generated manifests and add callback examples and cross-framework tests.
cliffy/manifest.py
cliffy/commander.py
cliffy/commanders/click.py
cliffy/commanders/typer.py
cliffy/tester.py
examples/callbacks.yaml
examples/cliffy_schema.json
tests/test_callbacks.py
cliffy/ai.py
Rework builds into self-contained, reproducible zipapps with typed results, dependency installation, framework pinning, and runtime Python compatibility checks.
  • Apply blueprints before build-time code generation and return BuildResult instead of Click testing results.
  • Install requirements through the running interpreter's pip, bundle the generated framework and dependencies, and avoid leaving an artifact on failure.
  • Normalize generated timestamps, use reproducible shiv options, report supported Python ranges, and add an unsupported-interpreter guard.
  • Verify framework selection, failure reporting, reproducibility, archive contents, and isolated execution including MCP flows.
cliffy/builder.py
cliffy/cli.py
tests/test_builder.py
Correct Click option rendering and propagate blueprint-aware behavior across CLI workflows.
  • Render explicit boolean Click parameters as --flag/--no-flag switches and fix explicit option/short-name formatting.
  • Add blueprint selection to load, run, render, build, dev, test, validate, and docs, with CLI selection overriding file metadata.
  • Make update and reload reuse stored blueprint references and return nonzero status for failed builds.
cliffy/cli.py
cliffy/transformer.py
cliffy/reloader.py
cliffy/commander.py
cliffy/commanders/click.py
cliffy/tester.py
tests/test_callbacks.py
tests/test_blueprint.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue, and 1 other issue

Security issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="examples/blueprints/mcp_tools.py" line_range="53" />
<code_context>
+
+def _handle_message(message):
+    """Answer one JSON-RPC message. Returns None for notifications, which get no reply."""
+    method, params, message_id = message.get("method"), message.get("params") or {}, message.get("id")
+    if message_id is None:
+        return None
</code_context>
<issue_to_address>
**Invalid JSON-RPC messages stop server**

When the MCP peer sends valid JSON whose top-level value is not an object, `_handle_message` calls `.get()` on the decoded value, raising `AttributeError`; `_serve` catches only `ValueError`, so the server exits instead of returning an invalid-request error and continuing to read stdin.

Check that each decoded message is a JSON-RPC object and return an invalid-request response for other JSON values.

Also at `examples/blueprints/mcp_tools.py:82-86`.
</issue_to_address>

### Comment 2
<location path="cliffy/builder.py" line_range="89" />
<code_context>
        pip = subprocess.run([*pip_install, "--target", staging, *requirements], capture_output=True, text=True)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and a code-generation or dependency-compatibility defect could leave a loaded CLI or built zipapp behaving incorrectly after this change is reverted. Those generated artifacts can be removed, reloaded, or rebuilt; this diff does not itself make irreversible changes to user data or access.

Blocking findings: examples/blueprints/mcp_tools.py:53, cliffy/builder.py:89


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread examples/blueprints/mcp_tools.py Resolved
Comment thread cliffy/builder.py Resolved
rich colours the help on GitHub Actions, which split '--verbose' across
escape sequences and failed the assertion there only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jaykv
jaykv changed the base branch from feat/revive-ai-commands to main October 12, 2026 21:23
@jaykv
jaykv merged commit c67d708 into main Oct 12, 2026
7 checks passed
@jaykv
jaykv deleted the feat/blueprints branch October 12, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant