Skip to content

feat: expand template presets, rosters, and add inheritance (#80, #81, #84)#162

Merged
Aureliolo merged 4 commits intomainfrom
feat/templates-presets-rosters
Mar 7, 2026
Merged

feat: expand template presets, rosters, and add inheritance (#80, #81, #84)#162
Aureliolo merged 4 commits intomainfrom
feat/templates-presets-rosters

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

  • Template inheritance: Templates can now extends other templates, with merge semantics for agents (by role+department key), departments (by name), config (deep merge), and scalars (child wins). Circular detection via frozenset chain tracking, max depth 10.
  • Template presets & rosters: Expanded personality presets registry and built-in template YAML files (solo_founder, startup, dev_shop, product_team, agency, full_company, research_lab) with roster definitions.
  • Merge module (merge.py): New dedicated module for template config merging with _ParentEntry dataclass for type-safe agent tracking, _remove directive support, and department merge by name.
  • Schema enhancements: extends field with normalization validator, _remove alias on TemplateAgentConfig, agent count validation skip when extending.
  • Logging hardening: Added structured logging before all raise paths in renderer.py (9 paths) and loader.py (11 paths) using domain event constants.
  • Code quality: Extracted _resolve_agent_personality helper, narrowed exception catches, removed dead test fixtures, tightened test assertions to exact counts.
  • DESIGN_SPEC.md: Documented template inheritance in §14.1, added merge.py to §15.3 project structure, added inheritance and alias conventions to §15.5.
  • 10 new tests: extends/normalize, _remove alias, _collect_parent_variables, escalation_paths fallback, None child scalar merge.

Closes #80, closes #81, closes #84

Test plan

  • uv run ruff check src/ tests/ — all checks passed
  • uv run ruff format src/ tests/ — all formatted
  • uv run mypy src/ tests/ — no issues (318 files)
  • uv run pytest tests/ -n auto --cov=ai_company --cov-fail-under=80 — 3002 passed, 96.49% coverage

Review coverage

Pre-reviewed by 9 agents (code-reviewer, python-reviewer, pr-test-analyzer, silent-failure-hunter, comment-analyzer, type-design-analyzer, logging-audit, resilience-audit, docs-consistency). 26 findings identified, all 26 implemented. Polish pass applied by code-simplifier agent.

🤖 Generated with Claude Code

Aureliolo and others added 2 commits March 7, 2026 19:19
…#84)

Phase A — Personality Presets (#80):
- Add 5 new presets: user_advocate, process_optimizer, growth_hacker,
  technical_communicator, systems_thinker (total: 20)
- Add 12 auto-name pools covering all BUILTIN_ROLES

Phase B — Agent Rosters (#81):
- Expand all 7 template YAMLs to target sizes (2→25 agents)
- Add min_agents/max_agents metadata passthrough in loader
- full_company uses Jinja2 {% for %} loops for configurable
  num_backend_devs/num_frontend_devs variables

Phase C — Template Inheritance (#84):
- Add `extends` field to CompanyTemplate schema
- New merge.py module with agent merge by (role, department) key,
  department merge by name, _remove directive support
- Renderer resolves inheritance after Jinja2 rendering with circular
  detection and depth limiting (max 10 levels)
- New TemplateInheritanceError exception
- 5 new observability event constants for inheritance tracing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… docs

Pre-reviewed by 9 agents, 26 findings addressed:

- Add logging before all raise paths in renderer.py (9 paths) and
  loader.py (11 paths) using structured event constants
- Extract _resolve_agent_personality helper from _expand_single_agent
  to keep functions under 50 lines
- Replace list[list[Any]] with _ParentEntry dataclass in merge.py
- Add DEFAULT_MERGE_DEPARTMENT shared constant between merge/renderer
- Add field_validator to normalize extends field (lowercase/stripped)
- Narrow except Exception to except (ValidationError, TypeError) in
  presets.py
- Remove dead MULTI_LEVEL_CHILD_YAML from test conftest
- Tighten >= assertions to exact counts in inheritance tests
- Add 10 new tests: extends/normalize, _remove alias, parent variable
  collection, escalation_paths fallback, None child scalar merge
- Document template inheritance in DESIGN_SPEC.md §14.1
- Add merge.py to §15.3 project structure
- Add inheritance and alias conventions to §15.5

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 7, 2026 18:47
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 7, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 7, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b0a5df01-698b-4ebd-a952-64e2506a0275

📥 Commits

Reviewing files that changed from the base of the PR and between fae2abc and 0737ceb.

📒 Files selected for processing (8)
  • DESIGN_SPEC.md
  • src/ai_company/observability/events/template.py
  • src/ai_company/templates/builtins/startup.yaml
  • src/ai_company/templates/loader.py
  • src/ai_company/templates/merge.py
  • src/ai_company/templates/presets.py
  • src/ai_company/templates/renderer.py
  • tests/unit/templates/test_renderer.py

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Template inheritance with extends, deep merge/override semantics, _remove directive, and cycle/depth safeguards.
    • Min/max agent constraints and expanded built-in templates (many new roles, departments, personality presets).
    • Runtime merging so child templates can override, append, or prune parent configs; parent variables can fill missing child defaults.
  • Documentation

    • Updated design/docs describing inheritance and merge behavior.
  • Tests

    • Extensive unit tests covering inheritance, merging, loader, and rendering scenarios.

Walkthrough

Adds template inheritance with an extends directive and a merge engine (parent→child) including _remove semantics, schema and renderer changes for safe resolution (cycle/depth checks), expanded built-in templates and presets, observability event constants, and extensive unit tests for inheritance behavior.

Changes

Cohort / File(s) Summary
Design & Spec
DESIGN_SPEC.md
Documents inheritance model, merge semantics, YAML aliasing, and a broader public API footprint (templates + engine scaffolding).
Merge Engine
src/ai_company/templates/merge.py
New merge logic: merge_template_configs, agent/department merge rules, _remove handling, DEFAULT_MERGE_DEPARTMENT, and related logging/errors.
Renderer Integration
src/ai_company/templates/renderer.py
Render-time inheritance resolution, cycle/depth detection, parent-variable collection, uses merge_template_configs, adds _MAX_INHERITANCE_DEPTH and related events.
Schema & Errors
src/ai_company/templates/schema.py, src/ai_company/templates/errors.py, src/ai_company/templates/__init__.py
Adds extends field, merge_id, _remove alias (remove), normalization validator, and new TemplateInheritanceError export.
Loader & Observability
src/ai_company/templates/loader.py, src/ai_company/observability/events/template.py
Loader: MappingProxyType BUILTIN_TEMPLATES, enhanced parsing/structure logging, min/max metadata support; observability: new TEMPLATE_* event constants for inheritance/load/merge lifecycle and errors.
Presets & Defaults
src/ai_company/templates/presets.py
Expanded personality presets and _AUTO_NAMES, tightened preset validation handling, frozen public preset mappings.
Built-in Templates
src/ai_company/templates/builtins/*.yaml
All built-ins updated with min_agents/max_agents, new departments/roles, added merge_id variants, and dynamic variables in full_company.
Tests — Inheritance & Loader
tests/unit/templates/test_inheritance.py, tests/unit/templates/conftest.py, tests/unit/templates/test_loader.py, tests/unit/templates/test_schema.py
New comprehensive tests for inheritance merging (override, add, remove), cycle/depth, parent variable propagation, and metadata propagation.
Tests — Presets & Renderer
tests/unit/templates/test_presets.py, tests/unit/templates/test_renderer.py
Updated tests to expect expanded presets, exported _AUTO_NAMES, exported BUILTIN_TEMPLATES, and roster-count/renderer validations.
Engine scaffolding (cataloged)
src/ai_company/engine/... (new modules referenced in spec)
Design lists new engine modules (plan parsing/execution, recovery, run results, agent/task/workflow engines) cataloged in DESIGN_SPEC.md (not all files shown in this diff).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Caller
    participant Renderer as TemplateRenderer
    participant Loader as TemplateLoader
    participant Merge as MergeEngine
    participant Validator as SchemaValidator

    Client->>Renderer: render(template_name)
    Renderer->>Loader: load_template(child_name)
    Loader-->>Renderer: TemplateSource (child)
    alt child.extends present
        Renderer->>Loader: load_template(parent_name)
        Loader-->>Renderer: TemplateSource (parent)
        Renderer->>Renderer: render Jinja → child_dict / parent_dict
        Renderer->>Merge: merge_template_configs(parent_dict, child_dict)
        Merge-->>Renderer: merged_dict
    else no extends
        Renderer->>Renderer: render Jinja → child_dict
        Note right of Renderer: merged_dict = child_dict
    end
    Renderer->>Validator: validate(merged_dict) -> RootConfig
    Validator-->>Renderer: RootConfig
    Renderer-->>Client: RootConfig
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: expand template presets, rosters, and add inheritance (#80, #81, #84)' clearly summarizes the main changes: template inheritance, preset expansion, and roster definitions, with explicit issue references.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering template inheritance, presets, rosters, merge module, schema enhancements, logging, and test additions.
Linked Issues check ✅ Passed The PR successfully implements all major requirements from #80 (15-20 personality presets, integration with templates), #81 (full rosters for all 7 templates with realistic departments), and #84 (extends directive, agent/department/config merging, circular detection, multi-level support).
Out of Scope Changes check ✅ Passed All changes are within scope: template inheritance system, preset expansion, roster definitions, schema updates, logging enhancements, and comprehensive tests directly support the three linked issue objectives.
Docstring Coverage ✅ Passed Docstring coverage is 81.18% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/templates-presets-rosters
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/templates-presets-rosters

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

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a powerful template inheritance system, allowing for more modular and reusable company configurations. By enabling templates to extend others and defining clear merge strategies, it significantly enhances flexibility in defining agent rosters and company structures. The changes also expand the range of pre-defined company templates and personality presets, making it easier to set up diverse simulations. Alongside these core features, the PR includes robust logging improvements and code quality enhancements, ensuring a more stable and maintainable system.

Highlights

  • Template Inheritance: Introduced the ability for templates to extend other templates using an extends keyword, supporting merge semantics for agents (by role+department key), departments (by name), config (deep merge), and scalars (child wins). Includes circular detection and a maximum inheritance depth of 10 levels.
  • Expanded Template Presets & Rosters: Significantly expanded the personality presets registry and updated built-in template YAML files (solo_founder, startup, dev_shop, product_team, agency, full_company, research_lab) with more comprehensive roster definitions and agent count ranges.
  • New Merge Module: Added a dedicated merge.py module for handling template configuration merging, featuring a _ParentEntry dataclass for type-safe agent tracking and support for a _remove directive to explicitly remove parent agents.
  • Schema Enhancements: Enhanced the template schema with an extends field including a normalization validator, an _remove alias on TemplateAgentConfig for clear YAML directives, and a mechanism to skip agent count validation when extending templates.
  • Logging Hardening: Implemented structured logging before all raise paths in renderer.py (9 paths) and loader.py (11 paths) using new domain event constants for improved observability.
  • Code Quality Improvements: Refactored code by extracting the _resolve_agent_personality helper, narrowing exception catches, removing dead test fixtures, and tightening test assertions to exact counts.
  • Documentation Updates: Updated DESIGN_SPEC.md to document template inheritance in detail, including merge strategies, project structure additions for merge.py, and new inheritance/alias conventions.
  • Comprehensive Testing: Added 10 new tests covering inheritance features such as extends normalization, _remove alias functionality, _collect_parent_variables logic, escalation paths fallback, and None child scalar merging.
Changelog
  • DESIGN_SPEC.md
    • Added documentation for workflow_handoffs and escalation_paths fields.
    • Documented the new template inheritance feature, including the extends keyword and merge semantics.
    • Updated the project structure section to include the new merge.py module.
    • Added conventions for template inheritance and Pydantic aliases for YAML directives.
  • src/ai_company/observability/events/template.py
    • Added new event constants for template inheritance resolution and loading errors.
  • src/ai_company/templates/init.py
    • Exported the new TemplateInheritanceError.
  • src/ai_company/templates/builtins/agency.yaml
    • Updated min_agents and max_agents metadata.
    • Revised department budget percentages.
    • Expanded the agent roster with new roles and updated personality presets.
  • src/ai_company/templates/builtins/dev_shop.yaml
    • Updated min_agents and max_agents metadata.
    • Changed the personality preset for 'Software Architect'.
    • Expanded the agent roster with new roles and updated personality presets.
  • src/ai_company/templates/builtins/full_company.yaml
    • Updated min_agents and max_agents metadata.
    • Added num_backend_devs and num_frontend_devs variables for dynamic agent generation.
    • Revised department budget percentages and introduced new departments.
    • Expanded the agent roster, including dynamic agent generation and new C-suite roles.
  • src/ai_company/templates/builtins/product_team.yaml
    • Updated min_agents and max_agents metadata.
    • Revised department budget percentages and introduced new departments.
    • Expanded the agent roster with new roles and updated personality presets.
  • src/ai_company/templates/builtins/research_lab.yaml
    • Updated min_agents and max_agents metadata.
    • Revised department budget percentages and introduced new departments.
    • Expanded the agent roster with new roles and updated personality presets.
  • src/ai_company/templates/builtins/solo_founder.yaml
    • Updated min_agents and max_agents metadata.
  • src/ai_company/templates/builtins/startup.yaml
    • Updated min_agents and max_agents metadata.
    • Added a personality preset for the 'Product Manager' role.
  • src/ai_company/templates/errors.py
    • Added the TemplateInheritanceError class for specific inheritance-related issues.
  • src/ai_company/templates/loader.py
    • Imported new template event constants for logging.
    • Added structured logging calls before raising TemplateNotFoundError, TemplateRenderError, and TemplateValidationError.
    • Refined _normalize_template_data to correctly handle min_agents, max_agents, and the new extends fields.
  • src/ai_company/templates/merge.py
    • Added a new module merge.py to encapsulate template configuration merging logic.
    • Implemented merge_template_configs with specific strategies for scalars, config, agents, and departments.
    • Defined _ParentEntry dataclass for tracking parent agents during merge operations.
    • Included helper functions _merge_agents, _merge_departments, _apply_child_agent, _find_unmatched, _collect_merged_agents, and _agent_key for detailed merge control.
  • src/ai_company/templates/presets.py
    • Imported ValidationError for more specific exception handling.
    • Expanded the _RAW_PRESETS dictionary with several new personality presets.
    • Expanded the _AUTO_NAMES dictionary with new role-based name pools.
    • Narrowed the exception catch for personality preset validation to ValidationError and TypeError.
  • src/ai_company/templates/renderer.py
    • Updated the module docstring to reflect template inheritance resolution.
    • Imported new template event constants and TemplateInheritanceError.
    • Imported DEFAULT_MERGE_DEPARTMENT and merge_template_configs from the new merge.py module.
    • Defined _MAX_INHERITANCE_DEPTH for inheritance chain limits.
    • Modified render_template to use _render_to_dict for handling inheritance.
    • Added _render_to_dict function for recursive template rendering and inheritance resolution.
    • Added _resolve_inheritance function to manage parent template loading, circular dependency detection, depth limits, and merging.
    • Added _collect_parent_variables to facilitate variable flow from child to parent templates.
    • Refactored _build_config_dict to use rendered_data.get("company") or {} for cleaner handling.
    • Added structured logging calls before raising TemplateRenderError for type mismatches and invalid numeric values.
    • Refactored _expand_agents for improved conciseness.
    • Extracted _resolve_agent_personality into a dedicated helper function.
    • Added structured logging for TEMPLATE_RENDER_VARIABLE_ERROR and TEMPLATE_PERSONALITY_PRESET_UNKNOWN events.
  • src/ai_company/templates/schema.py
    • Imported field_validator for new validation patterns.
    • Added a remove field with an _remove alias to TemplateAgentConfig to support agent removal during merges.
    • Added an extends field to CompanyTemplate for specifying parent templates.
    • Implemented _normalize_extends field validator to standardize parent template names.
    • Modified _validate_agent_count_in_range to skip validation when the extends field is present.
  • tests/unit/templates/conftest.py
    • Added new YAML string constants (CHILD_EXTENDS_STARTUP_YAML, CHILD_OVERRIDE_AGENT_YAML, CHILD_REMOVE_AGENT_YAML, CIRCULAR_SELF_YAML) for inheritance testing.
  • tests/unit/templates/test_inheritance.py
    • Added a new test file test_inheritance.py to cover template inheritance and merge logic.
    • Included unit tests for _merge_agents, _merge_departments, merge_template_configs, and _collect_parent_variables.
    • Implemented integration tests for single-level and multi-level inheritance, agent overriding and removal, variable flow, and circular/depth limit detection.
  • tests/unit/templates/test_loader.py
    • Added TestMinMaxPassthrough to verify correct parsing of min_agents and max_agents from YAML.
  • tests/unit/templates/test_presets.py
    • Increased the expected count of personality presets.
    • Added specific tests to validate the new personality presets.
    • Added TestAutoNameCoverage to ensure all built-in roles have corresponding auto-name pools.
  • tests/unit/templates/test_renderer.py
    • Removed a redundant import of BUILTIN_TEMPLATES.
    • Added _EXPECTED_AGENT_COUNTS dictionary for roster count validation.
    • Added TestRosterCounts class to verify agent counts for built-in templates and variable overrides.
    • Added a test to confirm all built-in templates render to a valid RootConfig.
Activity
  • Pre-reviewed by 9 agents (code-reviewer, python-reviewer, pr-test-analyzer, silent-failure-hunter, comment-analyzer, type-design-analyzer, logging-audit, resilience-audit, docs-consistency).
  • 26 findings identified and all 26 implemented.
  • Polish pass applied by code-simplifier agent.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a powerful template inheritance feature. A thorough security audit confirmed no significant vulnerabilities, with the implementation following best practices such as SSTI Mitigation, Path Traversal Protection, Safe Merging, Input Validation, Recursion Control, and Secure Defaults. The implementation is well-structured with a new merge.py module and clean refactoring in renderer.py, and includes comprehensive tests, documentation updates, and logging hardening. I have one minor suggestion for improving code clarity.

Comment on lines +526 to +529
return [
_expand_single_agent(agent, idx, used_names)
for idx, agent in enumerate(raw_agents)
]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using a list comprehension with a function (_expand_single_agent) that has side effects on a shared object (used_names) can be less clear than an explicit for loop. For better readability and to make the state mutation more obvious, consider reverting to a standard loop as was used previously.

Suggested change
return [
_expand_single_agent(agent, idx, used_names)
for idx, agent in enumerate(raw_agents)
]
expanded_agents: list[dict[str, Any]] = []
for idx, agent in enumerate(raw_agents):
expanded_agents.append(_expand_single_agent(agent, idx, used_names))
return expanded_agents

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements template inheritance (extends directive), expands personality presets and agent rosters for all 7 built-in company templates, and adds a dedicated merge module for template config composition. It closes issues #80 (personality presets), #81 (full agent rosters), and #84 (template inheritance).

Changes:

  • Template inheritance via extends field on CompanyTemplate, with circular detection (frozenset chain tracking, max depth 10), and merge semantics for agents (by role+department), departments (by name), config (deep merge), and scalars (child wins).
  • New merge.py module with _ParentEntry dataclass, _remove directive support, and merge_template_configs as the public merge entry point. Schema additions include _remove alias on TemplateAgentConfig and extends field with normalization validator.
  • Expanded personality presets (5 new: user_advocate, process_optimizer, growth_hacker, technical_communicator, systems_thinker), expanded auto-name pools, full agent rosters for all 7 templates with min/max agent constraints, and structured logging before all raise paths.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/ai_company/templates/merge.py New module for template config merging with agent/department/scalar merge strategies
src/ai_company/templates/renderer.py Inheritance resolution (_render_to_dict, _resolve_inheritance, _collect_parent_variables), extracted _resolve_agent_personality, added structured logging
src/ai_company/templates/schema.py Added extends field with normalization, remove alias field, agent count validation skip for extending templates
src/ai_company/templates/loader.py Added min_agents/max_agents passthrough, extends passthrough, structured logging on error paths
src/ai_company/templates/presets.py 5 new personality presets, expanded auto-name pools, narrowed exception catches
src/ai_company/templates/errors.py New TemplateInheritanceError exception class
src/ai_company/templates/__init__.py Exported TemplateInheritanceError
src/ai_company/observability/events/template.py 11 new event constants for inheritance and logging hardening
src/ai_company/templates/builtins/*.yaml Full agent rosters, min/max constraints, personality presets for all 7 templates
tests/unit/templates/test_inheritance.py Comprehensive tests for merge, inheritance, circular detection, depth limits
tests/unit/templates/test_renderer.py Roster count range tests, variable override test
tests/unit/templates/test_schema.py Tests for _remove alias, extends field, normalization
tests/unit/templates/test_presets.py Tests for new presets validation, auto-name coverage
tests/unit/templates/test_loader.py Tests for min/max passthrough
tests/unit/templates/conftest.py Test YAML fixtures for inheritance scenarios
DESIGN_SPEC.md Documented template inheritance in §14.1, merge.py in §15.3, conventions in §15.5

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +279 to +282
def _normalize_extends(cls, value: str | None) -> str | None:
"""Normalize extends to lowercase stripped form."""
if value is None:
return None
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

The _normalize_extends validator with mode="before" could receive non-string types from malformed YAML (e.g., extends: 123 produces an int). Calling .strip() on a non-string would raise AttributeError instead of a clean ValidationError. Consider adding a type guard: if the value is not a string, either coerce it with str(value) or return the value as-is to let Pydantic's type validation reject it.

Suggested change
def _normalize_extends(cls, value: str | None) -> str | None:
"""Normalize extends to lowercase stripped form."""
if value is None:
return None
def _normalize_extends(cls, value: Any) -> Any:
"""Normalize extends to lowercase stripped form."""
if value is None:
return None
if not isinstance(value, str):
# Let Pydantic's type system handle or reject non-string values.
return value

Copilot uses AI. Check for mistakes.
Comment on lines +272 to +275
extends: NotBlankStr | None = Field(
default=None,
description="Parent template name for inheritance",
)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

The extends field is missing from the CompanyTemplate docstring's Attributes section. The docstring lists all other attributes but omits extends, which is inconsistent. Please add an entry like:
extends: Parent template name for inheritance (``None`` for standalone).

Similarly, the TemplateAgentConfig docstring above (line 83-93) is missing the remove attribute.

Copilot uses AI. Check for mistakes.
DESIGN_SPEC.md Outdated
Comment on lines +2210 to +2212
- role: "full_stack_developer"
department: "engineering"
_remove: true # removes matching parent agent by (role, department)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

The example uses role: "full_stack_developer" (snake_case) but the actual templates and role catalog use "Full-Stack Developer" (Title-Case with hyphens). Since the merge key uses role.lower(), this role name wouldn't match the parent's "Full-Stack Developer" ("full-stack developer" vs "full_stack_developer"). The example should use "Full-Stack Developer" to accurately illustrate how _remove works.

Copilot uses AI. Check for mistakes.
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR delivers template inheritance (extends), an expanded personality preset registry, seven updated builtin YAML templates with roster definitions, a dedicated merge.py module, schema additions (extends, merge_id, _remove alias), structured logging across all raise paths in renderer.py and loader.py, and 10 new tests. The architecture is well-decomposed and the four previously-flagged issues (nameless-department overwrite, _remove silent no-op, misleading TEMPLATE_LOAD_NOT_FOUND for path-traversal, del-without-guard) are all properly addressed.

One critical logic bug remains unfixed:

  • merge_id disambiguation is silently broken (renderer.py, _expand_single_agent): The entire merge_id mechanism — which is documented in DESIGN_SPEC §14.1, wired into the schema, and annotated in all seven builtin YAML files — does not function. _expand_single_agent constructs agent_dict with role, name, department, level, personality, and model, but never copies merge_id into it. Because both parent_agents and child_agents arrive at merge.py's _merge_agents as already-expanded dicts, _agent_key always computes merge_id="" and the three-part (role, department, merge_id) key degenerates to (role, department, ""). Templates with multiple same-(role, department) agents (e.g. the two Full-Stack Developer entries in startup.yaml) cannot be individually targeted from a child template. The test suite doesn't cover this case, so the bug passes all 3002 tests.

Confidence Score: 2/5

  • The PR has architectural soundness and proper logging hardening, but the merge_id disambiguation mechanism — the primary tool for handling templates with duplicate (role, department) pairs — is non-functional and invisible to tests.
  • The inheritance feature works for simple cases tested, but the merge_id mechanism that enables targeting specific agents among duplicates is silently broken at the _expand_single_agent level — merge_id is never copied to the expanded agent dict, so _agent_key always returns (role, department, "") regardless of YAML annotations. All seven builtin templates annotate agents with merge_id, the DESIGN_SPEC documents the three-part key contract, and the merge.py module correctly implements the key logic, but the critical connection in the renderer is missing. This is invisible in tests because no test attempts merge_id-based targeting of duplicate agents.
  • src/ai_company/templates/renderer.py — _expand_single_agent needs to preserve merge_id when has_extends=True. Additionally, tests/unit/templates/test_inheritance.py should add a test verifying that a child can target a specific agent out of two same-(role, department) agents using merge_id.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant R as renderer.py
    participant L as loader.py
    participant M as merge.py

    U->>R: render_template(loaded, variables)
    R->>R: _render_to_dict(loaded, _chain=∅)
    R->>R: _collect_variables(template, variables)
    R->>R: Jinja2 render → YAML parse
    R->>R: _build_config_dict() → child_config<br/>[_expand_agents drops merge_id ⚠️]

    alt template.extends is None
        R-->>U: child_config (no inheritance)
    else template.extends = "parent_name"
        R->>R: _validate_inheritance_chain(child_id, parent_name, _chain)<br/>• circular check: parent_name ∈ _chain?<br/>• depth check: len(_chain) ≥ 10?
        R->>L: load_template(parent_name)
        L-->>R: parent_loaded
        R->>R: _collect_parent_variables(parent, child_vars)
        R->>R: _render_to_dict(parent_loaded,<br/>_chain=_chain ∪ {parent_name})
        note over R: Recursive — handles multi-level chains
        R->>M: merge_template_configs(parent_config, child_config)
        M->>M: scalars: child wins if non-None
        M->>M: config dict: deep_merge()
        M->>M: agents: _merge_agents() by (role, dept, merge_id)<br/>[merge_id always "" due to renderer bug ⚠️]
        M->>M: departments: _merge_departments() by name
        M->>M: workflow_handoffs / escalation_paths: child replaces
        M-->>R: merged_config
        R->>R: deep_merge(defaults, merged_config)
        R->>R: _validate_as_root_config()
        R-->>U: RootConfig
    end
Loading

Comments Outside Diff (1)

  1. src/ai_company/templates/renderer.py, line 609-619 (link)

    merge_id silently dropped — inheritance disambiguation non-functional

    _expand_single_agent builds agent_dict with only role, name, department, level, personality, and model. The merge_id from the raw YAML agent dict is never transferred into agent_dict.

    Because both parent_agents and child_agents passed to _merge_agents are the output of _expand_agents → _expand_single_agent, by the time _agent_key runs it always sees agent.get("merge_id", "") == "". The three-part key (role, department, merge_id) degenerates to (role, department, "") for every agent regardless of what merge_id was declared in YAML.

    Concrete breakage: startup.yaml declares two "Full-Stack Developer" agents with merge_id: "fullstack-senior" and merge_id: "fullstack-mid". After expansion both resolve to the same key. A child template that writes

    - role: "Full-Stack Developer"
      merge_id: "fullstack-mid"
      department: "engineering"
      _remove: true

    will remove the first Full-Stack Developer positionally (whichever is at index 0), not the "fullstack-mid" one, silently violating the documented contract in DESIGN_SPEC §14.1.

    The fix is to preserve merge_id in the expanded dict when has_extends=True:

    agent_dict: dict[str, Any] = {
        "role": role,
        "name": name,
        "department": agent.get("department", _DEFAULT_DEPARTMENT),
        "level": agent.get("level", "mid"),
    }
    # Preserve merge_id so _agent_key can use it during inheritance merge.
    if has_extends:
        agent_dict["merge_id"] = str(agent.get("merge_id", ""))

    Without this, all the merge_id annotations in the builtin YAML files (fullstack-senior, fullstack-mid, etc.) and the three-part key design documented in DESIGN_SPEC and implemented in merge.py have no effect.

Last reviewed commit: 0737ceb

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/ai_company/templates/loader.py (1)

427-439: ⚠️ Potential issue | 🟠 Major

Pass through the new handoff and escalation fields during normalization.

CompanyTemplate has dedicated workflow_handoffs and escalation_paths fields, but _normalize_template_data() drops both. That means pass-1 validation and inheritance resolution never see YAML that defines them.

🛠️ Proposed fix
     result: dict[str, Any] = {
         "metadata": metadata,
         "variables": data.get("variables", ()),
         "agents": data.get("agents", ()),
         "departments": data.get("departments", ()),
         "workflow": data.get("workflow", "agile_kanban"),
         "communication": data.get("communication", "hybrid"),
         "budget_monthly": _to_float(company.get("budget_monthly", 50.0)),
         "autonomy": _to_float(company.get("autonomy", 0.5)),
+        "workflow_handoffs": data.get("workflow_handoffs", ()),
+        "escalation_paths": data.get("escalation_paths", ()),
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/templates/loader.py` around lines 427 - 439, The normalization
currently drops workflow_handoffs and escalation_paths so definitions never
reach CompanyTemplate; update _normalize_template_data (the function building
result: dict[str, Any]) to include "workflow_handoffs":
data.get("workflow_handoffs", ()) and "escalation_paths":
data.get("escalation_paths", ()) in the result dict (same style as
"agents"/"departments") so pass-1 validation and inheritance see these fields.
src/ai_company/templates/renderer.py (1)

417-421: ⚠️ Potential issue | 🟡 Minor

Don't mask invalid company values.

Line 417 converts any falsy value into {}, so company: [], company: "", or company: 0 bypasses the mapping check below and silently falls back to defaults. Only substitute an empty mapping when the field is actually missing or null.

🐛 Proposed fix
-    company = rendered_data.get("company") or {}
+    company = rendered_data.get("company")
+    if company is None:
+        company = {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/templates/renderer.py` around lines 417 - 421, The current
code masks invalid non-mapping `company` values by using
`rendered_data.get("company") or {}`; change the logic in the renderer so you
only default to an empty mapping when the `company` key is absent or its value
is explicitly null, otherwise preserve the original value and then validate its
type; specifically, replace the `company = rendered_data.get("company") or {}`
pattern with a check like "if 'company' not in rendered_data or
rendered_data['company'] is None: company = {} else: company =
rendered_data['company']" and keep the existing type check that logs
TEMPLATE_RENDER_YAML_ERROR and raises TemplateRenderError when `company` is
present but not a dict.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ai_company/templates/builtins/agency.yaml`:
- Around line 79-103: The duplicate engineering entries must be made uniquely
addressable so template inheritance can target by level; update the built-in
agent specs (e.g., the two "Full-Stack Developer" entries) to include a unique
identifier or include "level" in the merge key used by the inheritance logic so
`_merge_agents()` can distinguish them (for example add a unique "id" or "seat"
field or modify the merge key to combine role+level); ensure the change is
applied to the entries with role "Full-Stack Developer" (the senior and mid
entries) so a child template can override or remove a specific level without
restating duplicates.

In `@src/ai_company/templates/builtins/product_team.yaml`:
- Around line 59-68: Two Backend Developer entries collide under the new
inheritance merge key (which uses (role, department)), so child templates cannot
target or override one entry selectively; update the YAML to make each agent
uniquely addressable by either consolidating the two entries into a single
Backend Developer agent that contains both level variants, or add a distinct
identifier field (e.g., id, rank or include level in the inheritance key) so
(role, department, id) or (role, department, level) becomes unique; ensure any
references using extends are updated to the chosen unique key and that the YAML
keys mentioned (role, department, level, extends) reflect the new uniqueness.

In `@src/ai_company/templates/builtins/research_lab.yaml`:
- Around line 53-67: The two entries with role "Data Analyst" and department
"data_analytics" produce identical YAML merge keys and must be made unique so
descendants can override them individually; modify one of the Data Analyst
entries (e.g., the one with model "small" or "medium") to include a distinct
identifier used as the merge key—either add an explicit unique field such as
"id" or "seat_id" (e.g., "seat_id: data_analyst_small") or change the role to a
unique value (e.g., "Data Analyst (small)") so that the entries for role "Data
Analyst" in this template can be surgically overridden instead of colliding.

In `@src/ai_company/templates/builtins/startup.yaml`:
- Around line 5-6: The startup template currently sets min_agents: 4 and
max_agents: 7 which violates the agreed 3–5 agent contract; update the
template's startup bounds by changing min_agents to 3 and max_agents to 5 so the
startup configuration (min_agents and max_agents) enforces the required 3–5
agent constraint.

In `@src/ai_company/templates/loader.py`:
- Around line 404-412: The code masks falsy non-mapping values by using company
= data.get("company") or {}; change this to company = data.get("company", {}) so
the default empty dict is only used when the key is missing, and keep the
subsequent isinstance(company, dict) check (and the existing logging/TypeError)
to ensure values like "", 0, False, or [] are properly treated as malformed;
locate the assignment to company and the following isinstance check in loader.py
to make this change.

In `@src/ai_company/templates/merge.py`:
- Around line 107-116: _current implementation uses (role.lower(),
department.lower()) as the merge key which is unstable for duplicate-role
rosters; update _apply_child_agent to support a stable per-agent identity by
using an explicit merge id (e.g., agent.get("merge_id") or "slot") as part of
the index key: build parent index by (role.lower(), department.lower(),
merge_id) where merge_id defaults to None, and when matching a child entry
prefer exact merge_id matches (consume that specific parent) and fall back to
the first unmatched parent only if child has no merge_id; strip the merge_id
from replacement/appended dicts and apply the same change to the other merge
functions referenced (the blocks currently around the second and third
occurrences of agent merging) so all matching/consumption logic consistently
honors merge_id when present.
- Around line 90-94: The merged result is copying references from parent/child
into result (e.g., the loop assigning result[key] for "workflow_handoffs" and
"escalation_paths"), causing downstream mutations to leak into source templates;
fix by importing copy and types.MappingProxyType, and when assigning collection
values use result[key] = copy.deepcopy(child[key]) or copy.deepcopy(parent[key])
as appropriate, and ensure any final returned merged collections (agents,
departments, handoffs, workflow_handoffs, escalation_paths, etc.) are wrapped in
MappingProxyType for read-only enforcement; apply the same deep-copy +
MappingProxyType pattern to the other merge sites referenced (the other merge
blocks handling agents/departments/handoffs).

In `@src/ai_company/templates/renderer.py`:
- Around line 218-225: The child config being passed to merge_template_configs
is already materialized with synthesized defaults, causing it to clobber parent
scalar values; before calling merge_template_configs in the block that builds
parent_config (where _render_to_dict(parent_loaded, parent_vars, _chain=_chain |
{parent_name}) is used and merged with child_config), strip out scalar keys from
child_config that the child did not explicitly supply. Implement this by having
_render_to_dict (or the rendering call site) return or expose which keys were
explicitly set by the child (e.g., a provided_keys set) and then filter
child_config to remove scalar entries (e.g., company_name and other simple
scalars) unless they appear in that provided_keys set; then call
merge_template_configs with the filtered child_config so parent scalars are
preserved.
- Around line 151-230: The _resolve_inheritance function is too long and mixes
concerns; refactor it into small helpers: extract the circular check and depth
enforcement (using _MAX_INHERITANCE_DEPTH and raising
TemplateInheritanceError/TEMPLATE_INHERIT_CIRCULAR or
TEMPLATE_INHERIT_DEPTH_EXCEEDED) into a helper like
_validate_inheritance_chain(child_id, parent_name, _chain), extract template
loading into a helper like _load_parent_template(parent_name) that wraps
load_template and returns parent_loaded, and extract the parent
render/merge/logging into another helper like
_render_and_merge_parent(parent_loaded, child_config, vars_dict, _chain) which
calls _collect_parent_variables, _render_to_dict, merge_template_configs and
emits TEMPLATE_INHERIT_RESOLVE_START / TEMPLATE_INHERIT_RESOLVE_SUCCESS logs;
then have _resolve_inheritance orchestrate these helpers (call validation, load,
render/merge) so each helper is small and testable while preserving the same
exceptions and log messages.

In `@src/ai_company/templates/schema.py`:
- Around line 277-283: The validator _normalize_extends currently assumes the
incoming YAML value is a string and calls .strip(), which will raise an
AttributeError for non-string inputs; change _normalize_extends (the
`@field_validator` for "extends") to explicitly check the input type: if value is
None return None; if not isinstance(value, str) raise a TypeError or ValueError
with a clear message (so Pydantic converts it to a ValidationError); otherwise
return value.strip().lower().

In `@tests/unit/templates/test_renderer.py`:
- Around line 516-554: The tests duplicate agent-count bounds and a validity
check; replace the hardcoded _EXPECTED_AGENT_COUNTS and the redundant
test_all_templates_render_to_valid_root_config by sourcing min/max from each
template's metadata: in TestRosterCounts (functions
test_template_agent_count_in_range and test_full_company_variable_override) call
load_template(name) once, read bounds from loaded.template.metadata.min_agents
and .max_agents, assert len(config.agents) falls within those bounds, and remove
or fold the duplicate RootConfig validity assertion (you may still assert
isinstance(config, RootConfig) where you already load/render to ensure the
object type); keep using load_template, render_template, BUILTIN_TEMPLATES, and
the full_company override logic but compare against metadata-derived bounds
instead of _EXPECTED_AGENT_COUNTS.

---

Outside diff comments:
In `@src/ai_company/templates/loader.py`:
- Around line 427-439: The normalization currently drops workflow_handoffs and
escalation_paths so definitions never reach CompanyTemplate; update
_normalize_template_data (the function building result: dict[str, Any]) to
include "workflow_handoffs": data.get("workflow_handoffs", ()) and
"escalation_paths": data.get("escalation_paths", ()) in the result dict (same
style as "agents"/"departments") so pass-1 validation and inheritance see these
fields.

In `@src/ai_company/templates/renderer.py`:
- Around line 417-421: The current code masks invalid non-mapping `company`
values by using `rendered_data.get("company") or {}`; change the logic in the
renderer so you only default to an empty mapping when the `company` key is
absent or its value is explicitly null, otherwise preserve the original value
and then validate its type; specifically, replace the `company =
rendered_data.get("company") or {}` pattern with a check like "if 'company' not
in rendered_data or rendered_data['company'] is None: company = {} else: company
= rendered_data['company']" and keep the existing type check that logs
TEMPLATE_RENDER_YAML_ERROR and raises TemplateRenderError when `company` is
present but not a dict.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dc1df9f6-1fd1-4b8b-a40f-c97df078ed8e

📥 Commits

Reviewing files that changed from the base of the PR and between 10b2299 and 51b114d.

📒 Files selected for processing (22)
  • DESIGN_SPEC.md
  • src/ai_company/observability/events/template.py
  • src/ai_company/templates/__init__.py
  • src/ai_company/templates/builtins/agency.yaml
  • src/ai_company/templates/builtins/dev_shop.yaml
  • src/ai_company/templates/builtins/full_company.yaml
  • src/ai_company/templates/builtins/product_team.yaml
  • src/ai_company/templates/builtins/research_lab.yaml
  • src/ai_company/templates/builtins/solo_founder.yaml
  • src/ai_company/templates/builtins/startup.yaml
  • src/ai_company/templates/errors.py
  • src/ai_company/templates/loader.py
  • src/ai_company/templates/merge.py
  • src/ai_company/templates/presets.py
  • src/ai_company/templates/renderer.py
  • src/ai_company/templates/schema.py
  • tests/unit/templates/conftest.py
  • tests/unit/templates/test_inheritance.py
  • tests/unit/templates/test_loader.py
  • tests/unit/templates/test_presets.py
  • tests/unit/templates/test_renderer.py
  • tests/unit/templates/test_schema.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Always use native lazy annotations (PEP 649) — do not use from __future__ import annotations
Use PEP 758 except syntax: except A, B: without parentheses for multiple exception types
Maintain line length of 88 characters (enforced by ruff)

Files:

  • tests/unit/templates/conftest.py
  • tests/unit/templates/test_presets.py
  • src/ai_company/observability/events/template.py
  • src/ai_company/templates/__init__.py
  • src/ai_company/templates/renderer.py
  • tests/unit/templates/test_inheritance.py
  • src/ai_company/templates/merge.py
  • src/ai_company/templates/loader.py
  • tests/unit/templates/test_schema.py
  • src/ai_company/templates/presets.py
  • tests/unit/templates/test_renderer.py
  • src/ai_company/templates/schema.py
  • tests/unit/templates/test_loader.py
  • src/ai_company/templates/errors.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Use pytest markers @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, and @pytest.mark.slow to categorize tests
Prefer @pytest.mark.parametrize for testing similar test cases
Use generic vendor-agnostic names in test fixtures and configuration: test-provider, test-small-001, test-medium-001, test-large-001

Files:

  • tests/unit/templates/conftest.py
  • tests/unit/templates/test_presets.py
  • tests/unit/templates/test_inheritance.py
  • tests/unit/templates/test_schema.py
  • tests/unit/templates/test_renderer.py
  • tests/unit/templates/test_loader.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: All public functions and classes must have type hints and Google-style docstrings (enforced by ruff D rules)
Never use import logging or logging.getLogger() in application code — use from ai_company.observability import get_logger with variable name logger
Use structured logging with constants from ai_company.observability.events.<domain> — always log as logger.info(EVENT_CONSTANT, key=value) never with format strings
For all identifier/name fields in Pydantic models, use NotBlankStr type from core.types instead of manual whitespace validators (including optional and tuple variants)
Use @computed_field for derived values in Pydantic models instead of storing and validating redundant fields
Enforce immutability: create new objects instead of mutating. For non-Pydantic internal collections use copy.deepcopy() at construction and MappingProxyType for read-only enforcement
For config/identity data use frozen Pydantic models; for runtime state that evolves use separate mutable-via-copy models with model_copy(update=...)
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code instead of bare create_task
Functions must be under 50 lines, files must be under 800 lines
Handle errors explicitly — never silently swallow exceptions
Never use real vendor names (Anthropic, OpenAI, Claude, GPT) in project-owned code, docstrings, comments, or tests — use generic names: example-provider, example-large-001, test-provider, test-small-001

Files:

  • src/ai_company/observability/events/template.py
  • src/ai_company/templates/__init__.py
  • src/ai_company/templates/renderer.py
  • src/ai_company/templates/merge.py
  • src/ai_company/templates/loader.py
  • src/ai_company/templates/presets.py
  • src/ai_company/templates/schema.py
  • src/ai_company/templates/errors.py
DESIGN_SPEC.md

📄 CodeRabbit inference engine (CLAUDE.md)

Update DESIGN_SPEC.md after approved deviations to reflect the new reality

Files:

  • DESIGN_SPEC.md
🧠 Learnings (4)
📚 Learning: 2026-03-07T17:28:05.391Z
Learnt from: CR
Repo: Aureliolo/ai-company PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T17:28:05.391Z
Learning: Applies to tests/**/*.py : Use generic vendor-agnostic names in test fixtures and configuration: `test-provider`, `test-small-001`, `test-medium-001`, `test-large-001`

Applied to files:

  • tests/unit/templates/test_presets.py
📚 Learning: 2026-03-07T17:28:05.391Z
Learnt from: CR
Repo: Aureliolo/ai-company PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T17:28:05.391Z
Learning: Applies to src/**/*.py : Use structured logging with constants from `ai_company.observability.events.<domain>` — always log as `logger.info(EVENT_CONSTANT, key=value)` never with format strings

Applied to files:

  • src/ai_company/observability/events/template.py
📚 Learning: 2026-03-07T17:28:05.391Z
Learnt from: CR
Repo: Aureliolo/ai-company PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T17:28:05.391Z
Learning: Applies to DESIGN_SPEC.md : Update `DESIGN_SPEC.md` after approved deviations to reflect the new reality

Applied to files:

  • DESIGN_SPEC.md
📚 Learning: 2026-03-07T17:28:05.391Z
Learnt from: CR
Repo: Aureliolo/ai-company PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T17:28:05.391Z
Learning: Applies to src/ai_company/{api,config,providers}/**/*.py : Validate at system boundaries (user input, external APIs, config files)

Applied to files:

  • src/ai_company/templates/schema.py
🧬 Code graph analysis (8)
tests/unit/templates/test_presets.py (2)
src/ai_company/core/agent.py (1)
  • PersonalityConfig (25-122)
src/ai_company/templates/presets.py (1)
  • get_personality_preset (405-427)
src/ai_company/templates/__init__.py (1)
src/ai_company/templates/errors.py (2)
  • TemplateError (6-7)
  • TemplateInheritanceError (23-28)
src/ai_company/templates/renderer.py (6)
src/ai_company/templates/errors.py (2)
  • TemplateInheritanceError (23-28)
  • TemplateRenderError (14-20)
src/ai_company/templates/merge.py (1)
  • merge_template_configs (35-97)
src/ai_company/config/utils.py (1)
  • deep_merge (37-61)
src/ai_company/config/defaults.py (1)
  • default_config_dict (6-32)
src/ai_company/templates/loader.py (1)
  • LoadedTemplate (77-88)
src/ai_company/templates/schema.py (1)
  • CompanyTemplate (206-328)
src/ai_company/templates/merge.py (3)
src/ai_company/config/utils.py (1)
  • deep_merge (37-61)
src/ai_company/observability/_logger.py (1)
  • get_logger (8-28)
src/ai_company/templates/errors.py (1)
  • TemplateInheritanceError (23-28)
src/ai_company/templates/loader.py (3)
src/ai_company/templates/errors.py (2)
  • TemplateRenderError (14-20)
  • TemplateValidationError (31-67)
src/ai_company/config/errors.py (1)
  • ConfigLocation (7-21)
src/ai_company/tools/registry.py (1)
  • get (71-96)
tests/unit/templates/test_schema.py (1)
src/ai_company/templates/schema.py (2)
  • TemplateAgentConfig (77-132)
  • CompanyTemplate (206-328)
tests/unit/templates/test_renderer.py (3)
src/ai_company/templates/loader.py (2)
  • load_template (158-214)
  • load_template_file (217-238)
src/ai_company/templates/renderer.py (1)
  • render_template (69-102)
src/ai_company/config/schema.py (1)
  • RootConfig (361-504)
tests/unit/templates/test_loader.py (2)
tests/unit/templates/conftest.py (2)
  • tmp_template_file (211-219)
  • TemplateFileFactory (12-15)
src/ai_company/templates/loader.py (1)
  • load_template_file (217-238)
🪛 YAMLlint (1.38.0)
src/ai_company/templates/builtins/full_company.yaml

[warning] 128-128: comment not indented like content

(comments-indentation)


[error] 129-129: syntax error: found character '%' that cannot start any token

(syntax)

🔇 Additional comments (10)
src/ai_company/templates/builtins/solo_founder.yaml (1)

5-6: LGTM!

The min_agents: 2 and max_agents: 3 constraints are consistent with the template's 2 defined agents (CEO and Full-Stack Developer). This aligns well with the "Solo Founder" minimal setup concept.

src/ai_company/templates/builtins/full_company.yaml (4)

5-27: LGTM!

The new min_agents/max_agents bounds and configurable developer count variables are well-designed. The min_agents: 1 provides flexibility for inheritance scenarios where child templates may remove agents, while max_agents: 50 aligns with the PR objectives for full_company templates (20-50+ agents).


34-61: LGTM!

Department budget allocations correctly sum to 100% (10+30+10+8+10+8+8+8+8=100). The new departments (design, data_analytics, security, creative_marketing) with appropriate head roles provide comprehensive enterprise coverage.


128-155: Static analysis hints are false positives (Jinja2 template context).

The YAMLlint warnings about line 128 comment indentation and line 129 % character are false positives—this is a Jinja2 template file, not pure YAML. The {% for %} syntax is valid Jinja2 that renders to proper YAML.

The Jinja2 loops are well-structured with appropriate | default() | int filters and sensible seniority progression (first dev is senior, subsequent devs are mid-level).

Minor style note: The comment on line 128 uses 4-space indentation while the Jinja2 control block at line 129 has no indentation. Consider aligning the comment with the Jinja2 block for visual consistency, though this is purely cosmetic.

[approve_code_changes, suggest_optional_refactor]


156-215: LGTM!

The expanded agent roster covers all departments with appropriate roles, levels, models, and personality presets. The structure properly reflects enterprise team composition with specialized roles across QA, data analytics, operations, security, and marketing.

tests/unit/templates/test_presets.py (1)

120-129: Nice drift check for auto-name coverage.

This will fail as soon as BUILTIN_ROLES grows without a matching _AUTO_NAMES entry.

src/ai_company/observability/events/template.py (1)

18-28: Good event-surface expansion.

Centralizing the new inheritance and load-failure event names here keeps the renderer/loader logging string-free and consistent.

src/ai_company/templates/presets.py (1)

431-437: Current syntax is correct—no change needed.

The parentheses in except (ValidationError, TypeError) as _exc: are required. PEP 758's parenthesis-free syntax only applies when there is no as clause; with as, parentheses must be used per PEP 758. The code is already compliant with the style guide.

src/ai_company/templates/loader.py (1)

356-358: The code at line 356 is already compliant with PEP 758. When catching multiple exception types and binding to an alias using as exc, parentheses are required by PEP 758. The current syntax except (ValidationError, ValueError, TypeError) as exc: is correct and does not need modification.

			> Likely an incorrect or invalid review comment.
tests/unit/templates/test_inheritance.py (1)

41-512: Nice coverage of the new inheritance surface.

This exercises merge semantics, parent-variable propagation, cycle/depth failures, and end-to-end rendering paths, which should keep the new renderer flow from regressing.

Comment on lines +218 to +225
parent_config = _render_to_dict(
parent_loaded,
parent_vars,
_chain=_chain | {parent_name},
)

merged = merge_template_configs(parent_config, child_config)
logger.info(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Let the parent keep its scalar defaults when the child didn't set them.

At this merge point, child_config has already been materialized as a full standalone config, so the parent-fallback branch in merge_template_configs() becomes unreachable for render-time inheritance. In practice, a child template that only adds agents will still override inherited scalars like company_name with its own synthesized defaults. Please omit those scalar keys unless the child explicitly provided them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/templates/renderer.py` around lines 218 - 225, The child
config being passed to merge_template_configs is already materialized with
synthesized defaults, causing it to clobber parent scalar values; before calling
merge_template_configs in the block that builds parent_config (where
_render_to_dict(parent_loaded, parent_vars, _chain=_chain | {parent_name}) is
used and merged with child_config), strip out scalar keys from child_config that
the child did not explicitly supply. Implement this by having _render_to_dict
(or the rendering call site) return or expose which keys were explicitly set by
the child (e.g., a provided_keys set) and then filter child_config to remove
scalar entries (e.g., company_name and other simple scalars) unless they appear
in that provided_keys set; then call merge_template_configs with the filtered
child_config so parent scalars are preserved.

…type safety, and function split

- Add merge_id field to TemplateAgentConfig for stable per-agent identity
  during inheritance merge (prevents extra="forbid" rejection)
- Add merge_id to all 6 YAML templates with duplicate-role agents
- Update merge key from (role, department) to (role, department, merge_id)
- Enforce deep-copy at all merge boundaries to prevent reference sharing
- Fix or-falsy anti-pattern: explicit `if value is None` checks
- Fix _normalize_extends validator: guard non-string inputs
- Narrow TemplateVariable.default from Any to str|int|float|bool|None
- Split _resolve_inheritance into 3 focused functions (<50 lines each)
- Wrap BUILTIN_TEMPLATES in MappingProxyType for read-only enforcement
- Fix startup.yaml min_agents: 4 → 3 to match actual roster
- Update DESIGN_SPEC.md §14.1: Title-Case roles, merge_id, 3-tuple key
- Replace hardcoded agent count dict with dynamic min/max bounds in tests
- Fix logger.error → logger.exception in loader.py exception handler
- Fix ruff lint: Union→pipe syntax, assert noqa, line length

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/ai_company/templates/renderer.py (1)

426-478: 🧹 Nitpick | 🔵 Trivial

_build_config_dict slightly exceeds the 50-line limit.

This function is 53 lines. Consider extracting the workflow_handoffs/escalation_paths handling (lines 474-476) into a small helper to stay within the guideline.

♻️ Suggested extraction
+def _extract_optional_lists(
+    rendered_data: dict[str, Any],
+    result: dict[str, Any],
+) -> None:
+    """Extract optional list fields from rendered data into result."""
+    for key in ("workflow_handoffs", "escalation_paths"):
+        if key in rendered_data and rendered_data[key] is not None:
+            result[key] = _validate_list(rendered_data, key)
+
+
 def _build_config_dict(
     rendered_data: dict[str, Any],
     template: CompanyTemplate,
     variables: dict[str, Any],
 ) -> dict[str, Any]:
     # ... existing code until line 472 ...
 
-    for key in ("workflow_handoffs", "escalation_paths"):
-        if key in rendered_data and rendered_data[key] is not None:
-            result[key] = _validate_list(rendered_data, key)
+    _extract_optional_lists(rendered_data, result)
 
     return result

As per coding guidelines, Functions must be under 50 lines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/templates/renderer.py` around lines 426 - 478, The
_build_config_dict function is over the 50-line limit; extract the
post-processing that adds "workflow_handoffs" and "escalation_paths" into a
small helper to shorten the function. Create a helper (e.g.,
_attach_optional_list_keys or _add_optional_lists) that accepts the result dict
and rendered_data, iterates over the tuple ("workflow_handoffs",
"escalation_paths"), checks key presence and non-None, calls
_validate_list(rendered_data, key) and assigns into result[key]; then replace
the final for-loop in _build_config_dict with a single call to that helper.
Ensure the helper uses the same _validate_list symbol and preserves behavior and
return types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@DESIGN_SPEC.md`:
- Around line 2218-2222: Update the "agents" merge-key description to explicitly
state that when merge_id is omitted the implementation uses an empty string as
the default (i.e., the key becomes the tuple (role, department, "") at runtime),
and mention this matches the current behavior of the merge logic for `agents`
and `merge_id`; adjust the sentence that currently reads "`(role, department,
"")` when `merge_id` is omitted" to clearly say this is the implementation
default used by the `agents` merging code.

In `@src/ai_company/templates/builtins/startup.yaml`:
- Around line 5-6: The startup template's max_agents value violates the 3–5
agent contract; update the startup template's max_agents from 7 to 5 so the
min_agents/min and max_agents constraints (min_agents: 3, max_agents: 5) enforce
the required 3–5 range in the startup template configuration (look for
min_agents and max_agents entries in the startup template YAML).

In `@src/ai_company/templates/presets.py`:
- Around line 434-437: Replace the current except clause that uses parentheses
with the PEP 758 style: change "except (ValidationError, TypeError) as _exc:" to
use the comma form "except ValidationError, TypeError as _exc:" in the block
that constructs msg and raises ValueError (the block referencing _preset_name
and _preset_dict) so the except handles both exception types using the requested
syntax and then re-raises ValueError from _exc; leave the rest of the logic (msg
creation and raise ValueError(msg) from _exc) unchanged.

In `@tests/unit/templates/test_renderer.py`:
- Around line 530-538: The assertion in test_full_company_variable_override is
brittle because it assumes a +2 delta; update the test to assert a clear,
deterministic expectation instead of an unexplained delta: load_template and
render_template as before, compute the previous and new counts of backend
developer agents by inspecting default_config.agents and override_config.agents
(e.g., count by role or name for "backend_dev"), and assert that the backend dev
count increased from the default to the expected value (or assert the explicit
total agent count equals a documented expected number). Also add a short comment
in test_full_company_variable_override explaining why the expected increase is
what it is so future template changes are easier to reason about.

---

Outside diff comments:
In `@src/ai_company/templates/renderer.py`:
- Around line 426-478: The _build_config_dict function is over the 50-line
limit; extract the post-processing that adds "workflow_handoffs" and
"escalation_paths" into a small helper to shorten the function. Create a helper
(e.g., _attach_optional_list_keys or _add_optional_lists) that accepts the
result dict and rendered_data, iterates over the tuple ("workflow_handoffs",
"escalation_paths"), checks key presence and non-None, calls
_validate_list(rendered_data, key) and assigns into result[key]; then replace
the final for-loop in _build_config_dict with a single call to that helper.
Ensure the helper uses the same _validate_list symbol and preserves behavior and
return types.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0543d695-79da-4192-84f0-d067d544f9ee

📥 Commits

Reviewing files that changed from the base of the PR and between 51b114d and fae2abc.

📒 Files selected for processing (13)
  • DESIGN_SPEC.md
  • src/ai_company/templates/builtins/agency.yaml
  • src/ai_company/templates/builtins/dev_shop.yaml
  • src/ai_company/templates/builtins/full_company.yaml
  • src/ai_company/templates/builtins/product_team.yaml
  • src/ai_company/templates/builtins/research_lab.yaml
  • src/ai_company/templates/builtins/startup.yaml
  • src/ai_company/templates/loader.py
  • src/ai_company/templates/merge.py
  • src/ai_company/templates/presets.py
  • src/ai_company/templates/renderer.py
  • src/ai_company/templates/schema.py
  • tests/unit/templates/test_renderer.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Always use native lazy annotations (PEP 649) — do not use from __future__ import annotations
Use PEP 758 except syntax: except A, B: without parentheses for multiple exception types
Maintain line length of 88 characters (enforced by ruff)

Files:

  • tests/unit/templates/test_renderer.py
  • src/ai_company/templates/renderer.py
  • src/ai_company/templates/merge.py
  • src/ai_company/templates/presets.py
  • src/ai_company/templates/loader.py
  • src/ai_company/templates/schema.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Use pytest markers @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, and @pytest.mark.slow to categorize tests
Prefer @pytest.mark.parametrize for testing similar test cases
Use generic vendor-agnostic names in test fixtures and configuration: test-provider, test-small-001, test-medium-001, test-large-001

Files:

  • tests/unit/templates/test_renderer.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: All public functions and classes must have type hints and Google-style docstrings (enforced by ruff D rules)
Never use import logging or logging.getLogger() in application code — use from ai_company.observability import get_logger with variable name logger
Use structured logging with constants from ai_company.observability.events.<domain> — always log as logger.info(EVENT_CONSTANT, key=value) never with format strings
For all identifier/name fields in Pydantic models, use NotBlankStr type from core.types instead of manual whitespace validators (including optional and tuple variants)
Use @computed_field for derived values in Pydantic models instead of storing and validating redundant fields
Enforce immutability: create new objects instead of mutating. For non-Pydantic internal collections use copy.deepcopy() at construction and MappingProxyType for read-only enforcement
For config/identity data use frozen Pydantic models; for runtime state that evolves use separate mutable-via-copy models with model_copy(update=...)
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code instead of bare create_task
Functions must be under 50 lines, files must be under 800 lines
Handle errors explicitly — never silently swallow exceptions
Never use real vendor names (Anthropic, OpenAI, Claude, GPT) in project-owned code, docstrings, comments, or tests — use generic names: example-provider, example-large-001, test-provider, test-small-001

Files:

  • src/ai_company/templates/renderer.py
  • src/ai_company/templates/merge.py
  • src/ai_company/templates/presets.py
  • src/ai_company/templates/loader.py
  • src/ai_company/templates/schema.py
DESIGN_SPEC.md

📄 CodeRabbit inference engine (CLAUDE.md)

Update DESIGN_SPEC.md after approved deviations to reflect the new reality

Files:

  • DESIGN_SPEC.md
🧠 Learnings (7)
📚 Learning: 2026-03-07T17:28:05.391Z
Learnt from: CR
Repo: Aureliolo/ai-company PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T17:28:05.391Z
Learning: Applies to tests/**/*.py : Use generic vendor-agnostic names in test fixtures and configuration: `test-provider`, `test-small-001`, `test-medium-001`, `test-large-001`

Applied to files:

  • tests/unit/templates/test_renderer.py
📚 Learning: 2026-03-07T17:28:05.391Z
Learnt from: CR
Repo: Aureliolo/ai-company PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T17:28:05.391Z
Learning: Applies to src/**/*.py : Functions must be under 50 lines, files must be under 800 lines

Applied to files:

  • src/ai_company/templates/renderer.py
📚 Learning: 2026-03-07T17:28:05.391Z
Learnt from: CR
Repo: Aureliolo/ai-company PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T17:28:05.391Z
Learning: Applies to src/ai_company/{providers,tools,engine,communication}/**/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) to prevent unintended mutations

Applied to files:

  • src/ai_company/templates/merge.py
  • src/ai_company/templates/builtins/product_team.yaml
📚 Learning: 2026-03-07T17:28:05.391Z
Learnt from: CR
Repo: Aureliolo/ai-company PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T17:28:05.391Z
Learning: Applies to src/**/*.py : Enforce immutability: create new objects instead of mutating. For non-Pydantic internal collections use `copy.deepcopy()` at construction and `MappingProxyType` for read-only enforcement

Applied to files:

  • src/ai_company/templates/merge.py
📚 Learning: 2026-03-07T17:28:05.391Z
Learnt from: CR
Repo: Aureliolo/ai-company PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T17:28:05.391Z
Learning: Applies to DESIGN_SPEC.md : Update `DESIGN_SPEC.md` after approved deviations to reflect the new reality

Applied to files:

  • DESIGN_SPEC.md
📚 Learning: 2026-03-07T17:28:05.391Z
Learnt from: CR
Repo: Aureliolo/ai-company PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T17:28:05.391Z
Learning: Applies to src/**/*.py : Use structured logging with constants from `ai_company.observability.events.<domain>` — always log as `logger.info(EVENT_CONSTANT, key=value)` never with format strings

Applied to files:

  • DESIGN_SPEC.md
📚 Learning: 2026-03-07T17:28:05.391Z
Learnt from: CR
Repo: Aureliolo/ai-company PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T17:28:05.391Z
Learning: Applies to src/ai_company/{api,config,providers}/**/*.py : Validate at system boundaries (user input, external APIs, config files)

Applied to files:

  • src/ai_company/templates/schema.py
🧬 Code graph analysis (3)
src/ai_company/templates/renderer.py (6)
src/ai_company/templates/errors.py (2)
  • TemplateInheritanceError (23-28)
  • TemplateRenderError (14-20)
src/ai_company/templates/merge.py (1)
  • merge_template_configs (36-98)
src/ai_company/config/utils.py (1)
  • deep_merge (37-61)
src/ai_company/config/defaults.py (1)
  • default_config_dict (6-32)
src/ai_company/templates/loader.py (1)
  • LoadedTemplate (79-90)
src/ai_company/templates/schema.py (1)
  • CompanyTemplate (218-344)
src/ai_company/templates/merge.py (3)
src/ai_company/config/utils.py (1)
  • deep_merge (37-61)
src/ai_company/observability/_logger.py (1)
  • get_logger (8-28)
src/ai_company/templates/errors.py (1)
  • TemplateInheritanceError (23-28)
src/ai_company/templates/loader.py (1)
src/ai_company/templates/errors.py (3)
  • TemplateNotFoundError (10-11)
  • TemplateRenderError (14-20)
  • TemplateValidationError (31-67)
🪛 YAMLlint (1.38.0)
src/ai_company/templates/builtins/full_company.yaml

[warning] 128-128: comment not indented like content

(comments-indentation)


[error] 129-129: syntax error: found character '%' that cannot start any token

(syntax)

🔇 Additional comments (28)
src/ai_company/templates/presets.py (1)

263-342: Well-structured preset expansion with proper immutability.

The new personality presets (user_advocate, process_optimizer, growth_hacker, technical_communicator, systems_thinker) are validated at import time and properly frozen via nested MappingProxyType. The expanded _AUTO_NAMES pool provides good role coverage for auto-name generation.

src/ai_company/templates/builtins/dev_shop.yaml (1)

37-80: Well-structured roster with proper merge_id disambiguation.

The template correctly uses merge_id to distinguish multiple agents with the same role (three Backend Developers with distinct identities). Agent count (8) is within the declared range [6, 10], and department budgets sum to 100%.

src/ai_company/templates/builtins/research_lab.yaml (1)

53-69: Past review concern addressed with distinct merge_ids.

The two Data Analyst entries now have unique merge identifiers (analyst-primary and analyst-secondary), resolving the previous concern about duplicate merge keys that would prevent surgical overrides in child templates.

tests/unit/templates/test_renderer.py (1)

516-528: Good implementation using metadata bounds instead of hardcoded values.

The test now reads bounds from loaded.template.metadata.min_agents and max_agents, keeping the test in sync with template definitions and addressing the previous review concern about duplicate data.

src/ai_company/templates/builtins/startup.yaml (1)

49-64: Proper merge_id usage for duplicate Full-Stack Developer roles.

The two Full-Stack Developer entries have distinct merge identifiers (fullstack-senior, fullstack-mid), enabling child templates to target specific agents during inheritance.

src/ai_company/templates/merge.py (4)

36-98: Well-designed merge semantics with proper immutability.

The merge_template_configs function implements clear merge strategies: scalars (child wins), config (deep-merged), agents (by role/department/merge_id key), departments (by name). Deep-copy is correctly applied at merge boundaries (lines 93, 95) to prevent reference sharing, addressing the previous review concern.


101-144: Agent merge now uses stable three-part key.

The (role, department, merge_id) key addresses the previous review concern about unstable merge keys when rosters contain duplicate roles. The _ParentEntry dataclass properly tracks match state, and agents are deep-copied at index time (line 136).


146-181: Proper _remove directive handling with clear error reporting.

The _apply_child_agent function correctly handles the _remove directive by marking matched parent entries for removal, and raises TemplateInheritanceError with structured logging when no matching parent is found.


208-263: Department merge with deep-copy enforcement.

The _merge_departments function properly deep-copies all department entries (lines 236, 248, 254, 261), preventing reference sharing across inheritance levels. Warning logs for nameless departments provide useful debugging context.

src/ai_company/templates/loader.py (2)

415-425: Past review concern addressed: proper handling of falsy company values.

The code now explicitly checks if company is None before defaulting to {}, ensuring that invalid falsy values like "", 0, or [] are properly caught by the isinstance(company, dict) check and raise a TypeError.


48-58: Proper immutability for built-in templates registry.

BUILTIN_TEMPLATES is correctly wrapped in MappingProxyType to prevent accidental mutation of the template registry.

src/ai_company/templates/schema.py (3)

291-299: Past review concern addressed: proper type guard in extends normalizer.

The _normalize_extends validator now explicitly checks isinstance(value, str) before calling .strip(), preventing AttributeError on non-string inputs. Non-string values are passed through for Pydantic's type validation to reject.


125-133: Proper merge directive fields with alias support.

The merge_id field provides stable identity for inheritance merging, and remove with alias="_remove" enables the removal directive in YAML while maintaining Pythonic attribute naming.


301-324: Correct skip of agent count validation during inheritance.

Skipping _validate_agent_count_in_range when extends is set is correct because child templates may define zero agents (inheriting all from parent). The docstring clearly explains the rationale.

src/ai_company/templates/renderer.py (5)

69-102: LGTM — Clean separation of concerns in render flow.

The refactored render_template now delegates inheritance resolution to _render_to_dict and keeps the top-level function focused on merging with defaults and validation. This is a good improvement.


151-199: Refactor addressed — function is now within the 50-line limit.

The _resolve_inheritance function has been split into smaller helpers (_validate_inheritance_chain, _render_and_merge_parent) as suggested in past reviews, bringing it to 49 lines.


235-254: Verify that circular import is acceptable here.

The load_template import inside _render_and_merge_parent is a deferred import to break a circular dependency. This is a valid pattern, but ensure it doesn't introduce import-time performance issues in hot paths.


596-600: Good addition — preserving _remove directive for inheritance.

The _remove merge directive is correctly preserved through the agent expansion, enabling child templates to remove parent agents.


604-649: Clean extraction of personality resolution logic.

The _resolve_agent_personality helper properly handles both inline personality configs and preset lookups with appropriate error handling and logging.

src/ai_company/templates/builtins/full_company.yaml (2)

128-142: Static analysis false positive — Jinja2 syntax is valid.

YAMLlint reports a syntax error on line 129 because it doesn't recognize Jinja2 template syntax ({% for ... %}). This is expected and not a real issue — the template will be processed by Jinja2 before YAML parsing.

The merge_id: "backend-{{ i }}" pattern correctly ensures each dynamically-generated agent is uniquely addressable for inheritance, addressing the collision concerns raised in past reviews of other templates.


5-6: Well-structured enterprise roster with proper agent differentiation.

The template now includes:

  • Dynamic developer generation with merge_id for unique addressability
  • Comprehensive department coverage (9 departments, budgets sum to 100%)
  • Clear role differentiation with appropriate personality presets

Also applies to: 63-217

src/ai_company/templates/builtins/product_team.yaml (2)

59-70: Collision issue resolved — agents now have unique merge keys.

The two Backend Developer entries now have distinct merge_id values (backend-senior and backend-mid), making them uniquely addressable for inheritance. This addresses the concern raised in past reviews.


26-41: Department structure is well-balanced.

Budget percentages sum to 100% (25+35+15+15+10), and each department has an appropriate head_role assigned.

src/ai_company/templates/builtins/agency.yaml (2)

79-84: Collision issue resolved — Full-Stack Developers now uniquely addressable.

The two Full-Stack Developer entries now have distinct merge_id values (fullstack-senior on line 80, fullstack-mid on line 101), enabling child templates to selectively override or remove individual agents.

Also applies to: 100-105


26-41: Budget and department structure correct.

Budget percentages sum to 100% (20+35+20+10+15), with appropriate head_role assignments. The addition of quality_assurance and creative_marketing departments rounds out the agency structure.

DESIGN_SPEC.md (3)

2203-2225: Excellent documentation of template inheritance semantics.

The inheritance documentation clearly explains:

  • Scalar merge behavior (child wins)
  • Config deep-merge
  • Agent merge by (role, department, merge_id) key with _remove support
  • Department merge by name
  • Circular inheritance detection

This aligns well with the implementation in merge.py and renderer.py.


2573-2574: Engineering conventions table updated appropriately.

The new conventions for template inheritance and Pydantic alias usage are well-documented with clear rationale. The _ParentEntry dataclass and DEFAULT_MERGE_DEPARTMENT constant are properly referenced.


2514-2514: Project structure updated with merge.py.

The addition of merge.py to the templates directory in the project structure is correctly documented.

Comment on lines +434 to +437
except (ValidationError, TypeError) as _exc:
msg = f"Invalid personality preset {_preset_name!r}: {_exc}"
raise ValueError(msg) from _exc
# Clean up loop variables only if the loop body executed (non-empty dict).
if PERSONALITY_PRESETS:
del _preset_name, _preset_dict
del _preset_name, _preset_dict
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Use PEP 758 except syntax without parentheses.

As per coding guidelines, use except ValidationError, TypeError: instead of parentheses.

♻️ Suggested fix
-    except (ValidationError, TypeError) as _exc:
+    except ValidationError, TypeError as _exc:

As per coding guidelines: "Use PEP 758 except syntax: except A, B: without parentheses for multiple exception types"

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except (ValidationError, TypeError) as _exc:
msg = f"Invalid personality preset {_preset_name!r}: {_exc}"
raise ValueError(msg) from _exc
# Clean up loop variables only if the loop body executed (non-empty dict).
if PERSONALITY_PRESETS:
del _preset_name, _preset_dict
del _preset_name, _preset_dict
except ValidationError, TypeError as _exc:
msg = f"Invalid personality preset {_preset_name!r}: {_exc}"
raise ValueError(msg) from _exc
del _preset_name, _preset_dict
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/templates/presets.py` around lines 434 - 437, Replace the
current except clause that uses parentheses with the PEP 758 style: change
"except (ValidationError, TypeError) as _exc:" to use the comma form "except
ValidationError, TypeError as _exc:" in the block that constructs msg and raises
ValueError (the block referencing _preset_name and _preset_dict) so the except
handles both exception types using the requested syntax and then re-raises
ValueError from _exc; leave the rest of the logic (msg creation and raise
ValueError(msg) from _exc) unchanged.

…n extraction

- Reject _remove directive in standalone templates (no extends) with error
- Add TEMPLATE_LOAD_INVALID_NAME event for path-traversal rejections
- Strip _remove from surviving parent agents in merge output
- Extract _attach_optional_lists helper from _build_config_dict (<50 lines)
- Restore empty-dict guard on del _preset_name at module level
- Tighten startup.yaml max_agents: 7 → 5 per issue #81 contract
- Clarify DESIGN_SPEC.md merge_id defaults-to-empty-string wording
- Make test_full_company_variable_override assert on role counts not delta

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 7, 2026 19:31
@Aureliolo Aureliolo merged commit 15a9134 into main Mar 7, 2026
10 of 11 checks passed
@Aureliolo Aureliolo deleted the feat/templates-presets-rosters branch March 7, 2026 19:31
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

| **State coordination** | Planned (M4) | Centralized single-writer: `TaskEngine` owns all task/project mutations via `asyncio.Queue`. Agents submit requests, engine applies `model_copy(update=...)` sequentially and publishes snapshots. `version: int` field on state models for future optimistic concurrency if multi-process scaling is needed. | Prevents lost updates by design. Trivial in single-threaded asyncio (no locks). Perfect audit trail. Industry consensus: MetaGPT, CrewAI, AutoGen all use prevention-by-design, not conflict resolution. See §6.8 State Coordination table. |
| **Workspace isolation** | Planned (M4) | Pluggable `WorkspaceIsolationStrategy` protocol. Default: planner + git worktrees. Each agent works in an isolated worktree; sequential merge on completion. Textual conflicts detected by git; semantic conflicts reviewed by agent or human. | Industry standard (Codex, Cursor, Claude Code, VS Code). Maximum parallelism. Leverages mature git infrastructure. See §6.8. |
| **Graceful shutdown** | Adopted (M3) | Pluggable `ShutdownStrategy` protocol. Default: cooperative with 30s timeout. Agents check shutdown event at turn boundaries. Force-cancel after timeout. `INTERRUPTED` status for force-cancelled tasks. M4/M5: upgrade to checkpoint-and-stop. | Cross-platform (Windows `signal.signal()` fallback). Bounded shutdown time. Mirrors cooperative shutdown in §6.7. |
| **Template inheritance** | Adopted (M2.5) | `extends` field on `CompanyTemplate` triggers parent resolution at render time. `merge.py` merges configs by field type: scalars (child wins), config dicts (deep merge), agents (by `(role, department)` key with `_remove` support), departments (by name). `_ParentEntry` dataclass tracks merge state. `DEFAULT_MERGE_DEPARTMENT = "engineering"` shared between merge and renderer. Circular chains detected via `frozenset` tracking; max depth = 10. | Enables template composition without copy-paste. Merge-by-key preserves parent order. `_remove` directive enables clean agent removal without workarounds. |
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

The conventions table entry for Template inheritance describes the agent merge key as (role, department) but the actual code and the detailed documentation in §14.1 (line 2221) correctly describe it as (role, department, merge_id). This row in the conventions table should be updated to mention merge_id for consistency with the actual merge key.

Suggested change
| **Template inheritance** | Adopted (M2.5) | `extends` field on `CompanyTemplate` triggers parent resolution at render time. `merge.py` merges configs by field type: scalars (child wins), config dicts (deep merge), agents (by `(role, department)` key with `_remove` support), departments (by name). `_ParentEntry` dataclass tracks merge state. `DEFAULT_MERGE_DEPARTMENT = "engineering"` shared between merge and renderer. Circular chains detected via `frozenset` tracking; max depth = 10. | Enables template composition without copy-paste. Merge-by-key preserves parent order. `_remove` directive enables clean agent removal without workarounds. |
| **Template inheritance** | Adopted (M2.5) | `extends` field on `CompanyTemplate` triggers parent resolution at render time. `merge.py` merges configs by field type: scalars (child wins), config dicts (deep merge), agents (by `(role, department, merge_id)` key with `_remove` support), departments (by name). `_ParentEntry` dataclass tracks merge state. `DEFAULT_MERGE_DEPARTMENT = "engineering"` shared between merge and renderer. Circular chains detected via `frozenset` tracking; max depth = 10. | Enables template composition without copy-paste. Merge-by-key preserves parent order. `_remove` directive enables clean agent removal without workarounds. |

Copilot uses AI. Check for mistakes.
Comment on lines 609 to +636
agent_dict: dict[str, Any] = {
"name": name,
"role": role,
"department": agent.get("department", _DEFAULT_DEPARTMENT),
"level": agent.get("level", "mid"),
}

_resolve_agent_personality(agent, name, agent_dict)

model_tier = agent.get("model", "medium")
agent_dict["model"] = {"provider": _DEFAULT_PROVIDER, "model_id": model_tier}

# Preserve _remove merge directive for inheritance.
if agent.get("_remove"):
if not has_extends:
msg = (
f"Agent {name!r} uses '_remove' but the template "
"has no 'extends' — directive has no effect"
)
logger.warning(
TEMPLATE_RENDER_VARIABLE_ERROR,
agent=name,
field="_remove",
)
raise TemplateRenderError(msg)
agent_dict["_remove"] = True

return agent_dict
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

_expand_single_agent does not propagate merge_id from the raw agent dict into the expanded agent_dict. Since _build_config_dict calls _expand_agents before the merge happens, the merge_id key is lost. When _merge_agents later computes _agent_key, it always sees merge_id="" for all agents.

This makes the merge_id field in YAML templates (e.g., startup.yaml line 50/56, dev_shop.yaml line 44/50) ineffective for inheritance merges. Two Full-Stack Developers with different merge_id values will both get key ("full-stack developer", "engineering", ""), defeating the purpose of merge_id as a disambiguation tool for child template overrides.

The fix is to include merge_id in the returned agent_dict when it is present in the input agent, e.g., adding if agent.get("merge_id"): agent_dict["merge_id"] = agent["merge_id"] after building the base dict.

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +175
clean = copy.deepcopy(
{k: v for k, v in child_agent.items() if k not in ("_remove", "merge_id")}
)

if is_remove:
if matched_entry is None:
msg = f"Cannot remove agent with key {key}: no matching parent agent found"
logger.error(
TEMPLATE_INHERIT_MERGE_ERROR,
action="remove_failed",
key=key,
)
raise TemplateInheritanceError(msg)
matched_entry.matched = True
matched_entry.agent = None # mark for removal
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

When is_remove is True and a matching parent is found, the clean dict (line 161-163) is computed via copy.deepcopy but never used — the code immediately sets matched_entry.agent = None on line 175. Consider moving the clean computation after the is_remove check to avoid the unnecessary deep copy in the remove path.

Copilot uses AI. Check for mistakes.
Aureliolo added a commit that referenced this pull request Mar 7, 2026
Address 13 findings from post-merge bot reviews on PRs #157-#162:

Communication layer (PR #157):
- Wake blocked receive() callers on unsubscribe via None sentinel
- Distinguish shutdown vs timeout in receive() logging
- Add AgentMessenger.receive() facade method
- Validate MessageHandler.handle() is async at registration
- Await cancelled tasks in _await_with_shutdown to prevent warnings

Observability (PR #158):
- Add log-before-raise to all validators missing it (company.py,
  schema.py) — 14 raise sites across 11 validators

Parallel execution (PR #161):
- Log suppressed ExceptionGroup instead of silent pass
- Add PARALLEL_AGENT_CANCELLED structured event for cancellations
- Fix progress.in_progress semantics (increment after semaphore)
- Use PARALLEL_LOCK_RELEASE_ERROR for lock release failures
- Remove duplicate plan_parsing.py from DESIGN_SPEC file tree

Template inheritance (PR #162):
- Update DESIGN_SPEC merge key docs to include merge_id
- Preserve merge_id in _expand_single_agent (confirmed bug fix)
- Defer deepcopy in _apply_child_agent past _remove early-return

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants