#!/usr/bin/env python3 """Implementer response-builder MCP server. Spawned per-attempt by the worker controller. Validates each builder call against ImplementerOutputV1 and enforces outcome-specific invariants at finalize: - outcome='resolved' → requires ≥1 commit AND ≥1 file modified - outcome='rebase-failed' → no extra requirements; controller routes to CONFLICT_RESOLVING - outcome='blocked' → requires ≥1 entry in blockers - outcome='noop' → forbids commits + files + blockers - outcome='competence-failure' → no extra requirements """ from __future__ import annotations import sys from datetime import datetime, timezone from pathlib import Path from typing import Any from mcp.server.fastmcp import FastMCP _REPO_ROOT = Path(__file__).resolve().parents[3] if str(_REPO_ROOT) not in sys.path: sys.path.insert(0, str(_REPO_ROOT)) from tools._mcp_common import make_main # noqa: E402 from tools.controller.contracts.v1 import ImplementerOutputV1 # noqa: E402 from tools.controller.mcp._builder_base import ( # noqa: E402 BuilderError, BuilderState, call_with_invariants, finalize_and_emit, ok, ) server = FastMCP("implementer-builder") _STATE = BuilderState() _OUTCOME_VALUES = { "resolved", "rebase-failed", "noop", "blocked", "competence-failure" } _CONFIDENCE_VALUES = {"high", "medium", "low"} _TIER_VALUES = {0, 1, 2} @server.tool() def implementer_start( workflow_id: int, attempt_id: int, pr_number: int, tier: int ) -> dict[str, Any]: """Initialize the implementer session. MUST be the first tool called.""" args = {"workflow_id": workflow_id, "attempt_id": attempt_id, "pr_number": pr_number, "tier": tier} def body() -> dict[str, Any]: if _STATE.started: raise BuilderError("implementer_start may only be called once per attempt") if tier not in _TIER_VALUES: raise BuilderError(f"invalid tier {tier!r}; expected one of {sorted(_TIER_VALUES)}") _STATE.started = True _STATE.started_at = datetime.now(timezone.utc) _STATE.identity = { "workflow_id": workflow_id, "attempt_id": attempt_id, "pr_number": pr_number, "tier": tier, } _STATE.fields["used_tier"] = tier return ok(tier=tier) return call_with_invariants(_STATE, "implementer_start", body, args) @server.tool() def implementer_record_file_modified( path: str, lines_added: int = 0, lines_deleted: int = 0 ) -> dict[str, Any]: """Record one file the implementer modified.""" args = {"path": path, "lines_added": lines_added, "lines_deleted": lines_deleted} def body() -> dict[str, Any]: _STATE.require_started() _STATE.require_not_finalized() if not path.strip(): raise BuilderError("path must be non-empty") if lines_added < 0 or lines_deleted < 0: raise BuilderError("lines_added/lines_deleted must be ≥ 0") files = _STATE.fields.setdefault("files_touched", []) if path not in files: files.append(path) return ok(files_touched_count=len(files)) return call_with_invariants(_STATE, "implementer_record_file_modified", body, args) @server.tool() def implementer_record_commit(sha: str, message: str) -> dict[str, Any]: """Record a git commit the implementer pushed.""" args = {"sha": sha, "message_len": len(message)} def body() -> dict[str, Any]: _STATE.require_started() _STATE.require_not_finalized() if not sha.strip() or len(sha) < 7: raise BuilderError(f"sha must look like a git SHA (≥7 chars); got {sha!r}") if not message.strip(): raise BuilderError("commit message must be non-empty") commits = _STATE.fields.setdefault("commit_shas", []) if sha not in commits: commits.append(sha) return ok(commits_count=len(commits)) return call_with_invariants(_STATE, "implementer_record_commit", body, args) @server.tool() def implementer_add_blocker(description: str) -> dict[str, Any]: """Add a blocker explanation. Only valid when outcome will be 'blocked'.""" args = {"description": description} def body() -> dict[str, Any]: _STATE.require_started() _STATE.require_not_finalized() if not description.strip(): raise BuilderError("blocker description must be non-empty") current_outcome = _STATE.fields.get("outcome") if current_outcome and current_outcome != "blocked": raise BuilderError( f"cannot add blocker with outcome={current_outcome!r}; " "set outcome='blocked' first" ) blockers = _STATE.fields.setdefault("blockers", []) blockers.append(description) return ok(blockers_count=len(blockers)) return call_with_invariants(_STATE, "implementer_add_blocker", body, args) @server.tool() def implementer_set_outcome(outcome: str) -> dict[str, Any]: args = {"outcome": outcome} def body() -> dict[str, Any]: _STATE.require_started() _STATE.require_not_finalized() if outcome not in _OUTCOME_VALUES: raise BuilderError( f"invalid outcome {outcome!r}; expected one of {sorted(_OUTCOME_VALUES)}" ) _STATE.fields["outcome"] = outcome return ok(outcome=outcome) return call_with_invariants(_STATE, "implementer_set_outcome", body, args) @server.tool() def implementer_set_confidence(confidence: str) -> dict[str, Any]: args = {"confidence": confidence} def body() -> dict[str, Any]: _STATE.require_started() _STATE.require_not_finalized() if confidence not in _CONFIDENCE_VALUES: raise BuilderError( f"invalid confidence {confidence!r}; expected one of {sorted(_CONFIDENCE_VALUES)}" ) _STATE.fields["confidence"] = confidence return ok(confidence=confidence) return call_with_invariants(_STATE, "implementer_set_confidence", body, args) def _check_outcome_invariants(state: BuilderState) -> None: """Outcome-specific finalize check.""" outcome = state.fields.get("outcome") files = state.fields.get("files_touched") or [] commits = state.fields.get("commit_shas") or [] blockers = state.fields.get("blockers") or [] if outcome == "resolved": if not commits: raise BuilderError("outcome='resolved' requires ≥1 commit; " "call implementer_record_commit at least once") if not files: raise BuilderError("outcome='resolved' requires ≥1 file modified; " "call implementer_record_file_modified at least once") elif outcome == "blocked": if not blockers: raise BuilderError("outcome='blocked' requires ≥1 blocker; " "call implementer_add_blocker at least once") elif outcome == "noop": if commits or files or blockers: raise BuilderError( "outcome='noop' forbids commits/files/blockers; " f"got {len(commits)} commits, {len(files)} files, " f"{len(blockers)} blockers" ) @server.tool() def implementer_finalize() -> dict[str, Any]: def body() -> dict[str, Any]: _STATE.require_started() _STATE.require_not_finalized() missing = [ f for f in ("outcome", "confidence", "used_tier") if f not in _STATE.fields ] if missing: raise BuilderError( f"missing required fields: {missing}. Call the matching " "setters before finalize." ) started = _STATE.started_at wallclock = (datetime.now(timezone.utc) - started).total_seconds() if started else 0.0 _STATE.fields["output_version"] = "V1" _STATE.fields["wallclock_seconds"] = wallclock _STATE.fields.setdefault("files_touched", []) _STATE.fields.setdefault("commit_shas", []) _STATE.fields.setdefault("blockers", []) return finalize_and_emit( _STATE, ImplementerOutputV1, extra_required_check=_check_outcome_invariants, ) _STATE.record("implementer_finalize", {}) try: return body() except BuilderError as e: return {"error": str(e), "tool": "implementer_finalize"} @server.tool() def implementer_state() -> dict[str, Any]: return { "started": _STATE.started, "finalized": _STATE.finalized, "identity": dict(_STATE.identity), "fields_set": sorted(_STATE.fields.keys()), "files_count": len(_STATE.fields.get("files_touched") or []), "commits_count": len(_STATE.fields.get("commit_shas") or []), "blockers_count": len(_STATE.fields.get("blockers") or []), "audit_entries": len(_STATE.audit), } main = make_main(server, "implementer-builder") if __name__ == "__main__": raise SystemExit(main())