diff --git a/activity-definitions/weekly-coding-retro.md b/activity-definitions/weekly-coding-retro.md index 0ce7423..b96c460 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: true +enabled: false # flip to true once the coding_retro resolver + session-memory publish (AGENTIC-WP-0010) are verified owner: custodian-agent governance: custodian status: proposed diff --git a/docs/runbook.md b/docs/runbook.md index 75f5a30..d7944ed 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -288,10 +288,8 @@ 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 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. +`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. ## Ops inventory evidence posture diff --git a/scripts/schedule_coding_retro_dry_run.py b/scripts/schedule_coding_retro_dry_run.py deleted file mode 100644 index 089193c..0000000 --- a/scripts/schedule_coding_retro_dry_run.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/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/src/activity_core/context_resolvers/state_hub.py b/src/activity_core/context_resolvers/state_hub.py index 2b3738d..3467add 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}/workplans/index + - workplan_index: GET {STATE_HUB_URL}/workstreams/workplan-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("/workplans/index", query_params) + return _fetch_json("/workstreams/workplan-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( - "/workplans/index", + "/workstreams/workplan-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"/workplans/{workplan_id}") if workplan_id else {} - tasks = _fetch_json("/tasks/", {"workplan_id": workplan_id, "limit": 200}) + detail = _fetch_json(f"/workstreams/{workplan_id}") if workplan_id else {} + tasks = _fetch_json("/tasks/", {"workstream_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 313d7db..4689bfc 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/workplans/index", + "url": "http://state-hub.test/workstreams/workplan-index", "params": {"refresh": False}, "timeout": 10.0, }, @@ -617,7 +617,7 @@ def test_daily_triage_digest_is_curated_scalar_json(monkeypatch) -> None: } ], }, - "/workplans/index": { + "/workstreams/workplan-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", } ], - "/workplans/ws-1": { + "/workstreams/ws-1": { "planning_priority": "high", "planning_order": 45, }, diff --git a/workplans/ACTIVITY-WP-0008-weekly-coding-retro.md b/workplans/ACTIVITY-WP-0008-weekly-coding-retro.md index c98478b..5453dc6 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: finished +status: active owner: codex topic_slug: custodian created: "2026-06-07" -updated: "2026-07-08" +updated: "2026-07-03" 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: done +status: wait priority: medium state_hub_task_id: "9dcbebe7-13dd-4957-9a72-858418049aef" ``` @@ -115,12 +115,3 @@ 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.