Skip to content
Open
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
8 changes: 7 additions & 1 deletion .claude/templates/coding_prompt.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ After thorough verification, mark the feature as passing:
Use the feature_mark_passing tool with feature_id=42
```

If the project has quality gates enabled, `feature_mark_passing` first runs the project's linters and type checkers. If it returns a "Quality gate failed" error, fix the reported errors and call it again. When quality gates are enabled, you can also run the same checks yourself at any time with `feature_verify_quality with feature_id=42` (the tool is only available, and only works, when quality gates are enabled for the project).

**NEVER:**

- Delete features
Expand Down Expand Up @@ -233,13 +235,17 @@ feature_mark_in_progress with feature_id={id}
feature_mark_passing with feature_id={id}

# 5. Mark a feature as failing (if you discover it's broken)
feature_mark_failing with feature_id={id}
feature_mark_failing with feature_id={id} and reason="what is broken"

# 6. Skip a feature (moves to end of queue) - ONLY when blocked by external dependency
feature_skip with feature_id={id}

# 7. Clear in-progress status (when abandoning a feature)
feature_clear_in_progress with feature_id={id}

# 8. Run lint/type-check quality gates and store the results as evidence
# (only available when quality gates are enabled for the project)
feature_verify_quality with feature_id={id}
```

### RULES:
Expand Down
28 changes: 26 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ python test_security_integration.py # Integration tests (9 tests)
python -m pytest test_client.py # Client tests (20 tests)
python -m pytest test_dependency_resolver.py # Dependency resolver tests (12 tests)
python -m pytest test_rate_limit_utils.py # Rate limit tests (22 tests)
python -m pytest test_quality_gates.py # Quality gates tests (30 tests)
```

### React UI
Expand Down Expand Up @@ -175,6 +176,7 @@ Publishing: `npm publish` (triggers `prepublishOnly` which builds UI, then publi
- `api/database.py` - SQLAlchemy models (Feature, Schedule, ScheduleOverride)
- `api/dependency_resolver.py` - Cycle detection (Kahn's algorithm + DFS) and dependency validation
- `api/migration.py` - JSON-to-SQLite migration utility
- `quality_gates.py` - Lint/type-check auto-detection and enforcement before `feature_mark_passing`

### Project Registry

Expand Down Expand Up @@ -235,8 +237,9 @@ MCP tools available to the agent:
- `feature_get_graph` - Get full dependency graph
- `feature_claim_and_get` - Atomically claim next available feature (for parallel mode)
- `feature_mark_in_progress` - Mark feature as in progress
- `feature_mark_passing` - Mark feature complete
- `feature_mark_failing` - Mark feature as failing
- `feature_mark_passing` - Mark feature complete (runs quality gates first when enabled)
- `feature_mark_failing` - Mark feature as failing (optional `reason`; increments `failure_count`)
- `feature_verify_quality` - Run detected linters/type checkers and store results as evidence (only when quality gates are enabled)
- `feature_skip` - Move feature to end of queue
- `feature_clear_in_progress` - Clear in-progress status
- `feature_create_bulk` - Initialize all features (used by initializer)
Expand All @@ -245,6 +248,27 @@ MCP tools available to the agent:
- `feature_remove_dependency` - Remove a dependency
- `feature_set_dependencies` - Set all dependencies for a feature at once

### Quality Gates

Opt-in per project via `.autoforge/config.json` (disabled by default):

```json
{
"quality_gates": {
"enabled": true,
"strict_mode": true,
"checks": {"lint": true, "type_check": true},
"timeout_seconds": 300
}
}
```

- Auto-detects ESLint or Biome, TypeScript (`tsc`), ruff or flake8, and mypy. A tool runs only if the project both configures it and has it installed (`node_modules/.bin`, `.venv`/`venv`, or PATH). Commands are never read from config.
- When enabled, `feature_mark_passing` runs the checks first. Strict mode (default) refuses to mark the feature passing on failure; non-strict marks it passing and returns a warning.
- Results are stored on the feature (`quality_result`), along with `failure_reason` and `failure_count`.
- `feature_verify_quality` is only offered to coding/testing agents when gates are enabled, and refuses to run anything when they are disabled.
- The MCP server runs outside the bash sandbox, and linters load agent-written config files (e.g. `eslint.config.js`), so only enable this for projects you trust the agent with.

### React UI (ui/)

- Tech stack: React 19, TypeScript, Vite 7, TanStack Query, Tailwind CSS v4, Radix UI, dagre (graph layout), xterm.js (terminal)
Expand Down
25 changes: 25 additions & 0 deletions api/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ class Feature(Base):
human_input_request = Column(JSON, nullable=True, default=None) # Agent's structured request
human_input_response = Column(JSON, nullable=True, default=None) # Human's response

# Quality gates: evidence of lint/type-check runs and failure tracking
quality_result = Column(JSON, nullable=True, default=None) # Last quality check report
failure_reason = Column(Text, nullable=True, default=None) # Why the feature last failed
failure_count = Column(Integer, nullable=False, default=0) # Number of recorded failures

def to_dict(self) -> dict:
"""Convert feature to dictionary for JSON serialization."""
return {
Expand All @@ -84,6 +89,10 @@ def to_dict(self) -> dict:
"needs_human_input": self.needs_human_input if self.needs_human_input is not None else False,
"human_input_request": self.human_input_request,
"human_input_response": self.human_input_response,
# Quality gate fields
"quality_result": self.quality_result,
"failure_reason": self.failure_reason,
"failure_count": self.failure_count if self.failure_count is not None else 0,
}

def get_dependencies_safe(self) -> list[int]:
Expand Down Expand Up @@ -326,6 +335,21 @@ def _migrate_add_human_input_columns(engine) -> None:
conn.commit()


def _migrate_add_quality_gate_columns(engine) -> None:
"""Add quality gate columns to existing databases that don't have them."""
with engine.connect() as conn:
result = conn.execute(text("PRAGMA table_info(features)"))
columns = [row[1] for row in result.fetchall()]

if "quality_result" not in columns:
conn.execute(text("ALTER TABLE features ADD COLUMN quality_result TEXT DEFAULT NULL"))
if "failure_reason" not in columns:
conn.execute(text("ALTER TABLE features ADD COLUMN failure_reason TEXT DEFAULT NULL"))
if "failure_count" not in columns:
conn.execute(text("ALTER TABLE features ADD COLUMN failure_count INTEGER NOT NULL DEFAULT 0"))
conn.commit()


def _migrate_add_schedules_tables(engine) -> None:
"""Create schedules and schedule_overrides tables if they don't exist."""
from sqlalchemy import inspect
Expand Down Expand Up @@ -450,6 +474,7 @@ def create_database(project_dir: Path) -> tuple:
_migrate_add_dependencies_column(engine)
_migrate_add_testing_columns(engine)
_migrate_add_human_input_columns(engine)
_migrate_add_quality_gate_columns(engine)

# Migrate to add schedules tables
_migrate_add_schedules_tables(engine)
Expand Down
35 changes: 29 additions & 6 deletions client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from claude_agent_sdk.types import HookContext, HookInput, HookMatcher, SyncHookJSONOutput
from dotenv import load_dotenv

from quality_gates import load_config as load_quality_config
from security import SENSITIVE_DIRECTORIES, bash_security_hook

# Load environment variables from .env file if present
Expand Down Expand Up @@ -190,6 +191,31 @@ def get_extra_read_paths() -> list[Path]:
set(CODING_AGENT_TOOLS) | set(TESTING_AGENT_TOOLS) | set(INITIALIZER_AGENT_TOOLS)
)

# Runs project linters/type checkers from the feature MCP server, which sits
# outside the bash sandbox. Only exposed to coding/testing agents when quality
# gates are enabled for the project (.autoforge/config.json).
QUALITY_GATE_TOOLS = [
"mcp__features__feature_verify_quality",
]


def get_feature_tools(agent_type: str, project_dir: Path) -> list[str]:
"""Return the feature MCP tools an agent of this type may see.

Quality gate tools are added for coding/testing agents only when the
project has quality gates enabled.
"""
feature_tools_map = {
"coding": CODING_AGENT_TOOLS,
"testing": TESTING_AGENT_TOOLS,
"initializer": INITIALIZER_AGENT_TOOLS,
}
tools = list(feature_tools_map.get(agent_type, CODING_AGENT_TOOLS))
if agent_type != "initializer" and load_quality_config(project_dir).enabled:
tools.extend(QUALITY_GATE_TOOLS)
return tools


# Built-in tools available to agents.
# WebFetch and WebSearch are included so coding agents can look up current
# documentation for frameworks and libraries they are implementing.
Expand Down Expand Up @@ -234,12 +260,8 @@ def create_client(
The Claude SDK auto-detects credentials from the Claude CLI configuration
"""
# Select the feature MCP tools appropriate for this agent type
feature_tools_map = {
"coding": CODING_AGENT_TOOLS,
"testing": TESTING_AGENT_TOOLS,
"initializer": INITIALIZER_AGENT_TOOLS,
}
feature_tools = feature_tools_map.get(agent_type, CODING_AGENT_TOOLS)
feature_tools = get_feature_tools(agent_type, project_dir)
quality_tools = [t for t in QUALITY_GATE_TOOLS if t in feature_tools]

# Select max_turns based on agent type:
# - coding/initializer: 300 turns (complex multi-step implementation)
Expand Down Expand Up @@ -273,6 +295,7 @@ def create_client(
"WebSearch(*)",
# Allow Feature MCP tools for feature management
*ALL_FEATURE_MCP_TOOLS,
*quality_tools,
]

# Add extra read paths from environment variable (read-only access)
Expand Down
Loading