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_templatewith newly requested capabilities (--deployment-target cloud_run,--cicd-runner github_actions,--bq-analytics) and merges the new files into the current working directory while skippingapp/agent.pyand.env. - Three-way template merge (
scaffold upgrade) - Compares the old template (
acli_versioninagents-cli-manifest.yaml), the new template (installedagents-cliversion), 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 orderedrunsteps. - 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
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:
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.
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:
agents-cli scaffold enhance . \
--deployment-target cloud_run \
--cicd-runner github_actions \
--bq-analytics \
--region us-central1 \
-yNote 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):.envandapp/agent.pyare never overwritten, so yourdocuctl_docsagent andread_pagetool stay intact. - Preserved (
you modified, template unchanged):GEMINI.mdkeeps your project notes. - Auto-updated (
unchanged by you):README.mdupdates 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, anddeployment/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:
- Old template: rendered from the
acli_versionrecorded inagents-cli-manifest.yaml(for example,1.9.0). - New template: rendered from the currently installed
agents-cliversion (1.11.0). - Project files: your current working directory.
Always preview the merge first with --dry-run:
# 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-runSuppose 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:
"""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 resolvedAdd 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:
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
- -vAn extension manifest supports two command sections under commands:
commands.override: replaces a built-in command (such asdeploy) in the current scope. When you runagents-cli --help, the CLI marks the command with[↑ docuctl-docs]so developers can see that the project extension interceptsdeploy.commands.add: registers a new top-level command (such asrelease-check).
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 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 -qInspect 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.
{
"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:
cd "$LAB_DIR"
"$LAB_DIR/.venv/bin/python" -m unittest -v test_upgrade_extend.pyWatch 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.
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’sbase_templateand adding Terraform and CI/CD files without touchingapp/agent.pyor.env.agents-cli scaffold upgradeperforms a 3-way merge between the recordedacli_versiontemplate, the new CLI template, and your working tree;--forceoverwrites user-modified scaffolding files (Dockerfile) but still preservesapp/agent.py,.env, and*.tfvars.agents-cli-extension.yaml(agents-cli-extension/v1alpha1) adds or overrides CLI commands using shell-freeargvlists and never allows overriding reserved recovery commands.
Best practices
- Always run
agents-cli scaffold upgrade . --dry-runon a clean git working tree before applying a template upgrade. - Avoid
--forcewhen you have customizedDockerfileor.github/workflows/*.yaml, since--forcediscards local edits on template-owned scaffolding files. - Use
release-checkfor the test action. This example’scommands.override.deployreplaces 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.
- Google Agents CLI scaffold enhance and upgrade guide: prototype-to-production enhancement and 3-way template upgrades.
- Google Agents CLI extensions reference:
agents-cli-extension/v1alpha1schema, scope precedence, andcommands.add/commands.override.