ACTIVITY-WP-0044: disabled repo-manager-bounded-freshness draft (RMGR-WP-0020-T03)
Register bounded operation repo_manager_freshness (single attempt, 300 s, code-fixed 'rmgr freshness run --limit N --max-seconds S [--dry-run]') and add the disabled cron definition (40 */2 * * * UTC, limit 6, max_seconds 240). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 692292@bnt-lap001 Assistant-Session: 89c7d8e8-e1b3-47eb-9411-a0ab227fca2f
This commit is contained in:
parent
c6df768ef0
commit
ba4ef00128
9 changed files with 449 additions and 2 deletions
63
activity-definitions/repo-manager-bounded-freshness.md
Normal file
63
activity-definitions/repo-manager-bounded-freshness.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
id: repo-manager-bounded-freshness
|
||||
name: Repo Manager Bounded Freshness
|
||||
type: activity-definition
|
||||
version: "0.1"
|
||||
enabled: false
|
||||
owner: repo-manager
|
||||
governance: custodian
|
||||
status: proposed
|
||||
trigger:
|
||||
type: cron
|
||||
cron_expression: "40 */2 * * *"
|
||||
timezone: UTC
|
||||
misfire_policy: skip
|
||||
context_sources:
|
||||
- type: repo-manager
|
||||
query: freshness_run
|
||||
operation: repo_manager_freshness
|
||||
required: true
|
||||
params:
|
||||
limit: 6
|
||||
max_seconds: 240
|
||||
apply: true
|
||||
bind_to: context.freshness
|
||||
---
|
||||
|
||||
# Repo Manager Bounded Freshness
|
||||
|
||||
Draft from RMGR-WP-0020-T03, tracked in ACTIVITY-WP-0044. **Not activated.**
|
||||
`enabled: false` prevents any Temporal schedule; the founder flips it only after
|
||||
the RMGR-WP-0020-T04 activation steps and T05 proof.
|
||||
|
||||
Each fire runs one bounded operation `repo_manager_freshness`
|
||||
(ACT-ADR-007 registry), which invokes the Repo Manager CLI with a code-fixed argv:
|
||||
|
||||
```text
|
||||
rmgr freshness run --limit 6 --max-seconds 240
|
||||
```
|
||||
|
||||
(`--dry-run` is appended when `apply: false`.) Repo Manager owns repository
|
||||
selection, Forge HEAD resolution, reconciliation from immutable Forge commits
|
||||
and the durable `repo-manager.freshness-receipt.v1` receipts. Activity Core
|
||||
persists only a counted projection (receipt count per outcome, exit code).
|
||||
|
||||
- Every 2 h at :40 UTC; 141 repositories / 6 per fire x 12 fires/day is about a
|
||||
two-day rotation. Target SLA once enabled: every admissible repository has a
|
||||
receipt younger than 48 h.
|
||||
- Single Temporal attempt, timeout 300 s (240 s CLI budget + grace); misfires
|
||||
are skipped, never replayed.
|
||||
- No workstation checkout reads (ADR-002), no legacy `--all --remote` sweep, no
|
||||
`for_each` task spawning; the rule below never emits tasks.
|
||||
|
||||
Rollback: set `enabled: false` (or `status: paused`).
|
||||
|
||||
```rule
|
||||
id: repo-manager-bounded-freshness-no-tasks
|
||||
condition: "False"
|
||||
action:
|
||||
task_template: Repo Manager freshness (operation-only; never rule-emitted)
|
||||
description: The bounded operation does the work; this rule never emits tasks.
|
||||
priority: low
|
||||
labels: ["automated", "freshness"]
|
||||
```
|
||||
|
|
@ -25,7 +25,8 @@ schedule, retention action, or production rollout follows from role acceptance.
|
|||
Scaleway physical recovery or Forgejo archive recovery evidence. Its old
|
||||
host kubeconfig names and RPO wording are not proof of current cluster
|
||||
routing or an accepted guarantee; review those before reusing the wiring.
|
||||
- `src/activity_core/bounded_operations.py` permits exactly three operations.
|
||||
- `src/activity_core/bounded_operations.py` permits exactly four operations
|
||||
(the fourth, `repo_manager_freshness`, backs a disabled draft; ACTIVITY-WP-0044).
|
||||
CNPG backup has one Temporal attempt because no durable remote idempotency
|
||||
receipt is available. New restore dispatch must not inherit retry defaults.
|
||||
- The stale-token blocker was resolved under RPF-WP-0045. Read-only inspection
|
||||
|
|
|
|||
|
|
@ -254,6 +254,10 @@ async def execute_bounded_operation(payload: dict[str, Any]) -> dict[str, Any]:
|
|||
from activity_core.context_resolvers.cnpg_backup import cnpg_option_a_backup
|
||||
|
||||
result = cnpg_option_a_backup(params)
|
||||
elif spec.operation_id == "repo_manager_freshness":
|
||||
from activity_core.context_resolvers.repo_manager import repo_manager_freshness
|
||||
|
||||
result = repo_manager_freshness(params)
|
||||
else:
|
||||
raise ApplicationError(
|
||||
f"bounded operation {spec.operation_id!r} uses a dedicated activity",
|
||||
|
|
|
|||
|
|
@ -60,6 +60,19 @@ _SPECS = (
|
|||
credential_route="railiance-cnpg-option-a-backup",
|
||||
evidence_mode="context-evidence-sink",
|
||||
),
|
||||
# RMGR-WP-0020-T03 / ACTIVITY-WP-0044: Repo Manager owns the freshness
|
||||
# engine; Activity Core only invokes its fixed CLI with bounded arguments.
|
||||
BoundedOperationSpec(
|
||||
operation_id="repo_manager_freshness",
|
||||
source_type="repo-manager",
|
||||
query="freshness_run",
|
||||
resolve_before_execute=False,
|
||||
max_timeout_seconds=300,
|
||||
temporal_max_attempts=1,
|
||||
idempotency="single Temporal attempt; per-commit projections are idempotent (RMGR-WP-0017)",
|
||||
credential_route="repo-manager-freshness-runner",
|
||||
evidence_mode="repo-manager-freshness-receipt",
|
||||
),
|
||||
)
|
||||
|
||||
BOUNDED_OPERATION_REGISTRY = {spec.operation_id: spec for spec in _SPECS}
|
||||
|
|
@ -80,6 +93,12 @@ _CANONICAL_SCRIPT_PATHS = {
|
|||
"forgejo_package_prune": "/opt/railiance-platform/tools/cmd/forgejo-package-prune",
|
||||
"cnpg_option_a_backup": "/opt/railiance-platform/tools/cmd/cnpg-option-a-backup",
|
||||
}
|
||||
# repo-manager.freshness-receipt.v1 outcomes (RMGR-WP-0020-T01).
|
||||
REPO_MANAGER_FRESHNESS_OUTCOMES = (
|
||||
"applied", "noop", "queued", "failed", "refused", "skipped",
|
||||
)
|
||||
REPO_MANAGER_FRESHNESS_MAX_LIMIT = 12
|
||||
REPO_MANAGER_FRESHNESS_MAX_SECONDS = 270
|
||||
_TARGET_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
||||
|
||||
|
||||
|
|
@ -136,6 +155,26 @@ def normalize_operation_result(operation_id: str, raw: Any) -> dict[str, Any]:
|
|||
"failed": _safe_count(result.get("failed")),
|
||||
"script_exit_code": exit_code,
|
||||
}
|
||||
if operation_id == "repo_manager_freshness":
|
||||
receipts = result.get("receipts")
|
||||
outcomes = {name: 0 for name in REPO_MANAGER_FRESHNESS_OUTCOMES}
|
||||
if isinstance(receipts, list):
|
||||
for receipt in receipts:
|
||||
outcome = receipt.get("outcome") if isinstance(receipt, dict) else None
|
||||
if outcome in outcomes:
|
||||
outcomes[outcome] += 1
|
||||
exit_code = result.get("exit_code", 0)
|
||||
if isinstance(exit_code, bool) or not isinstance(exit_code, int):
|
||||
exit_code = 1
|
||||
return {
|
||||
"kind": operation_id,
|
||||
"apply": bool(result.get("apply")),
|
||||
"limit": _safe_count(result.get("limit")),
|
||||
"max_seconds": _safe_count(result.get("max_seconds")),
|
||||
"receipt_count": len(receipts) if isinstance(receipts, list) else 0,
|
||||
"outcomes": outcomes,
|
||||
"exit_code": exit_code,
|
||||
}
|
||||
raise ValueError(f"no result projection for bounded operation {operation_id!r}")
|
||||
|
||||
|
||||
|
|
@ -192,6 +231,8 @@ def validate_bounded_operations(
|
|||
_validate_prune(params)
|
||||
elif spec.operation_id == "cnpg_option_a_backup":
|
||||
_validate_backup(params)
|
||||
elif spec.operation_id == "repo_manager_freshness":
|
||||
_validate_repo_manager_freshness(params)
|
||||
|
||||
|
||||
def _validate_timeout(spec: BoundedOperationSpec, params: dict[str, Any]) -> None:
|
||||
|
|
@ -259,6 +300,36 @@ def _validate_backup(params: dict[str, Any]) -> None:
|
|||
_require_context_evidence(params, "cnpg_option_a_backup")
|
||||
|
||||
|
||||
def _validate_repo_manager_freshness(params: dict[str, Any]) -> None:
|
||||
_explicit_bool(params, "apply", "repo_manager_freshness")
|
||||
limit = params.get("limit")
|
||||
if (
|
||||
isinstance(limit, bool)
|
||||
or not isinstance(limit, int)
|
||||
or not 1 <= limit <= REPO_MANAGER_FRESHNESS_MAX_LIMIT
|
||||
):
|
||||
raise ValueError(
|
||||
"bounded operation repo_manager_freshness limit must be in "
|
||||
f"1..{REPO_MANAGER_FRESHNESS_MAX_LIMIT}"
|
||||
)
|
||||
max_seconds = params.get("max_seconds")
|
||||
if (
|
||||
isinstance(max_seconds, bool)
|
||||
or not isinstance(max_seconds, int)
|
||||
or not 1 <= max_seconds <= REPO_MANAGER_FRESHNESS_MAX_SECONDS
|
||||
):
|
||||
raise ValueError(
|
||||
"bounded operation repo_manager_freshness max_seconds must be in "
|
||||
f"1..{REPO_MANAGER_FRESHNESS_MAX_SECONDS}"
|
||||
)
|
||||
for forbidden in ("command", "argv", "rmgr_bin", "script"):
|
||||
if forbidden in params:
|
||||
raise ValueError(
|
||||
f"bounded operation repo_manager_freshness must not declare {forbidden}; "
|
||||
"the CLI invocation is code-fixed"
|
||||
)
|
||||
|
||||
|
||||
def _validate_canonical_script(
|
||||
params: dict[str, Any], field: str, operation_id: str
|
||||
) -> None:
|
||||
|
|
|
|||
63
src/activity_core/context_resolvers/repo_manager.py
Normal file
63
src/activity_core/context_resolvers/repo_manager.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""Repo Manager bounded freshness invocation (RMGR-WP-0020-T03, ACTIVITY-WP-0044).
|
||||
|
||||
Invokes the Repo Manager CLI ``rmgr freshness run --limit N --max-seconds S
|
||||
[--dry-run]`` (RMGR-WP-0020-T02 interface). The argv is code-fixed: a
|
||||
definition supplies only the validated ``limit``, ``max_seconds`` and ``apply``
|
||||
values. The executable is ``rmgr`` on PATH unless the worker sets
|
||||
``ACTIVITY_CORE_RMGR_BIN``; definitions cannot choose it. Stdout is expected to
|
||||
be a JSON object carrying ``receipts`` (``repo-manager.freshness-receipt.v1``);
|
||||
only the counted projection in ``bounded_operations`` is persisted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
# Grace on top of the CLI's own --max-seconds budget for start-up/teardown.
|
||||
_TIMEOUT_GRACE_SECONDS = 30
|
||||
|
||||
|
||||
def build_freshness_argv(params: dict[str, Any]) -> list[str]:
|
||||
limit = int(params["limit"])
|
||||
max_seconds = int(params["max_seconds"])
|
||||
argv = [
|
||||
os.environ.get("ACTIVITY_CORE_RMGR_BIN", "rmgr"),
|
||||
"freshness",
|
||||
"run",
|
||||
"--limit",
|
||||
str(limit),
|
||||
"--max-seconds",
|
||||
str(max_seconds),
|
||||
]
|
||||
if params.get("apply") is not True:
|
||||
argv.append("--dry-run")
|
||||
return argv
|
||||
|
||||
|
||||
def repo_manager_freshness(params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Run one bounded freshness pass and return its JSON summary."""
|
||||
argv = build_freshness_argv(params)
|
||||
timeout = int(params["max_seconds"]) + _TIMEOUT_GRACE_SECONDS
|
||||
completed = subprocess.run(
|
||||
argv, capture_output=True, text=True, timeout=timeout, check=False
|
||||
)
|
||||
stdout = (completed.stdout or "").strip()
|
||||
if completed.returncode != 0 and not stdout:
|
||||
raise RuntimeError(
|
||||
f"repo_manager_freshness failed (exit {completed.returncode}): "
|
||||
f"{(completed.stderr or '').strip()[:300]}"
|
||||
)
|
||||
try:
|
||||
summary = json.loads(stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError(f"repo_manager_freshness returned invalid JSON: {exc}") from exc
|
||||
if not isinstance(summary, dict):
|
||||
raise RuntimeError("repo_manager_freshness output must be a JSON object")
|
||||
summary.setdefault("apply", params.get("apply") is True)
|
||||
summary.setdefault("limit", int(params["limit"]))
|
||||
summary.setdefault("max_seconds", int(params["max_seconds"]))
|
||||
summary["exit_code"] = completed.returncode
|
||||
return summary
|
||||
|
|
@ -40,6 +40,7 @@ _AUDIT_BUFFER: deque[dict[str, Any]] = deque(maxlen=100)
|
|||
SIDE_EFFECT_MARKERS = (
|
||||
"forgejo_package_prune",
|
||||
"cnpg_option_a_backup",
|
||||
"repo_manager_freshness",
|
||||
"apply: true",
|
||||
'"apply": true',
|
||||
"'apply': true",
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@ def _evidence_sinks(event_type: str) -> list[dict[str, str]]:
|
|||
return [{"type": "state-hub-progress", "event_type": event_type}]
|
||||
|
||||
|
||||
def test_registry_names_only_the_three_accepted_operations() -> None:
|
||||
def test_registry_names_only_the_accepted_operations() -> None:
|
||||
assert set(BOUNDED_OPERATION_REGISTRY) == {
|
||||
"sbom_nexus_ingest",
|
||||
"forgejo_package_prune",
|
||||
"cnpg_option_a_backup",
|
||||
# ACTIVITY-WP-0044 / RMGR-WP-0020-T03 (draft definition, disabled)
|
||||
"repo_manager_freshness",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
170
tests/test_repo_manager_freshness_definition.py
Normal file
170
tests/test_repo_manager_freshness_definition.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
"""Draft repo-manager-bounded-freshness definition (ACTIVITY-WP-0044 / RMGR-WP-0020-T03)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from activity_core import activities, sync_activity_definitions, sync_schedules
|
||||
from activity_core.bounded_operations import (
|
||||
BOUNDED_OPERATION_REGISTRY,
|
||||
normalize_operation_result,
|
||||
operation_sources,
|
||||
validate_bounded_operations,
|
||||
)
|
||||
from activity_core.context_resolvers import repo_manager
|
||||
from activity_core.definition_parser import parse_file
|
||||
|
||||
DEF_PATH = (
|
||||
Path(__file__).parent.parent / "activity-definitions" / "repo-manager-bounded-freshness.md"
|
||||
)
|
||||
|
||||
|
||||
def _source(**params):
|
||||
base = {"limit": 6, "max_seconds": 240, "apply": True}
|
||||
base.update(params)
|
||||
return {
|
||||
"type": "repo-manager",
|
||||
"query": "freshness_run",
|
||||
"operation": "repo_manager_freshness",
|
||||
"required": True,
|
||||
"params": base,
|
||||
"bind_to": "context.freshness",
|
||||
}
|
||||
|
||||
|
||||
def test_definition_parses_disabled_with_reviewed_shape():
|
||||
d = parse_file(DEF_PATH)
|
||||
assert d.id == "repo-manager-bounded-freshness"
|
||||
assert d.enabled is False
|
||||
tc = d.trigger_config
|
||||
assert tc["trigger_type"] == "cron"
|
||||
assert tc["cron_expression"] == "40 */2 * * *"
|
||||
assert tc["timezone"] == "UTC"
|
||||
assert tc["misfire_policy"] == "skip"
|
||||
[(source, spec)] = operation_sources(d.context_sources)
|
||||
assert spec.operation_id == "repo_manager_freshness"
|
||||
assert source["params"] == {"limit": 6, "max_seconds": 240, "apply": True}
|
||||
assert all(rule["condition"] == "False" for rule in d.rules)
|
||||
|
||||
|
||||
def test_operation_is_registered_with_single_attempt_and_timeout():
|
||||
spec = BOUNDED_OPERATION_REGISTRY["repo_manager_freshness"]
|
||||
assert spec.temporal_max_attempts == 1
|
||||
assert spec.resolve_before_execute is False
|
||||
assert spec.max_timeout_seconds >= 240 + repo_manager._TIMEOUT_GRACE_SECONDS
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params,error",
|
||||
[
|
||||
({"limit": 0}, "limit"),
|
||||
({"limit": 13}, "limit"),
|
||||
({"limit": True}, "limit"),
|
||||
({"max_seconds": 271}, "max_seconds"),
|
||||
({"max_seconds": "240"}, "max_seconds"),
|
||||
({"apply": "yes"}, "apply"),
|
||||
({"command": "rm -rf /"}, "code-fixed"),
|
||||
({"rmgr_bin": "/tmp/x"}, "code-fixed"),
|
||||
],
|
||||
)
|
||||
def test_admission_fails_closed(params, error):
|
||||
with pytest.raises(ValueError, match=error):
|
||||
validate_bounded_operations([_source(**params)], [])
|
||||
|
||||
|
||||
def test_argv_is_code_fixed(monkeypatch):
|
||||
monkeypatch.delenv("ACTIVITY_CORE_RMGR_BIN", raising=False)
|
||||
assert repo_manager.build_freshness_argv({"limit": 6, "max_seconds": 240, "apply": True}) == [
|
||||
"rmgr", "freshness", "run", "--limit", "6", "--max-seconds", "240",
|
||||
]
|
||||
assert repo_manager.build_freshness_argv(
|
||||
{"limit": 3, "max_seconds": 60, "apply": False}
|
||||
)[-1] == "--dry-run"
|
||||
|
||||
|
||||
def test_invocation_projects_counts_only(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_run(argv, **kwargs):
|
||||
calls.append((argv, kwargs["timeout"]))
|
||||
out = {"receipts": [
|
||||
{"repo": "a", "outcome": "applied", "detail": "must not persist"},
|
||||
{"repo": "b", "outcome": "noop"},
|
||||
{"repo": "c", "outcome": "refused"},
|
||||
{"repo": "d", "outcome": "bogus"},
|
||||
]}
|
||||
return subprocess.CompletedProcess(argv, 0, json.dumps(out), "")
|
||||
|
||||
monkeypatch.setattr(repo_manager.subprocess, "run", fake_run)
|
||||
raw = repo_manager.repo_manager_freshness({"limit": 6, "max_seconds": 240, "apply": True})
|
||||
projected = normalize_operation_result("repo_manager_freshness", raw)
|
||||
assert calls[0][1] == 240 + repo_manager._TIMEOUT_GRACE_SECONDS
|
||||
assert projected["receipt_count"] == 4
|
||||
assert projected["outcomes"]["applied"] == 1
|
||||
assert projected["outcomes"]["noop"] == 1
|
||||
assert projected["outcomes"]["refused"] == 1
|
||||
assert projected["apply"] is True and projected["exit_code"] == 0
|
||||
assert "must not persist" not in json.dumps(projected)
|
||||
|
||||
|
||||
def test_invocation_fails_on_non_json(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
repo_manager.subprocess, "run",
|
||||
lambda argv, **kw: subprocess.CompletedProcess(argv, 2, "", "boom"),
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="exit 2"):
|
||||
repo_manager.repo_manager_freshness({"limit": 1, "max_seconds": 10, "apply": False})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_bounded_operation_dispatches_to_repo_manager(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
repo_manager, "repo_manager_freshness",
|
||||
lambda params: {"receipts": [], "apply": False, "limit": 1, "max_seconds": 10},
|
||||
)
|
||||
patch = await activities.execute_bounded_operation({"source": _source(apply=False)})
|
||||
assert patch["freshness"]["kind"] == "repo_manager_freshness"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_upserts_disabled_row_and_paused_schedule(monkeypatch):
|
||||
d = parse_file(DEF_PATH)
|
||||
monkeypatch.setattr(sync_activity_definitions, "scan_and_parse", lambda: [d])
|
||||
statements = []
|
||||
session = MagicMock()
|
||||
session.execute = AsyncMock(side_effect=lambda stmt: statements.append(stmt))
|
||||
begin = MagicMock()
|
||||
begin.__aenter__ = AsyncMock(return_value=None)
|
||||
begin.__aexit__ = AsyncMock(return_value=False)
|
||||
session.begin = MagicMock(return_value=begin)
|
||||
ctx = MagicMock()
|
||||
ctx.__aenter__ = AsyncMock(return_value=session)
|
||||
ctx.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
assert await sync_activity_definitions.sync(lambda: ctx) == 1
|
||||
params = statements[0].compile(dialect=postgresql.dialect()).params
|
||||
assert params["enabled"] is False
|
||||
assert params["id"] == sync_activity_definitions._definition_uuid(d.id)
|
||||
|
||||
upserted = []
|
||||
|
||||
async def fake_upsert(client, defn):
|
||||
upserted.append(defn)
|
||||
|
||||
monkeypatch.setattr(sync_schedules, "upsert_schedule", fake_upsert)
|
||||
monkeypatch.setattr(sync_schedules, "list_schedules", AsyncMock(return_value=[]))
|
||||
row = SimpleNamespace(
|
||||
id=params["id"], name=d.name, enabled=params["enabled"],
|
||||
trigger_config=d.trigger_config, context_sources=d.context_sources,
|
||||
task_templates=[], dedupe_key_strategy="skip", version=1,
|
||||
)
|
||||
result = await sync_schedules.sync_schedule_rows(object(), [row])
|
||||
assert result.upserted == 0 and result.paused == 1
|
||||
assert upserted[0].enabled is False
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
id: ACTIVITY-WP-0044
|
||||
type: workplan
|
||||
title: "Repo Manager bounded freshness ActivityDefinition (RMGR-WP-0020-T03)"
|
||||
domain: infotech
|
||||
repo: activity-core
|
||||
status: active
|
||||
owner: claude-activity-core
|
||||
topic_slug: activity-core
|
||||
created: "2026-10-04"
|
||||
updated: "2026-10-04"
|
||||
flavor: implementation
|
||||
related: [RMGR-WP-0020, STATE-WP-0094]
|
||||
---
|
||||
|
||||
Origin: Repo Manager RMGR-WP-0020-T03 hands Activity Core a draft definition
|
||||
for the bounded Forge-sourced freshness runner. Repo Manager owns the engine
|
||||
(`rmgr freshness run`, RMGR-WP-0020-T02) and receipts (T01); Activity Core owns
|
||||
the schedule. Source level only: no schedule enabled, no deployment, no
|
||||
credential handling, no run dispatched.
|
||||
|
||||
## Draft definition and bounded operation (disabled)
|
||||
|
||||
```task
|
||||
id: ACTIVITY-WP-0044-T01
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
- `activity-definitions/repo-manager-bounded-freshness.md`: `enabled: false`,
|
||||
cron `40 */2 * * *` UTC, `misfire_policy: skip`, `limit: 6`,
|
||||
`max_seconds: 240`, `apply: true`; rule condition `False` (no task spawning).
|
||||
- ACT-ADR-007 registry entry `repo_manager_freshness` (source `repo-manager` /
|
||||
`freshness_run`): single Temporal attempt, timeout 300 s, admission requires
|
||||
explicit boolean `apply`, `limit` 1..12, `max_seconds` 1..270, and refuses
|
||||
any definition-supplied command/binary.
|
||||
- `context_resolvers/repo_manager.py`: code-fixed argv
|
||||
`rmgr freshness run --limit N --max-seconds S [--dry-run]` (executable from
|
||||
worker env `ACTIVITY_CORE_RMGR_BIN`, default `rmgr`); only a counted
|
||||
projection (receipt count per outcome, exit code) is persisted.
|
||||
- Tests: `tests/test_repo_manager_freshness_definition.py` (parse, admission,
|
||||
argv, projection, dispatch, DB sync stays disabled, schedule sync paused).
|
||||
|
||||
## Interface confirmation against the Repo Manager CLI
|
||||
|
||||
```task
|
||||
id: ACTIVITY-WP-0044-T02
|
||||
status: wait
|
||||
priority: medium
|
||||
needs_human: false
|
||||
blocking_reason: "rmgr freshness run is being built in RMGR-WP-0020-T02; JSON output (receipts[].outcome) assumed from the T01 receipt contract"
|
||||
depends_on: [RMGR-WP-0020-T01, RMGR-WP-0020-T02]
|
||||
```
|
||||
|
||||
When RMGR-WP-0020-T01/T02 land, confirm the CLI flags and that stdout is one
|
||||
JSON object with `receipts[]` carrying `repo-manager.freshness-receipt.v1`
|
||||
outcomes; adjust `normalize_operation_result` if the shape differs. Decide the
|
||||
worker image/packaging of `rmgr` (no deployment in this workplan).
|
||||
|
||||
## Founder-reviewed activation
|
||||
|
||||
```task
|
||||
id: ACTIVITY-WP-0044-T03
|
||||
status: wait
|
||||
priority: medium
|
||||
needs_human: true
|
||||
blocking_reason: "Activation follows RMGR-WP-0020-T04 and is a founder action"
|
||||
depends_on: [RMGR-WP-0020-T04]
|
||||
```
|
||||
|
||||
Flip `enabled: true` only after the RMGR-WP-0020-T04 manual dry-run/apply fires.
|
||||
Rollback: `enabled: false` (or `status: paused`).
|
||||
Loading…
Add table
Add a link
Reference in a new issue