Build a Polyglot Project Factory

Build an agent factory that reviews, generates, tests, and repairs apps.

Introduction

30 Second Summary

A promising software idea can unravel when coding begins before the design is settled. Changing direction later means undoing work that should never have started.

In this project, you will build a local factory where five specialist agents turn an idea into a reviewable architecture. Once you approve the plan, the factory generates a complete project and decides whether it is safe to release.

What You'll Build

Your final demo starts with a plain-language idea and ends with an approved Python, Node.js, or static web project whose checks support a visible released or blocked decision.

By the end of this project, you'll have:

  • A first architecture draft that turns your project idea into structured requirements and a stack-specific proposal.
  • A human approval gate where plain-language feedback produces numbered revisions until you explicitly approve the plan.
  • A polyglot factory run that writes a complete project under generated_projects/. Its final report shows check output, repair attempts, and an honest release status.
  • Secret Mission: Red-team the approval gate with conflicting stack requests and unsupported feedback before approving a safe architecture.

Are there any prerequisites?

No previous agent-building experience is required. The walkthrough covers every required setup task on macOS.

Before We Start

This commitment anchors your factory in one example project idea with an initial preferred stack. It also protects your control by requiring explicit architecture approval before generated project files can be written.

Set Up the Factory Workspace

Your approval-first factory needs dependable local runtimes before it can coordinate any agents. Python hosts the workflow. Node.js validates generated JavaScript projects.

This step prepares an isolated workspace for the Google Agent Development Kit. It also configures Gemini API authentication for the agents you build later.

In this step, get ready to:
  • Verify the host tools required by the factory.
  • Create an isolated workspace with pinned dependencies.
  • Protect your Gemini API key and pass the guard tests.
Verify the host tools

Google ADK 2.13.0 requires Python 3.11 or later. Node.js 24.22.1 LTS supplies the registered checker for Node.js projects.

  • Press Cmd+Space to open Spotlight.
  • Type Terminal into Spotlight.
  • Press Enter to open Terminal.
  • Check your installed Python version by running this command:
python3 --version

What does this command check?

This command asks the Python executable on your Mac to report its version. The result determines whether it can install the pinned ADK package.

✔️ I see version 3.11 or higher

Your Python runtime meets the factory requirement. Keep this Terminal window available for the workspace commands.

ⓧ I see an older version

Your installed Python version cannot run Google ADK 2.13.0.

  • Visit the official Python downloads page.
  • Download the stable Python 3.15.1 macOS installer.
  • Open the downloaded installer.
  • Complete the installation with the default options.
  • Return to Terminal.
  • Repeat the Python version check.

ⓧ Command not found

Your shell cannot find Python. Installing the stable macOS release adds the required runtime.

  • Visit the official Python downloads page.
  • Download the stable Python 3.15.1 macOS installer.
  • Open the downloaded installer.
  • Complete the installation with the default options.
  • Close Terminal.
  • Reopen Terminal through Spotlight.
  • Repeat the Python version check.
  • Check your installed Node.js version by running this command:
node --version

Why Verify Node.js Now?

The factory later uses Node.js to check JavaScript syntax. It also uses the built-in test runner for generated Node.js projects.

✔️ I see version 24.22.1 or higher

Your Node.js runtime is ready to validate generated JavaScript projects. Continue with the same Terminal window.

ⓧ I see an older version

Your installed release does not meet the factory requirement. Upgrade to Node.js 24.22.1 or later in the 24 LTS line.

  • Visit the official Node.js download page.
  • Download the macOS installer for Node.js 24.22.1 LTS.
  • Open the downloaded installer.
  • Complete the installation with the default options.
  • Return to Terminal.
  • Repeat the Node.js version check.

ⓧ Command not found

Node.js is missing from your Mac. Install the required LTS release before continuing.

  • Visit the official Node.js download page.
  • Download the macOS installer for Node.js 24.22.1 LTS.
  • Open the downloaded installer.
  • Complete the installation with the default options.
  • Close Terminal.
  • Reopen Terminal through Spotlight.
  • Repeat the Node.js version check.

Visual Studio Code keeps the factory package and generated projects together in one editor. Confirm that the application opens before creating the workspace.

  • Press Cmd+Space to open Spotlight.
  • Type Visual Studio Code into Spotlight.
  • Press Enter to open Visual Studio Code.

✔️ Visual Studio Code opens

Your editor is ready. Leave it open for the workspace setup.

ⓧ Visual Studio Code is missing

Install Visual Studio Code from its official macOS disk image.

  • Visit the Visual Studio Code macOS setup page.
  • Download the official macOS disk image.
  • Open the downloaded disk image.
  • Drag Visual Studio Code.app into Applications.
  • Return to Spotlight.
  • Search for Visual Studio Code.
  • Press Enter to open Visual Studio Code.
Build the isolated workspace

A virtual environment keeps the factory dependencies separate from other Python projects. The workspace gives every factory file one predictable home.

  • Switch back to Finder.
  • Select Desktop in the Finder sidebar.
  • Click File in the menu bar.
  • Click New Folder.
  • Enter architecture-first-polyglot-project-factory as the folder name.
  • Press Enter to create the folder.

Finder now shows the empty architecture-first-polyglot-project-factory folder on your Desktop. This folder becomes the local boundary for the factory.

  • Switch back to Visual Studio Code.
  • Click File in the menu bar.
  • Click Open Folder....
  • Select the architecture-first-polyglot-project-factory folder on your Desktop.
  • Click Open.
  • Click View in the menu bar.
  • Click Terminal to open the integrated terminal.

The Visual Studio Code title bar shows architecture-first-polyglot-project-factory. The integrated terminal starts inside that folder.

  • Create the isolated Python environment by running this command:
python3 -m venv .venv

What Does This Command Create?

This command creates a local Python environment inside .venv. Packages installed after activation stay inside this workspace.

  • Confirm that .venv appears in the Visual Studio Code Explorer sidebar.

Virtual Environment Missing?

Confirm that the integrated terminal is inside architecture-first-polyglot-project-factory. Check the Python installation if the command reports that Python is unavailable.

Help me diagnose the missing virtual environment.

  • Activate the virtual environment by running this command:
source .venv/bin/activate

How Do I Know Activation Worked?

Your terminal prompt begins with (.venv). The python and pip commands now resolve inside the workspace.

  • Confirm that (.venv) appears at the start of the terminal prompt.

Activation Did Not Change the Prompt?

Confirm that .venv exists in the Explorer sidebar. Repeat the activation command from the top-level workspace folder.

Help me activate the local environment.

The dependency installation can take a minute while your Mac downloads the packages. A changing list of package names means the installation is progressing.

  • Install Google ADK 2.13.0 and Pydantic 2.14.1 by running this command:
pip install google-adk==2.13.0 pydantic==2.14.1

Why Pin Both Dependencies?

Version pins keep the workspace reproducible. Pydantic validates the structured contracts exchanged by the specialist agents.

The installation succeeds when the terminal returns to the (.venv) prompt without a package error.

Package Installation Failed?

  • Confirm that the terminal prompt begins with (.venv).
  • Confirm that Python reports version 3.11 or later.
  • Retry the installation after checking your internet connection.

Help me diagnose the dependency installation.

  • Check the installed ADK version by running this command:
adk --version

What Should This Command Prove?

This command confirms that the ADK executable is available through the activated .venv. The required result identifies version 2.13.0.

✔️ I see version 2.13.0

The pinned ADK command is ready. You can now scaffold the agent package.

ⓧ I see an older version

The terminal is resolving a different ADK installation.

  • Confirm that the prompt begins with (.venv).
  • Repeat the pinned dependency installation command.
  • Repeat the ADK version check.

ⓧ Command not found

The virtual environment activation or package installation did not complete.

  • Repeat the virtual environment activation command.
  • Repeat the pinned dependency installation command.
  • Repeat the ADK version check.

The ADK scaffold supplies the initial package structure. Later steps replace its temporary agent with the architecture-first workflow.

  • Scaffold the factory package by running this command:
adk create factory

What Did ADK Create?

The scaffold creates factory/agent.py. It also creates factory/.env and factory/__init__.py.

The temporary root_agent keeps the package importable while you prepare the factory guards.

  • Confirm that the factory folder appears in the Explorer sidebar.

Factory Folder Missing?

Confirm that the terminal prompt begins with (.venv). Confirm that the terminal is inside the workspace folder.

Help me diagnose the missing scaffold.

The guard tests need three helper functions before the complete workflow exists. These helpers recognize approval responses and protect generated project paths.

  • Open factory/agent.py from the Explorer sidebar.
  • Add these imports to the existing import section:
import re
from pathlib import Path

Why Add These Imports?

  • The re module converts project titles into stable folder names.
  • The Path class checks generated paths before any file is written.
  • Save factory/agent.py.
  • Confirm that both imports appear above the scaffolded root_agent definition.
  • Find the scaffolded root_agent definition.
  • Add these helpers immediately above that definition:
def _is_approved(response: str) -> bool:
    return response.strip().upper() in {"APPROVE", "APPROVED", "YES"}


def _slugify(value: str) -> str:
    slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
    return slug or "generated-project"


def _safe_relative_path(value: str) -> Path:
    path = Path(value)
    if path.is_absolute() or not value.strip() or ".." in path.parts:
        raise ValueError(f"Unsafe generated path: {value!r}")
    return path

What Do These Guards Protect?

  • The approval helper accepts only the explicit responses supported by the architecture gate.
  • The slug helper converts a generated title into a stable folder name.
  • The path helper blocks absolute paths and parent traversal.
  • Save factory/agent.py.
  • Confirm that the scaffolded root_agent definition remains below the helpers.

The next files record how to rebuild the workspace. They also keep credentials and generated output away from source control.

  • Click New File in the Explorer sidebar.
  • Enter requirements.txt as the file name.
  • Paste this dependency manifest into the new file:
google-adk==2.13.0
pydantic==2.14.1

What Does This Manifest Record?

The manifest records the exact packages needed to rebuild the factory environment. Future installations can reproduce the same dependency set.

  • Save requirements.txt.
  • Confirm that requirements.txt appears beside the factory folder.
  • Click New File in the Explorer sidebar.
  • Enter .gitignore as the file name.
  • Paste these ignore rules into the new file:
.venv/
.adk/
__pycache__/
*.pyc
factory/.env
generated_projects/

What Stays Out of Source Control?

These rules exclude the virtual environment and ADK session data. They also exclude Python caches and generated projects.

The live factory/.env file stays untracked. That rule protects the Gemini API key from an accidental commit.

  • Save .gitignore.
  • Confirm that .gitignore appears beside requirements.txt.
  • Click New File in the Explorer sidebar.
  • Enter README.md as the file name.
  • Paste this project guide into the new file:
# Architecture-First Polyglot Project Factory

This Google ADK workflow turns a project idea into requirements, repeatedly revises an architecture with human feedback, and generates files only after explicit approval.

Supported build profiles:

- `python`: standard-library application with `unittest` tests
- `node`: dependency-free Node.js application with `node:test` tests
- `static-web`: HTML, CSS, and JavaScript with syntax checks and pure-logic tests

## Run

1. Activate the virtual environment.
2. Put your Gemini API key in `factory/.env` as `GOOGLE_API_KEY`.
3. Run `adk run factory`.
4. Enter an idea and optional stack suggestion.
5. Reply with architecture feedback until satisfied, then reply `APPROVE`.

Generated projects are written under `generated_projects/`.

## Security boundary

The writer confines generated paths and the checker uses fixed command adapters with `shell=False`. Generated tests still execute local code. Do not expose this local tutorial implementation to untrusted users. Use an isolated execution service for a public deployment.

Why Document the Security Boundary?

The completed factory runs generated tests on your local machine. The README makes that trust boundary visible before anyone considers using untrusted prompts.

  • Save README.md.
  • Press Cmd+Shift+V to open the Markdown preview.

The preview shows the project title. It also shows the three supported profiles and the local execution warning.

  • Select the factory folder in the Explorer sidebar.
  • Click New File beside the selected folder.
  • Enter .env.example as the file name.
  • Paste this safe credential template into the new file:
GOOGLE_API_KEY="your-api-key-here"

Why Keep an Environment Example?

The example records the required environment variable without containing a working credential. Someone can reproduce the file structure without receiving access to your Gemini usage.

  • Save factory/.env.example.
  • Confirm that .env.example appears inside the factory folder.
  • Open factory/__init__.py from the Explorer sidebar.
  • Replace its contents with this package export:
from .agent import root_agent

__all__ = ["root_agent"]

What Does the Package Export Do?

The import exposes root_agent whenever Python loads the factory package. The export list identifies the workflow entry point.

  • Save factory/__init__.py.
  • Confirm that the file contains the import and the __all__ assignment.
Configure authentication and prove the guards

A Gemini API key grants access to your Gemini usage. The live key belongs only in the ignored factory/.env file.

Keep the Live Key Private

The key may appear in full when you create it. Keep that screen out of screenshots.

