Skip to content
Merged
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
6 changes: 6 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
One `## YYYY-MM-DD` heading per merge day, newest first; one bullet of at most two lines per entry.
See the CHANGELOG rule in [AGENTS.md](../AGENTS.md).

## 2026-09-05

- Worker refusals now use the typed human-review story transition, and mega-brief telemetry ignores polling-only timestamps while retaining them in bounded evidence.
- Product Brief deploy observation now stops at a story no-deploy state with retained cause evidence, instead of spending the full deploy-Run wait.
- Production scaffolds now pin service-template `40b54d8`, whose generated CI verifies its exact uv release without consulting a mutable version manifest.

## 2026-09-04

- Worker success reports now stop their agent process group before exact-commit verification, preventing a late local commit from invalidating durable provenance; mega-brief logs bounded engineering transitions and terminal diagnostics.
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-ci-gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
# Trees whose image references are not this repository's to pin. Reason per line, same
# rule as the exclusions above: an unpinned image is a decision on the record.
UNPINNED_IMAGE_DIRS = {
"shared/tests/fixtures/service-template-91e582180b4295bce45155759bdad0dfa43b75f3": (
"shared/tests/fixtures/service-template-40b54d87dbfe64a9fa6ec379820e43137aaba04c": (
"a vendored copy of a service-template release, read by the template "
"compatibility tests; its compose files belong to that repository, and "
"editing them here would make the fixture stop matching the release it "
Expand Down
2 changes: 1 addition & 1 deletion scripts/system_configs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
description: "Production Copier template source"

- key: scheduler.service_template_ref
value: "91e582180b4295bce45155759bdad0dfa43b75f3"
value: "40b54d87dbfe64a9fa6ec379820e43137aaba04c"
category: scheduler
description: "Pinned service-template commit with the core settings and core jobs contracts"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from shared.contracts.dto.project import ProjectDTO
from shared.contracts.dto.run import RunStatus, RunType
from shared.contracts.dto.run_result import EngineeringRunResult
from shared.contracts.dto.story import StoryStatus
from shared.contracts.dto.task import TaskStatus
from shared.contracts.queues.deploy import DeployMessage, DeployTrigger
from shared.contracts.queues.worker_result import WorkerStopReason
Expand Down Expand Up @@ -314,10 +313,7 @@ async def handle_worker_gave_up(

if story_id:
try:
await api_client.patch(
f"stories/{story_id}",
json={"status": StoryStatus.WAITING_HUMAN_REVIEW.value},
)
await api_client.transition_story(story_id, "human-review")
except Exception:
logger.warning("story_whr_on_gave_up_failed", story_id=story_id, exc_info=True)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def mock_api():
api.patch = AsyncMock()
api.post = AsyncMock()
api.get = AsyncMock(return_value={"created_by": "user"})
api.transition_story = AsyncMock()
yield api


Expand Down Expand Up @@ -151,10 +152,8 @@ async def test_gave_up_transitions_story_to_whr(
redis=mock_redis,
)

story_patch_calls = [c for c in mock_api.patch.call_args_list if "stories" in str(c)]
assert len(story_patch_calls) >= 1
call_json = story_patch_calls[0][1].get("json", {})
assert call_json.get("status") == "waiting_human_review"
mock_api.transition_story.assert_awaited_once_with("story-1", "human-review")
assert not any("stories/" in str(call) for call in mock_api.patch.call_args_list)

@pytest.mark.asyncio
@patch(
Expand Down Expand Up @@ -265,5 +264,4 @@ async def test_gave_up_without_story_skips_story_transition(
redis=mock_redis,
)

story_patch_calls = [c for c in mock_api.patch.call_args_list if "stories" in str(c)]
assert len(story_patch_calls) == 0
mock_api.transition_story.assert_not_awaited()
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def mock_api():
with patch("src.consumers.engineering_result_handler.api_client") as api:
api.patch = AsyncMock()
api.post = AsyncMock()
api.transition_story = AsyncMock()
api.get = AsyncMock(return_value={"created_by": "system"})
yield api

Expand Down Expand Up @@ -86,7 +87,5 @@ async def test_reject_transitions_story_to_whr(
redis=mock_redis,
)

story_patch_calls = [c for c in mock_api.patch.call_args_list if "stories" in str(c)]
assert len(story_patch_calls) >= 1
call_json = story_patch_calls[0][1].get("json", {})
assert call_json.get("status") == "waiting_human_review"
mock_api.transition_story.assert_awaited_once_with("story-1", "human-review")
assert not any("stories/" in str(call) for call in mock_api.patch.call_args_list)
8 changes: 4 additions & 4 deletions services/langgraph/tests/unit/test_blocked_flow_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ async def test_handle_worker_gave_up_full_chain(self, mock_api, mock_po_event, m

mock_api.patch = AsyncMock()
mock_api.post = AsyncMock()
mock_api.transition_story = AsyncMock()
mock_api.get = AsyncMock(return_value={"created_by": "user"})
mock_notify.return_value = 1

Expand Down Expand Up @@ -128,10 +129,9 @@ async def test_handle_worker_gave_up_full_chain(self, mock_api, mock_po_event, m
metadata = task_patches[0][1]["json"]["failure_metadata"]
assert "56/78" in metadata["reason"]

# Story transitioned to WHR
story_patches = [c for c in mock_api.patch.call_args_list if "stories" in str(c)]
assert len(story_patches) >= 1
assert story_patches[0][1]["json"]["status"] == "waiting_human_review"
# Story transitioned to WHR through the lifecycle action, never PATCH.
mock_api.transition_story.assert_awaited_once_with("story-1", "human-review")
assert not any("stories/" in str(call) for call in mock_api.patch.call_args_list)

# Admin notified at warning level
mock_notify.assert_awaited_once()
Expand Down
8 changes: 4 additions & 4 deletions services/worker-manager/tests/unit/test_compose_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ async def test_limits_override_is_in_the_inspected_and_executed_invocation(self,
@pytest.mark.asyncio
async def test_real_service_template_resolution_passes_the_production_validator(self, tmp_path):
fixture = Path(__file__).parents[4] / (
"shared/tests/fixtures/service-template-91e582180b4295bce45155759bdad0dfa43b75f3"
"shared/tests/fixtures/service-template-40b54d87dbfe64a9fa6ec379820e43137aaba04c"
)
workspace = tmp_path / "workspace"
shutil.copytree(fixture, workspace)
Expand All @@ -342,15 +342,15 @@ async def test_real_service_template_resolution_passes_the_production_validator(

def test_service_template_has_no_label_file_compatibility_consumer(self):
fixture = Path(__file__).parents[4] / (
"shared/tests/fixtures/service-template-91e582180b4295bce45155759bdad0dfa43b75f3"
"shared/tests/fixtures/service-template-40b54d87dbfe64a9fa6ec379820e43137aaba04c"
)

assert all("label_file" not in source.read_text() for source in (fixture / "infra").glob("compose*.yml"))

@pytest.mark.asyncio
async def test_real_documented_integration_resolution_passes_the_production_validator(self, tmp_path):
fixture = Path(__file__).parents[4] / (
"shared/tests/fixtures/service-template-91e582180b4295bce45155759bdad0dfa43b75f3"
"shared/tests/fixtures/service-template-40b54d87dbfe64a9fa6ec379820e43137aaba04c"
)
workspace = tmp_path / "workspace"
shutil.copytree(fixture, workspace)
Expand All @@ -370,7 +370,7 @@ async def test_real_documented_integration_resolution_passes_the_production_vali
@pytest.mark.asyncio
async def test_documented_integration_source_flow_is_compiled(self, tmp_path):
fixture = Path(__file__).parents[4] / (
"shared/tests/fixtures/service-template-91e582180b4295bce45155759bdad0dfa43b75f3"
"shared/tests/fixtures/service-template-40b54d87dbfe64a9fa6ec379820e43137aaba04c"
)
workspace = tmp_path / "workspace"
shutil.copytree(fixture, workspace)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
_commit: 0.4.0-19-g91e5821
_commit: 0.4.0-20-g40b54d8
_src_path: gh:vladmesh/service-template
author_email: dev@example.com
author_name: Developer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Pinned toolchain versions for framework and generated-project CI."""

# Workflows repeat these literals; tooling tests enforce the exact pins.
# setup-uv v7.4.0 reads its bundled manifest, then falls back to this exact
# release URL for 0.11.29 instead of querying the mutable remote manifest.
SETUP_UV_ACTION = "astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a"

# Keep uv pinned exactly.
UV_VERSION = "0.11.29"
UV_LINUX_X86_64_CHECKSUM = "04f8b82f5d47f0512dcd32c67a4a6f16a0ea27c81537c338fd0ad6b23cebe829"
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ jobs:
python-version: "3.12"

- name: Set up uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a
with:
version: "0.11.29"
checksum: "04f8b82f5d47f0512dcd32c67a4a6f16a0ea27c81537c338fd0ad6b23cebe829"

- name: Set up project
run: make setup
Expand Down Expand Up @@ -127,9 +128,10 @@ jobs:
python-version: "3.12"

- name: Set up uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a
with:
version: "0.11.29"
checksum: "04f8b82f5d47f0512dcd32c67a4a6f16a0ea27c81537c338fd0ad6b23cebe829"

- name: Generate code from specs
run: |
Expand Down

This file was deleted.

21 changes: 19 additions & 2 deletions shared/tests/unit/test_env_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,13 +349,30 @@ def test_template_fixture_tracks_the_pinned_template_ref():
assert answers["_commit"].endswith(f"g{pinned_template_ref()[:7]}")


def test_template_fixture_pins_verified_uv_bootstrap():
"""The production render must not retain setup-uv's mutable manifest lookup."""
action = "astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a"
version = "0.11.29"
checksum = "04f8b82f5d47f0512dcd32c67a4a6f16a0ea27c81537c338fd0ad6b23cebe829"
fixture = template_fixture()
workflow = (fixture / ".github" / "workflows" / "ci.yml").read_text()
embedded_toolchain = (fixture / ".framework" / "framework" / "toolchain.py").read_text()

assert f"uses: {action}" in workflow
assert f'version: "{version}"' in workflow
assert f'checksum: "{checksum}"' in workflow
assert f'SETUP_UV_ACTION = "{action}"' in embedded_toolchain
assert f'UV_VERSION = "{version}"' in embedded_toolchain
assert f'UV_LINUX_X86_64_CHECKSUM = "{checksum}"' in embedded_toolchain


def test_template_fixture_content_matches_its_pinned_render():
"""A renamed fixture must not be able to pass provenance with stale rendered files."""
fixture = template_fixture()
answers = yaml.safe_load((fixture / ".copier-answers.yml").read_text())

assert answers == {
"_commit": "0.4.0-19-g91e5821",
"_commit": "0.4.0-20-g40b54d8",
"_src_path": "gh:vladmesh/service-template",
"author_email": "dev@example.com",
"author_name": "Developer",
Expand All @@ -368,7 +385,7 @@ def test_template_fixture_content_matches_its_pinned_render():
}
assert (
fixture_tree_digest(fixture)
== "38856e1a130cc792f201baa878bfc523bfbc1b256a15d714dd6b663407c42bea"
== "9dfa3bbd628c91c1deb54352bc4306e9df52b444be143de4406dc59bfdaa4f24"
)


Expand Down
2 changes: 1 addition & 1 deletion shared/tests/unit/test_settings_seed_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def test_core_v1_refusal_discriminators_match_the_pinned_template():
controller = (
Path(__file__).parents[1]
/ "fixtures"
/ "service-template-91e582180b4295bce45155759bdad0dfa43b75f3"
/ "service-template-40b54d87dbfe64a9fa6ec379820e43137aaba04c"
/ "services/backend/src/controllers/settings.py"
)

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/template/test_stage5_mock_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_production_template_is_loaded_from_system_config() -> None:
template = load_production_template()

assert template.source == "gh:vladmesh/service-template"
assert template.ref == "91e582180b4295bce45155759bdad0dfa43b75f3"
assert template.ref == "40b54d87dbfe64a9fa6ec379820e43137aaba04c"


def test_stage5_smoke_uses_an_isolated_workspace_and_project_name(tmp_path: Path) -> None:
Expand Down
19 changes: 18 additions & 1 deletion tests/live/brief_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,23 @@ def _engineering_run_line(run: dict, budget: int) -> str:
return f"{line}{details}"[:budget]


def _semantic_engineering_snapshot(
tasks: list[dict], runs: list[dict], runs_error: str | None
) -> dict[str, object]:
"""Return progress facts that should cause a transition event.

``updated_at`` changes on every control-plane poll. It remains in the
rendered snapshot for timing evidence, but cannot itself be progress.
"""
return {
"tasks": [
{key: value for key, value in task.items() if key != "updated_at"} for task in tasks
],
"runs": [{key: value for key, value in run.items() if key != "updated_at"} for run in runs],
"runs_error": runs_error,
}


def engineering_observation(
ctx: dict,
*,
Expand All @@ -150,7 +167,7 @@ def engineering_observation(
"""
_check_deadline(ctx, time.monotonic())
raw = json.dumps(
{"tasks": tasks, "runs": runs, "runs_error": runs_error},
_semantic_engineering_snapshot(tasks, runs, runs_error),
sort_keys=True,
default=str,
separators=(",", ":"),
Expand Down
51 changes: 50 additions & 1 deletion tests/live/pipeline_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@

GITHUB_ORG = "project-factory-organization"
TEMPLATE_REPO = "gh:vladmesh/service-template"
TEMPLATE_REF = "91e582180b4295bce45155759bdad0dfa43b75f3"
TEMPLATE_REF = "40b54d87dbfe64a9fa6ec379820e43137aaba04c"
ORCHESTRATOR_ROOT = resolve_repo_root(Path(__file__))

# Timeouts (seconds)
Expand Down Expand Up @@ -2287,6 +2287,55 @@ async def wait_deploy_run(
return None


_BRIEF_NO_DEPLOY_STORY_STATUSES = frozenset(
{
StoryStatus.WAITING_HUMAN_REVIEW.value,
StoryStatus.WAITING_USER_SECRET.value,
StoryStatus.COMPLETED.value,
StoryStatus.FAILED.value,
StoryStatus.ARCHIVED.value,
}
)


async def wait_brief_deploy_run(
api_internal: httpx.AsyncClient,
ctx: dict,
*,
timeout: float = DEPLOY_RUN_TIMEOUT,
poll_interval: float = DEPLOY_RUN_POLL_INTERVAL,
on_poll: Callable[[], None] | None = None,
) -> dict | None:
"""Wait for a Product Brief deploy, stopping when its story cannot create one.

A deploy Run appears only after the generated repository CI succeeds. If
CI or a worker refusal instead parks the story, waiting through the full
deploy budget obscures the terminal cause and spends paid time needlessly.
"""

async def story_alive() -> bool:
story_id = ctx["story_id"]
response = await api_internal.get(f"/api/stories/{story_id}")
response.raise_for_status()
status = response.json().get("status")
ctx["brief_deploy_story_status"] = status
if status not in _BRIEF_NO_DEPLOY_STORY_STATUSES:
return True
ctx["deploy_run_error"] = (
f"story {story_id} reached no-deploy state {status} before a deploy Run appeared"
)
return False

return await wait_deploy_run(
api_internal,
ctx,
timeout=timeout,
poll_interval=poll_interval,
on_poll=on_poll,
story_alive=story_alive,
)


async def _wait_for_followup_deploy_result(
api_internal: httpx.AsyncClient,
ctx: dict,
Expand Down
Loading
Loading