diff --git a/README.md b/README.md index dac011b..7130b6e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # openclaw-superpowers -**52 ready-to-use skills that make your AI agent autonomous, self-healing, and self-improving.** +**53 ready-to-use skills that make your AI agent autonomous, self-healing, and self-improving.** -[![Skills](https://img.shields.io/badge/skills-52-blue)](#skills-included) +[![Skills](https://img.shields.io/badge/skills-53-blue)](#skills-included) [![Security](https://img.shields.io/badge/security_skills-6-green)](#security--guardrails) -[![Cron](https://img.shields.io/badge/cron_scheduled-19-orange)](#openclaw-native-36-skills) -[![Scripts](https://img.shields.io/badge/companion_scripts-36-purple)](#companion-scripts) +[![Cron](https://img.shields.io/badge/cron_scheduled-20-orange)](#openclaw-native-37-skills) +[![Scripts](https://img.shields.io/badge/companion_scripts-37-purple)](#companion-scripts) [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) -A plug-and-play skill library for [OpenClaw](https://github.com/openclaw/openclaw) — the open-source AI agent runtime. Gives your agent structured thinking, security guardrails, persistent memory, cron scheduling, self-recovery, and the ability to write its own new skills during conversation. +A plug-and-play skill library for [OpenClaw](https://github.com/openclaw/openclaw) — the open-source AI agent runtime. Gives your agent structured thinking, security guardrails, persistent memory, cron scheduling, runtime verification, self-recovery, and the ability to write its own new skills during conversation. Built for developers who want their AI agent to run autonomously 24/7, not just respond to prompts in a chat window. @@ -20,11 +20,12 @@ Built for developers who want their AI agent to run autonomously 24/7, not just Most AI agent frameworks give you a chatbot that forgets everything between sessions. OpenClaw is different — it runs persistently, handles multi-hour tasks, and has native cron scheduling. But out of the box, it doesn't know *how* to use those capabilities well. -**openclaw-superpowers bridges that gap.** Install 52 skills in one command, and your agent immediately knows how to: +**openclaw-superpowers bridges that gap.** Install 53 skills in one command, and your agent immediately knows how to: - **Think before it acts** — brainstorming, planning, and systematic debugging skills prevent the "dive in and break things" failure mode - **Protect itself** — 6 security skills detect prompt injection, block dangerous actions, audit installed code, and scan for leaked credentials -- **Run unattended** — 19 cron-scheduled skills handle memory cleanup, health checks, budget tracking, and community monitoring while you sleep +- **Run unattended** — 20 cron-scheduled skills handle memory cleanup, health checks, budget tracking, and community monitoring while you sleep +- **Verify itself** — runtime verification catches missing cron registrations, stale state, dependency drift, and install layout mistakes before they silently break automation - **Recover from failures** — self-recovery, loop-breaking, and task handoff skills keep long-running work alive across crashes and restarts - **Never forget** — DAG-based memory compaction, integrity checking, context scoring, and SQLite session persistence ensure the agent preserves critical information even in month-long conversations - **Improve itself** — the agent can write new skills during normal conversation using `create-skill`, encoding your preferences as permanent behaviors @@ -81,7 +82,7 @@ Methodology skills that work in any AI agent runtime. Adapted from [obra/superpo | `skill-conflict-detector` | Detects name shadowing and description-overlap conflicts between installed skills | `detect.py` | | `skill-portability-checker` | Validates OS/binary dependencies in companion scripts; catches non-portable calls | `check.py` | -### OpenClaw-Native (36 skills) +### OpenClaw-Native (37 skills) Skills that require OpenClaw's persistent runtime — cron scheduling, session state, or long-running execution. These are the skills that make a 24/7 autonomous agent actually work reliably. @@ -109,6 +110,7 @@ Skills that require OpenClaw's persistent runtime — cron scheduling, session s | `installed-skill-auditor` | Weekly post-install audit of all skills for injection, credentials, and drift | Mondays 9am | `audit.py` | | `skill-loadout-manager` | Named skill profiles to manage active skill sets and prevent system prompt bloat | — | `loadout.py` | | `skill-compatibility-checker` | Checks installed skills against the current OpenClaw version for feature compatibility | — | `check.py` | +| `runtime-verification-dashboard` | Verifies cron registration, state freshness, install layout, and dependency readiness across the live runtime; can dry-run or apply safe remediations | every 6h | `check.py` | | `heartbeat-governor` | Enforces per-skill execution budgets for cron skills; auto-pauses runaway skills | every hour | `governor.py` | | `community-skill-radar` | Scans Reddit for OpenClaw pain points and feature requests; writes prioritized PROPOSALS.md | every 3 days | `radar.py` | | `memory-graph-builder` | Parses MEMORY.md into a knowledge graph; detects duplicates, contradictions, stale entries | daily 10pm | `graph.py` | @@ -153,16 +155,17 @@ Six skills form a defense-in-depth security layer for autonomous agents: | Feature | openclaw-superpowers | obra/superpowers | Custom prompts | |---|---|---|---| -| Skills included | **52** | 8 | 0 | +| Skills included | **53** | 8 | 0 | | Self-modifying (agent writes new skills) | Yes | No | No | -| Cron scheduling | **19 scheduled skills** | No | No | +| Cron scheduling | **20 scheduled skills** | No | No | | Persistent state across sessions | **YAML state schemas** | No | No | | Security guardrails | **6 defense-in-depth skills** | No | No | -| Companion scripts with CLI | **36 scripts** | No | No | +| Companion scripts with CLI | **37 scripts** | No | No | | Memory graph / knowledge graph | Yes | No | No | | SQLite session persistence + FTS5 search | Yes | No | No | | Sub-agent recall with token-budgeted grants | Yes | No | No | | MCP server health monitoring | Yes | No | No | +| Runtime verification / observability | Yes | No | No | | API spend tracking & budget enforcement | Yes | No | No | | Community feature radar (Reddit scanning) | Yes | No | No | | Multi-agent coordination | Yes | No | No | @@ -182,7 +185,7 @@ Six skills form a defense-in-depth security layer for autonomous agents: │ │ │ ├── SKILL.md │ │ │ └── TEMPLATE.md │ │ └── ... -│ ├── openclaw-native/ # 36 persistent-runtime skills +│ ├── openclaw-native/ # 37 persistent-runtime skills │ │ ├── memory-graph-builder/ │ │ │ ├── SKILL.md # Skill definition + YAML frontmatter │ │ │ ├── STATE_SCHEMA.yaml # State shape (committed, versioned) @@ -205,7 +208,7 @@ Six skills form a defense-in-depth security layer for autonomous agents: Skills marked with a script ship a small executable alongside their `SKILL.md`: -- **35 Python scripts** (`run.py`, `audit.py`, `check.py`, `guard.py`, `bridge.py`, `onboard.py`, `sync.py`, `doctor.py`, `loadout.py`, `governor.py`, `detect.py`, `test.py`, `radar.py`, `graph.py`, `optimize.py`, `compact.py`, `intercept.py`, `score.py`, `integrity.py`, `persist.py`, `recall.py`) — run directly to manipulate state, generate reports, or trigger actions. Install `PyYAML` for any helper that reads or writes skill state. +- **36 Python scripts** (`run.py`, `audit.py`, `check.py`, `guard.py`, `bridge.py`, `onboard.py`, `sync.py`, `doctor.py`, `loadout.py`, `governor.py`, `detect.py`, `test.py`, `radar.py`, `graph.py`, `optimize.py`, `compact.py`, `intercept.py`, `score.py`, `integrity.py`, `persist.py`, `recall.py`) — run directly to manipulate state, generate reports, or trigger actions. Install `PyYAML` for any helper that reads or writes skill state. - **`vet.sh`** — Pure bash scanner; runs on any system with grep. - Every script supports `--help` and `--format json`. Dry-run mode available on scripts that make changes. - See the `example-state.yaml` in each skill directory for sample state and a commented walkthrough of cron behaviour. diff --git a/scripts/check-readme-metrics.sh b/scripts/check-readme-metrics.sh index e2d1b8c..890755f 100755 --- a/scripts/check-readme-metrics.sh +++ b/scripts/check-readme-metrics.sh @@ -4,9 +4,9 @@ set -euo pipefail REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" README="$REPO_DIR/README.md" -SKILL_COUNT="$(git -C "$REPO_DIR" ls-files 'skills/*/*/SKILL.md' | wc -l | tr -d '[:space:]')" -CRON_COUNT="$(git -C "$REPO_DIR" grep -l '^cron:' -- 'skills/*/*/SKILL.md' | wc -l | tr -d '[:space:]')" -PYTHON_SCRIPT_COUNT="$(git -C "$REPO_DIR" ls-files 'skills/**/*.py' | wc -l | tr -d '[:space:]')" +SKILL_COUNT="$(find "$REPO_DIR/skills" -type f -name 'SKILL.md' | wc -l | tr -d '[:space:]')" +CRON_COUNT="$(find "$REPO_DIR/skills" -type f -name 'SKILL.md' -print0 | xargs -0 grep -l '^cron:' | wc -l | tr -d '[:space:]')" +PYTHON_SCRIPT_COUNT="$(find "$REPO_DIR/skills" -type f -name '*.py' | wc -l | tr -d '[:space:]')" COMPANION_SCRIPT_COUNT=$((PYTHON_SCRIPT_COUNT + 1)) assert_contains() { diff --git a/skills/openclaw-native/runtime-verification-dashboard/SKILL.md b/skills/openclaw-native/runtime-verification-dashboard/SKILL.md new file mode 100644 index 0000000..be6c192 --- /dev/null +++ b/skills/openclaw-native/runtime-verification-dashboard/SKILL.md @@ -0,0 +1,75 @@ +--- +name: runtime-verification-dashboard +version: "1.0" +category: openclaw-native +description: Verifies skill runtime health — cron registration, state freshness, dependency readiness, and install layout drift — so autonomous agents stay observable and trustworthy. +stateful: true +cron: "0 */6 * * *" +--- + +# Runtime Verification Dashboard + +## What it does + +A skill library is only useful if the skills actually trigger, write state, and stay registered after upgrades. Runtime Verification Dashboard audits the live OpenClaw install and writes a rolling health report you can inspect in one command. + +It catches the gap between "the files exist" and "the runtime is healthy." + +## When to invoke + +- Automatically every 6 hours +- After installing or upgrading openclaw-superpowers +- When cron skills stop firing, state looks stale, or the runtime feels drifted +- Before trusting long-running automation in production + +## What it checks + +| Check | Why it matters | +|---|---| +| Install layout | Detects repo-root vs skills-root path drift before scripts look in the wrong place | +| Cron registration | Finds scheduled skills that exist on disk but are not registered with OpenClaw | +| State freshness | Flags stateful cron skills whose state has gone stale relative to their schedule | +| Dependency readiness | Warns when `PyYAML` or the `openclaw` CLI is unavailable | +| Skill inventory | Records PASS / WARN / FAIL status per skill in one ledger | + +## How to use + +```bash +python3 check.py --scan # Full live runtime audit +python3 check.py --scan --failures # Show only WARN / FAIL items +python3 check.py --scan --skill morning-briefing +python3 check.py --remediate # Dry-run safe remediations +python3 check.py --remediate --apply # Apply safe remediations +python3 check.py --status # Summary from the last scan +python3 check.py --findings # Findings from the last scan +python3 check.py --format json +``` + +## Cron wakeup behaviour + +Every 6 hours: + +1. Resolve the installed skill root +2. Inspect every skill's frontmatter and runtime state +3. Check cron visibility through `openclaw cron list` when available +4. Write a summary ledger and rolling history to state +5. Surface FAILs first, then WARNs + +## Safe remediations + +Remediation is dry-run by default. The dashboard only auto-fixes deterministic runtime issues: + +- `STATE_MISSING` → create the missing `state.yaml` stub +- `CRON_NOT_REGISTERED` → re-register the cron entry through the OpenClaw CLI + +Everything else remains advisory. Missing dependencies, invalid frontmatter, and install-layout issues still require a human decision. + +## Operating rule + +Use this skill as the first stop when the agent's behaviour looks inconsistent with the skill files on disk. It tells you whether the problem is registration, state drift, dependency drift, or install layout drift. + +## State + +State file: `~/.openclaw/skill-state/runtime-verification-dashboard/state.yaml` + +Fields: `last_scan_at`, `last_remediation_at`, `summary`, `environment`, `global_findings`, `skills`, `scan_history`, `remediation_history`. diff --git a/skills/openclaw-native/runtime-verification-dashboard/STATE_SCHEMA.yaml b/skills/openclaw-native/runtime-verification-dashboard/STATE_SCHEMA.yaml new file mode 100644 index 0000000..58a92f0 --- /dev/null +++ b/skills/openclaw-native/runtime-verification-dashboard/STATE_SCHEMA.yaml @@ -0,0 +1,73 @@ +version: "1.0" +description: Runtime verification summary, per-skill health ledger, and rolling scan history. +fields: + last_scan_at: + type: datetime + last_remediation_at: + type: datetime + summary: + type: object + items: + total_skills: { type: integer, default: 0 } + pass_count: { type: integer, default: 0 } + warn_count: { type: integer, default: 0 } + fail_count: { type: integer, default: 0 } + stateful_count: { type: integer, default: 0 } + scheduled_count: { type: integer, default: 0 } + companion_script_count: { type: integer, default: 0 } + environment: + type: object + items: + openclaw_home: { type: string } + superpowers_dir: { type: string } + skills_root: { type: string } + openclaw_cli_found: { type: boolean } + pyyaml_found: { type: boolean } + cron_registry_ok: { type: boolean } + global_findings: + type: list + items: + level: { type: enum, values: [WARN, FAIL] } + check: { type: string } + detail: { type: string } + recommendation: { type: string } + skills: + type: list + items: + name: { type: string } + category: { type: string } + status: { type: enum, values: [PASS, WARN, FAIL] } + stateful: { type: boolean } + cron: { type: string } + state_file: { type: string } + companion_scripts: { type: list, items: { type: string } } + findings: + type: list + items: + level: { type: enum, values: [WARN, FAIL] } + check: { type: string } + detail: { type: string } + recommendation: { type: string } + scan_history: + type: list + items: + scanned_at: { type: datetime } + total_skills: { type: integer } + pass_count: { type: integer } + warn_count: { type: integer } + fail_count: { type: integer } + global_finding_count: { type: integer } + remediation_history: + type: list + items: + remediated_at: { type: datetime } + action_count: { type: integer } + applied_count: { type: integer } + actions: + type: list + items: + skill: { type: string } + check: { type: string } + action: { type: string } + outcome: { type: enum, values: [planned, applied, failed] } + detail: { type: string } diff --git a/skills/openclaw-native/runtime-verification-dashboard/check.py b/skills/openclaw-native/runtime-verification-dashboard/check.py new file mode 100755 index 0000000..a58437d --- /dev/null +++ b/skills/openclaw-native/runtime-verification-dashboard/check.py @@ -0,0 +1,625 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +""" +Runtime Verification Dashboard for openclaw-superpowers. + +Audits the live runtime: install layout, cron registration, state freshness, +dependency readiness, and per-skill PASS/WARN/FAIL status. + +Usage: + python3 check.py --scan + python3 check.py --scan --failures + python3 check.py --scan --skill morning-briefing + python3 check.py --remediate + python3 check.py --remediate --apply + python3 check.py --status + python3 check.py --findings + python3 check.py --format json +""" + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +from datetime import datetime +from pathlib import Path + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + +OPENCLAW_DIR = Path(os.environ.get("OPENCLAW_HOME", Path.home() / ".openclaw")) +STATE_FILE = OPENCLAW_DIR / "skill-state" / "runtime-verification-dashboard" / "state.yaml" +SUPERPOWERS_DIR = Path(os.environ.get("SUPERPOWERS_DIR", OPENCLAW_DIR / "extensions" / "superpowers")) +CATEGORIES = ["core", "openclaw-native", "community"] +CRON_RE = re.compile( + r'^[0-9*/,\-]+\s+' + r'[0-9*/,\-]+\s+' + r'[0-9*/,\-]+\s+' + r'[0-9*/,\-]+\s+' + r'[0-9*/,\-]+$' +) +MAX_HISTORY = 20 + + +def default_state() -> dict: + return { + "last_scan_at": "", + "last_remediation_at": "", + "summary": {}, + "environment": {}, + "global_findings": [], + "skills": [], + "scan_history": [], + "remediation_history": [], + } + + +def load_state() -> dict: + if not STATE_FILE.exists(): + return default_state() + try: + text = STATE_FILE.read_text() + if HAS_YAML: + return yaml.safe_load(text) or default_state() + return json.loads(text) + except Exception: + return default_state() + + +def save_state(state: dict) -> None: + STATE_FILE.parent.mkdir(parents=True, exist_ok=True) + if HAS_YAML: + with open(STATE_FILE, "w") as handle: + yaml.dump(state, handle, default_flow_style=False, allow_unicode=True, sort_keys=False) + else: + STATE_FILE.write_text(json.dumps(state, indent=2)) + + +def resolve_skills_root(base_dir: Path) -> Path: + for candidate in (base_dir / "skills", base_dir): + if all((candidate / category).exists() for category in CATEGORIES): + return candidate + return base_dir / "skills" + + +def parse_frontmatter(skill_md: Path) -> tuple[dict, str]: + try: + text = skill_md.read_text() + except Exception as exc: + return {}, f"Cannot read SKILL.md: {exc}" + + lines = text.splitlines() + if not lines or lines[0].strip() != "---": + return {}, "No frontmatter block found" + + end = None + for index, line in enumerate(lines[1:], start=1): + if line.strip() == "---": + end = index + break + + if end is None: + return {}, "Frontmatter block is not closed" + + block = "\n".join(lines[1:end]) + if HAS_YAML: + try: + return yaml.safe_load(block) or {}, "" + except Exception as exc: + return {}, f"Unparseable frontmatter: {exc}" + + fields = {} + for line in block.splitlines(): + if ":" in line: + key, _, value = line.partition(":") + fields[key.strip()] = value.strip().strip('"').strip("'") + return fields, "" + + +def find_companion_scripts(skill_dir: Path) -> list[str]: + ignored = {"SKILL.md", "STATE_SCHEMA.yaml", "example-state.yaml"} + scripts = [] + for path in sorted(skill_dir.iterdir()): + if not path.is_file() or path.name in ignored: + continue + if path.suffix in {".py", ".sh"}: + scripts.append(path.name) + return scripts + + +def get_registered_crons() -> tuple[list[str] | None, str]: + if shutil.which("openclaw") is None: + return None, "openclaw CLI not found" + + try: + proc = subprocess.run( + ["openclaw", "cron", "list"], + capture_output=True, + text=True, + timeout=10, + check=False, + ) + except Exception as exc: + return None, f"openclaw cron list failed: {exc}" + + if proc.returncode != 0: + detail = proc.stderr.strip() or proc.stdout.strip() or "unknown error" + return None, f"openclaw cron list failed: {detail}" + + return [line.strip() for line in proc.stdout.splitlines() if line.strip()], "" + + +def run_openclaw_cron(args: list[str]) -> tuple[bool, str]: + if shutil.which("openclaw") is None: + return False, "openclaw CLI not found" + + try: + proc = subprocess.run( + ["openclaw", "cron"] + args, + capture_output=True, + text=True, + timeout=10, + check=False, + ) + except Exception as exc: + return False, str(exc) + + if proc.returncode != 0: + detail = proc.stderr.strip() or proc.stdout.strip() or "unknown error" + return False, detail + return True, proc.stdout.strip() + + +def interval_minutes(cron_expr: str) -> int: + minute, hour, day, month, weekday = cron_expr.split() + + if minute.startswith("*/") and hour == day == month == weekday == "*": + return max(1, int(minute[2:])) + + if hour.startswith("*/") and day == month == weekday == "*": + return max(1, int(hour[2:])) * 60 + + if day != "*" or weekday != "*": + return 24 * 60 + + if hour != "*" or minute != "*": + return 24 * 60 + + return 60 + + +def make_finding(level: str, check: str, detail: str, recommendation: str) -> dict: + return { + "level": level, + "check": check, + "detail": detail, + "recommendation": recommendation, + } + + +def status_for(findings: list[dict]) -> str: + if any(item["level"] == "FAIL" for item in findings): + return "FAIL" + if findings: + return "WARN" + return "PASS" + + +def scan(single_skill: str | None = None) -> dict: + scanned_at = datetime.now().isoformat() + skills_root = resolve_skills_root(SUPERPOWERS_DIR) + cron_lines, cron_error = get_registered_crons() + global_findings = [] + + if not skills_root.exists(): + global_findings.append( + make_finding( + "FAIL", + "SKILLS_ROOT_MISSING", + f"Cannot find skills root at {skills_root}", + "Check OPENCLAW_HOME / SUPERPOWERS_DIR and reinstall the extension.", + ) + ) + + if skills_root == SUPERPOWERS_DIR: + global_findings.append( + make_finding( + "WARN", + "INSTALL_LAYOUT", + "Detected installed-extension layout where `superpowers` points directly at the skills root.", + "Use path resolution that accepts both repo-root and installed-extension layouts.", + ) + ) + + if not HAS_YAML: + global_findings.append( + make_finding( + "WARN", + "PYYAML_MISSING", + "PyYAML is unavailable; stateful helpers may read less and persist less reliably.", + "Install PyYAML with `python3 -m pip install PyYAML`.", + ) + ) + + if cron_lines is None: + global_findings.append( + make_finding( + "WARN", + "CRON_REGISTRY_UNAVAILABLE", + cron_error, + "Install the OpenClaw CLI or run the checker where `openclaw cron list` is available.", + ) + ) + + skills = [] + total_skills = 0 + stateful_count = 0 + scheduled_count = 0 + companion_script_count = 0 + + for category in CATEGORIES: + category_dir = skills_root / category + if not category_dir.exists(): + continue + for skill_dir in sorted(category_dir.iterdir()): + if not skill_dir.is_dir(): + continue + if single_skill and skill_dir.name != single_skill: + continue + + total_skills += 1 + skill_md = skill_dir / "SKILL.md" + frontmatter, parse_error = parse_frontmatter(skill_md) + findings = [] + + if parse_error: + findings.append( + make_finding( + "FAIL", + "FRONTMATTER", + parse_error, + "Fix the YAML frontmatter at the top of SKILL.md.", + ) + ) + + is_stateful = str(frontmatter.get("stateful", "")).lower() == "true" + cron_expr = str(frontmatter.get("cron", "")).strip() + companion_scripts = find_companion_scripts(skill_dir) + companion_script_count += len(companion_scripts) + + if is_stateful: + stateful_count += 1 + if cron_expr: + scheduled_count += 1 + if not CRON_RE.match(cron_expr): + findings.append( + make_finding( + "FAIL", + "CRON_FORMAT", + f"Invalid cron expression in frontmatter: {cron_expr}", + "Use a valid 5-field cron expression.", + ) + ) + + state_file = OPENCLAW_DIR / "skill-state" / skill_dir.name / "state.yaml" + if is_stateful and not state_file.exists(): + findings.append( + make_finding( + "WARN", + "STATE_MISSING", + f"Expected state file is missing: {state_file}", + "Run ./install.sh or execute the skill once to create its state directory.", + ) + ) + elif is_stateful and cron_expr: + age_minutes = (datetime.now().timestamp() - state_file.stat().st_mtime) / 60 + freshness_budget = interval_minutes(cron_expr) * 3 + if age_minutes > freshness_budget: + findings.append( + make_finding( + "WARN", + "STATE_STALE", + f"State file is {int(age_minutes)} minutes old, above the {freshness_budget}-minute freshness budget.", + "Inspect the cron logs and rerun the companion script manually.", + ) + ) + + if cron_expr and cron_lines is not None: + if not any(skill_dir.name in line for line in cron_lines): + findings.append( + make_finding( + "FAIL", + "CRON_NOT_REGISTERED", + "Skill is scheduled in frontmatter but was not found in `openclaw cron list`.", + "Re-run ./install.sh or register the cron entry manually.", + ) + ) + + skills.append( + { + "name": skill_dir.name, + "category": category, + "status": status_for(findings), + "stateful": is_stateful, + "cron": cron_expr, + "state_file": str(state_file), + "companion_scripts": companion_scripts, + "findings": findings, + } + ) + + pass_count = sum(1 for item in skills if item["status"] == "PASS") + warn_count = sum(1 for item in skills if item["status"] == "WARN") + fail_count = sum(1 for item in skills if item["status"] == "FAIL") + + summary = { + "total_skills": total_skills, + "pass_count": pass_count, + "warn_count": warn_count, + "fail_count": fail_count, + "stateful_count": stateful_count, + "scheduled_count": scheduled_count, + "companion_script_count": companion_script_count, + } + environment = { + "openclaw_home": str(OPENCLAW_DIR), + "superpowers_dir": str(SUPERPOWERS_DIR), + "skills_root": str(skills_root), + "openclaw_cli_found": shutil.which("openclaw") is not None, + "pyyaml_found": HAS_YAML, + "cron_registry_ok": cron_lines is not None, + } + return { + "last_scan_at": scanned_at, + "summary": summary, + "environment": environment, + "global_findings": global_findings, + "skills": skills, + } + + +def update_state(report: dict) -> dict: + state = load_state() + history = state.get("scan_history") or [] + history.insert( + 0, + { + "scanned_at": report["last_scan_at"], + "total_skills": report["summary"]["total_skills"], + "pass_count": report["summary"]["pass_count"], + "warn_count": report["summary"]["warn_count"], + "fail_count": report["summary"]["fail_count"], + "global_finding_count": len(report["global_findings"]), + }, + ) + report["scan_history"] = history[:MAX_HISTORY] + report["last_remediation_at"] = state.get("last_remediation_at", "") + report["remediation_history"] = state.get("remediation_history", []) + save_state(report) + return report + + +def record_remediation(actions: list[dict]) -> None: + state = load_state() + history = state.get("remediation_history") or [] + timestamp = datetime.now().isoformat() + history.insert( + 0, + { + "remediated_at": timestamp, + "action_count": len(actions), + "applied_count": sum(1 for item in actions if item["outcome"] == "applied"), + "actions": actions, + }, + ) + state["last_remediation_at"] = timestamp + state["remediation_history"] = history[:MAX_HISTORY] + save_state(state) + + +def remediate(report: dict, apply: bool) -> list[dict]: + actions = [] + dry_run = not apply + + for skill in report.get("skills", []): + for finding in skill.get("findings", []): + if finding["check"] == "STATE_MISSING": + state_file = Path(skill["state_file"]) + action = { + "skill": skill["name"], + "check": finding["check"], + "action": f"create {state_file}", + "outcome": "planned" if dry_run else "applied", + "detail": "", + } + if dry_run: + action["detail"] = "Would create the skill-state directory and stub state.yaml." + else: + state_file.parent.mkdir(parents=True, exist_ok=True) + if not state_file.exists(): + state_file.write_text( + f"# Runtime state for {skill['name']} - managed by openclaw-superpowers\n" + ) + action["detail"] = "Created missing state stub." + actions.append(action) + + if finding["check"] == "CRON_NOT_REGISTERED" and skill.get("cron"): + action = { + "skill": skill["name"], + "check": finding["check"], + "action": f"register cron {skill['cron']}", + "outcome": "planned" if dry_run else "applied", + "detail": "", + } + if dry_run: + action["detail"] = "Would run `openclaw cron remove` then `openclaw cron add`." + else: + removed, remove_detail = run_openclaw_cron(["remove", skill["name"]]) + added, add_detail = run_openclaw_cron(["add", skill["name"], skill["cron"]]) + if not added: + action["outcome"] = "failed" + action["detail"] = add_detail + else: + action["detail"] = add_detail or remove_detail or "Cron entry registered." + actions.append(action) + + return actions + + +def filter_report(report: dict, skill_name: str | None, failures_only: bool) -> dict: + filtered = { + "last_scan_at": report.get("last_scan_at"), + "summary": report.get("summary", {}), + "environment": report.get("environment", {}), + "global_findings": report.get("global_findings", []), + "skills": report.get("skills", []), + "scan_history": report.get("scan_history", []), + } + if skill_name: + filtered["skills"] = [item for item in filtered["skills"] if item["name"] == skill_name] + if failures_only: + filtered["global_findings"] = [ + item for item in filtered["global_findings"] if item["level"] in {"WARN", "FAIL"} + ] + filtered["skills"] = [item for item in filtered["skills"] if item["status"] != "PASS"] + return filtered + + +def print_summary(report: dict) -> None: + summary = report.get("summary", {}) + environment = report.get("environment", {}) + print("\nRuntime Verification Dashboard") + print("───────────────────────────────────────────────────────") + print( + f" Skills: {summary.get('total_skills', 0)} total | " + f"{summary.get('pass_count', 0)} PASS | " + f"{summary.get('warn_count', 0)} WARN | " + f"{summary.get('fail_count', 0)} FAIL" + ) + print( + f" Stateful: {summary.get('stateful_count', 0)} | " + f"Scheduled: {summary.get('scheduled_count', 0)} | " + f"Companion scripts: {summary.get('companion_script_count', 0)}" + ) + print(f" Skills root: {environment.get('skills_root', 'unknown')}") + print(f" Last scan: {report.get('last_scan_at', 'never')}") + + +def print_findings(report: dict, skill_name: str | None = None) -> None: + global_findings = report.get("global_findings", []) + if global_findings: + print("\nGlobal findings") + print("───────────────────────────────────────────────────────") + for item in global_findings: + print(f" {item['level']:4} {item['check']}") + print(f" {item['detail']}") + + skills = report.get("skills", []) + if skill_name and not skills: + print(f"\nNo data recorded for skill '{skill_name}'.") + return + + issue_skills = [item for item in skills if item["status"] != "PASS"] + if not issue_skills and not global_findings: + print("\nAll monitored skills look healthy.") + return + + if issue_skills: + print("\nSkill findings") + print("───────────────────────────────────────────────────────") + for item in issue_skills: + print(f" {item['status']:4} {item['name']} ({item['category']})") + for finding in item["findings"]: + print(f" {finding['check']}: {finding['detail']}") + + +def print_status(report: dict) -> None: + print_summary(report) + history = report.get("scan_history", []) + if history: + print("\nRecent scans") + print("───────────────────────────────────────────────────────") + for entry in history[:5]: + print( + f" {entry['scanned_at'][:19]} " + f"P:{entry['pass_count']} W:{entry['warn_count']} F:{entry['fail_count']}" + ) + remediations = report.get("remediation_history", []) + if remediations: + print("\nRecent remediations") + print("───────────────────────────────────────────────────────") + for entry in remediations[:5]: + print( + f" {entry['remediated_at'][:19]} " + f"actions:{entry['action_count']} applied:{entry['applied_count']}" + ) + + +def print_remediation(actions: list[dict], apply: bool) -> None: + mode = "Apply" if apply else "Dry run" + print(f"\nRuntime remediation ({mode})") + print("───────────────────────────────────────────────────────") + if not actions: + print(" No fixable findings in the current scan.") + return + for action in actions: + print(f" {action['outcome'].upper():7} {action['skill']} {action['check']}") + print(f" {action['action']}") + if action["detail"]: + print(f" {action['detail']}") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Runtime verification and observability for skills") + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--scan", action="store_true", help="Run a live runtime scan") + group.add_argument("--remediate", action="store_true", help="Plan or apply safe remediations") + group.add_argument("--status", action="store_true", help="Show summary from the last scan") + group.add_argument("--findings", action="store_true", help="Show findings from the last scan") + parser.add_argument("--skill", metavar="NAME", help="Limit output to a single skill") + parser.add_argument("--failures", action="store_true", help="Only show WARN / FAIL items") + parser.add_argument("--apply", action="store_true", help="Apply remediations instead of dry-run planning") + parser.add_argument("--format", choices=["human", "json"], default="human") + args = parser.parse_args() + + if args.scan: + report = update_state(scan(args.skill)) + elif args.remediate: + report = update_state(scan(args.skill)) + actions = remediate(report, args.apply) + if args.apply: + record_remediation(actions) + report = update_state(scan(args.skill)) + if args.format == "json": + print(json.dumps({"report": report, "actions": actions}, indent=2)) + return + print_summary(report) + print_findings(filter_report(report, args.skill, args.failures), args.skill) + print_remediation(actions, args.apply) + return + else: + report = load_state() + + report = filter_report(report, args.skill, args.failures) + + if args.format == "json": + print(json.dumps(report, indent=2)) + return + + if args.status: + print_status(report) + return + + print_summary(report) + print_findings(report, args.skill) + + +if __name__ == "__main__": + main() diff --git a/skills/openclaw-native/runtime-verification-dashboard/example-state.yaml b/skills/openclaw-native/runtime-verification-dashboard/example-state.yaml new file mode 100644 index 0000000..15aa64e --- /dev/null +++ b/skills/openclaw-native/runtime-verification-dashboard/example-state.yaml @@ -0,0 +1,103 @@ +# Example runtime state for runtime-verification-dashboard +last_scan_at: "2026-03-18T12:00:08.000000" +last_remediation_at: "2026-03-18T12:05:11.000000" +summary: + total_skills: 53 + pass_count: 49 + warn_count: 4 + fail_count: 0 + stateful_count: 39 + scheduled_count: 20 + companion_script_count: 37 +environment: + openclaw_home: "/Users/you/.openclaw" + superpowers_dir: "/Users/you/.openclaw/extensions/superpowers" + skills_root: "/Users/you/.openclaw/extensions/superpowers" + openclaw_cli_found: true + pyyaml_found: true + cron_registry_ok: true +global_findings: + - level: WARN + check: INSTALL_LAYOUT + detail: "Detected skills-root install layout; scripts must not assume a nested /skills directory." + recommendation: "Use path resolution that accepts both repo-root and installed-extension layouts." +skills: + - name: morning-briefing + category: openclaw-native + status: PASS + stateful: true + cron: "0 7 * * 1-5" + state_file: "/Users/you/.openclaw/skill-state/morning-briefing/state.yaml" + companion_scripts: + - run.py + findings: [] + - name: session-persistence + category: openclaw-native + status: WARN + stateful: true + cron: "*/15 * * * *" + state_file: "/Users/you/.openclaw/skill-state/session-persistence/state.yaml" + companion_scripts: + - persist.py + findings: + - level: WARN + check: STATE_STALE + detail: "State file is 132 minutes old, above the 45-minute freshness budget." + recommendation: "Inspect the cron logs and rerun `python3 persist.py --import`." + - name: daily-review + category: openclaw-native + status: PASS + stateful: true + cron: "0 18 * * 1-5" + state_file: "/Users/you/.openclaw/skill-state/daily-review/state.yaml" + companion_scripts: [] + findings: [] +scan_history: + - scanned_at: "2026-03-18T12:00:08.000000" + total_skills: 53 + pass_count: 49 + warn_count: 4 + fail_count: 0 + global_finding_count: 1 + - scanned_at: "2026-03-18T06:00:05.000000" + total_skills: 53 + pass_count: 50 + warn_count: 3 + fail_count: 0 + global_finding_count: 1 +remediation_history: + - remediated_at: "2026-03-18T12:05:11.000000" + action_count: 1 + applied_count: 1 + actions: + - skill: daily-review + check: CRON_NOT_REGISTERED + action: "register cron 0 18 * * 1-5" + outcome: applied + detail: "Cron entry registered." +# ── Walkthrough ────────────────────────────────────────────────────────────── +# Cron runs every 6 hours: python3 check.py --scan +# +# Runtime Verification Dashboard +# ─────────────────────────────────────────────────────── +# Skills: 53 total | 49 PASS | 4 WARN | 0 FAIL +# Stateful: 39 | Scheduled: 20 | Companion scripts: 37 +# Skills root: /Users/you/.openclaw/extensions/superpowers +# +# WARN INSTALL_LAYOUT +# Detected skills-root install layout; scripts must not assume a nested /skills directory. +# +# python3 check.py --findings +# +# Runtime Findings +# ─────────────────────────────────────────────────────── +# WARN session-persistence STATE_STALE +# WARN system INSTALL_LAYOUT +# +# python3 check.py --remediate --apply +# +# Runtime remediation (Apply) +# ─────────────────────────────────────────────────────── +# APPLIED daily-review CRON_NOT_REGISTERED +# register cron 0 18 * * 1-5 +# Cron entry registered.