Never paste the key into chat. Never commit it to source control.

  • Open the official Gemini API key guide in your browser.
  • Continue to Google AI Studio from the guide.
  • Sign in with your Google account if prompted.
  • Create a Gemini API key.
  • Copy the new key.
  • Switch back to Visual Studio Code.
  • Open factory/.env from the Explorer sidebar.
  • Replace its contents with this credential template:
GOOGLE_API_KEY="your-api-key-here"

How Does Authentication Load?

Gemini libraries detect GOOGLE_API_KEY from the environment. ADK loads the value when the factory starts.

  • Replace your-api-key-here with the key from your clipboard.
  • Save factory/.env.
  • Close the factory/.env editor tab.

Your credential now lives in the file excluded by .gitignore. Closing the editor tab keeps the live value away from later screenshots.

Cannot Create an API Key?

Use a Google Cloud project where your account can create keys. Ask the project administrator for access if key creation is unavailable.

Help me troubleshoot Gemini API key creation.

The guard tests prove the first factory boundaries before the multi-agent workflow exists. They check path confinement before checking explicit approval and stable folder naming.

  • Click New Folder in the Explorer sidebar.
  • Enter tests as the folder name.
  • Select the new tests folder.
  • Click New File beside the selected folder.
  • Enter __init__.py as the file name.
  • Save the empty tests/__init__.py file.
  • Click New File beside the selected folder.
  • Enter test_factory_guards.py as the file name.

The Explorer sidebar now shows both test files inside tests.

  • Paste the first three guard tests into tests/test_factory_guards.py using this code:
import unittest
from pathlib import Path

from factory.agent import _is_approved, _safe_relative_path, _slugify


class FactoryGuardTests(unittest.TestCase):
    def test_safe_relative_path_accepts_nested_file(self):
        self.assertEqual(
            _safe_relative_path("src/app.js"),
            Path("src/app.js"),
        )

    def test_safe_relative_path_rejects_parent_traversal(self):
        with self.assertRaises(ValueError):
            _safe_relative_path("../secrets.txt")

    def test_safe_relative_path_rejects_absolute_path(self):
        with self.assertRaises(ValueError):
            _safe_relative_path("/tmp/escape.txt")

What Do These Tests Prove?

  • The first test permits a nested file that stays inside a generated project.
  • The second test rejects a path that climbs into a parent folder.
  • The third test rejects an absolute filesystem destination.
  • Save tests/test_factory_guards.py.
  • Switch back to the activated integrated terminal.
  • Run the current guard tests with this command:
python -m unittest -v

What Should the First Run Show?

The test runner discovers three path tests. Each test reports success before the output ends with OK.

That is the first boundary proven. Generated file paths cannot point outside their future project folder.

First Guard Run Failed?

  • Confirm that the terminal prompt begins with (.venv).
  • Confirm that tests/test_factory_guards.py imports the three helper names exactly.
  • Confirm that the three helpers are saved in factory/agent.py.

Help me diagnose the first guard test run.

  • Return to tests/test_factory_guards.py in Visual Studio Code.
  • Append these tests and the runner entry point below the existing class methods:
    def test_approval_is_explicit(self):
        self.assertTrue(_is_approved("APPROVE"))
        self.assertFalse(_is_approved("looks good but change the database"))

    def test_slugify_creates_stable_folder_name(self):
        self.assertEqual(_slugify("My Project 2026"), "my-project-2026")


if __name__ == "__main__":
    unittest.main()

What Do the Final Guards Add?

The approval test rejects conversational feedback as approval. The slug test confirms that a project title becomes the stable folder name my-project-2026.

  • Save tests/test_factory_guards.py.
  • Confirm that the file now contains five test methods.

Before you check the environment, which three versions do you expect the activated terminal to report?

  • Verify the host runtimes and installed ADK package by running these commands:
python --version
node --version
adk --version

What Should the Version Checks Confirm?

  • Python reports version 3.11 or later.
  • Node.js reports version 24.22.1 or later in the 24 LTS line.
  • ADK reports version 2.13.0.

Before you run the final suite, do you expect all five guards to pass from this workspace?

  • Run the complete guard suite with this command:
python -m unittest -v

What Should the Final Run Show?

The runner discovers five tests covering path confinement and explicit approval. It also checks stable folder naming.

Every test reports success before the output finishes with OK.

That setup work is complete. Your activated workspace now has pinned tools, protected authentication, and passing guard tests.

Final Checks Are Not Passing?

  • Confirm that the terminal is inside architecture-first-polyglot-project-factory.
  • Confirm that the terminal prompt begins with (.venv).
  • Compare the imports in tests/test_factory_guards.py with the helper names in factory/agent.py.

Help me diagnose the final workspace checks.

✔️ Awesome, I've got everything!

Your factory workspace matches the required setup.

  • Save every open file.
  • Keep .venv active for the next step.

ⓧ I'd like to double check the full code

.venv/
.adk/
__pycache__/
*.pyc
factory/.env
generated_projects/

What Does This File Protect?

This file keeps local environments and generated output away from source control. It also excludes the live credential file.

# Architecture-First Polyglot Project Factory

This Google ADK workflow turns a project idea into requirements, repeatedly revises an architecture with human feedback, and generates files only after explicit approval.

Supported build profiles:

- `python`: standard-library application with `unittest` tests
- `node`: dependency-free Node.js application with `node:test` tests
- `static-web`: HTML, CSS, and JavaScript with syntax checks and pure-logic tests

## Run

1. Activate the virtual environment.
2. Put your Gemini API key in `factory/.env` as `GOOGLE_API_KEY`.
3. Run `adk run factory`.
4. Enter an idea and optional stack suggestion.
5. Reply with architecture feedback until satisfied, then reply `APPROVE`.

Generated projects are written under `generated_projects/`.

## Security boundary

The writer confines generated paths and the checker uses fixed command adapters with `shell=False`. Generated tests still execute local code. Do not expose this local tutorial implementation to untrusted users. Use an isolated execution service for a public deployment.

What Does This File Document?

This guide records the supported profiles and run flow. It also records the local execution boundary.

GOOGLE_API_KEY="your-api-key-here"

What Does This Example Show?

This example shows the required environment variable without storing a working key.

from .agent import root_agent

__all__ = ["root_agent"]

What Does This Package File Expose?

This package file exposes root_agent as the factory entry point.

google-adk==2.13.0
pydantic==2.14.1

What Does This Manifest Pin?

This manifest pins the ADK and Pydantic versions used by the workspace.

The tests/__init__.py file should exist with no content. Its empty state is correct.

import unittest
from pathlib import Path

from factory.agent import _is_approved, _safe_relative_path, _slugify


class FactoryGuardTests(unittest.TestCase):
    def test_safe_relative_path_accepts_nested_file(self):
        self.assertEqual(
            _safe_relative_path("src/app.js"),
            Path("src/app.js"),
        )

    def test_safe_relative_path_rejects_parent_traversal(self):
        with self.assertRaises(ValueError):
            _safe_relative_path("../secrets.txt")

    def test_safe_relative_path_rejects_absolute_path(self):
        with self.assertRaises(ValueError):
            _safe_relative_path("/tmp/escape.txt")

    def test_approval_is_explicit(self):
        self.assertTrue(_is_approved("APPROVE"))
        self.assertFalse(_is_approved("looks good but change the database"))

    def test_slugify_creates_stable_folder_name(self):
        self.assertEqual(_slugify("My Project 2026"), "my-project-2026")


if __name__ == "__main__":
    unittest.main()

What Does This Suite Verify?

This suite verifies safe generated paths and explicit approval. It also verifies stable generated folder names.

Your factory workspace is ready. Next, you will turn your example idea into structured requirements and its first visible architecture draft.

Generate the First Architecture Draft

Your workspace now has verified runtimes plus passing guard tests. The factory is ready to turn an open-ended project idea into a concrete technical plan.

This first workflow produces a visible architecture draft. It deliberately stops after that draft because it has no way to accept revisions yet.

In this step, get ready to:
  • Define structured contracts for every factory handoff.
  • Configure Requirements and Architecture specialist agents.
  • Run a temporary workflow that reveals the missing revision path.
Define the structured contracts

A structured output gives each agent a fixed shape for its response. Pydantic validates those shapes before another part of the factory uses them.

  • In the Explorer sidebar in Visual Studio Code, select factory/agent.py.
  • Replace the import section at the top of factory/agent.py with this import group:
from __future__ import annotations

import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Literal

from google.adk import Agent, Context, Workflow
from google.adk.events import RequestInput
from google.adk.workflow import node
from pydantic import BaseModel

What Do These Imports Prepare?

  • The standard-library imports prepare path handling plus the deterministic safeguards used later in the factory.
  • The Google ADK imports provide agents plus workflow orchestration primitives.
  • The Pydantic import provides the base class for each structured contract.
  • Save factory/agent.py.
  • Confirm the expanded imports load by running this command in the activated terminal:
python -m unittest -v

What Does This Check Prove?

The test runner imports factory.agent before executing the existing guards. A final OK confirms that every imported module is available.

You should see the existing guard tests finish with OK.

Do the Imports Stop the Tests?

  • Check that the terminal still shows the activated .venv environment.
  • Confirm the import group appears before every class in factory/agent.py.
  • Compare each imported name with the code block above.

Help me diagnose an import failure in my factory agent.

The first two contracts capture the original request plus each architecture revision. The profile type also limits executable plans to the factory's registered adapters.

  • Insert these constants plus contracts below the imports in factory/agent.py.
MODEL = "gemini-3.8-flash"
SUPPORTED_PROFILES = ("python", "node", "static-web")

Profile = Literal["python", "node", "static-web"]


class RequirementsSpec(BaseModel):
    title: str
    summary: str
    capabilities: list[str]
    constraints: list[str]
    requested_technologies: list[str]


class ArchitectureSpec(BaseModel):
    revision: int
    project_name: str
    selected_profile: Profile
    technology_stack: list[str]
    components: list[str]
    data_flow: list[str]
    file_plan: list[str]
    test_strategy: list[str]
    run_instructions: list[str]
    compromises: list[str]

What Do These Contracts Control?

  • The Profile type permits only python, node, or static-web.
  • The RequirementsSpec model preserves technology requests alongside capabilities plus constraints.
  • The ArchitectureSpec model records the selected profile plus any unsupported-stack compromise.
  • Save factory/agent.py.
  • Verify that the first contracts import successfully by running:
python -m unittest -v

What Does This Contract Check Prove?

The test run imports both new model classes before exercising the guards. A final OK confirms that their field definitions are valid.

You should see the tests finish with OK again.

Do the First Contracts Fail to Load?

  • Check that Profile appears before both model classes.
  • Confirm every model field remains indented beneath its class.
  • Check the quotes around each supported profile.

Help me find the syntax problem in my first Pydantic contracts.

The next contracts describe a generated bundle plus the workspace where it lands. They also give deterministic checks a standard report format.

  • Insert these project contracts directly below ArchitectureSpec.
class GeneratedFile(BaseModel):
    path: str
    content: str


class ProjectBundle(BaseModel):
    project_name: str
    profile: Profile
    files: list[GeneratedFile]


class WorkspaceInfo(BaseModel):
    project_path: str
    profile: Profile
    file_count: int


class CheckResult(BaseModel):
    passed: bool
    commands: list[str]
    stdout: str
    stderr: str

How Does Project Data Stay Structured?

  • The GeneratedFile model pairs a relative path with complete file content.
  • The ProjectBundle model groups every generated file under one project profile.
  • The workspace plus check contracts retain the project location plus command evidence.
  • Save factory/agent.py.
  • Confirm the four project contracts load by running:
python -m unittest -v

What Does This Project Check Prove?

The test run now imports the bundle plus check-report models. The final OK confirms that their nested types resolve correctly.

You should see another successful test run ending with OK.

Do the Project Contracts Break the Import?

  • Confirm GeneratedFile appears before ProjectBundle.
  • Check that every list annotation includes its item type.
  • Compare the class indentation with the code block above.

Help me diagnose the project contract definitions in factory/agent.py.

The final contracts capture independent quality findings plus the release decision. They preserve the evidence needed to report an honest outcome later.

  • Insert these QA plus release contracts directly below CheckResult.
class QaReport(BaseModel):
    passed: bool
    summary: str
    findings: list[str]


class ReleaseReport(BaseModel):
    status: Literal["released", "blocked"]
    project_path: str
    profile: Profile
    architecture_revision: int
    repair_rounds: int
    qa_summary: str
    checks: CheckResult

What Defines a Release Report?

  • The QaReport model requires a pass decision plus actionable findings.
  • The ReleaseReport model permits released or blocked as its status.
  • The release report retains the final checks plus the number of repair rounds.
  • Save factory/agent.py.
  • Verify all eight contracts by running:
python -m unittest -v

What Does the Full Contract Check Prove?

The test run imports all eight contracts as one dependency chain. A final OK confirms that the factory now has a valid structured vocabulary.

Good progress. Your factory can now describe every artifact from the initial request through the final release report.

Do the Release Contracts Cause an Error?

  • Confirm CheckResult appears before ReleaseReport.
  • Check that the status annotation uses exactly two literal values.
  • Confirm every release field remains inside the class indentation.

Help me troubleshoot the QA plus release contract definitions.

Configure the specialist agents

The first specialist protects the user's intent by retaining every requested technology. This gives the architecture specialist enough evidence to document a compromise when a request falls outside the registered profiles.

  • Insert the Requirements Agent directly below ReleaseReport.
requirements_agent = Agent(
    name="requirements_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Convert the user's project idea into a small, testable micro-project specification.
Preserve every technology suggestion in requested_technologies, even when it may
not fit the registered build profiles. Do not choose the final architecture.
Return only data matching RequirementsSpec.
""",
    output_schema=RequirementsSpec,
)

What Does the Requirements Agent Protect?

  • The agent uses the configured Gemini model for one focused turn.
  • Its instruction preserves technology suggestions in requested_technologies.
  • Its response must validate against RequirementsSpec.
  • Save factory/agent.py.
  • Confirm the Requirements Agent loads by running:
python -m unittest -v

What Does This Agent Check Prove?

Importing factory.agent now constructs the schema-bound Requirements Agent. The final OK confirms that its model plus output schema are valid.

You should see the guard suite complete successfully.

Does the Requirements Agent Fail to Load?

  • Confirm the agent definition appears after RequirementsSpec.
  • Check the opening plus closing delimiters around the instruction.
  • Confirm output_schema names RequirementsSpec.

Help me find the Requirements Agent configuration error.

The second specialist must select an executable profile without hiding unsupported requests. Its compromise field makes that decision visible to the user.

  • Insert the Architecture Agent directly below requirements_agent.
architecture_agent = Agent(
    name="architecture_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Design or revise a complete architecture from the input data. The only executable
profiles are python, node, and static-web. Honor the user's requested technologies
and latest feedback when they fit a registered profile. If they do not fit, choose
the closest safe profile and list the exact compromise instead of pretending the
technology is supported. Include a concrete file plan, data flow, test strategy,
and run instructions. Return only data matching ArchitectureSpec. The workflow
sets the authoritative revision number after your response.
""",
    output_schema=ArchitectureSpec,
)

How Does the Architecture Agent Stay Honest?

  • The instruction limits executable plans to the three profiles in SUPPORTED_PROFILES.
  • Unsupported technology requests remain visible through compromises.
  • The ArchitectureSpec schema requires a file plan plus a test strategy.
  • Save factory/agent.py.
  • Verify both specialist agents by running:
python -m unittest -v

What Does the Specialist Check Prove?

The import now constructs both schema-bound agents. A final OK confirms that their definitions load beside the existing guard helpers.

You should see every factory guard pass again.

Does the Architecture Agent Fail to Load?

  • Confirm the definition appears after ArchitectureSpec.
  • Check the triple-quoted instruction for a missing delimiter.
  • Confirm output_schema names ArchitectureSpec.

Help me diagnose the Architecture Agent configuration.

Run the first architecture draft

A graph workflow passes one node's validated result into the next node. This temporary route sends the original idea to the Requirements Agent before the Architecture Agent receives the structured requirements.

  • At the bottom of factory/agent.py, replace the existing root_agent definition with this temporary workflow:
root_agent = Workflow(
    name="polyglot_project_factory",
    edges=[("START", requirements_agent, architecture_agent)],
)

How Does This Route Work?

  • The START entry sends the project idea to requirements_agent.
  • The validated requirements become the input for architecture_agent.
  • The terminal node returns the first architecture as the workflow result.
  • Save factory/agent.py.
  • Confirm the temporary workflow loads by running:
python -m unittest -v

What Does the Workflow Check Prove?

Constructing the workflow validates its sequential graph. The final OK confirms that both agents are reachable from START.

Your guard tests should finish successfully. The temporary two-agent workflow is now executable.

Does the Workflow Fail to Load?

  • Confirm requirements_agent appears before architecture_agent in the edge sequence.
  • Check that both agent definitions appear above root_agent.
  • Confirm the workflow name matches the code block above.

Help me troubleshoot the temporary ADK workflow graph.

Compare your cumulative implementation before starting the interactive session.

✔️ Awesome, I've got everything!

Your eight contracts plus two specialist agents are connected through the temporary workflow. Confirm that factory/agent.py is saved.

ⓧ I'd like to double check the full code

from __future__ import annotations

import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Literal

from google.adk import Agent, Context, Workflow
from google.adk.events import RequestInput
from google.adk.workflow import node
from pydantic import BaseModel

MODEL = "gemini-3.8-flash"
SUPPORTED_PROFILES = ("python", "node", "static-web")

Profile = Literal["python", "node", "static-web"]


class RequirementsSpec(BaseModel):
    title: str
    summary: str
    capabilities: list[str]
    constraints: list[str]
    requested_technologies: list[str]


class ArchitectureSpec(BaseModel):
    revision: int
    project_name: str
    selected_profile: Profile
    technology_stack: list[str]
    components: list[str]
    data_flow: list[str]
    file_plan: list[str]
    test_strategy: list[str]
    run_instructions: list[str]
    compromises: list[str]


class GeneratedFile(BaseModel):
    path: str
    content: str


class ProjectBundle(BaseModel):
    project_name: str
    profile: Profile
    files: list[GeneratedFile]


class WorkspaceInfo(BaseModel):
    project_path: str
    profile: Profile
    file_count: int


class CheckResult(BaseModel):
    passed: bool
    commands: list[str]
    stdout: str
    stderr: str


class QaReport(BaseModel):
    passed: bool
    summary: str
    findings: list[str]


class ReleaseReport(BaseModel):
    status: Literal["released", "blocked"]
    project_path: str
    profile: Profile
    architecture_revision: int
    repair_rounds: int
    qa_summary: str
    checks: CheckResult


requirements_agent = Agent(
    name="requirements_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Convert the user's project idea into a small, testable micro-project specification.
Preserve every technology suggestion in requested_technologies, even when it may
not fit the registered build profiles. Do not choose the final architecture.
Return only data matching RequirementsSpec.
""",
    output_schema=RequirementsSpec,
)


architecture_agent = Agent(
    name="architecture_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Design or revise a complete architecture from the input data. The only executable
profiles are python, node, and static-web. Honor the user's requested technologies
and latest feedback when they fit a registered profile. If they do not fit, choose
the closest safe profile and list the exact compromise instead of pretending the
technology is supported. Include a concrete file plan, data flow, test strategy,
and run instructions. Return only data matching ArchitectureSpec. The workflow
sets the authoritative revision number after your response.
""",
    output_schema=ArchitectureSpec,
)


def _is_approved(response: str) -> bool:
    return response.strip().upper() in {"APPROVE", "APPROVED", "YES"}


def _slugify(value: str) -> str:
    slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
    return slug or "generated-project"


def _safe_relative_path(value: str) -> Path:
    path = Path(value)
    if path.is_absolute() or not value.strip() or ".." in path.parts:
        raise ValueError(f"Unsafe generated path: {value!r}")
    return path


root_agent = Workflow(
    name="polyglot_project_factory",
    edges=[("START", requirements_agent, architecture_agent)],
)

Use this reference to compare class order plus indentation. Keep the existing guard helpers unchanged.

Before you start the session, do you think this forward-only graph can ask you to revise its first proposal?

  • Start the interactive factory from the activated terminal by running:
adk run factory

What Does This Command Start?

The command loads root_agent from the factory package. The terminal interface sends your next idea through both specialists.

  • Enter this sample idea when the terminal requests input:
Make a habit tracker using Node.js.

What Is This Idea Testing?

The request combines a concrete capability with a preferred Node.js stack. It tests whether the first agent preserves that preference before the second agent selects the registered node profile.

You should see structured requirements followed by one architecture draft. The draft should show revision 1 plus a stack-specific file plan.

The workflow then finishes without asking whether you approve the draft. That visible shortfall is intentional because this version has no review node or revision route.

  • Stop the completed terminal session by pressing Ctrl+C if the interface remains open.

Does the Factory Stop Before the Draft?

  • Confirm the activated terminal remains inside the factory workspace.
  • Check that factory/.env still contains the GOOGLE_API_KEY entry from the previous step.
  • Compare the workflow edge order with the full-file reference above.

Help me troubleshoot why my two-agent workflow does not return an architecture draft.

That is your first visible factory result: one idea now becomes structured requirements plus a stack-specific architecture. Next, you will add the human review loop that keeps implementation locked until explicit approval.

Add the Architecture Review Loop

Your two-agent workflow already turns a project idea into requirements and a revision-1 architecture. The draft is visible in the terminal.

The current workflow accepts its first architecture without giving you control. This step adds a human-in-the-loop review that keeps implementation locked until you explicitly approve a draft.

In this step, get ready to:
  • Present every architecture through an interactive review request.
  • Replay the architecture workflow with each round of feedback.
  • Reject one draft before explicitly approving its revision.
Create the human review boundary

A review boundary turns each ArchitectureSpec into a readable proposal. The boundary pauses the Google Agent Development Kit workflow while it waits for your response.

  • In factory/agent.py, locate the end of the architecture_agent definition.
  • Add the architecture renderer directly below it by pasting this code:
def _render_architecture(architecture: ArchitectureSpec) -> str:
    sections = [
        f"Architecture revision {architecture.revision}",
        f"Project: {architecture.project_name}",
        f"Profile: {architecture.selected_profile}",
        "Stack: " + ", ".join(architecture.technology_stack),
        "Components: " + "; ".join(architecture.components),
        "Data flow: " + " -> ".join(architecture.data_flow),
        "Files: " + ", ".join(architecture.file_plan),
        "Tests: " + "; ".join(architecture.test_strategy),
        "Run: " + "; ".join(architecture.run_instructions),
        "Compromises: "
        + ("; ".join(architecture.compromises) or "None"),
        "Reply APPROVE to lock this architecture, or send revision feedback.",
    ]
    return "\n\n".join(sections)

What does the renderer do?

  • The renderer converts structured architecture fields into sections you can assess in the terminal.
  • The revision number identifies which proposal you are reviewing.
  • The final line makes approval or revision feedback the next decision.
  • Place the review node directly below _render_architecture() by pasting this code:
@node(name="request_architecture_review", rerun_on_resume=False)
async def request_architecture_review(
    ctx: Context, node_input: dict
):
    del ctx
    architecture = ArchitectureSpec.model_validate(node_input)
    yield RequestInput(
        message=_render_architecture(architecture),
        payload=architecture.model_dump(),
        response_schema=str,
    )


def _is_approved(response: str) -> bool:
    return response.strip().upper() in {"APPROVE", "APPROVED", "YES"}

How does the approval gate work?

  • The review node validates its input as an ArchitectureSpec before presenting it.
  • RequestInput pauses the workflow with the rendered architecture as its message.
  • The architecture remains available as a structured payload during the pause.
  • The _is_approved() helper accepts only the three explicit approval responses.
  • Save factory/agent.py.
  • Switch back to the activated terminal from earlier.
  • Verify the guard suite by running:
python -m unittest -v

What should you see?

You should see five passing tests followed by OK. The explicit-approval test proves that ordinary feedback cannot unlock the workflow.

Seeing an import or syntax error?

  • Check that request_architecture_review starts after the closing parenthesis of architecture_agent.
  • Confirm that the RequestInput and node imports remain at the top of factory/agent.py.
  • Compare the indentation inside _render_architecture() with the code above.

Help me fix the architecture review code in factory/agent.py.

That approval boundary is working. Regular architecture feedback now stays separate from the three responses that can unlock the workflow.

Replay rejected architecture drafts

A dynamic workflow can express architecture revision as an ordinary loop. Workflow replay preserves completed child-node results when the parent resumes after your response.

  • In factory/agent.py, locate the existing _safe_relative_path() helper.
  • Add the dynamic workflow scaffold directly below that helper by pasting this code:
@node(name="autonomous_factory", rerun_on_resume=True)
async def autonomous_factory(ctx: Context, node_input: str) -> dict:
    requirements = RequirementsSpec.model_validate(
        await ctx.run_node(requirements_agent, node_input=node_input)
    )

    revision = 1
    previous_architecture: dict | None = None
    feedback = "Create the first architecture proposal."

What does the scaffold prepare?

  • The parent node enables replay because it calls child nodes through ctx.run_node().
  • The Requirements Agent runs once before architecture review begins.
  • The three state values track the revision number, the previous draft, and the latest feedback.
  • Save factory/agent.py.
  • Confirm the new scaffold loads by running:
python -m unittest -v

What does this check prove?

You should see the guard tests finish with OK. This confirms that Python can import the dynamic node scaffold before you add its revision loop.

Does the scaffold fail to import?

  • Check that the autonomous_factory decorator begins at the left edge of the file.
  • Confirm that Context remains in the import from google.adk.
  • Verify that the activated terminal still points to the factory workspace.

