Compare commits
2 commits
2a841d91fe
...
f171bb037a
| Author | SHA1 | Date | |
|---|---|---|---|
| f171bb037a | |||
| f314bfc7f2 |
6 changed files with 137 additions and 14 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: weekly-coding-retro
|
id: weekly-coding-retro
|
||||||
name: Weekly Coding Retrospection
|
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
|
owner: custodian-agent
|
||||||
governance: custodian
|
governance: custodian
|
||||||
status: proposed
|
status: proposed
|
||||||
|
|
|
||||||
|
|
@ -288,8 +288,10 @@ The weekly schedule intentionally ignores broader retro windows such as 30-day
|
||||||
catch-up reports.
|
catch-up reports.
|
||||||
|
|
||||||
Keep `weekly-coding-retro` disabled until Helix Forge publishes the
|
Keep `weekly-coding-retro` disabled until Helix Forge publishes the
|
||||||
`coding_retro` read model and a smoke run confirms the resolver returns a
|
`coding_retro` read model and a live dry-run confirms the resolver returns the
|
||||||
non-empty suggestion set with no duplicate target tasks on re-run.
|
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
|
## Ops inventory evidence posture
|
||||||
|
|
||||||
|
|
|
||||||
112
scripts/schedule_coding_retro_dry_run.py
Normal file
112
scripts/schedule_coding_retro_dry_run.py
Normal file
|
|
@ -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())
|
||||||
|
|
@ -7,7 +7,7 @@ Supported queries:
|
||||||
bulk (repos:all) -> GET {STATE_HUB_URL}/repos/
|
bulk (repos:all) -> GET {STATE_HUB_URL}/repos/
|
||||||
- state_summary: GET {STATE_HUB_URL}/state/summary
|
- state_summary: GET {STATE_HUB_URL}/state/summary
|
||||||
- next_steps: GET {STATE_HUB_URL}/state/next_steps
|
- 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
|
- hub_inbox: GET {STATE_HUB_URL}/messages/?to_agent=hub&unread_only=true
|
||||||
- pending_decisions: GET {STATE_HUB_URL}/decisions/?status=open (topic_id/
|
- pending_decisions: GET {STATE_HUB_URL}/decisions/?status=open (topic_id/
|
||||||
workplan_id or legacy workstream_id/decision_type passed through)
|
workplan_id or legacy workstream_id/decision_type passed through)
|
||||||
|
|
@ -109,7 +109,7 @@ class StateHubContextResolver(ContextResolver):
|
||||||
return _fetch_json("/state/next_steps")
|
return _fetch_json("/state/next_steps")
|
||||||
if query == "workplan_index":
|
if query == "workplan_index":
|
||||||
query_params = dict(params)
|
query_params = dict(params)
|
||||||
return _fetch_json("/workstreams/workplan-index", query_params)
|
return _fetch_json("/workplans/index", query_params)
|
||||||
if query == "hub_inbox":
|
if query == "hub_inbox":
|
||||||
query_params = {
|
query_params = {
|
||||||
"to_agent": params.get("to_agent", "hub"),
|
"to_agent": params.get("to_agent", "hub"),
|
||||||
|
|
@ -717,7 +717,7 @@ def _daily_triage_digest(params: dict[str, Any]) -> str:
|
||||||
return "{}"
|
return "{}"
|
||||||
|
|
||||||
workplan_index = _fetch_json(
|
workplan_index = _fetch_json(
|
||||||
"/workstreams/workplan-index",
|
"/workplans/index",
|
||||||
{"refresh": params.get("refresh", False)},
|
{"refresh": params.get("refresh", False)},
|
||||||
)
|
)
|
||||||
if not isinstance(workplan_index, dict):
|
if not isinstance(workplan_index, dict):
|
||||||
|
|
@ -788,8 +788,8 @@ def _open_workplan_digest(
|
||||||
if workplan_row.get("status") not in _OPEN_WORKPLAN_STATUSES:
|
if workplan_row.get("status") not in _OPEN_WORKPLAN_STATUSES:
|
||||||
continue
|
continue
|
||||||
workplan_id = workplan_row.get("id")
|
workplan_id = workplan_row.get("id")
|
||||||
detail = _fetch_json(f"/workstreams/{workplan_id}") if workplan_id else {}
|
detail = _fetch_json(f"/workplans/{workplan_id}") if workplan_id else {}
|
||||||
tasks = _fetch_json("/tasks/", {"workstream_id": workplan_id, "limit": 200})
|
tasks = _fetch_json("/tasks/", {"workplan_id": workplan_id, "limit": 200})
|
||||||
if not isinstance(detail, dict):
|
if not isinstance(detail, dict):
|
||||||
detail = {}
|
detail = {}
|
||||||
if not isinstance(tasks, list):
|
if not isinstance(tasks, list):
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ def test_daily_triage_queries(monkeypatch) -> None:
|
||||||
"timeout": 10.0,
|
"timeout": 10.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "http://state-hub.test/workstreams/workplan-index",
|
"url": "http://state-hub.test/workplans/index",
|
||||||
"params": {"refresh": False},
|
"params": {"refresh": False},
|
||||||
"timeout": 10.0,
|
"timeout": 10.0,
|
||||||
},
|
},
|
||||||
|
|
@ -617,7 +617,7 @@ def test_daily_triage_digest_is_curated_scalar_json(monkeypatch) -> None:
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"/workstreams/workplan-index": {
|
"/workplans/index": {
|
||||||
"workstreams": {
|
"workstreams": {
|
||||||
"ws-1": {
|
"ws-1": {
|
||||||
"repo_slug": "the-custodian",
|
"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",
|
"created_at": "2026-05-19T05:00:00Z",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"/workstreams/ws-1": {
|
"/workplans/ws-1": {
|
||||||
"planning_priority": "high",
|
"planning_priority": "high",
|
||||||
"planning_order": 45,
|
"planning_order": 45,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ type: workplan
|
||||||
title: "Weekly Coding Retrospection schedule (Saturday evenings)"
|
title: "Weekly Coding Retrospection schedule (Saturday evenings)"
|
||||||
domain: custodian
|
domain: custodian
|
||||||
repo: activity-core
|
repo: activity-core
|
||||||
status: active
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: custodian
|
topic_slug: custodian
|
||||||
created: "2026-06-07"
|
created: "2026-06-07"
|
||||||
updated: "2026-07-03"
|
updated: "2026-07-08"
|
||||||
state_hub_workstream_id: "7387fc50-1f2c-471a-9d85-bb085cbd0b63"
|
state_hub_workstream_id: "7387fc50-1f2c-471a-9d85-bb085cbd0b63"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ repo with the coding-retro/improvement/automated labels. It remains
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: ACTIVITY-WP-0008-T03
|
id: ACTIVITY-WP-0008-T03
|
||||||
status: wait
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "9dcbebe7-13dd-4957-9a72-858418049aef"
|
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
|
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
|
the zero-suggestion proof), not a hard blocker. Workplan status returns to
|
||||||
`active`; T03 stays `wait`.
|
`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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue