generated from coulomb/repo-seed
Implement weekly coding retro schedule
This commit is contained in:
48
activity-definitions/weekly-coding-retro.md
Normal file
48
activity-definitions/weekly-coding-retro.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
owner: custodian-agent
|
||||||
|
governance: custodian
|
||||||
|
status: proposed
|
||||||
|
trigger:
|
||||||
|
type: cron
|
||||||
|
cron_expression: "0 19 * * 6" # Saturday 19:00
|
||||||
|
timezone: Europe/Berlin
|
||||||
|
misfire_policy: skip
|
||||||
|
context_sources:
|
||||||
|
- type: state-hub
|
||||||
|
query: coding_retro
|
||||||
|
params:
|
||||||
|
window_days: 7
|
||||||
|
limit: 100
|
||||||
|
bind_to: context.retro
|
||||||
|
# The coding_retro resolver returns the most recent event_type=coding_retro read
|
||||||
|
# model published to the hub by helix_forge session-memory (AGENTIC-WP-0010).
|
||||||
|
# Its detail.suggestions[] are already ranked (impact x frequency, cross-flavor
|
||||||
|
# first) and capped at 3 per repo, so the rule below just routes them.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Weekly Coding Retrospection
|
||||||
|
|
||||||
|
Runs every Saturday 19:00 Berlin time. Reads the previous week's coding-session
|
||||||
|
analysis (published to the hub by helix_forge session-memory) and opens one
|
||||||
|
improvement suggestion per relevant repo — the three most promising, already
|
||||||
|
ranked upstream.
|
||||||
|
|
||||||
|
```rule
|
||||||
|
id: propose-weekly-improvements
|
||||||
|
for_each: context.retro.suggestions
|
||||||
|
bind_as: s
|
||||||
|
condition: 'context.s.score > 0'
|
||||||
|
action:
|
||||||
|
task_template: context.s.title
|
||||||
|
description: context.s.recommendation
|
||||||
|
target_repo: context.s.repo
|
||||||
|
priority: context.s.priority
|
||||||
|
labels: ["coding-retro", "improvement", "automated"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Each suggestion carries `repo`, `title`, `recommendation`, `priority`, and
|
||||||
|
`score`. The upstream retro caps the list at three per repo, so this emits at most
|
||||||
|
three improvement tasks per relevant repository per week.
|
||||||
@@ -149,6 +149,26 @@ activity registration issues before the next scheduled run.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Weekly maintenance definitions
|
||||||
|
|
||||||
|
`weekly-sbom-staleness` is the canonical rule-only weekly maintenance schedule.
|
||||||
|
It runs Mondays at 09:00 Europe/Berlin, resolves State Hub SBOM status for all
|
||||||
|
repos, and emits one automated task per stale repo through explicit
|
||||||
|
`for_each: context.repos.repos`.
|
||||||
|
|
||||||
|
`weekly-coding-retro` follows the same cron -> context resolver -> per-repo task
|
||||||
|
pattern for coding-session retrospection. It runs Saturdays at 19:00
|
||||||
|
Europe/Berlin and resolves the latest State Hub `/progress/` item with
|
||||||
|
`event_type=coding_retro` into `context.retro.suggestions`. Each positive-score
|
||||||
|
suggestion emits one task to `context.s.repo` with labels
|
||||||
|
`coding-retro`, `improvement`, and `automated`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Temporal UI — filtering by activity
|
## Temporal UI — filtering by activity
|
||||||
|
|
||||||
With search attributes registered, you can filter in the Temporal Web UI:
|
With search attributes registered, you can filter in the Temporal Web UI:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Supported queries:
|
|||||||
- 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}/workstreams/workplan-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
|
||||||
|
- coding_retro: latest /progress/ item with event_type=coding_retro
|
||||||
- daily_triage_digest: curated scalar JSON digest for daily WSJF triage
|
- daily_triage_digest: curated scalar JSON digest for daily WSJF triage
|
||||||
- recently_on_scope_hourly: POST {STATE_HUB_URL}/recently-on-scope/hourly
|
- recently_on_scope_hourly: POST {STATE_HUB_URL}/recently-on-scope/hourly
|
||||||
|
|
||||||
@@ -94,6 +95,8 @@ class StateHubContextResolver(ContextResolver):
|
|||||||
"unread_only": params.get("unread_only", True),
|
"unread_only": params.get("unread_only", True),
|
||||||
}
|
}
|
||||||
return _fetch_json("/messages/", query_params)
|
return _fetch_json("/messages/", query_params)
|
||||||
|
if query == "coding_retro":
|
||||||
|
return _coding_retro(params)
|
||||||
if query == "daily_triage_digest":
|
if query == "daily_triage_digest":
|
||||||
return _daily_triage_digest(params)
|
return _daily_triage_digest(params)
|
||||||
if query == "recently_on_scope_hourly":
|
if query == "recently_on_scope_hourly":
|
||||||
@@ -206,6 +209,181 @@ def _sbom_age_days(last_sbom_at: Any) -> tuple[int, bool, str | None]:
|
|||||||
return max(0, delta.days), True, last_sbom_at
|
return max(0, delta.days), True, last_sbom_at
|
||||||
|
|
||||||
|
|
||||||
|
def _coding_retro(params: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Return the latest weekly coding-retro read model from State Hub progress.
|
||||||
|
|
||||||
|
Helix Forge publishes this as a `progress` item with event_type=coding_retro.
|
||||||
|
The resolver keeps the workflow-facing shape stable even before the first
|
||||||
|
publication exists, so rules can safely iterate over
|
||||||
|
`context.retro.suggestions`.
|
||||||
|
"""
|
||||||
|
event_type = str(params.get("event_type") or "coding_retro")
|
||||||
|
limit = _bounded_int(params.get("limit", 100), default=100, minimum=1, maximum=500)
|
||||||
|
items = _fetch_json("/progress/", {"limit": limit})
|
||||||
|
if not isinstance(items, list):
|
||||||
|
return _empty_coding_retro(event_type)
|
||||||
|
|
||||||
|
item = _latest_progress_item(items, event_type)
|
||||||
|
if item is None:
|
||||||
|
return _empty_coding_retro(event_type)
|
||||||
|
|
||||||
|
detail = _progress_detail(item)
|
||||||
|
return {
|
||||||
|
"suggestions": _normalise_coding_retro_suggestions(
|
||||||
|
detail.get("suggestions")
|
||||||
|
),
|
||||||
|
"window": _coding_retro_window(detail, params),
|
||||||
|
"generated_at": _string_or_none(
|
||||||
|
detail.get("generated_at") or item.get("created_at")
|
||||||
|
),
|
||||||
|
"source_progress_id": _string_or_none(item.get("id")),
|
||||||
|
"event_type": event_type,
|
||||||
|
"summary": _short_text(item.get("summary", ""), 200),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_coding_retro(event_type: str) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"suggestions": [],
|
||||||
|
"window": None,
|
||||||
|
"generated_at": None,
|
||||||
|
"source_progress_id": None,
|
||||||
|
"event_type": event_type,
|
||||||
|
"summary": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _latest_progress_item(
|
||||||
|
items: list[Any],
|
||||||
|
event_type: str,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
newest: dict[str, Any] | None = None
|
||||||
|
newest_key: tuple[datetime, int] | None = None
|
||||||
|
for index, item in enumerate(items):
|
||||||
|
if not isinstance(item, dict) or item.get("event_type") != event_type:
|
||||||
|
continue
|
||||||
|
key = (_parse_progress_timestamp(item.get("created_at")), index)
|
||||||
|
if newest_key is None or key > newest_key:
|
||||||
|
newest = item
|
||||||
|
newest_key = key
|
||||||
|
return newest
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_progress_timestamp(value: Any) -> datetime:
|
||||||
|
if not isinstance(value, str) or not value:
|
||||||
|
return datetime.min.replace(tzinfo=timezone.utc)
|
||||||
|
try:
|
||||||
|
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||||
|
except ValueError:
|
||||||
|
return datetime.min.replace(tzinfo=timezone.utc)
|
||||||
|
if parsed.tzinfo is None:
|
||||||
|
parsed = parsed.replace(tzinfo=timezone.utc)
|
||||||
|
return parsed.astimezone(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def _progress_detail(item: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
detail = item.get("detail")
|
||||||
|
if detail is None:
|
||||||
|
detail = item.get("details")
|
||||||
|
if isinstance(detail, str):
|
||||||
|
try:
|
||||||
|
detail = json.loads(detail)
|
||||||
|
except ValueError:
|
||||||
|
return {}
|
||||||
|
if isinstance(detail, dict):
|
||||||
|
return detail
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise_coding_retro_suggestions(value: Any) -> list[dict[str, Any]]:
|
||||||
|
if not isinstance(value, list):
|
||||||
|
return []
|
||||||
|
suggestions: list[dict[str, Any]] = []
|
||||||
|
for raw in value:
|
||||||
|
suggestion = _normalise_coding_retro_suggestion(raw)
|
||||||
|
if suggestion is not None:
|
||||||
|
suggestions.append(suggestion)
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise_coding_retro_suggestion(raw: Any) -> dict[str, Any] | None:
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return None
|
||||||
|
repo = _clean_scalar(
|
||||||
|
raw.get("repo") or raw.get("target_repo") or raw.get("repo_slug")
|
||||||
|
)
|
||||||
|
title = _clean_scalar(raw.get("title") or raw.get("summary"))
|
||||||
|
if not repo or not title:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"repo": repo,
|
||||||
|
"title": title,
|
||||||
|
"recommendation": _clean_scalar(
|
||||||
|
raw.get("recommendation") or raw.get("description") or raw.get("body")
|
||||||
|
),
|
||||||
|
"priority": _normalise_coding_retro_priority(raw.get("priority")),
|
||||||
|
"score": _normalise_score(raw.get("score")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _coding_retro_window(
|
||||||
|
detail: dict[str, Any],
|
||||||
|
params: dict[str, Any],
|
||||||
|
) -> Any:
|
||||||
|
window = detail.get("window")
|
||||||
|
if window is not None:
|
||||||
|
return window
|
||||||
|
derived = {
|
||||||
|
key: detail.get(key)
|
||||||
|
for key in ("window_start", "window_end", "since", "until")
|
||||||
|
if detail.get(key) is not None
|
||||||
|
}
|
||||||
|
if derived:
|
||||||
|
return derived
|
||||||
|
if params.get("window_days") is not None:
|
||||||
|
return {
|
||||||
|
"days": _bounded_int(
|
||||||
|
params.get("window_days"),
|
||||||
|
default=7,
|
||||||
|
minimum=1,
|
||||||
|
maximum=366,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise_coding_retro_priority(value: Any) -> str:
|
||||||
|
priority = str(value or "medium").strip().lower()
|
||||||
|
if priority in {"high", "medium", "low"}:
|
||||||
|
return priority
|
||||||
|
return "medium"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise_score(value: Any) -> float:
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _bounded_int(value: Any, *, default: int, minimum: int, maximum: int) -> int:
|
||||||
|
try:
|
||||||
|
number = int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
number = default
|
||||||
|
return max(minimum, min(maximum, number))
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_scalar(value: Any) -> str:
|
||||||
|
return " ".join(str(value or "").split())
|
||||||
|
|
||||||
|
|
||||||
|
def _string_or_none(value: Any) -> str | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
def _daily_triage_digest(params: dict[str, Any]) -> str:
|
def _daily_triage_digest(params: dict[str, Any]) -> str:
|
||||||
"""Return a compact JSON string safe to inject into an instruction prompt.
|
"""Return a compact JSON string safe to inject into an instruction prompt.
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from activity_core.rules.models import TaskRef, TaskSpec
|
|||||||
|
|
||||||
_DEFINITIONS_DIR = Path(__file__).parent.parent / "activity-definitions"
|
_DEFINITIONS_DIR = Path(__file__).parent.parent / "activity-definitions"
|
||||||
_SBOM_DEF_PATH = _DEFINITIONS_DIR / "weekly-sbom-staleness.md"
|
_SBOM_DEF_PATH = _DEFINITIONS_DIR / "weekly-sbom-staleness.md"
|
||||||
|
_CODING_RETRO_DEF_PATH = _DEFINITIONS_DIR / "weekly-coding-retro.md"
|
||||||
|
|
||||||
|
|
||||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
@@ -95,6 +96,69 @@ def test_sbom_definition_parses_correctly():
|
|||||||
assert defn.rules[0]["id"] == "flag-stale-sbom"
|
assert defn.rules[0]["id"] == "flag-stale-sbom"
|
||||||
|
|
||||||
|
|
||||||
|
def test_coding_retro_definition_parses_disabled_until_verified():
|
||||||
|
defn = parse_file(_CODING_RETRO_DEF_PATH)
|
||||||
|
|
||||||
|
assert defn.id == "weekly-coding-retro"
|
||||||
|
assert defn.enabled is False
|
||||||
|
assert defn.trigger_config["trigger_type"] == "cron"
|
||||||
|
assert defn.trigger_config["cron_expression"] == "0 19 * * 6"
|
||||||
|
assert defn.trigger_config["timezone"] == "Europe/Berlin"
|
||||||
|
assert defn.context_sources == [
|
||||||
|
{
|
||||||
|
"type": "state-hub",
|
||||||
|
"query": "coding_retro",
|
||||||
|
"params": {"window_days": 7, "limit": 100},
|
||||||
|
"bind_to": "context.retro",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert len(defn.rules) == 1
|
||||||
|
assert defn.rules[0]["id"] == "propose-weekly-improvements"
|
||||||
|
|
||||||
|
|
||||||
|
def test_coding_retro_rule_emits_one_task_per_positive_suggestion():
|
||||||
|
defn = parse_file(_CODING_RETRO_DEF_PATH)
|
||||||
|
rule = defn.rules[0]
|
||||||
|
context = {
|
||||||
|
"retro": {
|
||||||
|
"suggestions": [
|
||||||
|
{
|
||||||
|
"repo": "activity-core",
|
||||||
|
"title": "Harden coding retro smoke gates",
|
||||||
|
"recommendation": "Dry-run with fixture and live hub evidence.",
|
||||||
|
"priority": "high",
|
||||||
|
"score": 8.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"repo": "quiet-repo",
|
||||||
|
"title": "Do not emit zero-score suggestion",
|
||||||
|
"recommendation": "This should stay quiet.",
|
||||||
|
"priority": "low",
|
||||||
|
"score": 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
specs = expand_rule_actions([rule], _EmptyEvent(), context)
|
||||||
|
|
||||||
|
assert specs == [
|
||||||
|
{
|
||||||
|
"title": "Harden coding retro smoke gates",
|
||||||
|
"description": "Dry-run with fixture and live hub evidence.",
|
||||||
|
"target_repo": "activity-core",
|
||||||
|
"priority": "high",
|
||||||
|
"labels": ["coding-retro", "improvement", "automated"],
|
||||||
|
"due_in_days": None,
|
||||||
|
"source_type": "rule",
|
||||||
|
"source_id": "propose-weekly-improvements",
|
||||||
|
"triggering_event_id": "",
|
||||||
|
"activity_definition_id": "",
|
||||||
|
"condition": "context.s.score > 0",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_pipeline_emits_one_task_for_stale_repo_only():
|
def test_pipeline_emits_one_task_for_stale_repo_only():
|
||||||
"""Stale repo (45 days) matches; fresh repo (10 days) does not."""
|
"""Stale repo (45 days) matches; fresh repo (10 days) does not."""
|
||||||
defn = parse_file(_SBOM_DEF_PATH)
|
defn = parse_file(_SBOM_DEF_PATH)
|
||||||
|
|||||||
@@ -157,6 +157,128 @@ def test_repo_sbom_status_returns_empty_on_failure(monkeypatch) -> None:
|
|||||||
assert resolver.resolve("repo_sbom_status", None, {"repos": "all"}) == {}
|
assert resolver.resolve("repo_sbom_status", None, {"repos": "all"}) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_coding_retro_returns_latest_progress_suggestions(monkeypatch) -> None:
|
||||||
|
calls: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
def fake_get(url: str, **kwargs: Any) -> DummyResponse:
|
||||||
|
calls.append({"url": url, **kwargs})
|
||||||
|
return DummyResponse([
|
||||||
|
{
|
||||||
|
"id": "older-retro",
|
||||||
|
"event_type": "coding_retro",
|
||||||
|
"summary": "older",
|
||||||
|
"created_at": "2026-05-31T17:00:00Z",
|
||||||
|
"detail": {
|
||||||
|
"generated_at": "2026-05-31T17:00:00Z",
|
||||||
|
"suggestions": [
|
||||||
|
{
|
||||||
|
"repo": "old-repo",
|
||||||
|
"title": "Old recommendation",
|
||||||
|
"recommendation": "Do the older thing.",
|
||||||
|
"priority": "low",
|
||||||
|
"score": 1,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "note-1",
|
||||||
|
"event_type": "note",
|
||||||
|
"summary": "ignore me",
|
||||||
|
"created_at": "2026-06-07T17:05:00Z",
|
||||||
|
"detail": {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "newer-retro",
|
||||||
|
"event_type": "coding_retro",
|
||||||
|
"summary": "weekly coding retro ready",
|
||||||
|
"created_at": "2026-06-07T17:10:00Z",
|
||||||
|
"detail": {
|
||||||
|
"generated_at": "2026-06-07T17:09:30Z",
|
||||||
|
"window": {
|
||||||
|
"since": "2026-05-31T00:00:00Z",
|
||||||
|
"until": "2026-06-07T00:00:00Z",
|
||||||
|
},
|
||||||
|
"suggestions": [
|
||||||
|
{
|
||||||
|
"target_repo": "activity-core",
|
||||||
|
"title": "Harden schedule smoke gates",
|
||||||
|
"description": "Add a smoke proof before enablement.",
|
||||||
|
"priority": "HIGH",
|
||||||
|
"score": "8.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"repo_slug": "repo-without-title",
|
||||||
|
"recommendation": "missing title should be skipped",
|
||||||
|
"score": 9,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
monkeypatch.setenv("STATE_HUB_URL", "http://state-hub.test/")
|
||||||
|
monkeypatch.setattr(httpx, "get", fake_get)
|
||||||
|
|
||||||
|
result = StateHubContextResolver().resolve(
|
||||||
|
"coding_retro",
|
||||||
|
None,
|
||||||
|
{"limit": 20, "window_days": 7},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert calls == [
|
||||||
|
{
|
||||||
|
"url": "http://state-hub.test/progress/",
|
||||||
|
"params": {"limit": 20},
|
||||||
|
"timeout": 10.0,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert result["source_progress_id"] == "newer-retro"
|
||||||
|
assert result["generated_at"] == "2026-06-07T17:09:30Z"
|
||||||
|
assert result["window"] == {
|
||||||
|
"since": "2026-05-31T00:00:00Z",
|
||||||
|
"until": "2026-06-07T00:00:00Z",
|
||||||
|
}
|
||||||
|
assert result["summary"] == "weekly coding retro ready"
|
||||||
|
assert result["suggestions"] == [
|
||||||
|
{
|
||||||
|
"repo": "activity-core",
|
||||||
|
"title": "Harden schedule smoke gates",
|
||||||
|
"recommendation": "Add a smoke proof before enablement.",
|
||||||
|
"priority": "high",
|
||||||
|
"score": 8.5,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_coding_retro_returns_empty_shape_when_not_published(monkeypatch) -> None:
|
||||||
|
def fake_get(url: str, **kwargs: Any) -> DummyResponse:
|
||||||
|
return DummyResponse([
|
||||||
|
{
|
||||||
|
"id": "note-1",
|
||||||
|
"event_type": "note",
|
||||||
|
"created_at": "2026-06-07T17:10:00Z",
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
monkeypatch.setattr(httpx, "get", fake_get)
|
||||||
|
|
||||||
|
result = StateHubContextResolver().resolve(
|
||||||
|
"coding_retro",
|
||||||
|
None,
|
||||||
|
{"event_type": "coding_retro"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"suggestions": [],
|
||||||
|
"window": None,
|
||||||
|
"generated_at": None,
|
||||||
|
"source_progress_id": None,
|
||||||
|
"event_type": "coding_retro",
|
||||||
|
"summary": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_resolver_failure_returns_empty(monkeypatch) -> None:
|
def test_resolver_failure_returns_empty(monkeypatch) -> None:
|
||||||
def fake_get(url: str, **kwargs: Any) -> DummyResponse:
|
def fake_get(url: str, **kwargs: Any) -> DummyResponse:
|
||||||
raise httpx.ConnectError("offline")
|
raise httpx.ConnectError("offline")
|
||||||
|
|||||||
94
workplans/ACTIVITY-WP-0008-weekly-coding-retro.md
Normal file
94
workplans/ACTIVITY-WP-0008-weekly-coding-retro.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
id: ACTIVITY-WP-0008
|
||||||
|
type: workplan
|
||||||
|
title: "Weekly Coding Retrospection schedule (Saturday evenings)"
|
||||||
|
domain: custodian
|
||||||
|
repo: activity-core
|
||||||
|
status: blocked
|
||||||
|
owner: codex
|
||||||
|
topic_slug: custodian
|
||||||
|
created: "2026-06-07"
|
||||||
|
updated: "2026-06-07"
|
||||||
|
state_hub_workstream_id: "7387fc50-1f2c-471a-9d85-bb085cbd0b63"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Weekly Coding Retrospection schedule (Saturday evenings)
|
||||||
|
|
||||||
|
**Origin:** requested from the `helix_forge` domain. Every Saturday 19:00
|
||||||
|
Europe/Berlin, read the previous week's coding-session analysis (published to the
|
||||||
|
hub by helix_forge session-memory) and open **one improvement suggestion per
|
||||||
|
relevant repo — the three most promising**.
|
||||||
|
|
||||||
|
This is the same shape as the existing `weekly-sbom-staleness` activity-definition
|
||||||
|
(cron → context resolver → per-repo task emission); only the data source is new.
|
||||||
|
|
||||||
|
**Dependency:** `AGENTIC-WP-0010` (helix_forge) publishes the
|
||||||
|
`event_type=coding_retro` read model this schedule consumes. That side computes
|
||||||
|
and ranks (top-3 per repo, cross-flavor first, recommendations from the Pattern
|
||||||
|
Catalog); this side schedules and routes.
|
||||||
|
|
||||||
|
## `coding_retro` Context Resolver
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: ACTIVITY-WP-0008-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "26846304-f5f1-4edf-aba3-227c9b11c9fa"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a context resolver (`context_resolvers/`) returning the latest weekly
|
||||||
|
coding-retro published to the hub (`event_type=coding_retro`): its
|
||||||
|
`suggestions[]` (repo, title, recommendation, priority, score), window, and
|
||||||
|
`generated_at`. Bind under `context.retro`. Mirror the `repo_sbom_status` resolver
|
||||||
|
shape so rules can `for_each` over `context.retro.suggestions`.
|
||||||
|
|
||||||
|
**2026-06-07:** Implemented `query: coding_retro` in the State Hub context
|
||||||
|
resolver. It reads recent `/progress/` items, selects the latest
|
||||||
|
`event_type=coding_retro`, normalizes `suggestions[]`, and returns an empty
|
||||||
|
suggestion list while the upstream publisher has not produced a read model yet.
|
||||||
|
|
||||||
|
## `weekly-coding-retro` Activity-Definition
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: ACTIVITY-WP-0008-T02
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "09eeacb7-dc0d-4617-8398-a99a4e5a227e"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `activity-definitions/weekly-coding-retro.md`: cron `0 19 * * 6`
|
||||||
|
Europe/Berlin, `context_source` `coding_retro`, and a rule that `for_each` over
|
||||||
|
`context.retro.suggestions` emits one improvement task to `target_repo` with the
|
||||||
|
suggestion title + recommendation, priority, and labels
|
||||||
|
`[coding-retro, improvement, automated]`. Ship `enabled: false` until the resolver
|
||||||
|
+ publish are verified. A starter draft is provided at
|
||||||
|
`activity-definitions/weekly-coding-retro.md` (proposed by helix_forge).
|
||||||
|
|
||||||
|
**2026-06-07:** Updated the starter definition against the implemented resolver:
|
||||||
|
cron Saturday 19:00 Europe/Berlin, `context_source` `coding_retro` bound to
|
||||||
|
`context.retro`, and a rule that emits one positive-score suggestion per target
|
||||||
|
repo with the coding-retro/improvement/automated labels. It remains
|
||||||
|
`enabled: false` until live publish verification succeeds.
|
||||||
|
|
||||||
|
## Dry-Run Verify + Enable + Docs
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: ACTIVITY-WP-0008-T03
|
||||||
|
status: wait
|
||||||
|
priority: medium
|
||||||
|
state_hub_task_id: "9dcbebe7-13dd-4957-9a72-858418049aef"
|
||||||
|
```
|
||||||
|
|
||||||
|
Dry-run the definition end-to-end against a published `coding_retro` read model;
|
||||||
|
confirm one task per relevant repo (≤ 3) with correct routing and no duplicates on
|
||||||
|
re-run. Flip `enabled: true`. Document alongside `weekly-sbom-staleness`. After
|
||||||
|
workplan updates, run from `~/state-hub`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make fix-consistency REPO=activity-core
|
||||||
|
```
|
||||||
|
|
||||||
|
**2026-06-07:** Added fixture-level dry-run coverage and runbook documentation.
|
||||||
|
Live State Hub did not yet expose a published `event_type=coding_retro` progress
|
||||||
|
item, so the real dry-run, duplicate check, and `enabled: true` flip remain
|
||||||
|
blocked on `AGENTIC-WP-0010`.
|
||||||
Reference in New Issue
Block a user