Help me diagnose why the autonomous_factory scaffold cannot be imported.

  • Return to factory/agent.py.
  • Place the revision loop directly below the feedback assignment by pasting this code:
    while True:
        architecture_input = {
            "requirements": requirements.model_dump(),
            "supported_profiles": list(SUPPORTED_PROFILES),
            "previous_architecture": previous_architecture,
            "latest_user_feedback": feedback,
        }
        architecture_data = dict(
            await ctx.run_node(
                architecture_agent,
                node_input=architecture_input,
            )
        )
        architecture_data["revision"] = revision
        architecture = ArchitectureSpec.model_validate(architecture_data)

        review_response = await ctx.run_node(
            request_architecture_review,
            node_input=architecture.model_dump(),
        )
        if _is_approved(str(review_response)):
            break

        previous_architecture = architecture.model_dump()
        feedback = str(review_response)
        revision += 1

How does the revision loop work?

  • Every Architecture Agent call receives the original requirements and the registered profile list.
  • A rejected draft returns to the agent with the previous architecture and your latest feedback.
  • The workflow assigns the authoritative revision number after the agent responds.
  • Only an accepted response reaches break.
  • Scroll to the temporary root_agent workflow at the bottom of factory/agent.py.
  • Remove the entire temporary workflow block.
  • Replace it with this dynamic workflow connection:
root_agent = Workflow(
    name="polyglot_project_factory",
    edges=[("START", autonomous_factory)],
)

What does this connection change?

The workflow now enters autonomous_factory from START. The parent node controls every architecture revision before any later implementation stage can run.

  • Save factory/agent.py.

✔️ Awesome, I've got everything!

Your saved file now contains the review request and replay loop. Continue to the interactive check.

ⓧ I'd like to double check the full code

from __future__ import annotations

import re
from pathlib import Path
from typing import Literal

from google.adk import Agent, Context, Workflow
from google.adk.events import RequestInput
from google.adk.workflow import node
from pydantic import BaseModel

MODEL = "gemini-3.8-flash"
SUPPORTED_PROFILES = ("python", "node", "static-web")

Profile = Literal["python", "node", "static-web"]


class RequirementsSpec(BaseModel):
    title: str
    summary: str
    capabilities: list[str]
    constraints: list[str]
    requested_technologies: list[str]


class ArchitectureSpec(BaseModel):
    revision: int
    project_name: str
    selected_profile: Profile
    technology_stack: list[str]
    components: list[str]
    data_flow: list[str]
    file_plan: list[str]
    test_strategy: list[str]
    run_instructions: list[str]
    compromises: list[str]


class GeneratedFile(BaseModel):
    path: str
    content: str


class ProjectBundle(BaseModel):
    project_name: str
    profile: Profile
    files: list[GeneratedFile]


class WorkspaceInfo(BaseModel):
    project_path: str
    profile: Profile
    file_count: int


class CheckResult(BaseModel):
    passed: bool
    commands: list[str]
    stdout: str
    stderr: str


class QaReport(BaseModel):
    passed: bool
    summary: str
    findings: list[str]


class ReleaseReport(BaseModel):
    status: Literal["released", "blocked"]
    project_path: str
    profile: Profile
    architecture_revision: int
    repair_rounds: int
    qa_summary: str
    checks: CheckResult


requirements_agent = Agent(
    name="requirements_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Convert the user's project idea into a small, testable micro-project specification.
Preserve every technology suggestion in requested_technologies, even when it may
not fit the registered build profiles. Do not choose the final architecture.
Return only data matching RequirementsSpec.
""",
    output_schema=RequirementsSpec,
)


architecture_agent = Agent(
    name="architecture_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Design or revise a complete architecture from the input data. The only executable
profiles are python, node, and static-web. Honor the user's requested technologies
and latest feedback when they fit a registered profile. If they do not fit, choose
the closest safe profile and list the exact compromise instead of pretending the
technology is supported. Include a concrete file plan, data flow, test strategy,
and run instructions. Return only data matching ArchitectureSpec. The workflow
sets the authoritative revision number after your response.
""",
    output_schema=ArchitectureSpec,
)


def _render_architecture(architecture: ArchitectureSpec) -> str:
    sections = [
        f"Architecture revision {architecture.revision}",
        f"Project: {architecture.project_name}",
        f"Profile: {architecture.selected_profile}",
        "Stack: " + ", ".join(architecture.technology_stack),
        "Components: " + "; ".join(architecture.components),
        "Data flow: " + " -> ".join(architecture.data_flow),
        "Files: " + ", ".join(architecture.file_plan),
        "Tests: " + "; ".join(architecture.test_strategy),
        "Run: " + "; ".join(architecture.run_instructions),
        "Compromises: "
        + ("; ".join(architecture.compromises) or "None"),
        "Reply APPROVE to lock this architecture, or send revision feedback.",
    ]
    return "\n\n".join(sections)


@node(name="request_architecture_review", rerun_on_resume=False)
async def request_architecture_review(
    ctx: Context, node_input: dict
):
    del ctx
    architecture = ArchitectureSpec.model_validate(node_input)
    yield RequestInput(
        message=_render_architecture(architecture),
        payload=architecture.model_dump(),
        response_schema=str,
    )


def _is_approved(response: str) -> bool:
    return response.strip().upper() in {"APPROVE", "APPROVED", "YES"}


def _slugify(value: str) -> str:
    slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
    return slug or "generated-project"


def _safe_relative_path(value: str) -> Path:
    path = Path(value)
    if path.is_absolute() or not value.strip() or ".." in path.parts:
        raise ValueError(f"Unsafe generated path: {value!r}")
    return path


@node(name="autonomous_factory", rerun_on_resume=True)
async def autonomous_factory(ctx: Context, node_input: str) -> dict:
    requirements = RequirementsSpec.model_validate(
        await ctx.run_node(requirements_agent, node_input=node_input)
    )

    revision = 1
    previous_architecture: dict | None = None
    feedback = "Create the first architecture proposal."

    while True:
        architecture_input = {
            "requirements": requirements.model_dump(),
            "supported_profiles": list(SUPPORTED_PROFILES),
            "previous_architecture": previous_architecture,
            "latest_user_feedback": feedback,
        }
        architecture_data = dict(
            await ctx.run_node(
                architecture_agent,
                node_input=architecture_input,
            )
        )
        architecture_data["revision"] = revision
        architecture = ArchitectureSpec.model_validate(architecture_data)

        review_response = await ctx.run_node(
            request_architecture_review,
            node_input=architecture.model_dump(),
        )
        if _is_approved(str(review_response)):
            break

        previous_architecture = architecture.model_dump()
        feedback = str(review_response)
        revision += 1


root_agent = Workflow(
    name="polyglot_project_factory",
    edges=[("START", autonomous_factory)],
)

Use this reference to compare the complete cumulative file. Keep the existing contracts and guard helpers unchanged.

Reject and approve an architecture

The interactive run now proves both sides of the gate. You will reject the first draft before approving a higher revision.

Before you run the workflow, do you expect it to create project files before you approve the architecture?

  • Start the interactive factory from the activated terminal by running:
adk run factory

What happens first?

The terminal interface waits for your project idea. The Requirements Agent processes that idea before the Architecture Agent presents revision 1.

  • Enter make a habit tracker using Node.js as your project idea.

You should see structured requirements followed by Architecture revision 1. The proposal should use the registered Node.js profile.

Before you send feedback, do you think the workflow will finish or present a newly numbered draft?

  • Enter use a static web app instead as revision feedback.

You should see Architecture revision 2 with the updated stack decision. The project remains inside architecture review.

  • Enter APPROVE to accept revision 2.

The session should leave the review loop without presenting another architecture draft. No Developer Agent or project-writing node runs in this version.

Does the revision number stay at 1?

  • Confirm that revision += 1 remains inside the rejection path.
  • Check that previous_architecture receives the current architecture before the next loop iteration.
  • Verify that the parent decorator uses rerun_on_resume=True.

Help me debug why my architecture revision loop does not resume correctly.

You now control the architecture before implementation begins. Next up, the Developer Agent will turn an approved plan into real files and deterministic test results.

Build and Check Approved Projects

Your review loop now keeps implementation locked until the user approves an architecture. That gate is doing its job: no generated project can appear before approval.

An approved plan is still documentation. In this step, the Developer Agent turns that plan into real files. Deterministic nodes then confine those files to one workspace and run the registered checks.

In this step, get ready to:
  • Define the complete project bundle contract for the Developer Agent.
  • Confine generated files to the approved output directory.
  • Run deterministic checks through profile-specific adapters.
Define the generated project contract

The Developer Agent needs a structured output that represents an entire project. A complete ProjectBundle gives the writer every file in one validated object.

  • In factory/agent.py, add the missing standard library imports below import re by pasting:
import shutil
import subprocess
import sys

Why These Imports?

  • The shutil module clears an older generated project before replacing it.
  • The subprocess module runs each allowlisted check without invoking a shell.
  • The sys module points Python projects at the active virtual environment interpreter.
  • In factory/agent.py, add the bundle limits and output contracts below the existing workflow constants by pasting:
MAX_FILES = 40
MAX_BUNDLE_CHARACTERS = 250_000
COMMAND_TIMEOUT_SECONDS = 30
BASE_OUTPUT_DIR = (Path.cwd() / "generated_projects").resolve()


class GeneratedFile(BaseModel):
    path: str
    content: str


class ProjectBundle(BaseModel):
    project_name: str
    profile: Profile
    files: list[GeneratedFile]


class WorkspaceInfo(BaseModel):
    project_path: str
    profile: Profile
    file_count: int


class CheckResult(BaseModel):
    passed: bool
    commands: list[str]
    stdout: str
    stderr: str

What Do These Contracts Control?

  • The size limits stop one model response from creating an excessive number of files or characters.
  • The ProjectBundle contract carries every generated path with its complete content.
  • The WorkspaceInfo contract records where the writer placed the project.
  • The CheckResult contract preserves the commands and their captured evidence.
  • Save factory/agent.py.
  • Confirm the new contracts import cleanly by running:
python -m unittest -v

What Does This Check Prove?

The guard suite imports factory.agent before running its tests. A successful run proves the new contracts have valid Python syntax.

You should see all five guard tests pass. The final line should show OK.

Seeing an Import or Name Error?

  • Check that the three imports sit with the other standard library imports near the top of factory/agent.py.
  • Check that every contract sits below the existing BaseModel import.

Ask for help with the exact traceback: Help me diagnose an import or name error after adding the project bundle contracts to factory/agent.py.

With the contracts in place, a structured output can carry a whole micro-project without patches or omitted files.

  • In factory/agent.py, add the Developer Agent below architecture_agent by pasting:
