feat(toolchain): flag + WorkSession toolchain columns + migration

ROBOCO_TOOLCHAIN_MATCH_ENABLED (default-off) gates the whole subsystem.
Adds work_sessions.toolchain_python / toolchain_status (nullable VARCHAR(20))
recording the interpreter the workspace was provisioned with and whether the
target's suite can be executed (ok | broken | unknown). Migration 042 verified
with a real upgrade head + downgrade -1 + re-upgrade on a throwaway Postgres.
This commit is contained in:
Renn F
2026-06-22 01:31:45 +02:00
parent 9274cd4584
commit 6df083c594
6 changed files with 105 additions and 0 deletions
@@ -0,0 +1,45 @@
"""WorkSession toolchain columns.
Adds two nullable columns to ``work_sessions`` for agent-runtime toolchain
matching:
- ``toolchain_python`` (VARCHAR(20)) — the Python version the agent's workspace
was provisioned with (resolved from the target project's ``requires-python``).
- ``toolchain_status`` (VARCHAR(20)) — whether the project's test suite can
actually be executed in that interpreter: ``ok`` | ``broken`` | ``unknown``.
Pure schema change; no data backfill. Inert until ``ROBOCO_TOOLCHAIN_MATCH_ENABLED``.
Revision ID: 042_worksession_toolchain
Revises: 041_structured_content_columns
Create Date: 2026-06-22
NOTE: revision id is 25 chars — alembic's ``alembic_version.version_num`` is
``VARCHAR(32)`` and a longer id raises at record time.
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
revision = "042_worksession_toolchain"
down_revision = "041_structured_content_columns"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"work_sessions",
sa.Column("toolchain_python", sa.String(length=20), nullable=True),
)
op.add_column(
"work_sessions",
sa.Column("toolchain_status", sa.String(length=20), nullable=True),
)
def downgrade() -> None:
op.drop_column("work_sessions", "toolchain_status")
op.drop_column("work_sessions", "toolchain_python")
+16
View File
@@ -175,6 +175,22 @@ class Settings(BaseSettings):
description="Base URL for Ollama native API (embeddings, model mgmt)", description="Base URL for Ollama native API (embeddings, model mgmt)",
) )
# ==========================================================================
# Agent runtime toolchain matching (default-off)
# ==========================================================================
# When enabled, an agent's workspace is provisioned with the Python the
# TARGET project declares (uv resolves requires-python), and a delivery role
# that cannot execute the suite blocks instead of passing on a source read.
# When off, provisioning behaves exactly as today (system interpreter).
toolchain_match_enabled: bool = Field(
default=False,
description=(
"Provision the agent workspace with the target project's Python "
"(uv resolves requires-python) and block delivery gates when the "
"suite cannot be executed. Off => today's behavior."
),
)
# ========================================================================== # ==========================================================================
# Web Research (pluggable external search/fetch for Board + PM roles) # Web Research (pluggable external search/fetch for Board + PM roles)
# ========================================================================== # ==========================================================================
+5
View File
@@ -727,6 +727,11 @@ class WorkSessionTable(Base):
nullable=True, nullable=True,
) )
# Toolchain matching — the Python the workspace was provisioned with, and
# whether the project's test suite can actually be executed in it.
toolchain_python: Mapped[str | None] = mapped_column(String(20), nullable=True)
toolchain_status: Mapped[str | None] = mapped_column(String(20), nullable=True)
# Timestamps # Timestamps
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
+12
View File
@@ -78,6 +78,16 @@ class WorkSession(TimestampMixin):
default=None, description="Agent who approved/merged the PR" default=None, description="Agent who approved/merged the PR"
) )
# Toolchain matching
toolchain_python: str | None = Field(
default=None,
description="Python version the workspace was provisioned with",
)
toolchain_status: str | None = Field(
default=None,
description="Whether the project's suite can run: ok | broken | unknown",
)
class WorkSessionCreate(RobocoBase): class WorkSessionCreate(RobocoBase):
"""Schema for creating a work session.""" """Schema for creating a work session."""
@@ -102,4 +112,6 @@ class WorkSessionUpdate(RobocoBase):
pr_status: str | None = None pr_status: str | None = None
pr_created_at: datetime | None = None pr_created_at: datetime | None = None
pr_merged_at: datetime | None = None pr_merged_at: datetime | None = None
toolchain_python: str | None = None
toolchain_status: str | None = None
merged_by: UUID | None = None merged_by: UUID | None = None
@@ -440,6 +440,19 @@ def _seed_ws(setup: dict, **kwargs: Any) -> WorkSessionTable:
) )
@pytest.mark.asyncio
async def test_worksession_toolchain_columns_round_trip(
ws_client: dict, db_session: AsyncSession
) -> None:
"""The toolchain_python / toolchain_status columns persist + round-trip."""
ws = _seed_ws(ws_client, toolchain_python="3.14", toolchain_status="ok")
db_session.add(ws)
await db_session.flush()
await db_session.refresh(ws)
assert ws.toolchain_python == "3.14"
assert ws.toolchain_status == "ok"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_session_by_id_seeded( async def test_get_session_by_id_seeded(
ws_client: dict, db_session: AsyncSession ws_client: dict, db_session: AsyncSession
+14
View File
@@ -0,0 +1,14 @@
"""The toolchain-match subsystem is gated by a default-off flag."""
from __future__ import annotations
from roboco.config import Settings
def test_toolchain_match_disabled_by_default() -> None:
assert Settings().toolchain_match_enabled is False
def test_toolchain_match_reads_env(monkeypatch) -> None:
monkeypatch.setenv("ROBOCO_TOOLCHAIN_MATCH_ENABLED", "true")
assert Settings().toolchain_match_enabled is True