From f171bb037a3f7bd3dbd5a31469355da24b248c26 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 9 Jul 2026 00:29:25 +0200 Subject: [PATCH] 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.