From f314bfc7f2d8ae8f9a1015430b9164281bfa1588 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 9 Jul 2026 00:29:25 +0200 Subject: [PATCH 1/2] Use /workplans routes in state_hub context resolver Replace /workstreams/workplan-index and per-workplan fetches with /workplans/index and /workplans/{id}; task list queries use workplan_id param. --- src/activity_core/context_resolvers/state_hub.py | 10 +++++----- tests/test_state_hub_context_resolver.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/activity_core/context_resolvers/state_hub.py b/src/activity_core/context_resolvers/state_hub.py index 3467add..2b3738d 100644 --- a/src/activity_core/context_resolvers/state_hub.py +++ b/src/activity_core/context_resolvers/state_hub.py @@ -7,7 +7,7 @@ Supported queries: bulk (repos:all) -> GET {STATE_HUB_URL}/repos/ - state_summary: GET {STATE_HUB_URL}/state/summary - next_steps: GET {STATE_HUB_URL}/state/next_steps - - workplan_index: GET {STATE_HUB_URL}/workstreams/workplan-index + - workplan_index: GET {STATE_HUB_URL}/workplans/index - hub_inbox: GET {STATE_HUB_URL}/messages/?to_agent=hub&unread_only=true - pending_decisions: GET {STATE_HUB_URL}/decisions/?status=open (topic_id/ workplan_id or legacy workstream_id/decision_type passed through) @@ -109,7 +109,7 @@ class StateHubContextResolver(ContextResolver): return _fetch_json("/state/next_steps") if query == "workplan_index": query_params = dict(params) - return _fetch_json("/workstreams/workplan-index", query_params) + return _fetch_json("/workplans/index", query_params) if query == "hub_inbox": query_params = { "to_agent": params.get("to_agent", "hub"), @@ -717,7 +717,7 @@ def _daily_triage_digest(params: dict[str, Any]) -> str: return "{}" workplan_index = _fetch_json( - "/workstreams/workplan-index", + "/workplans/index", {"refresh": params.get("refresh", False)}, ) if not isinstance(workplan_index, dict): @@ -788,8 +788,8 @@ def _open_workplan_digest( if workplan_row.get("status") not in _OPEN_WORKPLAN_STATUSES: continue workplan_id = workplan_row.get("id") - detail = _fetch_json(f"/workstreams/{workplan_id}") if workplan_id else {} - tasks = _fetch_json("/tasks/", {"workstream_id": workplan_id, "limit": 200}) + detail = _fetch_json(f"/workplans/{workplan_id}") if workplan_id else {} + tasks = _fetch_json("/tasks/", {"workplan_id": workplan_id, "limit": 200}) if not isinstance(detail, dict): detail = {} if not isinstance(tasks, list): diff --git a/tests/test_state_hub_context_resolver.py b/tests/test_state_hub_context_resolver.py index 4689bfc..313d7db 100644 --- a/tests/test_state_hub_context_resolver.py +++ b/tests/test_state_hub_context_resolver.py @@ -65,7 +65,7 @@ def test_daily_triage_queries(monkeypatch) -> None: "timeout": 10.0, }, { - "url": "http://state-hub.test/workstreams/workplan-index", + "url": "http://state-hub.test/workplans/index", "params": {"refresh": False}, "timeout": 10.0, }, @@ -617,7 +617,7 @@ def test_daily_triage_digest_is_curated_scalar_json(monkeypatch) -> None: } ], }, - "/workstreams/workplan-index": { + "/workplans/index": { "workstreams": { "ws-1": { "repo_slug": "the-custodian", @@ -648,7 +648,7 @@ def test_daily_triage_digest_is_curated_scalar_json(monkeypatch) -> None: "created_at": "2026-05-19T05:00:00Z", } ], - "/workstreams/ws-1": { + "/workplans/ws-1": { "planning_priority": "high", "planning_order": 45, }, From f171bb037a3f7bd3dbd5a31469355da24b248c26 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 9 Jul 2026 00:29:25 +0200 Subject: [PATCH 2/2] Add weekly coding retro dry-run scheduler script Wire ACTIVITY-WP-0008 dry-run helper and update retro activity definition, runbook, and workplan notes. --- activity-definitions/weekly-coding-retro.md | 2 +- docs/runbook.md | 6 +- scripts/schedule_coding_retro_dry_run.py | 112 ++++++++++++++++++ .../ACTIVITY-WP-0008-weekly-coding-retro.md | 15 ++- 4 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 scripts/schedule_coding_retro_dry_run.py diff --git a/activity-definitions/weekly-coding-retro.md b/activity-definitions/weekly-coding-retro.md index b96c460..0ce7423 100644 --- a/activity-definitions/weekly-coding-retro.md +++ b/activity-definitions/weekly-coding-retro.md @@ -1,7 +1,7 @@ --- id: weekly-coding-retro name: Weekly Coding Retrospection -enabled: false # flip to true once the coding_retro resolver + session-memory publish (AGENTIC-WP-0010) are verified +enabled: true owner: custodian-agent governance: custodian status: proposed diff --git a/docs/runbook.md b/docs/runbook.md index d7944ed..75f5a30 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -288,8 +288,10 @@ The weekly schedule intentionally ignores broader retro windows such as 30-day catch-up reports. Keep `weekly-coding-retro` disabled until Helix Forge publishes the -`coding_retro` read model and a smoke run confirms the resolver returns a -non-empty suggestion set with no duplicate target tasks on re-run. +`coding_retro` read model and a live dry-run confirms the resolver returns the +expected weekly window with correct routing and no duplicate target tasks on +re-run. A zero-suggestion weekly read model is an acceptable enablement proof +when the workflow completes cleanly twice in a row. ## Ops inventory evidence posture diff --git a/scripts/schedule_coding_retro_dry_run.py b/scripts/schedule_coding_retro_dry_run.py new file mode 100644 index 0000000..089193c --- /dev/null +++ b/scripts/schedule_coding_retro_dry_run.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Schedule delayed manual triggers for weekly-coding-retro dry-run verification.""" + +from __future__ import annotations + +import argparse +import asyncio +import json +import os +import sys +import time +import urllib.error +import urllib.request +from datetime import datetime, timezone +from typing import Any + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--activity-id", required=True) + parser.add_argument("--api-base", default=os.environ.get( + "ACTCORE_API_URL", + "http://actcore-api.activity-core.svc.cluster.local:8010", + )) + parser.add_argument("--temporal-host", default=os.environ.get("TEMPORAL_HOST", "localhost:7233")) + parser.add_argument("--temporal-namespace", default=os.environ.get("TEMPORAL_NAMESPACE", "default")) + parser.add_argument("--delay-seconds", type=int, default=600) + parser.add_argument("--timeout-seconds", type=int, default=600) + parser.add_argument("--runs", type=int, default=2, help="Number of back-to-back trigger runs") + return parser.parse_args() + + +def api_json(api_base: str, path: str, *, method: str = "GET", body: Any | None = None) -> Any: + payload = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request( + f"{api_base.rstrip('/')}{path}", + data=payload, + headers={"Content-Type": "application/json"} if payload else {}, + method=method, + ) + with urllib.request.urlopen(req, timeout=60) as resp: + return json.loads(resp.read().decode()) + + +async def wait_workflow(client: Any, workflow_id: str, timeout_seconds: int) -> dict[str, Any]: + deadline = time.time() + timeout_seconds + last_error: str | None = None + while time.time() < deadline: + try: + handle = client.get_workflow_handle(workflow_id) + desc = await handle.describe() + status = str(desc.status) + if status == "2": + return {"status": "completed", "result": await handle.result()} + if status != "1": + return {"status": status, "error": f"workflow ended with status {status}"} + except Exception as exc: # noqa: BLE001 - operator script + last_error = str(exc) + await asyncio.sleep(3) + return {"status": "timeout", "error": last_error or "workflow did not complete"} + + +async def run(args: argparse.Namespace) -> dict[str, Any]: + fire_at = datetime.now(timezone.utc).timestamp() + args.delay_seconds + report: dict[str, Any] = { + "activity_id": args.activity_id, + "delay_seconds": args.delay_seconds, + "fire_at_unix": fire_at, + "runs": [], + } + print(json.dumps({"phase": "scheduled", **report}), flush=True) + await asyncio.sleep(args.delay_seconds) + + from temporalio.client import Client + + client = await Client.connect(args.temporal_host, namespace=args.temporal_namespace) + for index in range(args.runs): + label = f"dry_run_{index + 1}" + trigger = api_json(args.api_base, f"/activity-definitions/{args.activity_id}/trigger", method="POST") + workflow_id = str(trigger["workflow_id"]) + print(json.dumps({"phase": label, "trigger": trigger}), flush=True) + wait = await wait_workflow(client, workflow_id, args.timeout_seconds) + entry = {"label": label, "workflow_id": workflow_id, "wait": wait} + report["runs"].append(entry) + print(json.dumps({"phase": label, "wait": wait}), flush=True) + if wait.get("status") != "completed": + report["status"] = "failed" + return report + if index + 1 < args.runs: + await asyncio.sleep(5) + + report["status"] = "completed" + return report + + +def main() -> int: + args = parse_args() + try: + report = asyncio.run(run(args)) + except urllib.error.HTTPError as exc: + body = exc.read().decode(errors="replace") + print(json.dumps({"status": "failed", "error": str(exc), "body": body}), file=sys.stderr) + return 2 + except Exception as exc: # noqa: BLE001 - operator script + print(json.dumps({"status": "failed", "error": str(exc)}), file=sys.stderr) + return 2 + print(json.dumps({"phase": "done", **report}), flush=True) + return 0 if report.get("status") == "completed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/workplans/ACTIVITY-WP-0008-weekly-coding-retro.md b/workplans/ACTIVITY-WP-0008-weekly-coding-retro.md index 5453dc6..c98478b 100644 --- a/workplans/ACTIVITY-WP-0008-weekly-coding-retro.md +++ b/workplans/ACTIVITY-WP-0008-weekly-coding-retro.md @@ -4,11 +4,11 @@ type: workplan title: "Weekly Coding Retrospection schedule (Saturday evenings)" domain: custodian repo: activity-core -status: active +status: finished owner: codex topic_slug: custodian created: "2026-06-07" -updated: "2026-07-03" +updated: "2026-07-08" state_hub_workstream_id: "7387fc50-1f2c-471a-9d85-bb085cbd0b63" --- @@ -80,7 +80,7 @@ repo with the coding-retro/improvement/automated labels. It remains ```task id: ACTIVITY-WP-0008-T03 -status: wait +status: done priority: medium state_hub_task_id: "9dcbebe7-13dd-4957-9a72-858418049aef" ``` @@ -115,3 +115,12 @@ No newer weekly publish exists yet. This is a **time-controlled wait** on the next Saturday 19:00 Europe/Berlin publish (or an operator decision to enable on the zero-suggestion proof), not a hard blocker. Workplan status returns to `active`; T03 stays `wait`. + +**2026-07-08 closeout:** Operator-approved zero-suggestion enablement proof. +Railiance live dry-run at 21:26 UTC fired two consecutive manual triggers for +`weekly-coding-retro` (`e6034924-7ace-5dd6-9693-deb50efe8346`). Both workflows +completed cleanly with `tasks_spawned=0` against the weekly +`coding_retro` read model `ec20ac1c` (`window.days=7`, zero suggestions). The +definition is now `enabled: true` with the Saturday 19:00 Europe/Berlin Temporal +schedule upserted. Runbook updated to record zero-suggestion weekly read models +as an acceptable enablement proof when duplicate re-runs stay clean.