aiml
SearchMy learning
Menu

Lesson 2 of 6Module 1

Enhance, upgrade, and extend an Agents CLI project

Add Cloud Run and CI/CD scaffolding in place with scaffold enhance, run 3-way template merges with scaffold upgrade, and wire agents-cli-extension.yaml.

Prerequisites

Scaffold, lint, and run an ADK project with Google Agents CLI.

By the end, you can

  • Graduate a prototype project in place with agents-cli scaffold enhance without overwriting app/agent.py or .env.
  • Predict which files auto-update, stay preserved, or get overwritten during agents-cli scaffold upgrade and --force.
  • Author an agents-cli-extension.yaml manifest that adds a project command and replaces deploy with a unit-test action.
Runtime
Python 3.12+ and Google Agents CLI 1.11.0
Dependencies
google-agents-cli 1.11.0

Lab code, tests and fixtures (ZIP, 6 KB)Quiz check your understanding

Progress is saved in this browser.

Extend the project created in the first lesson, review a three-way template merge, and add a project command through an extension manifest. The downloaded lab lets you inspect merge decisions and extension validation before changing a generated project.

Problem

When you need this. When your team chooses Cloud Run hosting, CI checks or a template update, you need to add those project files while preserving the agent code. Starting a second repository and copying app/agent.py by hand splits your history and loses template tracking. In this lesson, you will enhance docuctl-docs in place with agents-cli scaffold enhance, inspect a 3-way template upgrade from 1.9.0 to 1.11.0 with agents-cli scaffold upgrade, and add a project extension (agents-cli-extension.yaml) that overrides deploy and registers release-check.

Concepts

agents-cli treats project scaffolding as a versioned dependency rather than a one-time code generator. Because agents-cli-manifest.yaml records base_template (adk), acli_version, and create_params, the CLI can reconstruct the exact template tree that generated your project and compare it against newer templates.

In-place enhancement (scaffold enhance .)
Re-renders the project’s base_template with newly requested capabilities (--deployment-target cloud_run, --cicd-runner github_actions, --bq-analytics) and merges the new files into the current working directory while skipping app/agent.py and .env.
Three-way template merge (scaffold upgrade)
Compares the old template (acli_version in agents-cli-manifest.yaml), the new template (installed agents-cli version), and your working tree so unmodified template files auto-update while edited files are preserved or reported as conflicts according to the three-way file comparison.
Extension manifest (agents-cli-extension.yaml)
A declarative YAML file (agents-cli-extension/v1alpha1) at the project root (or user config dir) that registers new subcommands (commands.add) or replaces built-in commands (commands.override) using ordered run steps.
Reserved recovery commands
Built-in commands (info, extension, install, scaffold, setup, login) that cannot be overridden by any extension to preserve recovery entry points when an extension is misconfigured.

Architecture

Old templateNew templateYour projectThree-way mergeUpdatedscaffoldingConflictingeditscleanOld templateNew templateYour projectThree-way mergeUpdated scaffoldingReview conflicting editsclean
Three separate trees feed the template merge. Clean changes can be written; conflicting edits require review. Extensions are configured separately.

The upgrade compares three trees: the template used to create the project, the new template, and your edited project. Template changes and local edits meet at the merge step. Extension commands enter through a separate manifest, so command policy can change without replacing the agent implementation.

Working code

Download the complete lesson lab, extract it, and open its agents-cli-upgrade-extend directory. It includes the source, tests and fixtures. With Python 3.12+ and uv installed, prepare the local environment:

Prepare the downloaded labTerminal · bash
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-lock.txt
LAB_DIR="$PWD"

Replace the project path below with the directory created in Lesson 1. Use the model credentials configured there for model activities; use your cloud credentials for cloud activities.

Open the generated project for the CLI activityTerminal · bash
export PROJECT_DIR="/absolute/path/to/docuctl-docs"
cd "$PROJECT_DIR"

For the CLI steps, continue in the generated docuctl-docs project from Lesson 1. Run the offline Python test at the end from this lesson’s extracted lab directory. These are different directories: the helper models file classification; it does not upgrade a project itself.

