From 86c356c631b4189433334de484a1395fe0cd24b6 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:43:56 +0100 Subject: [PATCH 01/18] refactor: extract shared protected-path verification policy --- foundry/verification/policy.py | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 foundry/verification/policy.py diff --git a/foundry/verification/policy.py b/foundry/verification/policy.py new file mode 100644 index 0000000..d3f56a9 --- /dev/null +++ b/foundry/verification/policy.py @@ -0,0 +1,46 @@ +"""Shared verification policy for protected-path changes. + +This policy originated in the historical Foundry RunEngine. It lives outside +that state machine so both the legacy lifecycle and the UCF transition adapter +apply the same safety boundary. +""" + +from __future__ import annotations + +import fnmatch + +from foundry.contracts.shared import TaskType + +PROTECTED_PATH_PREFIXES: tuple[str, ...] = ("migrations/", "auth/", "infra/") +PROTECTED_PATH_GLOBS: tuple[str, ...] = ("Dockerfile*", "docker-compose*") +PROTECTED_PATH_KEYWORDS: tuple[str, ...] = ("secret", "credential", "token") + +MIGRATION_GUARD_ALLOWED_TASK_TYPES: set[TaskType] = { + TaskType.ENDPOINT_BUILD, + TaskType.REFACTOR, + TaskType.MIGRATION_PLAN, + TaskType.CANON_UPDATE, +} + + +def match_protected_paths(changed_files: list[str]) -> list[str]: + """Return changed paths that require migration-guard scrutiny.""" + protected: list[str] = [] + for path in changed_files: + if any( + path.startswith(prefix) or f"/{prefix}" in path + for prefix in PROTECTED_PATH_PREFIXES + ): + protected.append(path) + continue + + basename = path.rsplit("/", 1)[-1] if "/" in path else path + if any(fnmatch.fnmatch(basename, pattern) for pattern in PROTECTED_PATH_GLOBS): + protected.append(path) + continue + + lowered = path.lower() + if any(keyword in lowered for keyword in PROTECTED_PATH_KEYWORDS): + protected.append(path) + + return protected From 5236416fb7e55e94991666e14342534a6a7fac19 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:43:59 +0100 Subject: [PATCH 02/18] refactor: share migration guard policy with UCF adapters --- foundry/orchestration/run_engine.py | 63 +++-------------------------- 1 file changed, 5 insertions(+), 58 deletions(-) diff --git a/foundry/orchestration/run_engine.py b/foundry/orchestration/run_engine.py index 6a6e90e..ba04fc1 100644 --- a/foundry/orchestration/run_engine.py +++ b/foundry/orchestration/run_engine.py @@ -33,6 +33,10 @@ from foundry.db.queries import artifacts as artifact_queries from foundry.db.queries import runs as run_queries from foundry.storage.artifact_store import ArtifactType +from foundry.verification.policy import ( + MIGRATION_GUARD_ALLOWED_TASK_TYPES, + match_protected_paths, +) if TYPE_CHECKING: from foundry.git.pr import PRCreator @@ -158,63 +162,6 @@ async def _build_run_response(session: AsyncSession, run_id: UUID) -> RunRespons QUEUE_KEY = "foundry:runs" -# --------------------------------------------------------------------------- -# Protected path patterns for migration guard. -# Any changed file matching these triggers the migration guard subagent. -# --------------------------------------------------------------------------- - -PROTECTED_PATH_PREFIXES: tuple[str, ...] = ("migrations/", "auth/", "infra/") -PROTECTED_PATH_GLOBS: tuple[str, ...] = ("Dockerfile*", "docker-compose*") -PROTECTED_PATH_KEYWORDS: tuple[str, ...] = ("secret", "credential", "token") - -# Task types allowed to modify protected paths (escalated to LLM review). -# All other task types that touch protected paths are auto-rejected. -MIGRATION_GUARD_ALLOWED_TASK_TYPES: set[TaskType] = { - TaskType.ENDPOINT_BUILD, - TaskType.REFACTOR, - TaskType.MIGRATION_PLAN, - TaskType.CANON_UPDATE, -} - - -def _match_protected_paths(changed_files: list[str]) -> list[str]: - """Return the subset of changed_files that match protected path patterns. - - Matches against: - - Prefix: migrations/, auth/, infra/ - - Glob: Dockerfile*, docker-compose* - - Keyword: *secret*, *credential*, *token* (case-insensitive) - - Args: - changed_files: List of file paths from the diff. - - Returns: - List of file paths that match at least one protected pattern. - """ - import fnmatch - - protected: list[str] = [] - for f in changed_files: - # Check prefix matches (handle both "migrations/..." and "some/migrations/...") - if any(f.startswith(prefix) or f"/{prefix}" in f for prefix in PROTECTED_PATH_PREFIXES): - protected.append(f) - continue - - # Check glob matches against the basename - basename = f.rsplit("/", 1)[-1] if "/" in f else f - if any(fnmatch.fnmatch(basename, g) for g in PROTECTED_PATH_GLOBS): - protected.append(f) - continue - - # Check keyword matches (case-insensitive) against the full path - f_lower = f.lower() - if any(kw in f_lower for kw in PROTECTED_PATH_KEYWORDS): - protected.append(f) - continue - - return protected - - def _extract_changed_files(diff: str) -> list[str]: """Extract changed file paths from a git diff.""" files = [] @@ -1245,7 +1192,7 @@ async def _check_migration_guard( ReviewVerdict if migration guard was triggered, None if no protected paths were touched. """ - protected_files = _match_protected_paths(changed_files) + protected_files = match_protected_paths(changed_files) if not protected_files: return None From 3d8a7f5bd07fb9151af9faf6bcf281e278384ac9 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:44:09 +0100 Subject: [PATCH 03/18] refactor: give transition verifier execution workspace --- foundry/runtime/interfaces.py | 1 + 1 file changed, 1 insertion(+) diff --git a/foundry/runtime/interfaces.py b/foundry/runtime/interfaces.py index 9b12ac7..e753e09 100644 --- a/foundry/runtime/interfaces.py +++ b/foundry/runtime/interfaces.py @@ -55,6 +55,7 @@ async def verify( after: StateSnapshot, action: ActionProposal, action_evidence: list[EvidenceRef], + workspace: str, ) -> VerificationDecision: ... From aa30aac1f4f070253a907d123c8e55e1fb9b5ceb Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:44:11 +0100 Subject: [PATCH 04/18] refactor: pass workspace into transition verification --- foundry/runtime/transition_engine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/foundry/runtime/transition_engine.py b/foundry/runtime/transition_engine.py index b68957d..f754df4 100644 --- a/foundry/runtime/transition_engine.py +++ b/foundry/runtime/transition_engine.py @@ -63,6 +63,7 @@ async def execute(self, request: TransitionRequest) -> TransitionOutcome: after=after, action=action, action_evidence=action_evidence, + workspace=workspace, ) observation = TransitionObservation( From 6b58aa492d79d9f8023a9dc330caef52ec261b2d Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:44:14 +0100 Subject: [PATCH 05/18] test: exercise verifier workspace capability --- tests/unit/runtime/test_transition_engine.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/runtime/test_transition_engine.py b/tests/unit/runtime/test_transition_engine.py index f4f13e9..f48c422 100644 --- a/tests/unit/runtime/test_transition_engine.py +++ b/tests/unit/runtime/test_transition_engine.py @@ -99,7 +99,9 @@ async def verify( after: StateSnapshot, action: ActionProposal, action_evidence: list[EvidenceRef], + workspace: str, ) -> VerificationDecision: + assert workspace.startswith("memory://") accepted = ( before.state["location"] == "dock-a" and after.state["location"] == "dock-b" From ba2aca07ef11220b891bb06ea385f46d7f9de81a Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:44:54 +0100 Subject: [PATCH 06/18] feat: add durable transition outcome artifact type --- foundry/storage/artifact_store.py | 1 + 1 file changed, 1 insertion(+) diff --git a/foundry/storage/artifact_store.py b/foundry/storage/artifact_store.py index d8d2230..50c6142 100644 --- a/foundry/storage/artifact_store.py +++ b/foundry/storage/artifact_store.py @@ -28,6 +28,7 @@ class ArtifactType(str, Enum): EVAL = "eval" ERROR_LOG = "error_log" PR_METADATA = "pr_metadata" + TRANSITION = "transition" class StoreResult(TypedDict): From 503df09f64e33c8d8e836e5f404c156c82ef4042 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:44:56 +0100 Subject: [PATCH 07/18] feat: adapt artifact storage to UCF transition journal --- foundry/storage/transition_journal.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 foundry/storage/transition_journal.py diff --git a/foundry/storage/transition_journal.py b/foundry/storage/transition_journal.py new file mode 100644 index 0000000..80d239a --- /dev/null +++ b/foundry/storage/transition_journal.py @@ -0,0 +1,21 @@ +"""Transition journal backed by the existing Foundry artifact store.""" + +from __future__ import annotations + +from foundry.contracts.transition_models import TransitionOutcome +from foundry.storage.artifact_store import ArtifactStore, ArtifactType + + +class ArtifactStoreTransitionJournal: + """Persist verified UCF outcomes using Foundry's durable artifact storage.""" + + def __init__(self, artifact_store: ArtifactStore) -> None: + self.artifact_store = artifact_store + + async def record(self, outcome: TransitionOutcome) -> None: + await self.artifact_store.store( + outcome.transition_id, + ArtifactType.TRANSITION, + outcome.model_dump_json(indent=2), + filename="transition_outcome.json", + ) From a41cb5da920320e76c5706a79a14ab17a12542d7 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:44:59 +0100 Subject: [PATCH 08/18] feat: observe Git workspaces as explicit UCF state --- foundry/environments/git_observer.py | 73 ++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 foundry/environments/git_observer.py diff --git a/foundry/environments/git_observer.py b/foundry/environments/git_observer.py new file mode 100644 index 0000000..3f3f934 --- /dev/null +++ b/foundry/environments/git_observer.py @@ -0,0 +1,73 @@ +"""Git-backed state observation for UCF execution environments.""" + +from __future__ import annotations + +import asyncio +import hashlib +from datetime import UTC, datetime + +from foundry.contracts.transition_models import EvidenceRef, StateSnapshot, TransitionRequest + + +async def _git(workspace: str, *args: str) -> str: + proc = await asyncio.create_subprocess_exec( + "git", + *args, + cwd=workspace, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + raise RuntimeError( + f"git {' '.join(args)} failed in {workspace}: " + f"{stderr.decode(errors='replace').strip()}" + ) + return stdout.decode(errors="replace") + + +def _changed_paths(status: str) -> list[str]: + paths: list[str] = [] + for raw_line in status.splitlines(): + if len(raw_line) < 4: + continue + path = raw_line[3:] + if " -> " in path: + path = path.split(" -> ", 1)[1] + paths.append(path) + return paths + + +class GitStateObserver: + """Represent the current Git workspace as explicit UCF state.""" + + async def observe(self, request: TransitionRequest, workspace: str) -> StateSnapshot: + head = (await _git(workspace, "rev-parse", "HEAD")).strip() + status = await _git(workspace, "status", "--porcelain") + diff = await _git(workspace, "diff", "HEAD") + checksum = hashlib.sha256(diff.encode("utf-8")).hexdigest() + changed_files = _changed_paths(status) + + return StateSnapshot( + environment=request.environment, + observed_at=datetime.now(UTC), + state={ + "head": head, + "dirty": bool(status.strip()), + "changed_files": changed_files, + "diff_checksum": checksum, + }, + evidence=[ + EvidenceRef( + kind="git-head", + uri=f"git://commit/{head}", + description="Observed repository HEAD", + ), + EvidenceRef( + kind="git-diff", + uri=f"sha256:{checksum}", + description="Checksum of working tree diff against HEAD", + checksum=checksum, + ), + ], + ) From 05985deab9a9f41f5d4876092ba7ec2c551b015e Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:45:48 +0100 Subject: [PATCH 09/18] feat: add UCF adapter package --- foundry/adapters/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 foundry/adapters/__init__.py diff --git a/foundry/adapters/__init__.py b/foundry/adapters/__init__.py new file mode 100644 index 0000000..92d2fd2 --- /dev/null +++ b/foundry/adapters/__init__.py @@ -0,0 +1 @@ +"""Adapters that map historical Foundry capabilities onto UCF interfaces.""" From 4ddada33cb9c6c426fe176b57d574106c665ea43 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:45:50 +0100 Subject: [PATCH 10/18] feat: adapt historical Foundry capabilities to UCF transitions --- foundry/adapters/foundry_transition.py | 306 +++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 foundry/adapters/foundry_transition.py diff --git a/foundry/adapters/foundry_transition.py b/foundry/adapters/foundry_transition.py new file mode 100644 index 0000000..9894c7e --- /dev/null +++ b/foundry/adapters/foundry_transition.py @@ -0,0 +1,306 @@ +"""Historical Foundry capabilities exposed through UCF transition interfaces.""" + +from __future__ import annotations + +import asyncio +import hashlib +from typing import Literal +from uuid import UUID, uuid4 + +from foundry.contracts.review_models import ReviewVerdict +from foundry.contracts.shared import ReviewVerdictType, TaskType +from foundry.contracts.task_types import PlanArtifact, TaskRequest +from foundry.contracts.transition_models import ( + ActionProposal, + EvidenceRef, + StateSnapshot, + TransitionOutcome, + TransitionRequest, + VerificationDecision, +) +from foundry.environments.git_observer import GitStateObserver +from foundry.environments.git_worktree import GitWorktreeEnvironment +from foundry.git.branch import generate_branch_name +from foundry.orchestration.agent_runner import AgentRunner +from foundry.runtime.transition_engine import TransitionEngine +from foundry.storage.artifact_store import ArtifactStore +from foundry.storage.transition_journal import ArtifactStoreTransitionJournal +from foundry.verification.policy import ( + MIGRATION_GUARD_ALLOWED_TASK_TYPES, + match_protected_paths, +) +from foundry.verification.runner import VerificationRunner + + +def _task_from_transition(request: TransitionRequest) -> TaskRequest: + raw = request.metadata.get("foundry_task") + if not isinstance(raw, dict): + raise ValueError("TransitionRequest metadata is missing foundry_task") + return TaskRequest.model_validate(raw) + + +def foundry_task_to_transition_request( + task: TaskRequest, + *, + transition_id: UUID | None = None, + language: Literal["go", "typescript"] = "go", +) -> TransitionRequest: + """Translate a historical Foundry task into the general UCF request shape.""" + return TransitionRequest( + id=transition_id or uuid4(), + environment=task.repo, + objective=task.prompt, + constraints={ + "task_type": task.task_type.value, + "target_paths": list(task.target_paths), + "verify": task.verify, + "open_pr": task.open_pr, + }, + metadata={ + "foundry_task": task.model_dump(), + "base_ref": task.base_branch, + "transition_name": generate_branch_name(task.task_type, task.title), + "language": language, + }, + ) + + +class FoundryPlannerAdapter: + """Expose AgentRunner planning through the UCF TransitionPlanner contract.""" + + def __init__(self, agent_runner: AgentRunner) -> None: + self.agent_runner = agent_runner + + async def plan( + self, + request: TransitionRequest, + current_state: StateSnapshot, + workspace: str, + ) -> ActionProposal: + task = _task_from_transition(request) + plan = await self.agent_runner.run_planner(task, workspace) + return ActionProposal( + kind="foundry-plan", + description=( + f"Apply {len(plan.steps)} planned steps to {request.environment} " + f"from {current_state.state.get('head', 'unknown state')}" + ), + payload={"plan": plan.model_dump()}, + ) + + +class FoundryExecutorAdapter: + """Expose AgentRunner implementation through the UCF ActionExecutor contract.""" + + def __init__(self, agent_runner: AgentRunner) -> None: + self.agent_runner = agent_runner + + async def execute( + self, + request: TransitionRequest, + action: ActionProposal, + workspace: str, + ) -> list[EvidenceRef]: + if action.kind != "foundry-plan": + raise ValueError(f"Unsupported Foundry action kind: {action.kind}") + + task = _task_from_transition(request) + plan = PlanArtifact.model_validate(action.payload["plan"]) + language = request.metadata.get("language", "go") + if language not in {"go", "typescript"}: + raise ValueError(f"Unsupported Foundry implementation language: {language}") + + diff = await self.agent_runner.run_implementer( + plan=plan, + task_request=task, + worktree_path=workspace, + language=language, + ) + checksum = hashlib.sha256(diff.encode("utf-8")).hexdigest() + return [ + EvidenceRef( + kind="git-diff", + uri=f"sha256:{checksum}", + description=f"Implementation produced {len(diff.encode('utf-8'))} diff bytes", + checksum=checksum, + ) + ] + + +async def _read_diff(workspace: str) -> str: + proc = await asyncio.create_subprocess_exec( + "git", + "diff", + "HEAD", + cwd=workspace, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + raise RuntimeError( + f"git diff failed in {workspace}: {stderr.decode(errors='replace').strip()}" + ) + return stdout.decode(errors="replace") + + +def _verification_evidence(check_type: str, passed: bool, output: str) -> EvidenceRef: + status = "pass" if passed else "fail" + return EvidenceRef( + kind="verification", + uri=f"verification://{check_type}/{status}", + description=output[:500] or f"{check_type}: {status}", + ) + + +def _review_evidence(kind: str, review: ReviewVerdict) -> EvidenceRef: + return EvidenceRef( + kind=kind, + uri=f"{kind}://{review.verdict.value}", + description=review.summary, + ) + + +class FoundryVerifierAdapter: + """Apply Foundry verification and blind review to a UCF transition.""" + + def __init__( + self, + verification_runner: VerificationRunner, + agent_runner: AgentRunner, + ) -> None: + self.verification_runner = verification_runner + self.agent_runner = agent_runner + + async def verify( + self, + request: TransitionRequest, + before: StateSnapshot, + after: StateSnapshot, + action: ActionProposal, + action_evidence: list[EvidenceRef], + workspace: str, + ) -> VerificationDecision: + del before, action, action_evidence + task = _task_from_transition(request) + changed_files = [ + str(path) for path in after.state.get("changed_files", []) + ] + evidence: list[EvidenceRef] = [] + + if task.verify: + results, passed = await self.verification_runner.run_all( + workspace, + changed_files, + run_id=request.id, + ) + evidence.extend( + _verification_evidence(result.check_type, result.passed, result.output) + for result in results + ) + if not passed: + failed = ", ".join( + result.check_type for result in results if not result.passed + ) + return VerificationDecision( + accepted=False, + reason=f"Deterministic verification failed: {failed}", + evidence=evidence, + ) + + diff = await _read_diff(workspace) + protected_files = match_protected_paths(changed_files) + if protected_files: + if task.task_type == TaskType.BUG_FIX: + return VerificationDecision( + accepted=False, + reason=( + "Protected-path policy rejected bug-fix transition: " + + ", ".join(protected_files) + ), + evidence=evidence, + ) + + if task.task_type not in MIGRATION_GUARD_ALLOWED_TASK_TYPES: + return VerificationDecision( + accepted=False, + reason=( + f"Task type {task.task_type.value} is not authorized " + f"for protected paths: {', '.join(protected_files)}" + ), + evidence=evidence, + ) + + guard = await self.agent_runner.run_migration_guard( + diff=diff, + changed_files=protected_files, + ) + evidence.append(_review_evidence("migration-guard", guard)) + if guard.verdict == ReviewVerdictType.REJECT: + return VerificationDecision( + accepted=False, + reason=f"Migration guard rejected transition: {guard.summary}", + evidence=evidence, + ) + + changed_summary = ", ".join(changed_files[:10]) or "no files detected" + if len(changed_files) > 10: + changed_summary += f" (+{len(changed_files) - 10} more)" + + review = await self.agent_runner.run_reviewer( + diff=diff, + pr_title=f"[Foundry] {task.task_type.value}: {task.title}", + pr_description=f"{task.prompt[:500]}\n\nChanged files: {changed_summary}", + changed_files=changed_files, + ) + evidence.append(_review_evidence("review", review)) + + accepted = review.verdict != ReviewVerdictType.REJECT + if review.verdict == ReviewVerdictType.REQUEST_CHANGES: + reason = ( + "Independent review requested changes; historical Foundry policy " + "treats this verdict as advisory." + ) + else: + reason = review.summary + + return VerificationDecision( + accepted=accepted, + reason=reason, + evidence=evidence, + ) + + +class FoundryTransitionRuntime: + """Run historical Foundry work through the provider-neutral UCF loop.""" + + def __init__( + self, + *, + worktree_manager, + agent_runner: AgentRunner, + verification_runner: VerificationRunner, + artifact_store: ArtifactStore, + ) -> None: + self.engine = TransitionEngine( + environment=GitWorktreeEnvironment(worktree_manager), + observer=GitStateObserver(), + planner=FoundryPlannerAdapter(agent_runner), + executor=FoundryExecutorAdapter(agent_runner), + verifier=FoundryVerifierAdapter(verification_runner, agent_runner), + journal=ArtifactStoreTransitionJournal(artifact_store), + ) + + async def execute_task( + self, + task: TaskRequest, + *, + transition_id: UUID | None = None, + language: Literal["go", "typescript"] = "go", + ) -> TransitionOutcome: + request = foundry_task_to_transition_request( + task, + transition_id=transition_id, + language=language, + ) + return await self.engine.execute(request) From 92fb1184680c92a3f793f069eab2230ffcee2176 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:46:47 +0100 Subject: [PATCH 11/18] chore: type Foundry worktree adapter dependency --- foundry/adapters/foundry_transition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/foundry/adapters/foundry_transition.py b/foundry/adapters/foundry_transition.py index 9894c7e..908bab3 100644 --- a/foundry/adapters/foundry_transition.py +++ b/foundry/adapters/foundry_transition.py @@ -21,6 +21,7 @@ from foundry.environments.git_observer import GitStateObserver from foundry.environments.git_worktree import GitWorktreeEnvironment from foundry.git.branch import generate_branch_name +from foundry.git.worktree import WorktreeManager from foundry.orchestration.agent_runner import AgentRunner from foundry.runtime.transition_engine import TransitionEngine from foundry.storage.artifact_store import ArtifactStore @@ -277,7 +278,7 @@ class FoundryTransitionRuntime: def __init__( self, *, - worktree_manager, + worktree_manager: WorktreeManager, agent_runner: AgentRunner, verification_runner: VerificationRunner, artifact_store: ArtifactStore, From 28277ee70116bee791afabfb201472df91ac96b5 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:46:49 +0100 Subject: [PATCH 12/18] test: run real Git Foundry path through UCF transition engine --- .../test_foundry_transition_adapter.py | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 tests/unit/runtime/test_foundry_transition_adapter.py diff --git a/tests/unit/runtime/test_foundry_transition_adapter.py b/tests/unit/runtime/test_foundry_transition_adapter.py new file mode 100644 index 0000000..0680a23 --- /dev/null +++ b/tests/unit/runtime/test_foundry_transition_adapter.py @@ -0,0 +1,313 @@ +"""Integration tests for historical Foundry capabilities running through UCF.""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch +from uuid import uuid4 + +import pytest + +from foundry.adapters.foundry_transition import ( + FoundryExecutorAdapter, + FoundryPlannerAdapter, + FoundryTransitionRuntime, + FoundryVerifierAdapter, + foundry_task_to_transition_request, +) +from foundry.contracts.review_models import ReviewVerdict +from foundry.contracts.shared import ( + Complexity, + MCPProfile, + ReviewVerdictType, + TaskType, +) +from foundry.contracts.task_types import PlanArtifact, PlanStep, TaskRequest +from foundry.contracts.transition_models import ( + ActionProposal, + StateSnapshot, +) +from foundry.git.worktree import WorktreeManager +from foundry.storage.artifact_store import ArtifactStore +from foundry.verification.go_verify import VerificationResult + + +def _task() -> TaskRequest: + return TaskRequest( + task_type=TaskType.BUG_FIX, + repo="example-repo", + base_branch="main", + title="Fix greeting", + prompt="Change the greeting from hello to hi", + mcp_profile=MCPProfile.NONE, + ) + + +def _plan() -> PlanArtifact: + return PlanArtifact( + task_id=uuid4(), + steps=[ + PlanStep( + file_path="main.py", + action="modify", + rationale="Update the greeting", + ) + ], + risks=[], + open_questions=[], + estimated_complexity=Complexity.SMALL, + ) + + +def _state(*, dirty: bool, changed_files: list[str]) -> StateSnapshot: + from datetime import UTC, datetime + + return StateSnapshot( + environment="example-repo", + observed_at=datetime.now(UTC), + state={ + "head": "abc123", + "dirty": dirty, + "changed_files": changed_files, + "diff_checksum": "0" * 64, + }, + ) + + +def _init_git_repo(path: Path) -> None: + path.mkdir() + subprocess.run(["git", "init"], cwd=path, check=True, capture_output=True) + subprocess.run( + ["git", "config", "user.email", "ucf-tests@example.invalid"], + cwd=path, + check=True, + ) + subprocess.run( + ["git", "config", "user.name", "UCF Tests"], + cwd=path, + check=True, + ) + (path / "main.py").write_text('GREETING = "hello"\n', encoding="utf-8") + subprocess.run(["git", "add", "main.py"], cwd=path, check=True) + subprocess.run( + ["git", "commit", "-m", "initial"], + cwd=path, + check=True, + capture_output=True, + ) + + +def test_foundry_task_maps_to_general_transition_request() -> None: + task = _task() + transition_id = uuid4() + + request = foundry_task_to_transition_request(task, transition_id=transition_id) + + assert request.id == transition_id + assert request.environment == "example-repo" + assert request.objective == task.prompt + assert request.constraints["task_type"] == "bug_fix" + assert request.metadata["base_ref"] == "main" + assert request.metadata["transition_name"].startswith("foundry/bug-fix-") + restored = TaskRequest.model_validate(request.metadata["foundry_task"]) + assert restored == task + + +@pytest.mark.asyncio +async def test_planner_and_executor_wrap_existing_agent_runner() -> None: + runner = MagicMock() + runner.run_planner = AsyncMock(return_value=_plan()) + runner.run_implementer = AsyncMock(return_value="diff --git a/main.py b/main.py\n") + + request = foundry_task_to_transition_request(_task()) + planner = FoundryPlannerAdapter(runner) + action = await planner.plan(request, _state(dirty=False, changed_files=[]), "/tmp/work") + + assert action.kind == "foundry-plan" + assert len(action.payload["plan"]["steps"]) == 1 + + executor = FoundryExecutorAdapter(runner) + evidence = await executor.execute(request, action, "/tmp/work") + + assert evidence[0].kind == "git-diff" + assert evidence[0].checksum is not None + runner.run_implementer.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_verifier_preserves_deterministic_and_blind_review_policy() -> None: + verification_runner = MagicMock() + verification_runner.run_all = AsyncMock( + return_value=( + [ + VerificationResult( + check_type="go_test", + passed=True, + output="ok", + duration_ms=4, + ) + ], + True, + ) + ) + agent_runner = MagicMock() + agent_runner.run_reviewer = AsyncMock( + return_value=ReviewVerdict( + verdict=ReviewVerdictType.APPROVE, + issues=[], + summary="Looks good", + confidence=0.95, + ) + ) + + verifier = FoundryVerifierAdapter(verification_runner, agent_runner) + request = foundry_task_to_transition_request(_task()) + + with patch( + "foundry.adapters.foundry_transition._read_diff", + new=AsyncMock(return_value="diff --git a/main.go b/main.go\n"), + ): + decision = await verifier.verify( + request=request, + before=_state(dirty=False, changed_files=[]), + after=_state(dirty=True, changed_files=["main.go"]), + action=ActionProposal( + kind="foundry-plan", + description="test", + payload={"plan": _plan().model_dump()}, + ), + action_evidence=[], + workspace="/tmp/work", + ) + + assert decision.accepted is True + assert {item.kind for item in decision.evidence} == {"verification", "review"} + agent_runner.run_reviewer.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_bug_fix_protected_path_is_rejected_before_review() -> None: + verification_runner = MagicMock() + verification_runner.run_all = AsyncMock( + return_value=( + [ + VerificationResult( + check_type="none", + passed=True, + output="ok", + duration_ms=0, + ) + ], + True, + ) + ) + agent_runner = MagicMock() + agent_runner.run_reviewer = AsyncMock() + + verifier = FoundryVerifierAdapter(verification_runner, agent_runner) + request = foundry_task_to_transition_request(_task()) + + with patch( + "foundry.adapters.foundry_transition._read_diff", + new=AsyncMock(return_value="diff --git a/migrations/001.sql b/migrations/001.sql\n"), + ): + decision = await verifier.verify( + request=request, + before=_state(dirty=False, changed_files=[]), + after=_state(dirty=True, changed_files=["migrations/001.sql"]), + action=ActionProposal( + kind="foundry-plan", + description="test", + payload={"plan": _plan().model_dump()}, + ), + action_evidence=[], + workspace="/tmp/work", + ) + + assert decision.accepted is False + assert "Protected-path policy" in (decision.reason or "") + agent_runner.run_reviewer.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_real_git_foundry_path_runs_through_transition_engine(tmp_path: Path) -> None: + source_repo = tmp_path / "source" + _init_git_repo(source_repo) + + transition_id = uuid4() + worktree_base = tmp_path / "worktrees" + worktree_manager = WorktreeManager( + repo_path=str(source_repo), + worktree_base=str(worktree_base), + ) + + agent_runner = MagicMock() + agent_runner.run_planner = AsyncMock(return_value=_plan()) + + async def implement(**kwargs) -> str: + workspace = Path(kwargs["worktree_path"]) + (workspace / "main.py").write_text('GREETING = "hi"\n', encoding="utf-8") + proc = subprocess.run( + ["git", "diff", "HEAD"], + cwd=workspace, + check=True, + capture_output=True, + text=True, + ) + return proc.stdout + + agent_runner.run_implementer = AsyncMock(side_effect=implement) + agent_runner.run_reviewer = AsyncMock( + return_value=ReviewVerdict( + verdict=ReviewVerdictType.APPROVE, + issues=[], + summary="Transition is valid", + confidence=0.99, + ) + ) + + verification_runner = MagicMock() + verification_runner.run_all = AsyncMock( + return_value=( + [ + VerificationResult( + check_type="python-test", + passed=True, + output="ok", + duration_ms=2, + ) + ], + True, + ) + ) + + artifact_store = ArtifactStore(base_path=str(tmp_path / "artifacts")) + runtime = FoundryTransitionRuntime( + worktree_manager=worktree_manager, + agent_runner=agent_runner, + verification_runner=verification_runner, + artifact_store=artifact_store, + ) + + outcome = await runtime.execute_task(_task(), transition_id=transition_id) + + assert outcome.accepted is True + assert outcome.before_state is not None + assert outcome.after_state is not None + assert outcome.before_state.state["dirty"] is False + assert outcome.after_state.state["dirty"] is True + assert outcome.after_state.state["changed_files"] == ["main.py"] + assert not (worktree_base / str(transition_id)).exists() + + artifact = ( + tmp_path + / "artifacts" + / "runs" + / str(transition_id) + / "transition_outcome.json" + ) + payload = json.loads(artifact.read_text(encoding="utf-8")) + assert payload["accepted"] is True + assert payload["after_state"]["state"]["changed_files"] == ["main.py"] From 795e1e6b8619a9dbcee587c5664523b643148013 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:47:06 +0100 Subject: [PATCH 13/18] ci: validate Foundry-to-UCF adapter path --- .github/workflows/ucf-foundation.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ucf-foundation.yml b/.github/workflows/ucf-foundation.yml index bf3dff5..1fcec51 100644 --- a/.github/workflows/ucf-foundation.yml +++ b/.github/workflows/ucf-foundation.yml @@ -3,9 +3,13 @@ name: UCF foundation on: pull_request: paths: + - "foundry/adapters/**" - "foundry/contracts/transition_models.py" - "foundry/environments/**" - "foundry/providers/base.py" + - "foundry/storage/artifact_store.py" + - "foundry/storage/transition_journal.py" + - "foundry/verification/policy.py" - "foundry/runtime/**" - "foundry/orchestration/agent_runner.py" - "foundry/orchestration/run_engine.py" @@ -36,9 +40,9 @@ jobs: run: python -m compileall -q foundry tests - name: Lint changed foundation run: | - ruff check foundry/contracts/transition_models.py foundry/contracts/task_types.py foundry/environments foundry/providers/base.py foundry/runtime foundry/orchestration/agent_runner.py foundry/orchestration/run_engine.py tests/unit/test_transition_models.py tests/unit/runtime/test_transition_engine.py tests/unit/orchestration/test_agent_runner.py + ruff check foundry/adapters foundry/contracts/transition_models.py foundry/contracts/task_types.py foundry/environments foundry/providers/base.py foundry/runtime foundry/storage/artifact_store.py foundry/storage/transition_journal.py foundry/verification/policy.py foundry/orchestration/agent_runner.py foundry/orchestration/run_engine.py tests/unit/test_transition_models.py tests/unit/runtime/test_transition_engine.py tests/unit/runtime/test_foundry_transition_adapter.py tests/unit/orchestration/test_agent_runner.py - name: Test foundation run: | - pytest -q tests/unit/test_transition_models.py tests/unit/runtime/test_transition_engine.py tests/unit/orchestration/test_agent_runner.py + pytest -q tests/unit/test_transition_models.py tests/unit/runtime/test_transition_engine.py tests/unit/runtime/test_foundry_transition_adapter.py tests/unit/orchestration/test_agent_runner.py - name: Test full suite run: pytest -q From b5f3d6ba58d7ae6911a9c98f74f12b9b3f0b0e77 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:48:15 +0100 Subject: [PATCH 14/18] chore: modernize artifact store for Python 3.12 --- foundry/storage/artifact_store.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/foundry/storage/artifact_store.py b/foundry/storage/artifact_store.py index 50c6142..aec1b47 100644 --- a/foundry/storage/artifact_store.py +++ b/foundry/storage/artifact_store.py @@ -7,8 +7,8 @@ import hashlib import logging -from datetime import datetime, timezone -from enum import Enum +from datetime import UTC, datetime +from enum import StrEnum from pathlib import Path from typing import TypedDict from uuid import UUID @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) -class ArtifactType(str, Enum): +class ArtifactType(StrEnum): """Types of artifacts produced by Foundry runs.""" PLAN = "plan" @@ -149,7 +149,7 @@ async def list_artifacts(self, run_id: UUID) -> list[ArtifactInfo]: filename=f.name, size_bytes=stat.st_size, modified=datetime.fromtimestamp( - stat.st_mtime, tz=timezone.utc, + stat.st_mtime, tz=UTC, ).isoformat(), )) return result From fa95ba897b130b7627142df1680a7939fcb97df3 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:49:22 +0100 Subject: [PATCH 15/18] test: target shared migration guard policy --- .../orchestration/test_migration_guard.py | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/unit/orchestration/test_migration_guard.py b/tests/unit/orchestration/test_migration_guard.py index 882ff5a..0aa94c8 100644 --- a/tests/unit/orchestration/test_migration_guard.py +++ b/tests/unit/orchestration/test_migration_guard.py @@ -26,10 +26,10 @@ ) from foundry.contracts.task_types import TaskRequest from foundry.db.queries.runs import create_run, get_run, get_run_events -from foundry.orchestration.run_engine import ( +from foundry.orchestration.run_engine import RunEngine +from foundry.verification.policy import ( MIGRATION_GUARD_ALLOWED_TASK_TYPES, - RunEngine, - _match_protected_paths, + match_protected_paths, ) from foundry.storage.artifact_store import ArtifactStore, ArtifactType @@ -286,55 +286,55 @@ class TestProtectedPathDetection: def test_migrations_prefix_detected(self): files = ["migrations/0042_add_signals_table.py"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_nested_migrations_prefix_detected(self): files = ["foundry/db/migrations/0042_add_signals_table.py"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_auth_prefix_detected(self): files = ["auth/middleware.go"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_infra_prefix_detected(self): files = ["infra/terraform/main.tf"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_dockerfile_glob_detected(self): files = ["Dockerfile"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_dockerfile_with_suffix_detected(self): files = ["Dockerfile.prod"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_nested_dockerfile_detected(self): files = ["services/api/Dockerfile"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_docker_compose_detected(self): files = ["docker-compose.yml"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_docker_compose_override_detected(self): files = ["docker-compose.override.yml"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_secret_keyword_detected(self): files = ["config/secrets.yaml"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_credential_keyword_detected(self): files = ["deploy/credentials.json"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_token_keyword_detected(self): files = ["auth/token_store.go"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_case_insensitive_keyword_matching(self): files = ["config/AWS_CREDENTIALS.json"] - assert _match_protected_paths(files) == files + assert match_protected_paths(files) == files def test_no_protected_paths_returns_empty(self): files = [ @@ -342,10 +342,10 @@ def test_no_protected_paths_returns_empty(self): "services/api/search/handler_test.go", "packages/contracts/openapi.yaml", ] - assert _match_protected_paths(files) == [] + assert match_protected_paths(files) == [] def test_empty_file_list_returns_empty(self): - assert _match_protected_paths([]) == [] + assert match_protected_paths([]) == [] def test_mixed_protected_and_normal_files(self): files = [ @@ -354,7 +354,7 @@ def test_mixed_protected_and_normal_files(self): "auth/middleware.go", "packages/contracts/openapi.yaml", ] - result = _match_protected_paths(files) + result = match_protected_paths(files) assert "migrations/0042_add_signals_table.py" in result assert "auth/middleware.go" in result assert "services/api/search/handler.go" not in result @@ -369,7 +369,7 @@ def test_multiple_protected_patterns_matched(self): "docker-compose.yml", "config/secrets.yaml", ] - result = _match_protected_paths(files) + result = match_protected_paths(files) assert len(result) == 6 From 04d7e12d840c9b2a7783f4aa924629f54d524191 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:51:31 +0100 Subject: [PATCH 16/18] docs: record exercised Foundry adapter milestone --- docs/runtime-decoupling-audit.md | 160 ++++++++++++++++++++++--------- 1 file changed, 113 insertions(+), 47 deletions(-) diff --git a/docs/runtime-decoupling-audit.md b/docs/runtime-decoupling-audit.md index 1698e23..b8b28f5 100644 --- a/docs/runtime-decoupling-audit.md +++ b/docs/runtime-decoupling-audit.md @@ -1,76 +1,142 @@ # Runtime Decoupling Audit -This audit separates the general UCF mechanism from assumptions inherited from Unicorn Foundry. +This audit separates the general UCF mechanism from assumptions inherited from +Unicorn Foundry and records which abstractions have been exercised by real +historical machinery. -## Already generalized in this branch +## Current architecture -### Intelligence provider boundary +The repository now has three related layers: -`AgentRunner` now depends on an `IntelligenceProvider` protocol. Claude remains the default historical implementation, but the orchestration boundary no longer requires the concrete Claude provider type. +1. **UCF foundation** — provider-neutral state, evidence, action, verification, + outcome, environment, and journal contracts. +2. **Foundry adapter path** — the original Git/agent/verification/artifact + machinery running through `TransitionEngine`. +3. **Historical RunEngine** — the backwards-compatible API/database/PR lifecycle + that still owns the old `queued -> ... -> pr_opened -> completed` state machine. -### Execution target boundary +The second layer is important: the general boundary is no longer proven only by +fakes. -`TaskRequest.repo` remains named for backwards compatibility, but it is no longer restricted to `unicorn-app` or `unicorn-foundry`. +## Generalized and exercised boundaries -### Environment boundary +### Intelligence provider -`ExecutionEnvironment` defines prepare, observe-changes, and cleanup operations. `GitWorktreeEnvironment` adapts the historical worktree implementation to that interface. +`AgentRunner` depends on `IntelligenceProvider`. Claude remains the default +historical backend, but provider identity is outside the UCF runtime contract. -### Transition vocabulary +### Execution environment and observation -`foundry/contracts/transition_models.py` defines provider-neutral state snapshots, evidence references, action proposals, transition requests, observations, verification decisions, and outcomes without assuming GitHub or Unicorn. +`ExecutionEnvironment` owns workspace preparation and cleanup. +`GitWorktreeEnvironment` adapts the historical `WorktreeManager`. -### Generic transition loop +`GitStateObserver` turns an actual Git workspace into explicit state containing +HEAD, dirty status, changed files, and a diff checksum. This supplies real +before/after state to the generic loop. -`TransitionEngine` now coordinates a minimal provider-neutral loop through injected observer, planner, executor, verifier, environment, and journal capabilities. The engine does not know about Claude, GitHub, Go, TypeScript, or Unicorn. +### Transition vocabulary and engine -The historical `RunEngine` is still unchanged and Git/PR-shaped. The new engine is a parallel foundation, not a claim that migration is complete. +`TransitionEngine` coordinates: + +```text +state(t) + -> plan + -> controlled action + -> observe + -> verify / independently review + -> durable outcome + -> state(t+1) +``` + +The engine does not know about Claude, GitHub, Go, TypeScript, Unicorn, or pull +requests. + +### Historical Foundry adapters + +`foundry/adapters/foundry_transition.py` maps the original capabilities onto +that loop: + +| Historical capability | UCF role | +| --- | --- | +| `TaskRequest` | `TransitionRequest` input | +| `AgentRunner.run_planner` | `TransitionPlanner` | +| `AgentRunner.run_implementer` | `ActionExecutor` | +| Git worktree | `ExecutionEnvironment` | +| Git HEAD/status/diff | `StateObserver` | +| `VerificationRunner` | deterministic verification | +| blind reviewer | independent transition evaluation | +| migration guard | shared protected-path verification policy | +| `ArtifactStore` | `TransitionJournal` | +| review/verification result | `TransitionOutcome` evidence | + +`REQUEST_CHANGES` remains advisory in the adapter because that is the historical +Foundry behavior. `REJECT` and deterministic verification failure reject the +transition. + +### Shared safety policy + +Protected-path matching and migration-guard authorization now live in +`foundry/verification/policy.py`. Both `RunEngine` and the UCF adapter use the +same policy instead of maintaining parallel copies. + +### Durable continuity + +`ArtifactStoreTransitionJournal` persists +`transition_outcome.json` under the transition ID before workspace cleanup. +The resulting after-state can seed the next transition. + +## Evidence + +The adapter integration test creates a real temporary Git repository and uses +the real `WorktreeManager` and `GitStateObserver`. It then runs planning, +implementation, verification, blind review, journaling, and cleanup through +`TransitionEngine`. + +Current CI evidence for this milestone: + +- 21 targeted UCF / Foundry-adapter tests pass; +- 495 full-suite tests pass; +- compile and targeted Ruff checks pass. ## Remaining historical couplings -| Coupling | Current form | General form | Migration priority | +| Coupling | Current state | Desired boundary | Priority | | --- | --- | --- | --- | -| Lifecycle terminal | `PR_OPENED -> COMPLETED` | outcome recorded / accepted | P0 | -| Action environment | Git worktree | ExecutionEnvironment | P0 | -| Action result | Git diff + PR | environment-specific action artifact | P0 | -| Implementer role | Go / TypeScript literal | executor capability | P1 | -| Verification | Go/TS/schema commands | verifier plugins | P1 | +| Run lifecycle terminal | `PR_OPENED -> COMPLETED` still lives in `RunEngine` | verified `TransitionOutcome` defines operation result | P0 | +| Publication | PR creation is embedded in run lifecycle | publisher is post-transition / environment-specific | P0 | +| Runtime convergence | adapter path and `RunEngine` both exist | legacy engine delegates core transition work | P0 | +| Persistence | run/PR/worktree tables | transition/outcome/workspace records + legacy projection | P1 | +| Executor language | Go / TypeScript literal | executor capability selection | P1 | +| Verification | code-oriented Go/TS/schema dispatch | verifier plugins by environment/capability | P1 | | Model routing | Claude model IDs | provider + capability routing | P1 | -| Prompt layer | coding-specific planner/implementer prompts | transition-role prompts | P1 | +| Prompts | coding-specific role prompts | environment-specific planner/executor adapters | P1 | | Canon | Unicorn/startup schemas | environment-specific state contracts | P2 | | Extraction | startup signal extraction | observer adapters | P2 | -| Config names | FOUNDRY, unicorn_app_* | UCF + legacy aliases | P2 | -| Persistence names | runs, PR URL, worktrees | transitions, outcomes, workspaces | P3 | -| API routes | /runs, /patches, /worktrees | /transitions, /actions, /environments | P3 | +| Config | `FOUNDRY_*`, Unicorn-era names | UCF names + compatibility aliases | P2 | +| API | `/runs`, `/patches`, `/worktrees` | transition/action/environment surfaces | P3 | -## P0 foundation status +## Next P0 milestone: converge RunEngine -The generic loop now exists alongside Foundry: +Do not delete or rename the historical state machine first. Make it a +compatibility projection over the UCF transition loop. -1. an execution environment prepares and cleans up an isolated workspace; -2. an observer establishes explicit before-state; -3. a planner proposes a provider-neutral action; -4. an executor applies the action; -5. the observer establishes after-state; -6. an independent verifier accepts or rejects the transition; -7. a journal records the verified outcome; -8. the resulting state can seed the next transition. +The next migration should: -`tests/unit/runtime/test_transition_engine.py` exercises this with a non-Unicorn environment and fake capabilities. +1. let a legacy `TaskRequest` create a UCF `TransitionRequest`; +2. delegate plan -> execute -> observe -> verify -> journal to + `FoundryTransitionRuntime`; +3. map a rejected `TransitionOutcome` into the existing failure states/events; +4. map an accepted outcome into the existing post-verification path; +5. treat PR creation as publication after an accepted transition, not as the + definition of the transition itself; +6. preserve current API responses, database rows, artifacts, and event history + while compatibility is required. -The next P0 task is **adapter migration**: make the historical Git/Claude workflow exercise these interfaces rather than maintaining a separate architectural path. In particular, `PR_OPENED -> COMPLETED` must stop being the general definition of a successful transition. +This is the point at which the two runtime paths actually converge. ## What should not be renamed yet -Do not mass-rename Foundry classes, database tables, routes, or artifact types merely to match the new vocabulary. Renaming before the generalized loop works would create churn without increasing capability. - -Keep the historical runtime operational while new interfaces are introduced alongside it. Once a non-Unicorn closed loop passes end to end, migrate internals incrementally. - -## Foundation boundary - -The repository now contains the code-level boundary required to test UCF independently from Unicorn. Because the historical runtime is not yet routed through `TransitionEngine`, the project is currently in a dual state: - -- **general UCF foundation:** provider/environment/transition interfaces plus a minimal loop; -- **historical Foundry runtime:** the working Git/PR orchestration implementation. - -The next milestone is to make those two paths converge without erasing the original implementation history. +Do not mass-rename Foundry classes, database tables, routes, or artifact types +merely to match the new vocabulary. Generalization is being earned through +exercised interfaces and regression tests. Rename only when a replacement +boundary is in use. From a50d57c4a8d794c54eca3a33c5aacbd194cb222d Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:51:35 +0100 Subject: [PATCH 17/18] docs: show exercised Foundry-to-UCF adapter path --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 67a28e2..7caeb13 100644 --- a/README.md +++ b/README.md @@ -147,16 +147,17 @@ The long-term architecture should not require Claude, GitHub, source code, or Un This repository is being reopened as UCF rather than maintained as an active Unicorn Foundry product. -A provider-neutral vNext foundation now lives alongside the historical Foundry runtime: +A provider-neutral UCF foundation now lives alongside the historical Foundry runtime: - `IntelligenceProvider` separates orchestration from a concrete model vendor; - `ExecutionEnvironment` separates isolated execution from Git worktrees; - provider-neutral transition contracts represent state, evidence, actions, observations, verification, and outcomes; -- `TransitionEngine` closes a minimal state → action → observation → verification → outcome loop; +- `TransitionEngine` closes a state → action → observation → verification → outcome loop; - `TransitionJournal` requires the verified outcome to survive the call; -- tests exercise that loop with a non-Unicorn environment and fake components. +- `FoundryTransitionRuntime` maps the historical planner, implementer, verifier, migration guard, worktree manager, and artifact store onto those interfaces; +- integration tests exercise that adapter with a real Git repository and real worktree isolation. -This does **not** mean the legacy Foundry runtime has already been generalized. `RunEngine`, verification, prompts, PR handling, persistence names, and parts of configuration remain software/Git/Claude-shaped. They will be migrated incrementally after the generic boundary is proven. +The abstraction is therefore exercised by the original machinery, not only by fakes. The remaining P0 boundary is **runtime convergence**: the backwards-compatible `RunEngine` still owns the old database/event/PR lifecycle and must delegate its core transition work to the UCF path. PR creation should become publication after an accepted transition rather than the definition of completion. See [RETROSPECTIVE.md](RETROSPECTIVE.md) for the present-day interpretation, [docs/runtime-decoupling-audit.md](docs/runtime-decoupling-audit.md) for the migration map, and [docs/architecture.md](docs/architecture.md) for the original Foundry architecture specification. @@ -178,9 +179,12 @@ UCf/ │ ├── runtime/ │ │ ├── interfaces.py # observer/planner/executor/verifier/journal │ │ └── transition_engine.py # provider-neutral state-transition loop +│ ├── adapters/ +│ │ └── foundry_transition.py # historical Foundry -> UCF capability bridge │ ├── environments/ │ │ ├── base.py # ExecutionEnvironment contract -│ │ └── git_worktree.py # first concrete environment adapter +│ │ ├── git_worktree.py # concrete execution environment adapter +│ │ └── git_observer.py # explicit before/after Git state │ ├── providers/ │ │ ├── base.py # IntelligenceProvider contract │ │ └── claude_*.py # historical/default Claude adapters @@ -189,7 +193,8 @@ UCf/ │ ├── git/ # historical Git/PR action surface │ ├── tasks/ # historical Foundry task implementations │ ├── db/ # historical run persistence -│ └── storage/ # historical artifact persistence +│ └── storage/ +│ └── transition_journal.py # durable UCF outcome adapter ├── app/ # historical FastAPI control plane ├── workers/ # historical background workers ├── canon/ # historical Unicorn domain contracts From d668f5f0bab8fdfa226be9e2f9a311181acd4172 Mon Sep 17 00:00:00 2001 From: sineth madduma <149978246+sinethxyz@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:51:49 +0100 Subject: [PATCH 18/18] docs: direct future migrations through FoundryTransitionRuntime --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8f68a2c..d75ff7b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,9 +37,9 @@ The original Unicorn chain — **Signals → Evidence → State → Legibility** Do not mass-rename or delete historical Foundry code merely to make terminology look generic. Generalization must be earned through exercised interfaces and tests. -When touching new UCF foundation code, prefer the contracts under `foundry/contracts/transition_models.py`, `foundry/runtime/`, `foundry/environments/`, and `foundry/providers/base.py`. +When touching new UCF foundation code, prefer `foundry/contracts/transition_models.py`, `foundry/runtime/`, `foundry/environments/`, `foundry/adapters/`, `foundry/providers/base.py`, `foundry/storage/transition_journal.py`, and `foundry/verification/policy.py`. -When touching historical Foundry code, preserve existing behavior unless the task explicitly migrates that behavior onto the new transition interfaces. +When touching historical Foundry code, preserve existing behavior unless the task explicitly migrates that behavior onto the new transition interfaces. `FoundryTransitionRuntime` is the concrete compatibility bridge; do not create a second parallel adapter path. ## Non-Negotiable Rules