From f6cfc28c3306bec48bda6f2a7dc2f4a7641e6436 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 18 Aug 2026 10:52:56 +0200 Subject: [PATCH] feat(ACTIVITY-WP-0029): inventory callers, retarget sweep, bound execution Map every State Hub/core-hub caller to a post-retirement owner. Keep the 15-minute sweep schedule here and point the engine at repo-manager (State Hub dual-run by default, REPO_MANAGER_URL when present). Publish GET /execution/semantics and 410 workplan launch routes so State Hub /execution/* is not re-homed as a task database. T03 still waits on HUB-WP-0004. --- .env.example | 6 ++ SCOPE.md | 2 +- docs/execution-queue-boundary.md | 62 +++++++++++++ docs/runbook.md | 4 + docs/state-hub-caller-map.md | 63 +++++++++++++ k8s/railiance/20-runtime.yaml | 2 + src/activity_core/api.py | 3 + .../context_resolvers/state_hub.py | 48 +++++++++- src/activity_core/execution_api.py | 88 +++++++++++++++++++ tests/test_execution_api.py | 39 ++++++++ tests/test_state_hub_context_resolver.py | 37 ++++++++ .../ACTIVITY-WP-0029-hub-port-alignment.md | 17 ++-- 12 files changed, 359 insertions(+), 12 deletions(-) create mode 100644 docs/execution-queue-boundary.md create mode 100644 docs/state-hub-caller-map.md create mode 100644 src/activity_core/execution_api.py create mode 100644 tests/test_execution_api.py diff --git a/.env.example b/.env.example index 44a085e..b8f7071 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,12 @@ NATS_URL=nats://nats:4222 # ── Service integrations (gracefully degraded if unavailable) ───────────────── # State Hub — used by the state-hub context adapter. Binds {} on failure. STATE_HUB_URL=http://127.0.0.1:8000 +# Consistency sweep engine is repo-manager (ACTIVITY-WP-0029). Default POST +# still hits State Hub dual-run. Set REPO_MANAGER_URL or CONSISTENCY_SWEEP_URL +# when repo-manager exposes the HTTP sweep. +# REPO_MANAGER_URL= +# CONSISTENCY_SWEEP_URL= +# CONSISTENCY_SWEEP_PATH=/consistency/sweep/remote-all # Repo scoping — used by the repo-scoping context adapter. Binds {} on failure. REPO_SCOPING_URL=http://127.0.0.1:8020 # Issue Core — task emission backend. diff --git a/SCOPE.md b/SCOPE.md index 389ddef..069516a 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -212,7 +212,7 @@ Open work (SBOM catch-up parked — CUST-WP-0062 / ACTIVITY-WP-0030): | **G8. Live-images hygiene** | Medium (ops) | Multi-cluster `live-images-all.txt` must be refreshed after deploys or prune can delete live tags (incident 2026-07-21, restored). `scripts/refresh_live_images.sh` (T04). | | **G9. Evidence federation** | Low | Progress often lands on railiance01 edge/hub; workstation primary hub may not show the same feed without tunnel/outbox health. | | **G10. API external access** | Done | WP-0025 finished: Traefik + Authelia SSO for ops + Temporal UI; LLDAP `activity-core-operators` (NK-WP-0021); port-forward break-glass only. | -| **G11. State Hub as permanent target** | High (retirement) | Sweeps, progress sinks, and `/execution/*` still assume State Hub. **ACTIVITY-WP-0029** retargets to repo-manager + hub-core. Do not add new State Hub authorities. | +| **G11. State Hub as permanent target** | Medium (T03 open) | Sweep is dual-run/repo-manager-retargetable; `/execution/semantics` is local. Progress/evidence sinks still wait on HUB-WP-0004 (`ACTIVITY-WP-0029-T03`). | ### Drift risks diff --git a/docs/execution-queue-boundary.md b/docs/execution-queue-boundary.md new file mode 100644 index 0000000..917002a --- /dev/null +++ b/docs/execution-queue-boundary.md @@ -0,0 +1,62 @@ +--- +id: ACTIVITY-WP-0029-T04 +type: contract +title: Execution queue boundary +updated: "2026-08-18" +--- + +# Execution queue boundary + +Replaces State Hub `/execution/*` as the place callers look for *when work +should fire*. It does **not** move workplan files, task lifecycle, or ITC +Task Model types into activity-core. + +## What activity-core owns + +| Surface | Meaning | +| --- | --- | +| Temporal Schedule + `POST /activity-definitions/{id}/trigger` | When an ActivityDefinition fires | +| `POST /ops-runs/claim` and complete/fail/heartbeat | Claimable **ops run** for that fire (ACT-ADR-005) | +| `GET /execution/semantics` | This contract | + +`port.schedule` (hub-extension 0.1) is the hub-core **outbound** port that +asks activity-core to wake a schedule or accept a launch of an +ActivityDefinition. activity-core implements the receiving side. + +## What activity-core does not own + +| Concern | Owner | +| --- | --- | +| Workplan / task **files** and their status | The repo (ADR-001) | +| Work-record **index** and C-rule consistency | repo-manager (`port.work`) | +| Task / Commitment / TaskState vocabulary | info-tech-canon Task Model | +| Assign / track / close a development task | issue-core / work records | +| Policy publication | policy-nexus | +| Workplan `execution_state` rows on State Hub | retiring; do not re-home as hub tables here | + +A workplan being `active` is a **lifecycle** fact in the repo file. An +ops_run being `open` is an **automation fire**. Those are different planes +(ACT-ADR-005). Mapping State Hub `POST /execution/launch-requests` onto +“create a workplan execution row here” would collapse them again. + +## Replacement for State Hub routes + +| State Hub route | Replacement | +| --- | --- | +| `GET /execution/semantics` | `GET /execution/semantics` on activity-core (this contract) | +| `POST /execution/launch-requests` (workplan) | Do not send here. Queue the workplan in the **repo file**; use an ActivityDefinition + ops_run if the work is a recurring/ops fire | +| `GET /execution/launch-requests` | `GET /ops-runs` | +| `GET /execution/workplan-stack` | repo-manager work index / hub-core projection — not this API | +| `PATCH /execution/workplans/{id}/intent` | Edit the workplan file; repo-manager indexes it | + +Callers that still hit `/execution/*` on activity-core for workplan ids get +**410** with `replacement_ref` pointing at the table above. + +## Semantics payload + +`GET /execution/semantics` returns: + +- `activity_core_owns` — schedules, triggers, ops_run claim/complete +- `activity_core_does_not_own` — workplan files, task lifecycle, C-rules +- `replacements` — map from retired State Hub paths to current surfaces +- `port` — `port.schedule` (consume; do not invent the hub-core side here) diff --git a/docs/runbook.md b/docs/runbook.md index f052e12..6aae6d2 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -778,6 +778,10 @@ retry transient 502/503/504 then **degrade** by default so required workflows do not thrash Temporal when the edge is briefly unavailable. Details: `docs/edge-relay-resilience.md` (ACTIVITY-WP-0027-T06). +The consistency sweep schedule stays in activity-core; the C-rule engine is +repo-manager. Default POST still uses the State Hub dual-run adapter. Override +with `CONSISTENCY_SWEEP_URL` or `REPO_MANAGER_URL` (`docs/state-hub-caller-map.md`). + ## Troubleshooting ### Worker fails to start: "ACTCORE_DB_URL is required" diff --git a/docs/state-hub-caller-map.md b/docs/state-hub-caller-map.md new file mode 100644 index 0000000..a627725 --- /dev/null +++ b/docs/state-hub-caller-map.md @@ -0,0 +1,63 @@ +--- +id: ACTIVITY-WP-0029-T01 +type: inventory +title: State Hub and hub-core callers +updated: "2026-08-18" +--- + +# State Hub / hub-core caller map + +Inventory for ACTIVITY-WP-0029-T01. Current URLs still go to State Hub unless +noted. **Target owner** is who should own the *capability* after retirement. +activity-core stays the scheduler even when it is not the engine. + +Hub-core port names are from `helixforge.hub-extension` 0.1 +(`prj-state-hub-retirement/architecture/hub-extension-contract_v0.1.yaml`). +HUB-WP-0004 has not shipped those ports yet. + +| Caller | Current URL | SHR-INV | Target owner | Port (when named) | +| --- | --- | --- | --- | --- | +| `StateHubContextResolver.domain_summary` | `GET /state/domain/{domain}` | progress / state-projections | hub-core | `port.projection.query` | +| `state_summary` | `GET /state/summary` | state-projections | hub-core | `port.projection.query` | +| `next_steps` | `GET /state/next_steps` | state-projections | hub-core | `port.projection.query` | +| `workplan_index` | `GET /workplans/index` | work-records | repo-manager | `port.work` | +| `hub_inbox` | `GET /messages/` | messaging | hub-core | `port.messaging` | +| `pending_decisions` | `GET /decisions/` | work-records | repo-manager | `port.work` | +| `coding_retro` | `GET /progress/?event_type=coding_retro` | progress-telemetry | hub-core | `port.events.progress` | +| `daily_triage_digest` | `GET /progress/` + workplan/task reads | progress-telemetry | hub-core | `port.events.progress` | +| `recently_on_scope_hourly` | `POST /recently-on-scope/hourly` | state-projections | hub-core (generation stays on hub host) | `port.projection.query` | +| `consistency_sweep_remote_all` | `POST /consistency/sweep/remote-all` | `job:consistency-sweep-remote-all` + `route:POST:/consistency/sweep/remote-all` | **repo-manager** engine; activity-core schedule | none (RM command, not a hub port) | +| `phase5_stabilization_check` | `GET /state/health`, `/state/summary`, `/progress/` | state-projections | hub-core | `port.projection.query` | +| `legacy_meter_weekly_review` | `GET /legacy-meter/weekly-review` | catalogs | the-custodian / hub-core catalog | `port.projection.query` | +| `binky_rhythm_status` / `fi_brief_status` | `GET /progress/` | progress-telemetry | hub-core | `port.events.progress` | +| `repo_sbom_status` | `GET /sbom/{slug}` or `GET /repos/` | sbom-inventory | sbom-nexus (parked: ACTIVITY-WP-0030) | n/a | +| `todo_md_staleness` | `GET /repos/todo-md-staleness` | repo-consistency | repo-manager | `port.repo` | +| `kaizen` / `reuse_surface` repo list | `GET /repos/` | repo-registry | repo-manager | `port.repo` | +| `StateHubProgressSink` | `POST /progress/` | progress-telemetry | hub-core | `port.events.progress` | +| `report_sinks` state-hub-progress | `POST /progress/` | progress-telemetry | hub-core | `port.events.progress` | +| `ops_evidence_sinks` | `POST /progress/` | progress-telemetry | hub-core | `port.events.interaction` | +| `schedule_health` miss reports | `POST /progress/` | progress-telemetry | hub-core | `port.events.progress` | +| `ops_console` audit notes | `POST /progress/` | progress-telemetry | hub-core | `port.events.progress` | +| `automation_status` / `review_cli` | `GET /progress/` | progress-telemetry | hub-core | `port.events.progress` | +| `CoreHubContextResolver.stabilization_check` | `https://hub.coulomb.social` `/healthz` `/readyz` `/api/v2/*` | CORE-WP-0010 | hub-core runtime (core-hub archived after cutover) | compatibility surface | +| Manual trigger | `POST /activity-definitions/{id}/trigger` | — | **activity-core** | consumed via `port.schedule` | +| Ops-run claim queue | `/ops-runs/*` | workplan-execution-queue (replace) | **activity-core** | consumed via `port.schedule` | + +## Sweep split (T02) + +- **Schedule:** activity-core cron `*/15 * * * *` UTC (`state-hub-consistency-sweep`). +- **Engine:** repo-manager C-rules / `rmgr reconcile`. +- **Adapter until RM HTTP exists:** State Hub `POST /consistency/sweep/remote-all` + (dual-run when `RM_RECONCILE` is on). Override with `CONSISTENCY_SWEEP_URL` + or `REPO_MANAGER_URL` + optional `CONSISTENCY_SWEEP_PATH`. + +## Evidence sink (T03 — wait) + +All `POST /progress/` callers stay on the State Hub beachhead until +**HUB-WP-0004** publishes `port.events.progress` / `port.events.interaction`. +Do not add a second permanent progress host. + +## Execution (T04) + +State Hub `/execution/*` is **not** a task database and must not move here as +workplan rows. See `docs/execution-queue-boundary.md`. diff --git a/k8s/railiance/20-runtime.yaml b/k8s/railiance/20-runtime.yaml index ab7bc2f..806c36e 100644 --- a/k8s/railiance/20-runtime.yaml +++ b/k8s/railiance/20-runtime.yaml @@ -243,6 +243,8 @@ data: Kubernetes projection of the Custodian-owned definition in `/home/worsch/the-custodian/activity-definitions/state-hub-consistency-sweep.md`. + ACTIVITY-WP-0029: activity-core schedules; repo-manager owns the engine; + State Hub remains the default dual-run adapter until REPO_MANAGER_URL is set. daily-todo-md-stale-review.md: | --- id: "b8e4f1a2-3c6d-4e9f-a1b2-7d8e9f0a1b2c" diff --git a/src/activity_core/api.py b/src/activity_core/api.py index c0923c3..d9e7107 100644 --- a/src/activity_core/api.py +++ b/src/activity_core/api.py @@ -11,6 +11,7 @@ Endpoints: DELETE /activity-definitions/{id} — delete POST /activity-definitions/{id}/trigger — manual one-shot run GET/POST /ops/... — operator console (inventory, status, control) + GET /execution/semantics — launch/ops_run contract (ACTIVITY-WP-0029) Schedule lifecycle: - POST/PUT with trigger_type='cron' upserts a Temporal Schedule. @@ -40,6 +41,7 @@ from temporalio.api.workflowservice.v1 import GetSystemInfoRequest from temporalio.client import Client from activity_core.models import ActivityDefinition, CronTriggerConfig +from activity_core.execution_api import router as execution_router from activity_core.ops_api import bind_ops_deps, router as ops_router from activity_core.ops_runs_api import bind_ops_runs_deps, router as ops_runs_router from activity_core.orm import ActivityDefinition as ActivityDefinitionRow, EventType as EventTypeRow @@ -85,6 +87,7 @@ app = FastAPI(title="activity-core API", lifespan=lifespan) app.include_router(webhook_router) app.include_router(ops_router) app.include_router(ops_runs_router) +app.include_router(execution_router) def _get_db() -> async_sessionmaker[AsyncSession]: diff --git a/src/activity_core/context_resolvers/state_hub.py b/src/activity_core/context_resolvers/state_hub.py index 4cc7a11..a129285 100644 --- a/src/activity_core/context_resolvers/state_hub.py +++ b/src/activity_core/context_resolvers/state_hub.py @@ -14,7 +14,10 @@ Supported queries: - coding_retro: latest /progress/ item with event_type=coding_retro - daily_triage_digest: curated scalar JSON digest for daily WSJF triage - recently_on_scope_hourly: POST {STATE_HUB_URL}/recently-on-scope/hourly - - consistency_sweep_remote_all: POST {STATE_HUB_URL}/consistency/sweep/remote-all + - consistency_sweep_remote_all: POST {sweep_base}/consistency/sweep/remote-all + sweep_base = CONSISTENCY_SWEEP_URL or REPO_MANAGER_URL or STATE_HUB_URL + (ACTIVITY-WP-0029: engine is repo-manager; State Hub is the dual-run + adapter until REPO_MANAGER_URL is set) - phase5_stabilization_check: hub-visible Phase 5 stabilization gates - legacy_meter_weekly_review: GET {STATE_HUB_URL}/legacy-meter/weekly-review - binky_rhythm_status: due-items for the Binky operating-rhythm definitions, @@ -68,6 +71,33 @@ def _base_url() -> str: return os.environ.get("STATE_HUB_URL", _DEFAULT_STATE_HUB_URL).rstrip("/") +def _sweep_target() -> tuple[str, str, str]: + """Return (base_url, path, adapter) for the consistency sweep POST. + + Engine owner is always repo-manager. Default adapter is State Hub's + dual-run ``POST /consistency/sweep/remote-all``. Point + ``CONSISTENCY_SWEEP_URL`` or ``REPO_MANAGER_URL`` at repo-manager when + that HTTP surface exists. + """ + path = (os.environ.get("CONSISTENCY_SWEEP_PATH") or "/consistency/sweep/remote-all").strip() + if not path.startswith("/"): + path = f"/{path}" + explicit = (os.environ.get("CONSISTENCY_SWEEP_URL") or "").strip().rstrip("/") + repo_mgr = (os.environ.get("REPO_MANAGER_URL") or "").strip().rstrip("/") + state_hub = _base_url() + if explicit: + if repo_mgr and explicit == repo_mgr: + adapter = "repo-manager" + elif explicit == state_hub: + adapter = "state-hub-dual-run" + else: + adapter = "explicit" + return explicit, path, adapter + if repo_mgr: + return repo_mgr, path, "repo-manager" + return state_hub, path, "state-hub-dual-run" + + def _fetch_json(path: str, params: dict[str, Any] | None = None) -> Any: url = f"{_base_url()}{path}" try: @@ -104,9 +134,10 @@ def _post_json( *, timeout: float = _TIMEOUT_SECONDS, retries: int | None = None, + base_url: str | None = None, ) -> Any: """POST JSON with retries on transient edge/hub failures (ACTIVITY-WP-0027).""" - url = f"{_base_url()}{path}" + url = f"{(base_url or _base_url()).rstrip('/')}{path}" attempts = _post_retry_attempts() if retries is None else max(1, retries) backoff = _post_retry_backoff_seconds() last_exc: Exception | None = None @@ -261,11 +292,13 @@ class StateHubContextResolver(ContextResolver): for key, value in params.items() if key not in {"required", "degrade_on_unavailable"} } + sweep_base, sweep_path, adapter = _sweep_target() try: result = _post_json( - "/consistency/sweep/remote-all", + sweep_path, payload, timeout=_SWEEP_TIMEOUT_SECONDS, + base_url=sweep_base, ) except (httpx.HTTPError, ValueError) as exc: if not _want_degrade(params): @@ -283,8 +316,15 @@ class StateHubContextResolver(ContextResolver): "skipped_budget": [], "degraded": True, "degraded_reason": str(exc)[:300], + "engine_owner": "repo-manager", + "adapter": adapter, + "sweep_url": f"{sweep_base}{sweep_path}", } - return _validate_consistency_sweep_remote_all(result) + validated = _validate_consistency_sweep_remote_all(result) + validated.setdefault("engine_owner", "repo-manager") + validated.setdefault("adapter", adapter) + validated.setdefault("sweep_url", f"{sweep_base}{sweep_path}") + return validated if query == "phase5_stabilization_check": return _phase5_stabilization_check(params) if query == "legacy_meter_weekly_review": diff --git a/src/activity_core/execution_api.py b/src/activity_core/execution_api.py new file mode 100644 index 0000000..d7db9d3 --- /dev/null +++ b/src/activity_core/execution_api.py @@ -0,0 +1,88 @@ +"""Execution-queue contract surface (ACTIVITY-WP-0029-T04). + +Workplan launch rows stay out of this API. Callers that send State Hub +``/execution/*`` workplan shapes get 410 with a replacement pointer. +""" + +from __future__ import annotations + +from fastapi import APIRouter +from fastapi.responses import JSONResponse + +router = APIRouter(prefix="/execution", tags=["execution"]) + +SEMANTICS = { + "port": "port.schedule", + "document": "docs/execution-queue-boundary.md", + "activity_core_owns": [ + "schedules, wakeups, and recurring ActivityDefinition fires", + "POST /activity-definitions/{id}/trigger", + "ops_run claim, heartbeat, complete, and fail", + ], + "activity_core_does_not_own": [ + "workplan and task files (ADR-001 / repo-manager index)", + "task lifecycle assign/track/close (issue-core / work records)", + "ITC Task Model types", + "policy publication (policy-nexus)", + "C-rule consistency engine (repo-manager)", + ], + "replacements": { + "GET /execution/semantics": "GET /execution/semantics", + "GET /execution/launch-requests": "GET /ops-runs", + "POST /execution/launch-requests": ( + "POST /activity-definitions/{id}/trigger for a schedule fire; " + "do not POST workplan ids here" + ), + "GET /execution/workplan-stack": "repo-manager port.work / hub-core projection", + "PATCH /execution/workplans/{id}/intent": "edit the workplan file in its repo", + }, +} + + +@router.get("/semantics") +async def execution_semantics() -> dict: + return SEMANTICS + + +def _gone(replacement: str, detail: str) -> JSONResponse: + return JSONResponse( + status_code=410, + content={ + "detail": detail, + "replacement_ref": replacement, + "document": "docs/execution-queue-boundary.md", + }, + ) + + +@router.api_route("/launch-requests", methods=["GET", "POST"]) +async def launch_requests_retired() -> JSONResponse: + return _gone( + SEMANTICS["replacements"]["GET /execution/launch-requests"], + "Workplan launch-requests are not stored in activity-core. " + "Use GET /ops-runs or POST /activity-definitions/{id}/trigger.", + ) + + +@router.get("/workplan-stack") +async def workplan_stack_retired() -> JSONResponse: + return _gone( + SEMANTICS["replacements"]["GET /execution/workplan-stack"], + "Workplan stacks are a work-record index, not an ops-run queue.", + ) + + +@router.patch("/workplans/{workplan_id}/intent") +async def workplan_intent_retired(workplan_id: str) -> JSONResponse: + return _gone( + SEMANTICS["replacements"]["PATCH /execution/workplans/{id}/intent"], + "Execution intent for a workplan belongs in the repo file, not here.", + ) + + +@router.patch("/workstreams/{workstream_id}/intent") +async def workstream_intent_retired(workstream_id: str) -> JSONResponse: + return _gone( + SEMANTICS["replacements"]["PATCH /execution/workplans/{id}/intent"], + "Legacy workstream execution intent is retired with State Hub.", + ) diff --git a/tests/test_execution_api.py b/tests/test_execution_api.py new file mode 100644 index 0000000..15a3323 --- /dev/null +++ b/tests/test_execution_api.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from activity_core.execution_api import router + + +def _client() -> TestClient: + app = FastAPI() + app.include_router(router) + return TestClient(app) + + +def test_execution_semantics_names_boundary() -> None: + client = _client() + response = client.get("/execution/semantics") + assert response.status_code == 200 + body = response.json() + assert body["port"] == "port.schedule" + assert any("ops_run" in item for item in body["activity_core_owns"]) + assert any("workplan" in item.lower() for item in body["activity_core_does_not_own"]) + assert body["replacements"]["GET /execution/launch-requests"] == "GET /ops-runs" + + +def test_execution_workplan_routes_are_gone() -> None: + client = _client() + gone = [ + client.get("/execution/launch-requests"), + client.post("/execution/launch-requests", json={"workplan_id": "x"}), + client.get("/execution/workplan-stack"), + client.patch("/execution/workplans/abc/intent", json={}), + client.patch("/execution/workstreams/abc/intent", json={}), + ] + for response in gone: + assert response.status_code == 410 + payload = response.json() + assert "replacement_ref" in payload + assert "execution-queue-boundary" in payload["document"] diff --git a/tests/test_state_hub_context_resolver.py b/tests/test_state_hub_context_resolver.py index b200206..d3bce6f 100644 --- a/tests/test_state_hub_context_resolver.py +++ b/tests/test_state_hub_context_resolver.py @@ -553,6 +553,9 @@ def test_consistency_sweep_remote_all_posts_batch(monkeypatch) -> None: ) monkeypatch.setenv("STATE_HUB_URL", "http://state-hub.test/") + monkeypatch.delenv("REPO_MANAGER_URL", raising=False) + monkeypatch.delenv("CONSISTENCY_SWEEP_URL", raising=False) + monkeypatch.delenv("CONSISTENCY_SWEEP_PATH", raising=False) monkeypatch.setattr(httpx, "post", fake_post) result = StateHubContextResolver().resolve( @@ -563,6 +566,9 @@ def test_consistency_sweep_remote_all_posts_batch(monkeypatch) -> None: assert result["exit_code"] == 0 assert result["repos_processed"][0]["repo_slug"] == "state-hub" + assert result["engine_owner"] == "repo-manager" + assert result["adapter"] == "state-hub-dual-run" + assert result["sweep_url"] == "http://state-hub.test/consistency/sweep/remote-all" assert calls == [ { "url": "http://state-hub.test/consistency/sweep/remote-all", @@ -572,6 +578,37 @@ def test_consistency_sweep_remote_all_posts_batch(monkeypatch) -> None: ] +def test_consistency_sweep_uses_repo_manager_url(monkeypatch) -> None: + calls: list[dict[str, Any]] = [] + + def fake_post(url: str, **kwargs: Any) -> DummyResponse: + calls.append({"url": url, **kwargs}) + return DummyResponse( + { + "exit_code": 0, + "lock_skipped": False, + "repos_processed": [], + } + ) + + monkeypatch.setenv("STATE_HUB_URL", "http://state-hub.test/") + monkeypatch.setenv("REPO_MANAGER_URL", "http://repo-manager.test") + monkeypatch.setenv("CONSISTENCY_SWEEP_PATH", "/v1/consistency/sweep/remote-all") + monkeypatch.setattr(httpx, "post", fake_post) + + result = StateHubContextResolver().resolve( + "consistency_sweep_remote_all", + None, + {"max_seconds": 300}, + ) + + assert result["adapter"] == "repo-manager" + assert result["engine_owner"] == "repo-manager" + assert calls[0]["url"] == ( + "http://repo-manager.test/v1/consistency/sweep/remote-all" + ) + + def test_consistency_sweep_remote_all_failure_bubbles(monkeypatch) -> None: def fake_post(url: str, **kwargs: Any) -> DummyResponse: raise httpx.ConnectError("offline") diff --git a/workplans/ACTIVITY-WP-0029-hub-port-alignment.md b/workplans/ACTIVITY-WP-0029-hub-port-alignment.md index b0608d4..d5281fd 100644 --- a/workplans/ACTIVITY-WP-0029-hub-port-alignment.md +++ b/workplans/ACTIVITY-WP-0029-hub-port-alignment.md @@ -77,7 +77,7 @@ Out of this workplan: SBOM catch-up (CUST-WP-0062 / ACTIVITY-WP-0030, parked). ```task id: ACTIVITY-WP-0029-T01 -status: todo +status: done priority: high state_hub_task_id: "03021be2-5d60-455b-9c9c-b8681d65911c" ``` @@ -93,7 +93,7 @@ Do not retarget a caller to activity-core just because we schedule it. ```task id: ACTIVITY-WP-0029-T02 -status: todo +status: done priority: high state_hub_task_id: "f8107e2b-ab1f-4a3b-9348-8d5dcfb647b4" ``` @@ -122,7 +122,7 @@ smokes when the port exists. Until then, keep the State Hub beachhead ```task id: ACTIVITY-WP-0029-T04 -status: todo +status: done priority: medium state_hub_task_id: "a34b42c3-8a08-482b-936f-14675ee254ca" ``` @@ -134,7 +134,10 @@ index, issue-core lifecycle, ITC Task Model types, policy-nexus publication. ## Acceptance -- [ ] Caller map lists owner + current URL for every State Hub / core-hub call -- [ ] Sweep still fires from activity-core and hits repo-manager (or dual-run) -- [ ] Evidence sink plan names hub-core as the permanent host; T03 stays wait until that port exists -- [ ] Execution boundary is written so `/execution/*` cannot be read as "activity-core owns tasks" +- [x] Caller map lists owner + current URL for every State Hub / core-hub call + (`docs/state-hub-caller-map.md`) +- [x] Sweep still fires from activity-core and hits repo-manager (or dual-run) + (`CONSISTENCY_SWEEP_URL` / `REPO_MANAGER_URL`; default State Hub adapter) +- [x] Evidence sink plan names hub-core as the permanent host; T03 stays wait until that port exists +- [x] Execution boundary is written so `/execution/*` cannot be read as "activity-core owns tasks" + (`docs/execution-queue-boundary.md`, `GET /execution/semantics`, 410 on workplan routes)