diff --git a/src/activity_core/automation_status.py b/src/activity_core/automation_status.py index 7eb0121..116899a 100644 --- a/src/activity_core/automation_status.py +++ b/src/activity_core/automation_status.py @@ -150,6 +150,7 @@ def file_definitions() -> list[dict[str, Any]]: "trigger_type": trigger_type, "trigger_config": trigger, "instructions": list(definition.instructions or []), + "context_sources": list(definition.context_sources or []), "source": "files", }) return sorted(records, key=lambda item: item["name"]) @@ -166,7 +167,7 @@ def filter_definitions(definitions: list[dict[str, Any]], ids: list[str], names: ] -def progress_event_types(args: argparse.Namespace) -> list[str | None]: +def progress_event_types(args: argparse.Namespace, definitions: list[dict[str, Any]] | None = None) -> list[str | None]: raw = args.progress_event_type if raw is None: env_value = os.environ.get("AUTOMATION_STATUS_PROGRESS_EVENT_TYPES") @@ -175,11 +176,31 @@ def progress_event_types(args: argparse.Namespace) -> list[str | None]: "schedule_miss", "ops_inventory_probe", "legacy_meter_weekly_review", + *declared_progress_event_types(definitions or []), ] - values = [item.strip() for item in raw if item and item.strip()] + values = list(dict.fromkeys(item.strip() for item in raw if item and item.strip())) return [None if item == "all" else item for item in values] +def declared_progress_event_types(definitions: list[dict[str, Any]]) -> list[str]: + """Event types the definitions' State Hub report/evidence sinks write.""" + sinks: list[Any] = [] + for definition in definitions: + for instruction in definition.get("instructions") or []: + if isinstance(instruction, dict): + sinks.extend(instruction.get("report_sinks") or []) + for source in definition.get("context_sources") or []: + params = source.get("params") if isinstance(source, dict) else None + if isinstance(params, dict): + raw = params.get("evidence_sinks") or params.get("evidence_sink") or [] + sinks.extend(raw if isinstance(raw, list) else [raw]) + found: list[str] = [] + for sink in sinks: + if isinstance(sink, dict) and sink.get("type") == "state-hub-progress" and sink.get("event_type"): + found.append(str(sink["event_type"])) + return list(dict.fromkeys(found)) + + def expected_fires(definition: dict[str, Any], window: dict[str, Any]) -> list[str]: cfg = definition.get("trigger_config") or {} if definition.get("trigger_type") == "scheduled": @@ -252,7 +273,7 @@ async def db_definitions(db_url: str) -> list[dict[str, Any]]: try: async with engine.connect() as conn: result = await conn.execute(text( - "select id, name, enabled, trigger_type, trigger_config, instructions_json, version " + "select id, name, enabled, trigger_type, trigger_config, instructions_json, context_sources, version " "from activity_definitions where trigger_type in ('cron', 'scheduled') order by name" )) return [{ @@ -262,6 +283,7 @@ async def db_definitions(db_url: str) -> list[dict[str, Any]]: "trigger_type": row["trigger_type"], "trigger_config": dict(row["trigger_config"] or {}), "instructions": list(row["instructions_json"] or []), + "context_sources": list(row["context_sources"] or []), "version": row["version"], "source": "db", } for row in result.mappings().all()] @@ -544,7 +566,7 @@ async def build_report(args: argparse.Namespace) -> tuple[dict[str, Any], int]: window, limit=args.progress_limit, timeout_seconds=timeout, - event_types=progress_event_types(args), + event_types=progress_event_types(args, definitions), ) wm_evidence, sources["working_memory"] = load_working_memory_evidence(args.working_memory_dir, window) temporal_by_activity, sources["temporal"] = await load_temporal_visibility( diff --git a/tests/test_automation_status.py b/tests/test_automation_status.py index ea9679e..c81cac2 100644 --- a/tests/test_automation_status.py +++ b/tests/test_automation_status.py @@ -323,3 +323,38 @@ def test_inventory_cli_emits_json(monkeypatch, capsys) -> None: assert exit_code == 0 assert payload["mode"] == "automation-inventory" assert payload["automations"][0]["name"] == "Daily Check" + + +def test_default_progress_event_types_include_declared_sinks() -> None: + definitions = [ + { + "instructions": [{"report_sinks": [ + {"type": "state-hub-progress", "event_type": "sbom_catchup"}, + {"type": "working-memory"}, + ]}], + "context_sources": [ + {"type": "shell", "params": {"evidence_sinks": [ + {"type": "state-hub-progress", "event_type": "forgejo_package_prune"}, + ]}}, + {"type": "shell", "params": {"evidence_sinks": [ + {"type": "state-hub-progress", "event_type": "daily_triage"}, + ]}}, + ], + }, + ] + args = status.parse_args([]) + + event_types = status.progress_event_types(args, definitions) + + assert "forgejo_package_prune" in event_types + assert "sbom_catchup" in event_types + assert event_types.count("daily_triage") == 1 + + +def test_explicit_progress_event_types_ignore_declared_sinks() -> None: + definitions = [{"context_sources": [{"params": {"evidence_sinks": [ + {"type": "state-hub-progress", "event_type": "forgejo_package_prune"}, + ]}}]}] + args = status.parse_args(["--progress-event-type", "daily_triage"]) + + assert status.progress_event_types(args, definitions) == ["daily_triage"] diff --git a/workplans/ACTIVITY-WP-0035-intent-boundary-guardrails.md b/workplans/ACTIVITY-WP-0035-intent-boundary-guardrails.md index 710c162..63d9570 100644 --- a/workplans/ACTIVITY-WP-0035-intent-boundary-guardrails.md +++ b/workplans/ACTIVITY-WP-0035-intent-boundary-guardrails.md @@ -4,13 +4,13 @@ type: workplan title: "Make the execution boundary enforceable and the review contract truthful" domain: infotech repo: activity-core -status: active +status: finished flavor: planning owner: codex topic_slug: activity-core priority: high created: "2026-08-23" -updated: "2026-09-04" +updated: "2026-09-21" quality_dor: DoR-Ok quality_dor_at: "2026-08-23" quality_dor_by: codex @@ -271,7 +271,7 @@ activity-core is a general task executor. ```task id: ACTIVITY-WP-0035-T08 -status: progress +status: done priority: high state_hub_task_id: "2ae1aca9-1c86-5b1a-b9fd-95059a24cd65" ``` @@ -298,7 +298,7 @@ Depends on T02–T07 as applicable. - [x] Instruction audit wording matches persisted evidence and raw sensitive payloads remain excluded - [x] `review_required` has truthful, tested hold/release or advisory-only semantics - [x] Misleading executor/CRUD/README surfaces are removed or precisely qualified -- [ ] Full tests pass and production rollout preserves current schedules and evidence +- [x] Full tests pass and production rollout preserves current schedules and evidence ## Implementation evidence — 2026-08-23 @@ -363,6 +363,28 @@ Depends on T02–T07 as applicable. lifecycle. That residual must preserve multi-cluster over-protection and the existing apply refusal when the inventory is absent or empty. +## Closeout — 2026-09-21 + +- The third bounded-operation class now has natural post-rollout evidence. + Weekly Forgejo Package Prune fired unassisted on 2026-09-06, 09-13, and + 09-20 against the durable inventory path, each with `apply: true`, + `error_count=0`, and protected skips honoured: + + | Run | Deleted | Protected skips | State Hub progress | + | --- | --- | --- | --- | + | `65354265-6425-51fb-be5f-20bfae99f550` | 175 | 4 | 2026-09-06T03:30:16Z | + | `d0ce2658-0bab-5fe3-9377-36d5c56193a4` | 85 | 5 | 2026-09-13T03:30:14Z | + | `fac35de6-095d-59ec-8af0-f8a15440d3b4` | 52 | 5 | `c5e538a7-e2a6-49df-9da5-1aef315ee77e` | + + No schedule was manually fired. CNPG backup and SBOM catch-up remained + `completed` for every expected fire since 2026-09-05. +- Residual found during closeout: the deterministic status surface reported + `evidence=0` for these runs because its default State Hub query covered only + four fixed event types. Source now also queries every `state-hub-progress` + event type declared by definition report/evidence sinks (504 passed, + 1 skipped). It takes effect in production at the next image rollout; the + evidence above was read directly from State Hub. + ## Gap disposition | 2026-08-23 gap | This workplan |