developer_agent = Agent(
    name="developer_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Generate the complete project described by the approved architecture. Return every
file in one ProjectBundle. Include a README and meaningful automated tests. Do not
return patches, explanations, markdown fences, secrets, placeholders, or omitted
files. Keep the project small and dependency-free.

Profile rules:
- python: use only the Python standard library and put unittest tests under tests/.
- node: use only Node.js built-ins and create at least one *.test.js file for node --test.
- static-web: create index.html, CSS, JavaScript, and at least one *.test.js file
  that tests pure application logic with Node.js built-ins.
""",
    output_schema=ProjectBundle,
)

What Does the Developer Agent Guarantee?

  • The instruction requires a full replacement bundle instead of a patch.
  • Each supported profile stays dependency-free.
  • Every generated project must include documentation and meaningful tests.
  • The output schema makes Pydantic validate the bundle before the writer receives it.
  • Save factory/agent.py.
  • Confirm the Developer Agent imports successfully by running:
python -m unittest -v

What Does a Passing Run Mean?

A passing run means the new Agent declaration and its ProjectBundle schema load correctly. The five existing guard tests should still end with OK.

Seeing a Schema Error?

  • Confirm that output_schema points to ProjectBundle.
  • Confirm that the closing parenthesis sits below the instruction string.

Review the declaration with AI help: Help me fix the Developer Agent schema declaration in factory/agent.py.

Confine writes and register checks

Generated paths come from a model response, so the writer treats every path as untrusted input. Path confinement resolves each destination before any content is written.

Why Custom Nodes Instead of Arbitrary Commands?

A custom writer limits output to generated_projects/. Registered adapters also prevent generated projects from inventing commands for the host machine.

These controls narrow the factory's authority to known paths and fixed argument arrays. That boundary makes every write and check auditable.

  • In factory/agent.py, find _slugify().
  • Add the path validator directly below it by pasting:
def _safe_relative_path(value: str) -> Path:
    path = Path(value)
    if path.is_absolute() or not value.strip() or ".." in path.parts:
        raise ValueError(f"Unsafe generated path: {value!r}")
    return path

What Does the Path Validator Reject?

The validator rejects absolute paths and empty values. It also rejects any path containing a parent traversal component.

A safe nested path such as src/app.js remains available to the writer.

  • Save factory/agent.py.
  • Run the guard suite to exercise the new path validator:
python -m unittest -v

What Should Pass Now?

The nested-path test should pass. The traversal and absolute-path tests should also pass because those unsafe values raise ValueError.

You should see all five tests finish with OK. Your path boundary now has direct test coverage.

The writer is a little fiddly because it validates the project root and each file destination. Add both snippets before saving the function.

  • Add the first part of write_project_bundle below _safe_relative_path() by pasting:
@node(name="write_project_bundle")
def write_project_bundle(node_input: dict) -> dict:
    bundle = ProjectBundle.model_validate(node_input)
    if not 1 <= len(bundle.files) <= MAX_FILES:
        raise ValueError(f"A bundle must contain between 1 and {MAX_FILES} files.")

    total_characters = sum(len(item.content) for item in bundle.files)
    if total_characters > MAX_BUNDLE_CHARACTERS:
        raise ValueError("Generated bundle exceeds the configured size limit.")

    BASE_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    project_root = (BASE_OUTPUT_DIR / _slugify(bundle.project_name)).resolve()
    if BASE_OUTPUT_DIR not in project_root.parents:
        raise ValueError("Project root escaped the generated project directory.")

    if project_root.exists():
        shutil.rmtree(project_root)
    project_root.mkdir(parents=True)

What Does the First Half Check?

  • The bundle must contain between one file and the configured maximum.
  • The combined file content must stay under the character limit.
  • The resolved project root must remain beneath generated_projects/.
  • An older folder with the same slug is replaced before the new bundle is written.
  • Continue the same write_project_bundle function directly below project_root.mkdir(parents=True) by pasting:
    seen_paths: set[Path] = set()
    for generated_file in bundle.files:
        relative_path = _safe_relative_path(generated_file.path)
        if relative_path in seen_paths:
            raise ValueError(f"Duplicate generated path: {relative_path}")
        seen_paths.add(relative_path)

        destination = (project_root / relative_path).resolve()
        if project_root not in destination.parents:
            raise ValueError(f"Generated path escaped its project: {relative_path}")
        destination.parent.mkdir(parents=True, exist_ok=True)
        destination.write_text(generated_file.content, encoding="utf-8")

    return WorkspaceInfo(
        project_path=str(project_root),
        profile=bundle.profile,
        file_count=len(bundle.files),
    ).model_dump()

How Does the Writer Finish Safely?

  • A set catches duplicate relative paths before one file can overwrite another.
  • Each resolved destination must remain beneath the resolved project folder.
  • Parent folders are created only after the destination passes confinement.
  • The returned WorkspaceInfo gives later nodes a validated path and profile.
  • Save factory/agent.py.
  • Check the completed writer for syntax errors by running:
python -m unittest -v

What Does This Run Confirm?

The import succeeds only when both writer snippets form one valid function. You should see the five guard tests finish with OK.

Writer Function Not Importing?

  • Check that seen_paths has four leading spaces.
  • Check that the body beneath the for statement has eight leading spaces.
  • Check that return WorkspaceInfo aligns with seen_paths.

Use the traceback to inspect the indentation: Help me repair the indentation in write_project_bundle without changing its path checks.

The checker runs fixed argument lists through subprocess. It captures output and enforces a timeout without passing generated text to a shell.

  • Add the subprocess helper below write_project_bundle by pasting:
def _run_command(args: list[str], cwd: Path) -> tuple[int, str, str]:
    try:
        completed = subprocess.run(
            args,
            cwd=cwd,
            capture_output=True,
            text=True,
            timeout=COMMAND_TIMEOUT_SECONDS,
            check=False,
            shell=False,
        )
        return completed.returncode, completed.stdout, completed.stderr
    except FileNotFoundError as error:
        return 127, "", str(error)
    except subprocess.TimeoutExpired as error:
        return 124, error.stdout or "", error.stderr or "Command timed out."

What Does the Helper Capture?

  • The argument list selects a registered executable without shell parsing.
  • The working directory keeps each check inside the generated project.
  • The timeout bounds how long one check can occupy the factory.
  • Missing commands and timed-out commands become deterministic return codes.
  • Save factory/agent.py.
  • Confirm the subprocess helper imports by running:
python -m unittest -v

What Should the Suite Show?

The existing guards should still pass because the helper performs no work during import. You should see five passing tests and OK.

The profile checker is another long function, so add its adapter selection first. Complete it with the result collection snippet before saving.

  • Add the first part of run_project_checks below _run_command() by pasting:
@node(name="run_project_checks")
def run_project_checks(node_input: dict) -> dict:
    workspace = WorkspaceInfo.model_validate(node_input)
    project_root = Path(workspace.project_path).resolve()
    if BASE_OUTPUT_DIR not in project_root.parents:
        raise ValueError("Check target is outside the generated project directory.")

    commands: list[list[str]] = []
    preflight_errors: list[str] = []

    if workspace.profile == "python":
        if not (project_root / "tests").is_dir():
            preflight_errors.append("Python profile requires a tests directory.")
        commands.append(
            [sys.executable, "-m", "unittest", "discover", "-s", "tests", "-v"]
        )
    else:
        javascript_files = sorted(project_root.rglob("*.js"))
        if not javascript_files:
            preflight_errors.append("JavaScript profile requires at least one .js file.")
        for script in javascript_files:
            commands.append(
                ["node", "--check", script.relative_to(project_root).as_posix()]
            )

How Are Adapters Selected?

  • Python projects must contain a tests directory.
  • The Python adapter invokes unittest discovery with the active interpreter.
  • Node.js and static web projects must contain JavaScript files.
  • Every discovered JavaScript file receives a syntax check.
  • Continue the same run_project_checks function directly below the JavaScript syntax-check command by pasting:
        test_files = sorted(project_root.rglob("*.test.js"))
        if not test_files:
            preflight_errors.append("JavaScript profile requires a *.test.js file.")
        commands.append(["node", "--test"])
        if workspace.profile == "static-web" and not (
            project_root / "index.html"
        ).is_file():
            preflight_errors.append("Static web profile requires index.html.")

    command_labels = [" ".join(command) for command in commands]
    stdout_parts: list[str] = []
    stderr_parts = list(preflight_errors)
    passed = not preflight_errors

    for command in commands:
        return_code, stdout, stderr = _run_command(command, project_root)
        label = " ".join(command)
        stdout_parts.append(f"$ {label}\n{stdout}".rstrip())
        if stderr:
            stderr_parts.append(f"$ {label}\n{stderr}".rstrip())
        if return_code != 0:
            passed = False

    return CheckResult(
        passed=passed,
        commands=command_labels,
        stdout="\n\n".join(stdout_parts),
        stderr="\n\n".join(stderr_parts),
    ).model_dump()

How Is the Evidence Preserved?

  • JavaScript profiles must provide at least one discovered test file.
  • Static web projects must also provide index.html.
  • Every command contributes its captured standard output and standard error.
  • A preflight failure or nonzero return code makes the complete check result fail.
  • Save factory/agent.py.
  • Confirm the completed checker imports cleanly by running:
python -m unittest -v

What Does the Green Suite Prove?

A green suite proves that the writer and all three adapters load without breaking the existing approval guards. You should see five tests finish with OK.

Checker Function Failing to Import?

  • Confirm that the second snippet remains inside run_project_checks.
  • Confirm that test_files aligns with javascript_files.
  • Confirm that return CheckResult aligns with command_labels.

Trace the failing line with AI help: Help me fix run_project_checks while preserving its Python, Node.js, and static web adapters.

Connect approval to build and verify

The new nodes must remain behind the approval gate. The orchestrator calls the Developer Agent only after _is_approved() ends the review loop.

  • In autonomous_factory(), find the end of the architecture review loop.
  • Add the build-and-check sequence directly after the loop by pasting:
    bundle = ProjectBundle.model_validate(
        await ctx.run_node(
            developer_agent,
            node_input={
                "requirements": requirements.model_dump(),
                "approved_architecture": architecture.model_dump(),
            },
        )
    )
    workspace = WorkspaceInfo.model_validate(
        await ctx.run_node(write_project_bundle, node_input=bundle.model_dump())
    )
    checks = CheckResult.model_validate(
        await ctx.run_node(run_project_checks, node_input=workspace.model_dump())
    )

What Does This Sequence Enforce?

  • The Developer Agent receives the original requirements and the approved architecture.
  • The writer receives only a validated ProjectBundle.
  • The checker receives only a validated WorkspaceInfo.
  • No build node appears inside the review loop, so rejected architectures cannot create project files.
  • Save factory/agent.py.

✔️ Awesome, I've got everything!

Great. Double-check that you saved factory/agent.py before running the factory.

ⓧ I'd like to double check the full code

from __future__ import annotations

import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Literal

from google.adk import Agent, Context, Workflow
from google.adk.events import RequestInput
from google.adk.workflow import node
from pydantic import BaseModel

MODEL = "gemini-3.8-flash"
SUPPORTED_PROFILES = ("python", "node", "static-web")
MAX_FILES = 40
MAX_BUNDLE_CHARACTERS = 250_000
COMMAND_TIMEOUT_SECONDS = 30
BASE_OUTPUT_DIR = (Path.cwd() / "generated_projects").resolve()

Profile = Literal["python", "node", "static-web"]


class RequirementsSpec(BaseModel):
    title: str
    summary: str
    capabilities: list[str]
    constraints: list[str]
    requested_technologies: list[str]


class ArchitectureSpec(BaseModel):
    revision: int
    project_name: str
    selected_profile: Profile
    technology_stack: list[str]
    components: list[str]
    data_flow: list[str]
    file_plan: list[str]
    test_strategy: list[str]
    run_instructions: list[str]
    compromises: list[str]


class GeneratedFile(BaseModel):
    path: str
    content: str


class ProjectBundle(BaseModel):
    project_name: str
    profile: Profile
    files: list[GeneratedFile]


class WorkspaceInfo(BaseModel):
    project_path: str
    profile: Profile
    file_count: int


class CheckResult(BaseModel):
    passed: bool
    commands: list[str]
    stdout: str
    stderr: str


requirements_agent = Agent(
    name="requirements_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Convert the user's project idea into a small, testable micro-project specification.
Preserve every technology suggestion in requested_technologies, even when it may
not fit the registered build profiles. Do not choose the final architecture.
Return only data matching RequirementsSpec.
""",
    output_schema=RequirementsSpec,
)


architecture_agent = Agent(
    name="architecture_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Design or revise a complete architecture from the input data. The only executable
profiles are python, node, and static-web. Honor the user's requested technologies
and latest feedback when they fit a registered profile. If they do not fit, choose
the closest safe profile and list the exact compromise instead of pretending the
technology is supported. Include a concrete file plan, data flow, test strategy,
and run instructions. Return only data matching ArchitectureSpec. The workflow
sets the authoritative revision number after your response.
""",
    output_schema=ArchitectureSpec,
)


developer_agent = Agent(
    name="developer_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Generate the complete project described by the approved architecture. Return every
file in one ProjectBundle. Include a README and meaningful automated tests. Do not
return patches, explanations, markdown fences, secrets, placeholders, or omitted
files. Keep the project small and dependency-free.

Profile rules:
- python: use only the Python standard library and put unittest tests under tests/.
- node: use only Node.js built-ins and create at least one *.test.js file for node --test.
- static-web: create index.html, CSS, JavaScript, and at least one *.test.js file
  that tests pure application logic with Node.js built-ins.
""",
    output_schema=ProjectBundle,
)


def _render_architecture(architecture: ArchitectureSpec) -> str:
    sections = [
        f"Architecture revision {architecture.revision}",
        f"Project: {architecture.project_name}",
        f"Profile: {architecture.selected_profile}",
        "Stack: " + ", ".join(architecture.technology_stack),
        "Components: " + "; ".join(architecture.components),
        "Data flow: " + " -> ".join(architecture.data_flow),
        "Files: " + ", ".join(architecture.file_plan),
        "Tests: " + "; ".join(architecture.test_strategy),
        "Run: " + "; ".join(architecture.run_instructions),
        "Compromises: "
        + ("; ".join(architecture.compromises) or "None"),
        "Reply APPROVE to lock this architecture, or send revision feedback.",
    ]
    return "\n\n".join(sections)


@node(name="request_architecture_review", rerun_on_resume=False)
async def request_architecture_review(
    ctx: Context, node_input: dict
):
    del ctx
    architecture = ArchitectureSpec.model_validate(node_input)
    yield RequestInput(
        message=_render_architecture(architecture),
        payload=architecture.model_dump(),
        response_schema=str,
    )


def _is_approved(response: str) -> bool:
    return response.strip().upper() in {"APPROVE", "APPROVED", "YES"}


def _slugify(value: str) -> str:
    slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
    return slug or "generated-project"


def _safe_relative_path(value: str) -> Path:
    path = Path(value)
    if path.is_absolute() or not value.strip() or ".." in path.parts:
        raise ValueError(f"Unsafe generated path: {value!r}")
    return path