Enhance a prototype in place

In the first lesson, we created docuctl-docs with --prototype, which set deployment_target: none and cicd_runner: skip in agents-cli-manifest.yaml. Commit or back up your current project before applying changes. To add Cloud Run Terraform definitions, GitHub Actions workflows, and the BigQuery Agent Analytics plugin in place, run agents-cli scaffold enhance from inside the project directory:

Add Cloud Run, GitHub Actions, and BigQuery Analytics scaffolding in placeTerminal · bash
agents-cli scaffold enhance . \
  --deployment-target cloud_run \
  --cicd-runner github_actions \
  --bq-analytics \
  --region us-central1 \
  -y

Note what the positional argument . (TEMPLATE_PATH) means: scaffold enhance always enhances the current working directory. When agents-cli-manifest.yaml is present, scaffold enhance re-renders the template recorded in that manifest (base_template: adk) with your new flags (--deployment-target cloud_run, --cicd-runner github_actions, --bq-analytics) and merges the generated files into the current directory:

  • Skipped (your code): .env and app/agent.py are never overwritten, so your docuctl_docs agent and read_page tool stay intact.
  • Preserved (you modified, template unchanged): GEMINI.md keeps your project notes.
  • Auto-updated (unchanged by you): README.md updates to include the Cloud Run and CI/CD instructions.
  • Added: .github/workflows/deploy-to-prod.yaml, .github/workflows/pr_checks.yaml, .github/workflows/staging.yaml, deployment/terraform/cicd/*.tf, and deployment/terraform/single-project/*.tf.

Upgrade scaffolding with a three-way merge

When a newer version of google-agents-cli ships updated ADK dependency bounds or FastAPI wrappers, agents-cli scaffold upgrade compares three trees:

  1. Old template: rendered from the acli_version recorded in agents-cli-manifest.yaml (for example, 1.9.0).
  2. New template: rendered from the currently installed agents-cli version (1.11.0).
  3. Project files: your current working directory.

Always preview the merge first with --dry-run:

Preview standard and force template upgradesTerminal · bash
# Preview the 3-way merge without touching disk
agents-cli scaffold upgrade . --dry-run

# Preview force mode when you want fresh template scaffolding
agents-cli scaffold upgrade . --force --dry-run

Suppose you added a custom LABEL org.opencontainers.image.vendor="docuctl" line to Dockerfile. Here is how standard 3-way merge (scaffold upgrade) and --force mode treat each file category:

File or category Standard 3-way merge (scaffold upgrade) Force mode (scaffold upgrade --force)
app/agent.py, .env, *.tfvars Always skipped (Skipping (your code)). Always preserved (Not overwritten).
Unmodified template file (README.md) Auto-updated to the 1.11.0 template. Overwritten with the 1.11.0 template.
User-edited scaffolding (Dockerfile) Preserved (Will preserve) if the new template file is unchanged from the old template; flagged as a conflict if all three file contents differ. Overwritten (Will overwrite: ! Dockerfile), discarding your local Dockerfile edits.
pyproject.toml dependencies Bumps template dependency floors (for example, google-adk >=2.9.2 to >=2.12.0) while keeping packages you added. Updates template dependency floors without removing extra packages.

For ordinary scaffolding files, this is a three-way file comparison, not automatic merging of different edited lines: if your file, the old template and the new template all differ, the CLI reports a conflict. Dependencies have their own merge rules.

Here is the Python model (upgrade_extend.py) that models the 3-way merge classification rules and the extension manifest validator:

upgrade_extend.pySource · pythonDownload
"""3-way template merge and extension manifest validator for Google Agents CLI 1.11.0."""

from __future__ import annotations

from typing import Any

SUPPORTED_SCHEMA = "agents-cli-extension/v1alpha1"
RECOVERY_COMMANDS = frozenset({"info", "extension", "install", "scaffold", "setup", "login"})
USER_CODE_PATHS = frozenset(
    {
        ".env",
        "app/agent.py",
        "deployment/terraform/cicd/vars/env.tfvars",
        "deployment/terraform/single-project/vars/env.tfvars",
    }
)


def classify_three_way_merge(
    path: str,
    old_template: str | None,
    new_template: str | None,
    project_file: str | None,
    *,
    force: bool = False,
) -> str:
    """Classify how `agents-cli scaffold upgrade` handles a file across old/new templates."""
    if path in USER_CODE_PATHS:
        return "skip_user_code"
    if force:
        if new_template is not None and project_file != new_template:
            return "force_overwrite"
        return "unchanged"
    if old_template is None and new_template is not None and project_file is None:
        return "add_new"
    if project_file == old_template and new_template != old_template:
        return "auto_update"
    if project_file != old_template and new_template == old_template:
        return "preserve_user_edit"
    if project_file != old_template and new_template != old_template and project_file != new_template:
        return "conflict"
    return "unchanged"


def validate_extension_manifest(spec: dict[str, Any]) -> dict[str, list[str]]:
    """Validate an agents-cli-extension.yaml dictionary and return resolved commands."""
    schema = spec.get("schema")
    if schema != SUPPORTED_SCHEMA:
        raise ValueError(
            f"Unsupported or missing schema {schema!r}; expected {SUPPORTED_SCHEMA!r}."
        )

    commands = spec.get("commands") or {}
    unknown_keys = set(commands.keys()) - {"override", "add"}
    if unknown_keys:
        first_unknown = sorted(unknown_keys)[0]
        raise ValueError(
            f"agents-cli-extension.yaml: unknown key 'commands.{first_unknown}'; "
            "nest commands under 'commands.override' or 'commands.add'."
        )

    resolved: dict[str, list[str]] = {}
    for section in ("override", "add"):
        entries = commands.get(section) or {}
        for cmd_name, entry in entries.items():
            if section == "override" and cmd_name in RECOVERY_COMMANDS:
                raise ValueError(
                    f"Extension claims {cmd_name!r}, which cannot be overridden "
                    "(it is reserved for inspecting, setting up or repairing agents-cli)."
                )
            run_argv = entry.get("run") or []
            if not isinstance(run_argv, list) or not run_argv:
                raise ValueError(f"Command {cmd_name!r} must define a non-empty 'run' argv list.")
            first_token = str(run_argv[0])
            if " " in first_token:
                raise ValueError(
                    f"Command {cmd_name!r} could not run: executable {first_token!r} not found. "
                    "Pass each argv token as a separate list item under 'run:'."
                )
            resolved[f"{section}:{cmd_name}"] = [str(tok) for tok in run_argv]

    return resolved

Add and override commands with an extension

A project may need a release-check command that runs its tests. Instead of maintaining wrapper shell scripts outside the CLI, copy the supplied agents-cli-extension.yaml file to the generated project root using schema agents-cli-extension/v1alpha1:

agents-cli-extension.yamlSource · yamlDownload
schema: agents-cli-extension/v1alpha1
requires:
  agents_cli: ">=1.10,<2.0"
commands:
  override:
    deploy:
      description: Run unit tests before deploying docuctl-docs
      run:
        - uv
        - run
        - pytest
        - tests/unit
  add:
    release-check:
      description: Run lint and unit tests for release readiness
      run:
        - uv
        - run
        - pytest
        - tests/unit
        - -v

An extension manifest supports two command sections under commands:

  • commands.override: replaces a built-in command (such as deploy) in the current scope. When you run agents-cli --help, the CLI marks the command with [↑ docuctl-docs] so developers can see that the project extension intercepts deploy.
  • commands.add: registers a new top-level command (such as release-check).
Install the project-scoped extensionTerminal · bash
cp "$LAB_DIR/agents-cli-extension.yaml" "$PROJECT_DIR/agents-cli-extension.yaml"

This example replaces deploy with a test command; it does not continue into deployment after tests pass. Use release-check for a standalone test action. The command expects the generated project’s tests/unit directory. Inspect the registered extension with agents-cli info before invoking it:

Inspect and run project-scoped extension commandsTerminal · bash
# Inspect active project and user extensions
agents-cli info

# Run the newly added release-check command
agents-cli release-check

# Run the replacement test command (this does not deploy)
agents-cli deploy -q

Inspect the recorded output from scaffold enhance, scaffold upgrade --dry-run (1.9.0 -> 1.11.0), scaffold upgrade --force --dry-run, agents-cli release-check, and agents-cli deploy -q in recorded-upgrade-extend.json:

Inspect the preserved earlier example

These supplied records retain the earlier shipctl name. They are not a fresh execution of the renamed docuctl lab.

recorded-upgrade-extend.jsonExample output · jsonDownload
{
  "cli_version": "1.11.0",
  "enhance": {
    "command": "agents-cli scaffold enhance . --deployment-target cloud_run --cicd-runner github_actions --bq-analytics --region us-central1 -y",
    "auto_updated": ["README.md"],
    "preserved": ["GEMINI.md"],
    "skipped_user_code": [".env", "app/agent.py"],
    "files_added": [
      ".github/workflows/deploy-to-prod.yaml",
      ".github/workflows/pr_checks.yaml",
      ".github/workflows/staging.yaml",
      "deployment/terraform/cicd/apis.tf",
      "deployment/terraform/cicd/github.tf",
      "deployment/terraform/cicd/iam.tf",
      "deployment/terraform/cicd/service.tf",
      "deployment/terraform/cicd/telemetry.tf",
      "deployment/terraform/single-project/apis.tf",
      "deployment/terraform/single-project/iam.tf",
      "deployment/terraform/single-project/service.tf",
      "deployment/terraform/single-project/telemetry.tf"
    ],
    "manifest_diff": {
      "deployment_target": {"before": "none", "after": "cloud_run"},
      "cicd_runner": {"before": "skip", "after": "github_actions"}
    }
  },
  "upgrade_dry_run": {
    "command": "agents-cli scaffold upgrade . --dry-run",
    "version_transition": "1.9.0 -> 1.11.0",
    "preserved": ["Dockerfile", "GEMINI.md"],
    "skipped_user_code": [
      ".env",
      "app/agent.py",
      "deployment/terraform/cicd/vars/env.tfvars",
      "deployment/terraform/single-project/vars/env.tfvars"
    ],
    "dependency_updates": [
      "google-adk [gcp,otel-gcp,bigquery-analytics]>=2.9.2,<3.0.0 -> [gcp,otel-gcp,bigquery-analytics]>=2.12.0,<3.0.0",
      "gcsfs >=2024.11.0 -> >=2026.10.0",
      "google-cloud-aiplatform [evaluation]>=1.156.0,<2.0.0 -> [evaluation]>=2.2.0,<3.0.0"
    ]
  },
  "upgrade_force_dry_run": {
    "command": "agents-cli scaffold upgrade . --force --dry-run",
    "will_overwrite": ["Dockerfile"],
    "not_overwritten": [
      ".env",
      "GEMINI.md",
      "agents-cli-manifest.yaml",
      "app/agent.py",
      "pyproject.toml",
      "tests/eval/eval_config.yaml",
      "tests/eval/response_quality.py"
    ]
  },
  "extension_execution": {
    "info_extensions_section": [
      "deploy  (override, project) <- shipctl-docs: uv run pytest tests/unit  [requires agents-cli >=1.10,<2.0]",
      "release-check  (add, project) <- shipctl-docs: uv run pytest tests/unit -v  [requires agents-cli >=1.10,<2.0]"
    ],
    "release_check": {
      "command": "agents-cli release-check",
      "exit_code": 0,
      "summary": "2 passed in 0.29s"
    },
    "deploy_override": {
      "command": "agents-cli deploy -q",
      "exit_code": 0,
      "banner": "running 'deploy' via extension 'shipctl-docs' (project scope)",
      "summary": "2 passed in 0.29s"
    }
  },
  "failures": {
    "shell_string_in_run_list": {
      "bad_run": ["uv run pytest tests/unit -v"],
      "exit_code": 1,
      "error": "Extension 'shipctl-docs' command 'release-check' could not run: executable 'uv run pytest tests/unit -v' not found. Check the extension's `run:` entry."
    },
    "reserved_recovery_command_override": {
      "attempted_command": "info",
      "warning": "Extension 'shipctl-docs' claims 'info', which cannot be overridden (it is reserved for inspecting, setting up or repairing agents-cli); using the built-in."
    }
  }
}

Failure cases

Run the offline verification suite to test the 3-way merge classifier, the recorded upgrade and extension runs, and the two extension failure boundaries:

Run the offline upgrade and extension checksTerminal · bash
cd "$LAB_DIR"
"$LAB_DIR/.venv/bin/python" -m unittest -v test_upgrade_extend.py

Watch for two failure modes when writing agents-cli-extension.yaml:

Shell command string in extension run list. Unlike GitHub Actions run: blocks, run: in agents-cli-extension/v1alpha1 is an argv token list passed directly to exec without a shell, where run[0] is the executable name and run[1:] are its arguments. If you write a single space-separated string such as run: ["uv run pytest tests/unit -v"], agents-cli looks for an executable literally named "uv run pytest tests/unit -v" and exits with code 1:

Error: Extension 'docuctl-docs' command 'release-check' could not run: executable 'uv run pytest tests/unit -v' not found. Check the extension's `run:` entry.

Split each argument into its own YAML list item (["uv", "run", "pytest", "tests/unit", "-v"]).

Overriding a reserved recovery command. agents-cli reserves diagnostic and repair commands (info, extension, install, scaffold, setup, login) so a broken extension can never lock you out of inspecting or fixing the CLI. If an extension declares commands.override.info, agents-cli logs a warning and runs the built-in command instead:

Warning: Extension 'docuctl-docs' claims 'info', which cannot be overridden (it is reserved for inspecting, setting up or repairing agents-cli); using the built-in.
What happens when you edit Dockerfile in your project and then run agents-cli scaffold upgrade without --force after updating agents-cli?

Choose one answer, then check your reasoning.

Read the explanation

agents-cli scaffold upgrade performs a 3-way merge across the old template (from acli_version), the new template, and your working tree: unmodified files auto-update, user edits are preserved, and app/agent.py is always skipped.

Deployment

Upgrade and enhancement change the project files that a later deployment will use. Review the resulting diff, rerun lint and tests, and retain the previous template and dependency versions before deploying the changed project. A successful merge is not a deployment smoke test.

Scaling

Keep an upgrade repeatable across projects by pinning the CLI/template version and reviewing conflicts explicitly. Share tested extension commands where appropriate, while keeping project-specific configuration separate. This lesson does not measure upgrade throughput or production capacity.

Key takeaways

  • agents-cli scaffold enhance . graduates a prototype in place by re-rendering the manifest’s base_template and adding Terraform and CI/CD files without touching app/agent.py or .env.
  • agents-cli scaffold upgrade performs a 3-way merge between the recorded acli_version template, the new CLI template, and your working tree; --force overwrites user-modified scaffolding files (Dockerfile) but still preserves app/agent.py, .env, and *.tfvars.
  • agents-cli-extension.yaml (agents-cli-extension/v1alpha1) adds or overrides CLI commands using shell-free argv lists and never allows overriding reserved recovery commands.

Best practices

  • Always run agents-cli scaffold upgrade . --dry-run on a clean git working tree before applying a template upgrade.
  • Avoid --force when you have customized Dockerfile or .github/workflows/*.yaml, since --force discards local edits on template-owned scaffolding files.
  • Use release-check for the test action. This example’s commands.override.deploy replaces deployment; passing its tests does not invoke the built-in deploy command.

References

Commands reviewed against Google Agents CLI 1.11.0 and the linked official documentation on October 23, 2026.

Written by Arjun Prabhulal.

Module 1

Scaffold, run, and extend agent projects