Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 21 additions & 17 deletions factory/agents/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,13 @@
import logging
import os
from pathlib import Path
from typing import Literal

from factory.ace.injector import inject_playbook, load_playbook
from factory.runners import get_runner

logger = logging.getLogger(__name__)

AgentRole = Literal[
"researcher",
"strategist",
"builder",
"health_checker",
"code_reviewer",
"adversarial_tester",
"archivist",
"ceo",
"failure_analyst",
"refiner",
"profiler",
"refactory",
]
AgentRole = str

# Consecutive failure tracking
_consecutive_failures: int = 0
Expand Down Expand Up @@ -62,6 +48,7 @@ def __init__(self, failure_count: int, last_agent: str) -> None:

# Directory containing base agent prompts (shipped with the factory)
_PROMPTS_DIR = Path(__file__).parent / "prompts"
_USER_PROMPTS_DIR = Path.home() / ".factory" / "agents" / "prompts"


def resolve_prompt(
Expand All @@ -75,7 +62,8 @@ def resolve_prompt(

Resolution order:
1. Project-specific override: <project>/.factory/agents/<role>.md
2. Factory default: factory/agents/prompts/<role>.md
2. User-global: ~/.factory/agents/prompts/<role>.md
3. Factory default: factory/agents/prompts/<role>.md

When *use_profile* is True, loads ~/.factory/profile.md and appends it
after the ACE playbook injection.
Expand Down Expand Up @@ -103,14 +91,30 @@ def resolve_prompt(
prompt = _maybe_inject_skill(prompt, project_path, workflow_mode)
return prompt

# Check user-global prompts (~/.factory/agents/prompts/)
user_path = _USER_PROMPTS_DIR / f"{role}.md"
if user_path.exists():
logger.info("Using user-global prompt for %s: %s", role, user_path)
prompt = user_path.read_text()
playbook = load_playbook(role)
if playbook:
prompt = inject_playbook(prompt, playbook)
logger.info("Injected playbook for %s (user-global)", role)
if use_profile:
prompt = _maybe_inject_profile(prompt, role)
if role == "ceo" and workflow_mode and project_path is not None:
prompt = _maybe_inject_skill(prompt, project_path, workflow_mode)
return prompt

# Fall back to factory default
default_path = _PROMPTS_DIR / f"{role}.md"
if not default_path.exists():
override_hint = (
f" or {project_path / '.factory' / 'agents' / f'{role}.md'}" if project_path else ""
)
raise FileNotFoundError(
f"No prompt found for agent role '{role}'. Expected at {default_path}{override_hint}"
f"No prompt found for agent role '{role}'. "
f"Expected at {default_path}, {_USER_PROMPTS_DIR / f'{role}.md'}{override_hint}"
)

prompt = default_path.read_text()
Expand Down
5 changes: 2 additions & 3 deletions tests/test_refactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import os
import stat
from pathlib import Path
from typing import get_args
from unittest.mock import patch

import pytest
Expand Down Expand Up @@ -138,10 +137,10 @@ def test_corrupt_json_generates_new(self, tmp_path: Path) -> None:


class TestAgentRegistration:
def test_refactory_role_in_agent_role(self) -> None:
def test_agent_role_accepts_any_string(self) -> None:
from factory.agents.runner import AgentRole

assert "refactory" in get_args(AgentRole)
assert AgentRole is str

def test_refactory_in_agents_yml(self) -> None:
import yaml
Expand Down
Loading