@node(name="write_project_bundle")
def write_project_bundle(node_input: dict) -> dict:
    bundle = ProjectBundle.model_validate(node_input)
    if not 1 <= len(bundle.files) <= MAX_FILES:
        raise ValueError(f"A bundle must contain between 1 and {MAX_FILES} files.")

    total_characters = sum(len(item.content) for item in bundle.files)
    if total_characters > MAX_BUNDLE_CHARACTERS:
        raise ValueError("Generated bundle exceeds the configured size limit.")

    BASE_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    project_root = (BASE_OUTPUT_DIR / _slugify(bundle.project_name)).resolve()
    if BASE_OUTPUT_DIR not in project_root.parents:
        raise ValueError("Project root escaped the generated project directory.")

    if project_root.exists():
        shutil.rmtree(project_root)
    project_root.mkdir(parents=True)

    seen_paths: set[Path] = set()
    for generated_file in bundle.files:
        relative_path = _safe_relative_path(generated_file.path)
        if relative_path in seen_paths:
            raise ValueError(f"Duplicate generated path: {relative_path}")
        seen_paths.add(relative_path)

        destination = (project_root / relative_path).resolve()
        if project_root not in destination.parents:
            raise ValueError(f"Generated path escaped its project: {relative_path}")
        destination.parent.mkdir(parents=True, exist_ok=True)
        destination.write_text(generated_file.content, encoding="utf-8")

    return WorkspaceInfo(
        project_path=str(project_root),
        profile=bundle.profile,
        file_count=len(bundle.files),
    ).model_dump()


def _run_command(args: list[str], cwd: Path) -> tuple[int, str, str]:
    try:
        completed = subprocess.run(
            args,
            cwd=cwd,
            capture_output=True,
            text=True,
            timeout=COMMAND_TIMEOUT_SECONDS,
            check=False,
            shell=False,
        )
        return completed.returncode, completed.stdout, completed.stderr
    except FileNotFoundError as error:
        return 127, "", str(error)
    except subprocess.TimeoutExpired as error:
        return 124, error.stdout or "", error.stderr or "Command timed out."


@node(name="run_project_checks")
def run_project_checks(node_input: dict) -> dict:
    workspace = WorkspaceInfo.model_validate(node_input)
    project_root = Path(workspace.project_path).resolve()
    if BASE_OUTPUT_DIR not in project_root.parents:
        raise ValueError("Check target is outside the generated project directory.")

    commands: list[list[str]] = []
    preflight_errors: list[str] = []

    if workspace.profile == "python":
        if not (project_root / "tests").is_dir():
            preflight_errors.append("Python profile requires a tests directory.")
        commands.append(
            [sys.executable, "-m", "unittest", "discover", "-s", "tests", "-v"]
        )
    else:
        javascript_files = sorted(project_root.rglob("*.js"))
        if not javascript_files:
            preflight_errors.append("JavaScript profile requires at least one .js file.")
        for script in javascript_files:
            commands.append(
                ["node", "--check", script.relative_to(project_root).as_posix()]
            )
        test_files = sorted(project_root.rglob("*.test.js"))
        if not test_files:
            preflight_errors.append("JavaScript profile requires a *.test.js file.")
        commands.append(["node", "--test"])
        if workspace.profile == "static-web" and not (
            project_root / "index.html"
        ).is_file():
            preflight_errors.append("Static web profile requires index.html.")

    command_labels = [" ".join(command) for command in commands]
    stdout_parts: list[str] = []
    stderr_parts = list(preflight_errors)
    passed = not preflight_errors

    for command in commands:
        return_code, stdout, stderr = _run_command(command, project_root)
        label = " ".join(command)
        stdout_parts.append(f"$ {label}\n{stdout}".rstrip())
        if stderr:
            stderr_parts.append(f"$ {label}\n{stderr}".rstrip())
        if return_code != 0:
            passed = False

    return CheckResult(
        passed=passed,
        commands=command_labels,
        stdout="\n\n".join(stdout_parts),
        stderr="\n\n".join(stderr_parts),
    ).model_dump()


@node(name="autonomous_factory", rerun_on_resume=True)
async def autonomous_factory(ctx: Context, node_input: str) -> dict:
    requirements = RequirementsSpec.model_validate(
        await ctx.run_node(requirements_agent, node_input=node_input)
    )

    revision = 1
    previous_architecture: dict | None = None
    feedback = "Create the first architecture proposal."

    while True:
        architecture_input = {
            "requirements": requirements.model_dump(),
            "supported_profiles": list(SUPPORTED_PROFILES),
            "previous_architecture": previous_architecture,
            "latest_user_feedback": feedback,
        }
        architecture_data = dict(
            await ctx.run_node(
                architecture_agent,
                node_input=architecture_input,
            )
        )
        architecture_data["revision"] = revision
        architecture = ArchitectureSpec.model_validate(architecture_data)

        review_response = await ctx.run_node(
            request_architecture_review,
            node_input=architecture.model_dump(),
        )
        if _is_approved(str(review_response)):
            break

        previous_architecture = architecture.model_dump()
        feedback = str(review_response)
        revision += 1

    bundle = ProjectBundle.model_validate(
        await ctx.run_node(
            developer_agent,
            node_input={
                "requirements": requirements.model_dump(),
                "approved_architecture": architecture.model_dump(),
            },
        )
    )
    workspace = WorkspaceInfo.model_validate(
        await ctx.run_node(write_project_bundle, node_input=bundle.model_dump())
    )
    checks = CheckResult.model_validate(
        await ctx.run_node(run_project_checks, node_input=workspace.model_dump())
    )


root_agent = Workflow(
    name="polyglot_project_factory",
    edges=[("START", autonomous_factory)],
)

Use this reference to compare the cumulative build-and-check implementation. It intentionally stops after CheckResult because QA and repair belong to the next step.

Keep Generated Tests Local

The next run executes code created from your own prompt on your development machine. Keep this factory local and use only prompts you trust.

Path confinement limits where files are written, but it does not isolate the generated test process. Public execution needs a dedicated sandbox.

Before you run the factory, do you expect any project files to appear before you approve the architecture?

  • Start the interactive factory after making your prediction by running:
adk run factory

What Does This Command Start?

The command starts the Google Agent Development Kit terminal interface for the factory package. Your request enters the dynamic workflow as its initial input.

  • Enter Build a command-line habit tracker in Python as the project idea.
  • Review the returned Python architecture.
  • Reply with APPROVE when the proposal includes source files and tests.

The workflow should remain paused until your approval. After approval, the Developer Agent creates the bundle under generated_projects/<project-name>/.

What Should the Check Report Contain?

The Python adapter records this command in the commands field:

python -m unittest discover -s tests -v

The command discovers the generated tests beneath tests/. Its captured output should finish with OK when the generated project passes.

The result should also show an empty or non-failing stderr value and a true passed value.

  • In the Visual Studio Code Explorer sidebar from earlier, expand generated_projects/.
  • Expand the generated folder named in project_path.

You should see a README, source files, and a tests directory in the generated project. That is the hard proof that approval now unlocks a complete checked build.

No Generated Project or Passing Check?

  • Confirm that your approval reply contains only APPROVE.
  • Check the stderr evidence for a missing tests directory or a failing generated test.
  • Check that the generated path begins beneath the resolved generated_projects/ directory.

Share the non-secret check evidence with AI help: Help me diagnose why my approved Python project was not written or did not pass its deterministic checks.

Your approved architecture now becomes a confined project with deterministic test evidence. Next, you will add an independent QA review and bounded repairs before any build can claim release.

Add QA, Repair, and Release Gates

Your confined writer now creates projects only inside generated_projects/. Your deterministic checks also prove whether each registered profile passes its allowlisted commands.

A passing command can still hide a project that misses its approved architecture. In this step, you will add independent quality assurance, bounded repairs, and an honest release decision.

In this step, get ready to:
  • Add an independent QA Agent that reviews architecture compliance.
  • Route failed checks or QA findings through at most two repairs.
  • Return a released or blocked report with the final evidence.
Add independent QA review

Deterministic checks answer whether code executes successfully. The QA Agent answers whether the generated bundle matches the approved architecture.

A structured QaReport gives the orchestrator a strict pass state. It also carries a summary plus actionable findings for a repair attempt.

  • In factory/agent.py, locate the existing CheckResult contract.
  • Add the QaReport contract directly below it by pasting this code:
class QaReport(BaseModel):
    passed: bool
    summary: str
    findings: list[str]

What does this contract capture?

  • The passed field gives the workflow a strict QA decision.
  • The summary field explains the overall review result.
  • The findings field gives the Repair Agent specific problems to correct.
  • Save factory/agent.py.
  • Verify that the new contract loads by running this command:
python -m unittest

What does this check prove?

The test run imports factory.agent before exercising the existing guard tests. A successful run proves the new contract has valid Python syntax.

You should see all five factory guard tests pass. Good progress. The factory now has a structured format for independent QA evidence.

Seeing an import or syntax error?

  • Check that QaReport sits below the complete CheckResult class.
  • Confirm each field inside QaReport uses four spaces of indentation.

Ask for help with the contract error.

The QA Agent receives three complete views of the candidate release. Those views are the approved architecture, the generated bundle, and the deterministic check result.

  • In factory/agent.py, locate the end of the existing developer_agent definition.
  • Add the qa_agent definition directly below it by pasting this code:
qa_agent = Agent(
    name="qa_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Act as an independent release reviewer. Compare the approved architecture, the
complete ProjectBundle, and the deterministic CheckResult. Pass only when the
profile matches, promised files and capabilities exist, tests are meaningful,
README run instructions are accurate, no placeholders remain, and deterministic
checks passed. Return concise actionable findings using QaReport.
""",
    output_schema=QaReport,
)

How does the QA Agent judge a release?

The agent compares the approved plan with the complete output. It also checks the deterministic evidence before returning data that matches QaReport.

Its pass decision covers profile alignment, promised capabilities, meaningful tests, accurate run instructions, and placeholder removal. This makes architecture compliance part of the release gate.

  • Save factory/agent.py.
  • Verify that the QA Agent configuration imports by running this command:
python -m unittest

What does this check prove?

The test run imports the new qa_agent definition. Passing tests confirm that its model, mode, instruction, and output contract are wired with valid syntax.

You should see the five guard tests pass again. The independent reviewer is now defined and ready to join the workflow.

Does the QA Agent fail to import?

  • Confirm the qa_agent definition appears after QaReport has been declared.
  • Check that the instruction starts and ends with three double quotes.
  • Confirm output_schema points to QaReport.

Ask for help with the QA Agent definition.

Add the bounded repair loop

A failed review needs a controlled response. A bounded repair loop gives the Repair Agent limited opportunities to replace the complete bundle.

The limit prevents an endless cycle of generation attempts. Every replacement bundle goes through the same confined writer, deterministic checks, and independent QA review.

  • In factory/agent.py, locate the constant block that starts with MODEL.
  • Replace that constant block with this final version:
MODEL = "gemini-3.8-flash"
SUPPORTED_PROFILES = ("python", "node", "static-web")
MAX_REPAIR_ROUNDS = 2
MAX_FILES = 40
MAX_BUNDLE_CHARACTERS = 250_000
COMMAND_TIMEOUT_SECONDS = 30
BASE_OUTPUT_DIR = (Path.cwd() / "generated_projects").resolve()

Why cap the repair count?

The value MAX_REPAIR_ROUNDS = 2 gives the orchestrator a deterministic stopping point. Persistent failures reach a blocked release instead of consuming unlimited model calls.

The remaining limits continue to protect bundle size, file count, command duration, and output location. Repairs receive the same boundaries as first-generation bundles.

  • Save factory/agent.py.
  • Verify that the updated constants load by running this command:
python -m unittest

What does this check prove?

The import now evaluates the complete constant block. Passing tests confirm that the repair limit exists without breaking the earlier guard behavior.

You should see all five tests pass. The workflow now has a hard ceiling of two repair rounds.

Do the constants trigger an error?

  • Check that MAX_REPAIR_ROUNDS appears once in the constant block.
  • Confirm its value is the integer 2.
  • Keep BASE_OUTPUT_DIR below COMMAND_TIMEOUT_SECONDS.

Ask for help with the repair limit.

The Repair Agent receives the failed evidence plus the full current bundle. It returns a full replacement bundle so the writer can apply the same path and size controls from a clean state.

  • In factory/agent.py, locate the end of the qa_agent definition.
  • Add the repair_agent definition directly below it by pasting this code:
repair_agent = Agent(
    name="repair_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Repair the complete project using the approved architecture, current ProjectBundle,
deterministic check output, and QA findings. Return a full replacement ProjectBundle,
not a diff. Preserve the project_name and profile. Keep the project dependency-free
and remove every reported failure without weakening or deleting meaningful tests.
""",
    output_schema=ProjectBundle,
)

Why return a complete bundle?

A complete ProjectBundle gives the writer one authoritative replacement state. It avoids partial patches that could leave stale files behind.

The instruction also protects meaningful tests. A repair must fix the project without hiding failures by weakening the evidence.

  • Save factory/agent.py.
  • Verify that the Repair Agent configuration imports by running this command:
python -m unittest

What does this check prove?

The test run imports repair_agent with its ProjectBundle output contract. Passing tests confirm that the second new specialist is available to the orchestrator.

You should see the five tests pass. The factory now defines all five specialist agents.

Does the Repair Agent fail to load?

  • Confirm the definition appears after qa_agent.
  • Check that output_schema uses the existing ProjectBundle contract.
  • Confirm the multiline instruction closes before the final Agent parenthesis.

Ask for help with the Repair Agent definition.

The first QA pass must happen immediately after the initial deterministic checks. This gives the workflow both forms of evidence before it decides whether a repair is necessary.

  • Inside autonomous_factory, locate the existing assignment that stores the initial CheckResult.
  • Add the initial QA invocation directly below that assignment by pasting this code:
    qa = QaReport.model_validate(
        await ctx.run_node(
            qa_agent,
            node_input={
                "approved_architecture": architecture.model_dump(),
                "project_bundle": bundle.model_dump(),
                "check_result": checks.model_dump(),
            },
        )
    )

What reaches the first QA pass?

  • The approved ArchitectureSpec records what the user accepted.
  • The complete ProjectBundle records what the Developer Agent produced.
  • The CheckResult records the commands, output streams, and pass state.
  • Save factory/agent.py.
  • Verify that the first QA invocation has valid syntax by running this command:
python -m unittest

What does this check prove?

The test run imports the updated autonomous_factory function. Passing tests confirm that its first QA child invocation is structurally valid.

You should see the five tests pass. Every initial bundle now reaches an independent review after deterministic checking.

