generated from coulomb/repo-seed
Add automation inventory surface
This commit is contained in:
12
Makefile
12
Makefile
@@ -2,7 +2,7 @@
|
||||
export
|
||||
|
||||
.PHONY: sync-event-types sync-activity-definitions sync-schedules test migrate sync-all \
|
||||
automation-status automation-status-json \
|
||||
automation-status automation-status-json automation-list automation-list-json \
|
||||
dev-up dev-down railiance-up railiance-down \
|
||||
start-worker start-api start-event-router help
|
||||
|
||||
@@ -29,6 +29,10 @@ sync-all: sync-event-types sync-activity-definitions ## Sync event types and ac
|
||||
|
||||
SINCE ?= today
|
||||
FORMAT ?= human
|
||||
ENABLED ?= all
|
||||
TRIGGER ?=
|
||||
ACTIVITY_ID ?=
|
||||
ACTIVITY_NAME ?=
|
||||
|
||||
automation-status: ## Report recent automation status from repo-owned evidence
|
||||
uv run python scripts/automation_status.py --since "$(SINCE)" $(if $(UNTIL),--until "$(UNTIL)",) --format "$(FORMAT)"
|
||||
@@ -36,6 +40,12 @@ automation-status: ## Report recent automation status from repo-owned evidence
|
||||
automation-status-json: ## Report recent automation status as JSON
|
||||
$(MAKE) automation-status FORMAT=json
|
||||
|
||||
automation-list: ## List configured scheduled automations from repo-owned definitions
|
||||
@uv run python scripts/automation_inventory.py --format "$(FORMAT)" --enabled "$(ENABLED)" $(if $(TRIGGER),--trigger-type "$(TRIGGER)",) $(if $(ACTIVITY_ID),--activity-id "$(ACTIVITY_ID)",) $(if $(ACTIVITY_NAME),--activity-name "$(ACTIVITY_NAME)",)
|
||||
|
||||
automation-list-json: ## List configured scheduled automations as JSON
|
||||
@$(MAKE) --no-print-directory automation-list FORMAT=json
|
||||
|
||||
# ── Infrastructure ─────────────────────────────────────────────────────────────
|
||||
|
||||
dev-up: ## Start full dev stack (Temporal + PG + ES + NATS)
|
||||
|
||||
@@ -136,6 +136,36 @@ The response reports:
|
||||
- `schedules.deleted_orphans`
|
||||
- bounded `errors[]`
|
||||
|
||||
## Automation inventory
|
||||
|
||||
Use the repo-native inventory command to answer "what automations are scheduled
|
||||
at all?" before checking whether a recent window succeeded. The command is
|
||||
read-only: it loads ActivityDefinition rows or files and, when `TEMPORAL_HOST`
|
||||
is configured, describes Temporal schedules for visibility. It does not sync,
|
||||
upsert, pause, delete, or enqueue schedules.
|
||||
|
||||
```bash
|
||||
# Human-readable configured automation inventory.
|
||||
make automation-list
|
||||
|
||||
# JSON for scripts or assistant summarization.
|
||||
make automation-list-json
|
||||
|
||||
# Common filters.
|
||||
make automation-list ENABLED=true TRIGGER=cron
|
||||
make automation-list ACTIVITY_ID=6fca51fa-387a-4fd0-bc4e-d62c29eb859a
|
||||
```
|
||||
|
||||
Inventory answers what is configured; `make automation-status` answers what
|
||||
happened in a time window. Missing optional live sources are warnings, not
|
||||
silent omissions, so a degraded local run still lists repo definition files.
|
||||
|
||||
Compact human output looks like:
|
||||
|
||||
```text
|
||||
- Daily State Hub WSJF Triage [enabled cron] schedule=activity-schedule-... trigger=20 7 * * * tz=Europe/Berlin source=files temporal=not_checked
|
||||
```
|
||||
|
||||
## Automation status
|
||||
|
||||
Use the repo-native status command to answer operator questions such as "how did
|
||||
|
||||
8
scripts/automation_inventory.py
Normal file
8
scripts/automation_inventory.py
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI wrapper for the repo-native automation inventory report."""
|
||||
|
||||
from activity_core.automation_status import inventory_main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(inventory_main())
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Repo-native automation status reporting without LLM calls."""
|
||||
"""Repo-native automation status and inventory reporting without LLM calls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -789,6 +789,302 @@ def shorten(value: str | None, limit: int = 240) -> str | None:
|
||||
return value if len(value) <= limit else value[: limit - 3] + "..."
|
||||
|
||||
|
||||
def parse_inventory_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="List configured activity-core scheduled automations without LLMs."
|
||||
)
|
||||
parser.add_argument("--activity-id", action="append", default=_env_list("ACTIVITY_ID"))
|
||||
parser.add_argument("--activity-name", action="append", default=_env_list("ACTIVITY_NAME"))
|
||||
parser.add_argument(
|
||||
"--enabled",
|
||||
default=os.environ.get("ENABLED", "all"),
|
||||
help="Filter by enabled state: all, true/enabled, or false/disabled.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--trigger-type",
|
||||
"--trigger",
|
||||
dest="trigger_type",
|
||||
action="append",
|
||||
default=_env_list("TRIGGER"),
|
||||
help="Filter by trigger type: cron or scheduled. Repeatable or comma-separated.",
|
||||
)
|
||||
parser.add_argument("--db-url", default=os.environ.get("ACTCORE_DB_URL"))
|
||||
parser.add_argument("--temporal-host", default=os.environ.get("TEMPORAL_HOST"))
|
||||
parser.add_argument(
|
||||
"--temporal-namespace",
|
||||
default=os.environ.get("TEMPORAL_NAMESPACE", DEFAULT_TEMPORAL_NAMESPACE),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout-seconds",
|
||||
type=float,
|
||||
default=float(os.environ.get(
|
||||
"AUTOMATION_INVENTORY_TIMEOUT_SECONDS",
|
||||
os.environ.get("AUTOMATION_STATUS_TIMEOUT_SECONDS", "5"),
|
||||
)),
|
||||
)
|
||||
parser.add_argument("--format", choices=("human", "json"), default=os.environ.get("FORMAT", "human"))
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def _env_list(name: str) -> list[str]:
|
||||
raw = os.environ.get(name, "")
|
||||
return [item.strip() for item in raw.split(",") if item.strip()]
|
||||
|
||||
|
||||
def parse_enabled_filter(value: str | None) -> bool | None:
|
||||
if value is None or not str(value).strip() or str(value).strip().lower() == "all":
|
||||
return None
|
||||
lowered = str(value).strip().lower()
|
||||
if lowered in {"true", "yes", "1", "enabled"}:
|
||||
return True
|
||||
if lowered in {"false", "no", "0", "disabled"}:
|
||||
return False
|
||||
raise ValueError("enabled filter must be all, true/enabled, or false/disabled")
|
||||
|
||||
|
||||
def normalize_trigger_filters(values: list[str] | None) -> set[str]:
|
||||
result: set[str] = set()
|
||||
for value in values or []:
|
||||
for item in str(value).split(","):
|
||||
token = item.strip().lower()
|
||||
if not token or token == "all":
|
||||
continue
|
||||
if token not in {"cron", "scheduled"}:
|
||||
raise ValueError("trigger filter must be cron or scheduled")
|
||||
result.add(token)
|
||||
return result
|
||||
|
||||
|
||||
def filter_inventory_definitions(
|
||||
definitions: list[dict[str, Any]],
|
||||
ids: list[str],
|
||||
names: list[str],
|
||||
enabled: bool | None,
|
||||
trigger_types: set[str],
|
||||
) -> list[dict[str, Any]]:
|
||||
selected = filter_definitions(definitions, ids, names)
|
||||
if enabled is not None:
|
||||
selected = [item for item in selected if bool(item.get("enabled")) is enabled]
|
||||
if trigger_types:
|
||||
selected = [item for item in selected if str(item.get("trigger_type") or "").lower() in trigger_types]
|
||||
return selected
|
||||
|
||||
|
||||
def inventory_row(definition: dict[str, Any], temporal: dict[str, Any] | None) -> dict[str, Any]:
|
||||
trigger = public_trigger_config(definition.get("trigger_config", {}))
|
||||
row = {
|
||||
"id": definition["id"],
|
||||
"name": definition["name"],
|
||||
"enabled": bool(definition.get("enabled")),
|
||||
"trigger_type": definition["trigger_type"],
|
||||
"schedule_id": automation_schedule_id(definition),
|
||||
"definition_source": definition.get("source"),
|
||||
"sources": inventory_sources(definition, temporal),
|
||||
"trigger": trigger,
|
||||
"cron_expression": trigger.get("cron_expression"),
|
||||
"at": trigger.get("at"),
|
||||
"timezone": trigger.get("timezone"),
|
||||
"misfire_policy": trigger.get("misfire_policy"),
|
||||
"catchup_window_seconds": trigger.get("catchup_window_seconds"),
|
||||
"jitter_seconds": trigger.get("jitter_seconds"),
|
||||
"temporal": inventory_temporal_state(definition, temporal),
|
||||
"drift_hints": inventory_drift_hints(definition, temporal),
|
||||
}
|
||||
return row
|
||||
|
||||
|
||||
def inventory_sources(definition: dict[str, Any], temporal: dict[str, Any] | None) -> list[str]:
|
||||
sources = [str(definition.get("source") or "definition")]
|
||||
if temporal is None:
|
||||
sources.append("temporal:not_checked")
|
||||
elif temporal.get("available"):
|
||||
sources.append("temporal")
|
||||
else:
|
||||
sources.append("temporal:unavailable")
|
||||
return sources
|
||||
|
||||
|
||||
def inventory_temporal_state(definition: dict[str, Any], temporal: dict[str, Any] | None) -> dict[str, Any]:
|
||||
expected_schedule_id = automation_schedule_id(definition)
|
||||
if temporal is None:
|
||||
return {"status": "not_checked", "schedule_id": expected_schedule_id}
|
||||
if not temporal.get("available"):
|
||||
return {
|
||||
"status": "missing_or_unavailable",
|
||||
"schedule_id": temporal.get("schedule_id") or expected_schedule_id,
|
||||
"warning": temporal.get("warning"),
|
||||
}
|
||||
paused = bool(temporal.get("paused"))
|
||||
return {
|
||||
"status": "paused" if paused else "active",
|
||||
"schedule_id": temporal.get("schedule_id") or expected_schedule_id,
|
||||
"paused": paused,
|
||||
"last_fired_at": temporal.get("last_fired_at"),
|
||||
"missed_catchup_window": int(temporal.get("missed_catchup_window") or 0),
|
||||
}
|
||||
|
||||
|
||||
def inventory_drift_hints(definition: dict[str, Any], temporal: dict[str, Any] | None) -> list[str]:
|
||||
if temporal is None:
|
||||
return []
|
||||
hints: list[str] = []
|
||||
expected_schedule_id = automation_schedule_id(definition)
|
||||
observed_schedule_id = temporal.get("schedule_id")
|
||||
if observed_schedule_id and observed_schedule_id != expected_schedule_id:
|
||||
hints.append("temporal_schedule_id_mismatch")
|
||||
if not temporal.get("available"):
|
||||
hints.append("temporal_schedule_missing_or_unavailable")
|
||||
return hints
|
||||
paused = bool(temporal.get("paused"))
|
||||
enabled = bool(definition.get("enabled"))
|
||||
if enabled and paused:
|
||||
hints.append("temporal_paused_but_definition_enabled")
|
||||
if not enabled and not paused:
|
||||
hints.append("temporal_active_but_definition_disabled")
|
||||
if int(temporal.get("missed_catchup_window") or 0) > 0:
|
||||
hints.append("temporal_missed_catchup_window")
|
||||
return hints
|
||||
|
||||
|
||||
async def build_inventory_report(args: argparse.Namespace) -> tuple[dict[str, Any], int]:
|
||||
timeout = max(float(args.timeout_seconds), 0.1)
|
||||
warnings: list[str] = []
|
||||
sources: dict[str, dict[str, Any]] = {}
|
||||
|
||||
try:
|
||||
definitions, sources["definitions"] = await asyncio.wait_for(
|
||||
load_definitions(args, warnings),
|
||||
timeout=timeout,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
warning = "definition DB timed out; using file definitions"
|
||||
warnings.append(warning)
|
||||
definitions = file_definitions()
|
||||
sources["definitions"] = {"status": "degraded", "source": "files", "warning": warning}
|
||||
|
||||
enabled = parse_enabled_filter(args.enabled)
|
||||
trigger_types = normalize_trigger_filters(args.trigger_type)
|
||||
definitions = filter_inventory_definitions(
|
||||
definitions,
|
||||
args.activity_id,
|
||||
args.activity_name,
|
||||
enabled,
|
||||
trigger_types,
|
||||
)
|
||||
|
||||
temporal_by_activity, sources["temporal"] = await load_temporal_visibility(
|
||||
args.temporal_host,
|
||||
args.temporal_namespace,
|
||||
definitions,
|
||||
timeout_seconds=timeout,
|
||||
)
|
||||
for source in sources.values():
|
||||
if source.get("status") not in {"ok", "skipped"} and source.get("warning"):
|
||||
warnings.append(str(source["warning"]))
|
||||
|
||||
automations = [
|
||||
inventory_row(definition, temporal_by_activity.get(definition["id"]))
|
||||
for definition in definitions
|
||||
]
|
||||
report = {
|
||||
"mode": "automation-inventory",
|
||||
"generated_at": datetime.now(tz=timezone.utc).isoformat(),
|
||||
"sources": sources,
|
||||
"summary": summarize_inventory(automations),
|
||||
"automations": automations,
|
||||
"filters": {
|
||||
"activity_id": args.activity_id,
|
||||
"activity_name": args.activity_name,
|
||||
"enabled": args.enabled,
|
||||
"trigger_type": sorted(trigger_types),
|
||||
},
|
||||
"warnings": sorted(set(warnings)),
|
||||
}
|
||||
return report, 0
|
||||
|
||||
|
||||
def summarize_inventory(automations: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
trigger_counts: dict[str, int] = {}
|
||||
temporal_counts: dict[str, int] = {}
|
||||
for item in automations:
|
||||
trigger = str(item.get("trigger_type") or "unknown")
|
||||
trigger_counts[trigger] = trigger_counts.get(trigger, 0) + 1
|
||||
temporal_status = str((item.get("temporal") or {}).get("status") or "unknown")
|
||||
temporal_counts[temporal_status] = temporal_counts.get(temporal_status, 0) + 1
|
||||
return {
|
||||
"automation_count": len(automations),
|
||||
"enabled_count": sum(1 for item in automations if item.get("enabled")),
|
||||
"disabled_count": sum(1 for item in automations if not item.get("enabled")),
|
||||
"trigger_counts": trigger_counts,
|
||||
"temporal_status_counts": temporal_counts,
|
||||
"drift_count": sum(1 for item in automations if item.get("drift_hints")),
|
||||
}
|
||||
|
||||
|
||||
def render_inventory_human(report: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
f"Automation inventory generated_at={report['generated_at']}",
|
||||
render_source_line(report["sources"]),
|
||||
render_inventory_summary_line(report["summary"]),
|
||||
"",
|
||||
]
|
||||
for item in report["automations"]:
|
||||
state = "enabled" if item["enabled"] else "disabled"
|
||||
trigger_value = item.get("cron_expression") or item.get("at") or "-"
|
||||
temporal = item.get("temporal") or {}
|
||||
line = (
|
||||
f"- {item['name']} [{state} {item['trigger_type']}] "
|
||||
f"schedule={item['schedule_id']} trigger={trigger_value} "
|
||||
f"tz={item.get('timezone') or '-'} source={item.get('definition_source') or '-'} "
|
||||
f"temporal={temporal.get('status') or 'unknown'}"
|
||||
)
|
||||
lines.append(line)
|
||||
if item.get("misfire_policy") or item.get("catchup_window_seconds") is not None:
|
||||
lines.append(
|
||||
" policy="
|
||||
f"{item.get('misfire_policy') or '-'} catchup={item.get('catchup_window_seconds') if item.get('catchup_window_seconds') is not None else '-'}"
|
||||
)
|
||||
if item.get("drift_hints"):
|
||||
lines.append(" drift: " + ", ".join(item["drift_hints"]))
|
||||
if temporal.get("warning"):
|
||||
lines.append(f" temporal warning: {shorten(str(temporal['warning']))}")
|
||||
if report["warnings"]:
|
||||
lines.extend(["", "Warnings:"])
|
||||
lines.extend(f"- {warning}" for warning in report["warnings"])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_inventory_summary_line(summary: dict[str, Any]) -> str:
|
||||
trigger_counts = summary.get("trigger_counts") or {}
|
||||
trigger_text = ", ".join(f"{key}={value}" for key, value in sorted(trigger_counts.items())) or "none"
|
||||
return (
|
||||
"Summary: "
|
||||
f"total={summary.get('automation_count', 0)} "
|
||||
f"enabled={summary.get('enabled_count', 0)} "
|
||||
f"disabled={summary.get('disabled_count', 0)} "
|
||||
f"drift={summary.get('drift_count', 0)} "
|
||||
f"triggers=({trigger_text})"
|
||||
)
|
||||
|
||||
|
||||
async def async_inventory_main(argv: list[str] | None = None) -> int:
|
||||
args = parse_inventory_args(argv)
|
||||
try:
|
||||
report, exit_code = await build_inventory_report(args)
|
||||
except ValueError as exc:
|
||||
print(f"automation_inventory: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
if args.format == "json":
|
||||
print(json.dumps(report, indent=2, sort_keys=True))
|
||||
else:
|
||||
print(render_inventory_human(report))
|
||||
return exit_code
|
||||
|
||||
|
||||
def inventory_main(argv: list[str] | None = None) -> int:
|
||||
return asyncio.run(async_inventory_main(argv))
|
||||
|
||||
|
||||
async def async_main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
@@ -182,3 +184,106 @@ def test_working_memory_frontmatter_evidence(tmp_path: Path) -> None:
|
||||
assert source["status"] == "ok"
|
||||
assert evidence[0]["run_id"] == "run-1"
|
||||
assert evidence[0]["output_validated"] is False
|
||||
|
||||
|
||||
def _scheduled_definition(enabled: bool = False):
|
||||
return {
|
||||
"id": "00000000-0000-0000-0000-000000000456",
|
||||
"name": "One Shot",
|
||||
"enabled": enabled,
|
||||
"trigger_type": "scheduled",
|
||||
"trigger_config": {
|
||||
"trigger_type": "scheduled",
|
||||
"at": "2026-06-26T09:00:00+02:00",
|
||||
"timezone": "Europe/Berlin",
|
||||
},
|
||||
"source": "db",
|
||||
}
|
||||
|
||||
|
||||
def test_inventory_report_uses_db_definition_rows(monkeypatch) -> None:
|
||||
async def fake_load_definitions(args, warnings):
|
||||
return [dict(_definition(), source="db"), _scheduled_definition()], {"status": "ok", "source": "db"}
|
||||
|
||||
async def fake_temporal(host, namespace, definitions, *, timeout_seconds):
|
||||
return {
|
||||
ACTIVITY_ID: {
|
||||
"schedule_id": f"activity-schedule-{ACTIVITY_ID}",
|
||||
"available": True,
|
||||
"paused": False,
|
||||
"missed_catchup_window": 0,
|
||||
"last_fired_at": None,
|
||||
},
|
||||
}, {"status": "ok", "count": 1}
|
||||
|
||||
monkeypatch.setattr(status, "load_definitions", fake_load_definitions)
|
||||
monkeypatch.setattr(status, "load_temporal_visibility", fake_temporal)
|
||||
args = status.parse_inventory_args(["--format", "json"])
|
||||
|
||||
report, exit_code = asyncio.run(status.build_inventory_report(args))
|
||||
|
||||
assert exit_code == 0
|
||||
assert report["sources"]["definitions"] == {"status": "ok", "source": "db"}
|
||||
assert report["summary"]["automation_count"] == 2
|
||||
assert report["automations"][0]["definition_source"] == "db"
|
||||
assert report["automations"][0]["temporal"]["status"] == "active"
|
||||
assert report["automations"][1]["schedule_id"].endswith("-once")
|
||||
|
||||
|
||||
def test_inventory_file_fallback_when_db_url_missing(monkeypatch) -> None:
|
||||
monkeypatch.setattr(status, "file_definitions", lambda: [dict(_definition(), source="files")])
|
||||
args = status.parse_inventory_args(["--db-url", "", "--temporal-host", ""])
|
||||
|
||||
report, exit_code = asyncio.run(status.build_inventory_report(args))
|
||||
|
||||
assert exit_code == 0
|
||||
assert report["sources"]["definitions"]["status"] == "degraded"
|
||||
assert report["automations"][0]["definition_source"] == "files"
|
||||
assert "ACTCORE_DB_URL is not set" in report["warnings"][0]
|
||||
|
||||
|
||||
def test_inventory_filters_disabled_definitions() -> None:
|
||||
definitions = [_definition(enabled=True), _scheduled_definition(enabled=False)]
|
||||
|
||||
filtered = status.filter_inventory_definitions(
|
||||
definitions,
|
||||
ids=[],
|
||||
names=[],
|
||||
enabled=False,
|
||||
trigger_types=set(),
|
||||
)
|
||||
|
||||
assert [item["name"] for item in filtered] == ["One Shot"]
|
||||
|
||||
|
||||
def test_inventory_temporal_unavailable_is_warning_not_failure(monkeypatch) -> None:
|
||||
async def fake_load_definitions(args, warnings):
|
||||
return [_definition()], {"status": "ok", "source": "db"}
|
||||
|
||||
async def fake_temporal(host, namespace, definitions, *, timeout_seconds):
|
||||
return {}, {"status": "unavailable", "warning": "Temporal unavailable: nope"}
|
||||
|
||||
monkeypatch.setattr(status, "load_definitions", fake_load_definitions)
|
||||
monkeypatch.setattr(status, "load_temporal_visibility", fake_temporal)
|
||||
args = status.parse_inventory_args([])
|
||||
|
||||
report, exit_code = asyncio.run(status.build_inventory_report(args))
|
||||
|
||||
assert exit_code == 0
|
||||
assert report["automations"][0]["temporal"]["status"] == "not_checked"
|
||||
assert report["warnings"] == ["Temporal unavailable: nope"]
|
||||
|
||||
|
||||
def test_inventory_cli_emits_json(monkeypatch, capsys) -> None:
|
||||
monkeypatch.setattr(status, "file_definitions", lambda: [dict(_definition(), source="files")])
|
||||
|
||||
exit_code = asyncio.run(status.async_inventory_main([
|
||||
"--db-url", "",
|
||||
"--temporal-host", "",
|
||||
"--format", "json",
|
||||
]))
|
||||
|
||||
payload = json.loads(capsys.readouterr().out)
|
||||
assert exit_code == 0
|
||||
assert payload["mode"] == "automation-inventory"
|
||||
assert payload["automations"][0]["name"] == "Daily Check"
|
||||
|
||||
@@ -4,11 +4,11 @@ type: workplan
|
||||
title: "Automation schedule inventory Make targets"
|
||||
domain: infotech
|
||||
repo: activity-core
|
||||
status: ready
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: automation-inventory
|
||||
created: "2026-06-29"
|
||||
updated: "2026-06-29"
|
||||
updated: "2026-07-01"
|
||||
state_hub_workstream_id: "21c73763-9adc-42f6-8fd2-1b8b33c2c770"
|
||||
---
|
||||
|
||||
@@ -45,7 +45,7 @@ coding assistant automation infrastructure.
|
||||
|
||||
```task
|
||||
id: ACTIVITY-WP-0019-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8de24590-f9ee-4d0e-8692-b7ada9f232ed"
|
||||
```
|
||||
@@ -71,7 +71,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: ACTIVITY-WP-0019-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "538cb9a5-48f3-470c-8518-29ee66c96678"
|
||||
```
|
||||
@@ -98,7 +98,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: ACTIVITY-WP-0019-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "f2001721-07f3-42f5-a15e-0c7d1b0ed801"
|
||||
```
|
||||
@@ -120,7 +120,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: ACTIVITY-WP-0019-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "f687743b-3936-413e-ae50-d35484ae9a81"
|
||||
```
|
||||
@@ -142,7 +142,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: ACTIVITY-WP-0019-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "5317b532-5cef-4eff-b6d8-3e85bbca8e8a"
|
||||
```
|
||||
@@ -162,3 +162,43 @@ Acceptance:
|
||||
than hanging.
|
||||
- Focused tests pass and the result is recorded in this workplan before the
|
||||
workplan is moved to `finished`.
|
||||
|
||||
|
||||
## Implementation Result
|
||||
|
||||
Completed 2026-07-01: implemented the read-only scheduled automation inventory
|
||||
surface.
|
||||
|
||||
Delivered:
|
||||
|
||||
- `scripts/automation_inventory.py` exposes the inventory CLI backed by
|
||||
`activity_core.automation_status` shared definition and Temporal helpers.
|
||||
- `make automation-list` and `make automation-list-json` list configured
|
||||
scheduled ActivityDefinitions with filters for `ENABLED`, `TRIGGER`,
|
||||
`ACTIVITY_ID`, and `ACTIVITY_NAME`.
|
||||
- JSON output is script-safe; the Make JSON target suppresses command echo and
|
||||
recursive make directory chatter.
|
||||
- `docs/runbook.md` now distinguishes inventory (what is configured) from status
|
||||
(what happened in a time window).
|
||||
- Tests cover DB-backed rows, file fallback, disabled filtering, Temporal
|
||||
unavailable warnings, and JSON CLI output.
|
||||
|
||||
Verification:
|
||||
|
||||
```bash
|
||||
/home/worsch/.local/bin/uv run pytest tests/test_automation_status.py tests/test_daily_triage_verifier.py -q
|
||||
bash -lc 'export PATH="/home/worsch/.local/bin:$PATH"; make automation-list ACTCORE_DB_URL= TEMPORAL_HOST='
|
||||
bash -lc 'export PATH="/home/worsch/.local/bin:$PATH"; make automation-list-json ACTCORE_DB_URL= TEMPORAL_HOST= > /tmp/activity-core-inventory.json && python3 -m json.tool /tmp/activity-core-inventory.json >/tmp/activity-core-inventory.pretty'
|
||||
bash -lc 'export PATH="/home/worsch/.local/bin:$PATH"; make automation-list ACTCORE_DB_URL= TEMPORAL_HOST= ENABLED=true TRIGGER=cron'
|
||||
bash -lc 'export PATH="/home/worsch/.local/bin:$PATH"; make help'
|
||||
```
|
||||
|
||||
Results:
|
||||
|
||||
- focused tests: `16 passed`;
|
||||
- degraded Make inventory run listed 9 file-backed scheduled automations, with
|
||||
5 enabled and 4 disabled;
|
||||
- filtered Make run with `ENABLED=true TRIGGER=cron` listed 5 enabled cron
|
||||
automations;
|
||||
- `automation-list-json` emitted parseable JSON directly;
|
||||
- `make help` lists `automation-list` and `automation-list-json`.
|
||||
|
||||
Reference in New Issue
Block a user