Does the QA invocation break the import?

  • Confirm the entire block remains inside autonomous_factory with four spaces before qa.
  • Check that each input calls model_dump() on the matching structured object.
  • Confirm the closing parentheses align with the opening QaReport.model_validate( call.

Ask for help with the first QA invocation.

A repair begins when deterministic checks fail or QA rejects the candidate. The workflow then rewrites the complete bundle and reruns the registered adapter against the replacement files.

  • Inside autonomous_factory, locate the end of the initial qa assignment.
  • Add the bounded repair and recheck block directly below it by pasting this code:
    repair_rounds = 0
    while (not checks.passed or not qa.passed) and repair_rounds < MAX_REPAIR_ROUNDS:
        repair_rounds += 1
        bundle = ProjectBundle.model_validate(
            await ctx.run_node(
                repair_agent,
                node_input={
                    "approved_architecture": architecture.model_dump(),
                    "current_project_bundle": bundle.model_dump(),
                    "check_result": checks.model_dump(),
                    "qa_report": qa.model_dump(),
                    "repair_round": repair_rounds,
                },
            )
        )
        workspace = WorkspaceInfo.model_validate(
            await ctx.run_node(write_project_bundle, node_input=bundle.model_dump())
        )
        checks = CheckResult.model_validate(
            await ctx.run_node(run_project_checks, node_input=workspace.model_dump())
        )

How is the repair loop bounded?

The loop starts only when checks or QA fail. The second condition stops execution once repair_rounds reaches MAX_REPAIR_ROUNDS.

Each repair returns a complete bundle. The workflow writes that replacement through write_project_bundle before running the allowlisted checks again.

  • Save factory/agent.py.
  • Verify that the repair and recheck block imports by running this command:
python -m unittest

What does this check prove?

The test run imports the bounded while loop plus its repair, rewrite, and recheck calls. Passing tests confirm that the control flow has valid syntax.

You should see the five tests pass. A failed candidate can now receive no more than two complete-bundle repairs.

Does the repair loop cause a syntax error?

  • Confirm repair_rounds = 0 aligns with the initial qa assignment.
  • Check that every statement inside the while block has eight spaces of indentation.
  • Confirm the loop condition uses both checks.passed and qa.passed.

Ask for help with the bounded loop.

A successful deterministic recheck still needs architecture review. The loop therefore sends every repaired bundle through the QA Agent before deciding whether to stop.

  • Inside the repair loop, locate the updated checks assignment.
  • Add the repeated QA invocation directly below it by pasting this code:
        qa = QaReport.model_validate(
            await ctx.run_node(
                qa_agent,
                node_input={
                    "approved_architecture": architecture.model_dump(),
                    "project_bundle": bundle.model_dump(),
                    "check_result": checks.model_dump(),
                },
            )
        )

Why review every repair?

A repaired project can pass its commands while drifting away from the approved architecture. Repeating QA catches that mismatch before release.

The next loop condition uses the refreshed checks and qa results. The workflow stops early when both pass.

  • Save factory/agent.py.
  • Verify that repeated QA imports correctly by running this command:
python -m unittest

What does this check prove?

The import now covers the complete repair cycle. That cycle repairs, rewrites, rechecks, and reviews each candidate before another iteration can begin.

You should see all five tests pass. The bounded repair loop now refreshes both forms of release evidence.

Does repeated QA fail to import?

  • Confirm the repeated qa assignment remains inside the while block.
  • Check that it aligns with the updated checks assignment.
  • Confirm the input uses the current bundle plus the current checks.

Ask for help with repeated QA.

Enforce the final release decision

A release gate turns the final evidence into an honest outcome. The status becomes released only when deterministic checks and QA both pass.

Every other outcome becomes blocked. The report preserves the project path, approved profile, architecture revision, repair count, QA summary, and complete check evidence.

  • In factory/agent.py, locate the end of the QaReport contract.
  • Add the ReleaseReport contract directly below it by pasting this code:
class ReleaseReport(BaseModel):
    status: Literal["released", "blocked"]
    project_path: str
    profile: Profile
    architecture_revision: int
    repair_rounds: int
    qa_summary: str
    checks: CheckResult

What does the release report preserve?

The report keeps the decision beside the evidence used to make it. Its nested CheckResult retains the command list, standard output, standard error, and pass state.

The architecture revision links the result to the proposal the user approved. The repair count shows how much autonomous correction occurred before the gate closed.

  • Save factory/agent.py.
  • Verify that the release contract loads by running this command:
python -m unittest

What does this check prove?

The test run imports the typed release status plus every evidence field. Passing tests confirm that the final report contract has valid syntax.

You should see the five guard tests pass. The factory now has one structured format for successful and blocked outcomes.

Does the release contract fail to import?

  • Confirm ReleaseReport appears after QaReport.
  • Check that status uses the existing Literal import.
  • Confirm checks uses the existing CheckResult contract.

Ask for help with the release contract.

The final decision belongs after the repair loop. At that point, the latest checks and QA report represent either a passing candidate or the strongest evidence available after two attempts.

  • At the end of autonomous_factory, replace its current deterministic-check return with this final release block:
    release = ReleaseReport(
        status="released" if checks.passed and qa.passed else "blocked",
        project_path=workspace.project_path,
        profile=workspace.profile,
        architecture_revision=architecture.revision,
        repair_rounds=repair_rounds,
        qa_summary=qa.summary,
        checks=checks,
    )
    return release.model_dump()

How does the gate decide?

The conditional status requires both checks.passed and qa.passed to be true. One failure is enough to block the release.

The function returns the complete report as a dictionary. The terminal can therefore show the decision plus the evidence behind it.

  • Save factory/agent.py.
  • Verify the completed module by running this command:
python -m unittest

What does this check prove?

The test run imports the completed five-agent workflow. It also reruns the path, approval, and slug guard tests from the finished factory module.

You should see all five tests pass. That confirms the final module loads without breaking its existing guard behavior.

Do the final guard tests fail?

  • Check that the release block appears after the complete repair loop.
  • Confirm return release.model_dump() remains inside autonomous_factory.
  • Compare factory/agent.py with the full reference below.

Ask for help with the completed workflow.

Use the reference below to compare the completed file before running the full factory.

✔️ Awesome, I've got everything!

Great. Double-check that you saved factory/agent.py before starting the interactive factory.

ⓧ I'd like to double check the full code

from __future__ import annotations

import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Literal

from google.adk import Agent, Context, Workflow
from google.adk.events import RequestInput
from google.adk.workflow import node
from pydantic import BaseModel

MODEL = "gemini-3.8-flash"
SUPPORTED_PROFILES = ("python", "node", "static-web")
MAX_REPAIR_ROUNDS = 2
MAX_FILES = 40
MAX_BUNDLE_CHARACTERS = 250_000
COMMAND_TIMEOUT_SECONDS = 30
BASE_OUTPUT_DIR = (Path.cwd() / "generated_projects").resolve()

Profile = Literal["python", "node", "static-web"]


class RequirementsSpec(BaseModel):
    title: str
    summary: str
    capabilities: list[str]
    constraints: list[str]
    requested_technologies: list[str]


class ArchitectureSpec(BaseModel):
    revision: int
    project_name: str
    selected_profile: Profile
    technology_stack: list[str]
    components: list[str]
    data_flow: list[str]
    file_plan: list[str]
    test_strategy: list[str]
    run_instructions: list[str]
    compromises: list[str]


class GeneratedFile(BaseModel):
    path: str
    content: str


class ProjectBundle(BaseModel):
    project_name: str
    profile: Profile
    files: list[GeneratedFile]


class WorkspaceInfo(BaseModel):
    project_path: str
    profile: Profile
    file_count: int


class CheckResult(BaseModel):
    passed: bool
    commands: list[str]
    stdout: str
    stderr: str


class QaReport(BaseModel):
    passed: bool
    summary: str
    findings: list[str]


class ReleaseReport(BaseModel):
    status: Literal["released", "blocked"]
    project_path: str
    profile: Profile
    architecture_revision: int
    repair_rounds: int
    qa_summary: str
    checks: CheckResult


requirements_agent = Agent(
    name="requirements_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Convert the user's project idea into a small, testable micro-project specification.
Preserve every technology suggestion in requested_technologies, even when it may
not fit the registered build profiles. Do not choose the final architecture.
Return only data matching RequirementsSpec.
""",
    output_schema=RequirementsSpec,
)


architecture_agent = Agent(
    name="architecture_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Design or revise a complete architecture from the input data. The only executable
profiles are python, node, and static-web. Honor the user's requested technologies
and latest feedback when they fit a registered profile. If they do not fit, choose
the closest safe profile and list the exact compromise instead of pretending the
technology is supported. Include a concrete file plan, data flow, test strategy,
and run instructions. Return only data matching ArchitectureSpec. The workflow
sets the authoritative revision number after your response.
""",
    output_schema=ArchitectureSpec,
)


developer_agent = Agent(
    name="developer_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Generate the complete project described by the approved architecture. Return every
file in one ProjectBundle. Include a README and meaningful automated tests. Do not
return patches, explanations, markdown fences, secrets, placeholders, or omitted
files. Keep the project small and dependency-free.

Profile rules:
- python: use only the Python standard library and put unittest tests under tests/.
- node: use only Node.js built-ins and create at least one *.test.js file for node --test.
- static-web: create index.html, CSS, JavaScript, and at least one *.test.js file
  that tests pure application logic with Node.js built-ins.
""",
    output_schema=ProjectBundle,
)


qa_agent = Agent(
    name="qa_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Act as an independent release reviewer. Compare the approved architecture, the
complete ProjectBundle, and the deterministic CheckResult. Pass only when the
profile matches, promised files and capabilities exist, tests are meaningful,
README run instructions are accurate, no placeholders remain, and deterministic
checks passed. Return concise actionable findings using QaReport.
""",
    output_schema=QaReport,
)


repair_agent = Agent(
    name="repair_agent",
    model=MODEL,
    mode="single_turn",
    instruction="""
Repair the complete project using the approved architecture, current ProjectBundle,
deterministic check output, and QA findings. Return a full replacement ProjectBundle,
not a diff. Preserve the project_name and profile. Keep the project dependency-free
and remove every reported failure without weakening or deleting meaningful tests.
""",
    output_schema=ProjectBundle,
)


def _render_architecture(architecture: ArchitectureSpec) -> str:
    sections = [
        f"Architecture revision {architecture.revision}",
        f"Project: {architecture.project_name}",
        f"Profile: {architecture.selected_profile}",
        "Stack: " + ", ".join(architecture.technology_stack),
        "Components: " + "; ".join(architecture.components),
        "Data flow: " + " -> ".join(architecture.data_flow),
        "Files: " + ", ".join(architecture.file_plan),
        "Tests: " + "; ".join(architecture.test_strategy),
        "Run: " + "; ".join(architecture.run_instructions),
        "Compromises: "
        + ("; ".join(architecture.compromises) or "None"),
        "Reply APPROVE to lock this architecture, or send revision feedback.",
    ]
    return "\n\n".join(sections)


@node(name="request_architecture_review", rerun_on_resume=False)
async def request_architecture_review(
    ctx: Context, node_input: dict
):
    del ctx
    architecture = ArchitectureSpec.model_validate(node_input)
    yield RequestInput(
        message=_render_architecture(architecture),
        payload=architecture.model_dump(),
        response_schema=str,
    )


def _is_approved(response: str) -> bool:
    return response.strip().upper() in {"APPROVE", "APPROVED", "YES"}


def _slugify(value: str) -> str:
    slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
    return slug or "generated-project"


def _safe_relative_path(value: str) -> Path:
    path = Path(value)
    if path.is_absolute() or not value.strip() or ".." in path.parts:
        raise ValueError(f"Unsafe generated path: {value!r}")
    return path


@node(name="write_project_bundle")
def write_project_bundle(node_input: dict) -> dict:
    bundle = ProjectBundle.model_validate(node_input)
    if not 1 <= len(bundle.files) <= MAX_FILES:
        raise ValueError(f"A bundle must contain between 1 and {MAX_FILES} files.")

    total_characters = sum(len(item.content) for item in bundle.files)
    if total_characters > MAX_BUNDLE_CHARACTERS:
        raise ValueError("Generated bundle exceeds the configured size limit.")

    BASE_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    project_root = (BASE_OUTPUT_DIR / _slugify(bundle.project_name)).resolve()
    if BASE_OUTPUT_DIR not in project_root.parents:
        raise ValueError("Project root escaped the generated project directory.")

    if project_root.exists():
        shutil.rmtree(project_root)
    project_root.mkdir(parents=True)

    seen_paths: set[Path] = set()
    for generated_file in bundle.files:
        relative_path = _safe_relative_path(generated_file.path)
        if relative_path in seen_paths:
            raise ValueError(f"Duplicate generated path: {relative_path}")
        seen_paths.add(relative_path)

        destination = (project_root / relative_path).resolve()
        if project_root not in destination.parents:
            raise ValueError(f"Generated path escaped its project: {relative_path}")
        destination.parent.mkdir(parents=True, exist_ok=True)
        destination.write_text(generated_file.content, encoding="utf-8")

    return WorkspaceInfo(
        project_path=str(project_root),
        profile=bundle.profile,
        file_count=len(bundle.files),
    ).model_dump()


def _run_command(args: list[str], cwd: Path) -> tuple[int, str, str]:
    try:
        completed = subprocess.run(
            args,
            cwd=cwd,
            capture_output=True,
            text=True,
            timeout=COMMAND_TIMEOUT_SECONDS,
            check=False,
            shell=False,
        )
        return completed.returncode, completed.stdout, completed.stderr
    except FileNotFoundError as error:
        return 127, "", str(error)
    except subprocess.TimeoutExpired as error:
        return 124, error.stdout or "", error.stderr or "Command timed out."


@node(name="run_project_checks")
def run_project_checks(node_input: dict) -> dict:
    workspace = WorkspaceInfo.model_validate(node_input)
    project_root = Path(workspace.project_path).resolve()
    if BASE_OUTPUT_DIR not in project_root.parents:
        raise ValueError("Check target is outside the generated project directory.")

    commands: list[list[str]] = []
    preflight_errors: list[str] = []

    if workspace.profile == "python":
        if not (project_root / "tests").is_dir():
            preflight_errors.append("Python profile requires a tests directory.")
        commands.append(
            [sys.executable, "-m", "unittest", "discover", "-s", "tests", "-v"]
        )
    else:
        javascript_files = sorted(project_root.rglob("*.js"))
        if not javascript_files:
            preflight_errors.append("JavaScript profile requires at least one .js file.")
        for script in javascript_files:
            commands.append(
                ["node", "--check", script.relative_to(project_root).as_posix()]
            )
        test_files = sorted(project_root.rglob("*.test.js"))
        if not test_files:
            preflight_errors.append("JavaScript profile requires a *.test.js file.")
        commands.append(["node", "--test"])
        if workspace.profile == "static-web" and not (
            project_root / "index.html"
        ).is_file():
            preflight_errors.append("Static web profile requires index.html.")

    command_labels = [" ".join(command) for command in commands]
    stdout_parts: list[str] = []
    stderr_parts = list(preflight_errors)
    passed = not preflight_errors

    for command in commands:
        return_code, stdout, stderr = _run_command(command, project_root)
        label = " ".join(command)
        stdout_parts.append(f"$ {label}\n{stdout}".rstrip())
        if stderr:
            stderr_parts.append(f"$ {label}\n{stderr}".rstrip())
        if return_code != 0:
            passed = False

    return CheckResult(
        passed=passed,
        commands=command_labels,
        stdout="\n\n".join(stdout_parts),
        stderr="\n\n".join(stderr_parts),
    ).model_dump()


@node(name="autonomous_factory", rerun_on_resume=True)
async def autonomous_factory(ctx: Context, node_input: str) -> dict:
    requirements = RequirementsSpec.model_validate(
        await ctx.run_node(requirements_agent, node_input=node_input)
    )

    revision = 1
    previous_architecture: dict | None = None
    feedback = "Create the first architecture proposal."

    while True:
        architecture_input = {
            "requirements": requirements.model_dump(),
            "supported_profiles": list(SUPPORTED_PROFILES),
            "previous_architecture": previous_architecture,
            "latest_user_feedback": feedback,
        }
        architecture_data = dict(
            await ctx.run_node(
                architecture_agent,
                node_input=architecture_input,
            )
        )
        architecture_data["revision"] = revision
        architecture = ArchitectureSpec.model_validate(architecture_data)

        review_response = await ctx.run_node(
            request_architecture_review,
            node_input=architecture.model_dump(),
        )
        if _is_approved(str(review_response)):
            break

        previous_architecture = architecture.model_dump()
        feedback = str(review_response)
        revision += 1

    bundle = ProjectBundle.model_validate(
        await ctx.run_node(
            developer_agent,
            node_input={
                "requirements": requirements.model_dump(),
                "approved_architecture": architecture.model_dump(),
            },
        )
    )
    workspace = WorkspaceInfo.model_validate(
        await ctx.run_node(write_project_bundle, node_input=bundle.model_dump())
    )
    checks = CheckResult.model_validate(
        await ctx.run_node(run_project_checks, node_input=workspace.model_dump())
    )
    qa = QaReport.model_validate(
        await ctx.run_node(
            qa_agent,
            node_input={
                "approved_architecture": architecture.model_dump(),
                "project_bundle": bundle.model_dump(),
                "check_result": checks.model_dump(),
            },
        )
    )

    repair_rounds = 0
    while (not checks.passed or not qa.passed) and repair_rounds < MAX_REPAIR_ROUNDS:
        repair_rounds += 1
        bundle = ProjectBundle.model_validate(
            await ctx.run_node(
                repair_agent,
                node_input={
                    "approved_architecture": architecture.model_dump(),
                    "current_project_bundle": bundle.model_dump(),
                    "check_result": checks.model_dump(),
                    "qa_report": qa.model_dump(),
                    "repair_round": repair_rounds,
                },
            )
        )
        workspace = WorkspaceInfo.model_validate(
            await ctx.run_node(write_project_bundle, node_input=bundle.model_dump())
        )
        checks = CheckResult.model_validate(
            await ctx.run_node(run_project_checks, node_input=workspace.model_dump())
        )
        qa = QaReport.model_validate(
            await ctx.run_node(
                qa_agent,
                node_input={
                    "approved_architecture": architecture.model_dump(),
                    "project_bundle": bundle.model_dump(),
                    "check_result": checks.model_dump(),
                },
            )
        )

    release = ReleaseReport(
        status="released" if checks.passed and qa.passed else "blocked",
        project_path=workspace.project_path,
        profile=workspace.profile,
        architecture_revision=architecture.revision,
        repair_rounds=repair_rounds,
        qa_summary=qa.summary,
        checks=checks,
    )
    return release.model_dump()


root_agent = Workflow(
    name="polyglot_project_factory",
    edges=[("START", autonomous_factory)],
)

How should you use this reference?

Compare the imports, contracts, five agent definitions, helper nodes, orchestrator, and final root_agent declaration. Keep your local file identical to this cumulative version.

The final run tests the complete path with a Node.js request. You will revise its architecture before approval so the final report proves that the gate uses the approved revision.

Before you run the factory, do you expect the final status to depend only on the command exit codes or on both checks and QA?

  • Start the interactive factory by running this command:
adk run factory

What does this command start?

The command starts the interactive terminal interface for the factory agent package. The workflow collects your idea before producing an architecture for review.

Model generation can take a little while during development, QA, or repair. A quiet terminal during a response does not mean the workflow has stopped.

  • Enter a Node.js project idea with at least one testable capability.
  • Reply with architecture feedback that changes one design detail.
  • Enter APPROVE after the revised architecture matches your request.
  • Wait for the Developer Agent, deterministic checks, and QA Agent to finish.
  • Inspect the final report for its project path.
  • Inspect the same report for its QA summary.
  • Inspect the same report for its repair count.
  • Inspect the nested check result for commands, standard output, and standard error.

You should see a final status of either released or blocked. A released result appears only when both deterministic checks and QA pass.

The report should also show a confined path beneath generated_projects/, the approved profile, the architecture revision, the repair count, the QA summary, and the complete check evidence.

Does the final run stop or return incomplete evidence?

  • Confirm your activated terminal still has Gemini authentication available.
  • Check that both QA invocations pass approved_architecture, project_bundle, and check_result.
  • Confirm the final ReleaseReport receives checks=checks.

Ask for help tracing the final run.

That completes the factory. Five specialist agents can now plan, build, review, repair, and gate a confined polyglot project without claiming success when the evidence fails.

Secret mission

Red-Team the Approval Gate

Can contradictory stack feedback trick the factory into writing code too early? You will run a red-team review session that withholds approval until the Architecture Agent documents a safe supported compromise. The final session history proves the gate held.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep this workspace ready, pause its running processes, or remove its local artifacts. The local files create no ongoing compute charges while the factory is stopped.

Resources you used:

  • The .venv virtual environment containing Google Agent Development Kit and Pydantic.
  • The .adk session history containing your architecture revisions and red-team evidence.
  • The factory source in factory/. The guard tests in tests/. The supporting files requirements.txt, README.md, and .gitignore.
  • One or more generated project folders under generated_projects/.
  • A Gemini API key managed in Google AI Studio. Its local copy is stored in factory/.env.

Keep everything running

No action is needed. Choose this if you want to demonstrate the factory again or generate more micro-projects.

  • Keep .venv so the pinned Python packages remain ready.
  • Keep .adk so the approval history remains available as evidence.
  • Keep generated_projects/ so you can inspect or demonstrate each released bundle.
  • Keep factory/.env protected from source control through .gitignore.
  • Use the local factory only with prompts you trust because generated tests execute code on your Mac.

When Can Costs Occur?

Keeping these files does not generate API traffic. New factory runs can incur Gemini API charges if your usage exceeds the available free tier.

Pause - I'll come back to this later

Stop the interactive process to free the terminal while preserving the complete factory. Your source files and session evidence remain ready for another run.

  • Stop any active adk run factory session by pressing Ctrl+C.
  • Deactivate the virtual environment by typing deactivate in the terminal.
  • Leave .venv, .adk, factory/, tests/, and generated_projects/ in place.

Your factory is now dormant. The architecture history and generated projects stay ready for your next session.

Delete - I don't want to use this again

Complete removal erases the factory workspace permanently. The Gemini API key remains active until you disable or delete it separately in Google AI Studio.

  • Copy any generated project that you want to keep to a location outside the factory workspace.

Check Your Terminal Location

  • Confirm that the activated terminal remains inside the factory workspace before running the cleanup commands.
  • Check that you are ready to remove every relative path shown below.
  • Remove all listed local resources by running these commands:
rm -rf .venv .adk factory tests generated_projects
rm -f requirements.txt README.md .gitignore

What Do These Commands Remove?

  • The first command removes the virtual environment, ADK session history, factory package, guard tests, and generated projects.
  • The second command removes the dependency list, project documentation, and Git ignore rules.
  • Check which items remain in the factory workspace by running this command:
ls -la

What Should You See?

The output should no longer list .venv, .adk, factory, tests, or generated_projects.

It should also omit requirements.txt, README.md, and .gitignore.

Still Seeing a Local Resource?

  • Confirm that your terminal is still inside the same factory workspace.
  • Check the spelling of the remaining path against the cleanup command.
  • Help me safely diagnose why a local factory path remains after cleanup.
  • Return to the Gemini API key page in Google AI Studio from earlier.
  • Select the API key that authenticated this factory.
  • Delete or disable the selected key.

Cleanup complete. The removed workspace can no longer run the factory or use its former Gemini authentication.

Nice Work!

Nice Work!

You made it! Your architecture-first factory now coordinates five specialist agents through Google Agent Development Kit.

What you learned:

  • Connected five specialist agents through structured Pydantic contracts. The workflow turns one project idea into a complete micro-project.
  • Built an iterative human approval gate that returns rejected architecture drafts for revision. Explicit approval unlocks file generation. Workflow replay preserves completed work across every pause.
  • Confined generated files beneath generated_projects/. The Python adapter runs deterministic tests. The Node.js adapter performs syntax checks plus built-in tests. The static web adapter checks the required page plus JavaScript tests. Bounded repairs end with an honest ReleaseReport based on final evidence.
  • Secret Mission: Red-teamed the approval gate with conflicting revisions plus unsupported mobile-native feedback. The session proved that generation stayed locked until you explicitly approved a supported profile. The final bundle matched that approved revision.

Ready to quiz yourself?