diff --git a/README.md b/README.md index d7a3b7e..0d0ad4d 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # openclaw-superpowers -**52 ready-to-use skills that make your AI agent autonomous, self-healing, and self-improving.** +**56 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-56-blue)](#skills-included) [![Security](https://img.shields.io/badge/security_skills-6-green)](#security--guardrails) -[![Cron](https://img.shields.io/badge/cron_scheduled-16-orange)](#openclaw-native-36-skills) -[![Scripts](https://img.shields.io/badge/companion_scripts-23-purple)](#companion-scripts) +[![Cron](https://img.shields.io/badge/cron_scheduled-16-orange)](#openclaw-native-38-skills) +[![Scripts](https://img.shields.io/badge/companion_scripts-25-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. @@ -20,7 +20,7 @@ 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 56 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 @@ -51,13 +51,13 @@ cd ~/.openclaw/extensions/superpowers && ./install.sh openclaw gateway restart ``` -`install.sh` symlinks all 52 skills, creates state directories for stateful skills, and registers cron jobs — everything in one step. That's it. Your agent now has superpowers. +`install.sh` symlinks all 56 skills, creates state directories for stateful skills, and registers cron jobs — everything in one step. That's it. Your agent now has superpowers. --- ## Skills included -### Core (15 skills) +### Core (17 skills) Methodology skills that work in any AI agent runtime. Adapted from [obra/superpowers](https://github.com/obra/superpowers) plus new additions for skill quality assurance. @@ -78,8 +78,10 @@ Methodology skills that work in any AI agent runtime. Adapted from [obra/superpo | `skill-trigger-tester` | Scores a skill's description against sample prompts to predict trigger reliability | `test.py` | | `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` | +| `pull-request-feedback-loop` | Handles PR review comments as grouped, verified fix loops | — | +| `skill-effectiveness-auditor` | Reviews whether a skill will trigger reliably and guide useful behavior | — | -### OpenClaw-Native (36 skills) +### OpenClaw-Native (38 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. @@ -94,6 +96,7 @@ Skills that require OpenClaw's persistent runtime — cron scheduling, session s | `morning-briefing` | Daily briefing: priorities, active tasks, pending handoffs | weekdays 7am | `run.py` | | `secrets-hygiene` | Audits installed skills for stale credentials and orphaned secrets | Mondays 9am | `audit.py` | | `workflow-orchestration` | Chains skills into resumable named workflows with on-failure conditions | — | `run.py` | +| `quality-gate-orchestrator` | Tracks required validation gates and reports completion readiness | — | `gate.py` | | `context-budget-guard` | Estimates context usage and triggers compaction before overflow | — | `check.py` | | `prompt-injection-guard` | Detects injection attempts in external content before the agent acts | — | `guard.py` | | `spend-circuit-breaker` | Tracks API spend against a monthly budget; pauses crons at 100% | every 4h | `check.py` | @@ -111,6 +114,7 @@ Skills that require OpenClaw's persistent runtime — cron scheduling, session s | `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` | | `config-encryption-auditor` | Scans config directories for plaintext API keys, tokens, and world-readable permissions | Sundays 9am | `audit.py` | +| `openclaw-config-advisor` | Diagnoses provider, fallback, channel, MCP, and gateway config issues | — | `advise.py` | | `tool-description-optimizer` | Scores skill descriptions for trigger quality — clarity, specificity, keyword density — and suggests rewrites | — | `optimize.py` | | `mcp-health-checker` | Monitors MCP server connections for health, latency, and availability; detects stale connections | every 6h | `check.py` | | `memory-dag-compactor` | Builds hierarchical summary DAGs from MEMORY.md with depth-aware prompts (d0 leaf → d3+ durable) | daily 11pm | `compact.py` | @@ -151,12 +155,12 @@ 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 | **56** | 8 | 0 | | Self-modifying (agent writes new skills) | Yes | No | No | | Cron scheduling | **16 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 | **23 scripts** | No | No | +| Companion scripts with CLI | **25 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 | @@ -173,14 +177,14 @@ Six skills form a defense-in-depth security layer for autonomous agents: ``` ~/.openclaw/extensions/superpowers/ ├── skills/ -│ ├── core/ # 15 methodology skills (any runtime) +│ ├── core/ # 17 methodology skills (any runtime) │ │ ├── brainstorming/ │ │ │ └── SKILL.md │ │ ├── create-skill/ │ │ │ ├── SKILL.md │ │ │ └── TEMPLATE.md │ │ └── ... -│ ├── openclaw-native/ # 36 persistent-runtime skills +│ ├── openclaw-native/ # 38 persistent-runtime skills │ │ ├── memory-graph-builder/ │ │ │ ├── SKILL.md # Skill definition + YAML frontmatter │ │ │ ├── STATE_SCHEMA.yaml # State shape (committed, versioned) @@ -203,7 +207,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`: -- **23 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. No extra dependencies; `pyyaml` is optional but recommended. +- **25 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`, `advise.py`, `gate.py`) — run directly to manipulate state, generate reports, or trigger actions. No extra dependencies; `pyyaml` is optional but recommended. - **`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/docs/repo-fit-skill-pack-spec.md b/docs/repo-fit-skill-pack-spec.md new file mode 100644 index 0000000..c0f3006 --- /dev/null +++ b/docs/repo-fit-skill-pack-spec.md @@ -0,0 +1,46 @@ +# Repo-Fit Skill Pack Spec + +## Goal + +Add four skills that fit the current `openclaw-superpowers` library and improve day-to-day agent reliability: + +- PR feedback handling +- Semantic skill quality review +- OpenClaw configuration diagnosis +- Task completion gates + +These additions should extend the repo's existing autonomy, validation, and OpenClaw-native state patterns without introducing external marketplace structure or branding. + +## Skills + +### `pull-request-feedback-loop` + +Core skill for handling PR review feedback. It uses `gh` to fetch review comments, groups related comments by issue, fixes one group at a time, runs targeted verification, and prepares concise replies only after the fix is confirmed. + +### `skill-effectiveness-auditor` + +Core skill for semantic skill review. It checks whether a skill will trigger reliably, guide useful agent behavior, avoid overlap with existing skills, and produce testable outcomes. It complements structural tools such as `skill-doctor`, `skill-trigger-tester`, `skill-conflict-detector`, and `tool-description-optimizer`. + +### `openclaw-config-advisor` + +OpenClaw-native stateful skill for read-only config diagnosis. It scans likely OpenClaw config files, reports provider, fallback, channel, MCP, and gateway health hints, and stores scan summaries in its own state file. + +### `quality-gate-orchestrator` + +OpenClaw-native stateful skill for task completion gates. It tracks required and optional validation gates, commands, last results, waivers, and readiness so agents do not declare completion before required checks pass or are explicitly waived. + +## Non-Goals + +- Do not copy external plugin or marketplace structure. +- Do not require network access for normal behavior. +- Do not modify untracked `.claude/` or `CLAUDE.md`. +- Do not perform destructive Git or filesystem actions. +- Do not write state outside `$OPENCLAW_HOME/skill-state//state.yaml`. + +## Acceptance Criteria + +- All four skills validate with the existing repo scripts. +- New OpenClaw-native scripts are standard-library only. +- Missing local OpenClaw config produces useful warnings, not crashes. +- README skill counts and tables match the filesystem. +- Validation passes through `scripts/validate-skills.sh`, `tests/test-runner.sh`, `tests/test-repo-fit-skill-pack.sh`, and `git diff --check`. diff --git a/skills/core/pull-request-feedback-loop/SKILL.md b/skills/core/pull-request-feedback-loop/SKILL.md new file mode 100644 index 0000000..a0deae2 --- /dev/null +++ b/skills/core/pull-request-feedback-loop/SKILL.md @@ -0,0 +1,63 @@ +--- +name: pull-request-feedback-loop +description: Handles PR review feedback by fetching comments, grouping issues, fixing one group at a time, and verifying before replies. +--- + +# Pull Request Feedback Loop + +PR feedback is a queue of review obligations, not a pile of comments. Use this skill when a user asks you to address PR comments, requested changes, failed checks tied to review feedback, or stale review threads. + +## When to Use + +- A PR has unresolved review comments or requested changes +- The user asks you to "address review feedback" or "fix PR comments" +- You need to reconcile code changes, replies, and verification + +Do not use this for ordinary local refactors with no review thread. + +## Process + +1. Identify the PR. + - Prefer the PR number or URL from the user. + - If missing, run `gh pr status` or `gh pr view --json number,url,headRefName`. + +2. Fetch review context. + - Run `gh pr view --comments`. + - If needed, run `gh api` for review threads or inline comments. + - Capture author, file, line, status, and requested change. + +3. Group comments by issue. + - Merge duplicate comments that point at the same underlying fix. + - Separate behavior bugs, test gaps, naming/docs cleanup, and questions. + - Mark comments that need clarification before code changes. + +4. Fix one group at a time. + - Inspect the referenced code before editing. + - Make the smallest coherent change for that group. + - Avoid bundling unrelated cleanup into review fixes. + +5. Verify before replying. + - Run the narrowest relevant test or check for each group. + - If no test can run, explain the exact reason and inspect manually. + - Do not claim a thread is resolved until verification passes or is explicitly waived. + +6. Prepare replies. + - Summarize what changed and how it was verified. + - Keep replies short and factual. + - Do not submit replies automatically unless the user asked you to. + +## Output + +Report: + +- Groups handled +- Files changed +- Verification run +- Threads ready for reply +- Threads needing user or reviewer clarification + +## Guardrails + +- Do not force-push, delete branches, or dismiss reviews without explicit permission. +- Do not mark comments resolved only because code was edited. +- Do not ignore a comment because it looks small; either address it or explain why it is not applicable. diff --git a/skills/core/skill-effectiveness-auditor/SKILL.md b/skills/core/skill-effectiveness-auditor/SKILL.md new file mode 100644 index 0000000..25e5628 --- /dev/null +++ b/skills/core/skill-effectiveness-auditor/SKILL.md @@ -0,0 +1,62 @@ +--- +name: skill-effectiveness-auditor +description: Reviews whether a skill will trigger reliably, guide useful behavior, avoid overlap, and produce testable outcomes. +--- + +# Skill Effectiveness Auditor + +Structural validation proves a skill can load. Effectiveness review asks whether the agent will use it well. Use this skill when reviewing a new skill, improving an existing skill, or deciding whether a proposed skill belongs in the library. + +## When to Use + +- A skill passes format checks but may still be vague or redundant +- A contributor proposes a new skill +- A skill is not triggering when expected +- A skill seems too broad, too long, or hard to verify + +## Audit Process + +1. State the intended behavior. + - Write one sentence describing what the skill should make the agent do. + - List 3 user prompts that should trigger it. + - List 2 prompts that should not trigger it. + +2. Check trigger clarity. + - The frontmatter description should name the task and the trigger. + - Avoid generic descriptions such as "helps with quality" or "improves workflow". + - Prefer concrete verbs: reviews, validates, scans, plans, records, summarizes. + +3. Simulate agent use. + - Walk through the skill as if responding to a real prompt. + - Note any step where the agent must guess policy, inputs, output format, or stopping conditions. + - Flag steps that say "think about" without telling the agent what to produce. + +4. Check overlap. + - Compare with nearby skills before approving a new one. + - If overlap is mostly structural, merge or reference the existing skill. + - If the new skill owns a distinct trigger, state that difference clearly. + +5. Check testability. + - The output should show whether the skill was followed. + - Add verification criteria for high-risk workflows. + - For stateful skills, require `STATE_SCHEMA.yaml` rather than prose-only memory. + +## Verdicts + +Use one verdict: + +- `keep` - clear trigger, useful behavior, low overlap +- `revise` - useful idea with fixable trigger or process gaps +- `split` - too broad for one skill +- `remove` - duplicated, vague, or not a skill-level behavior + +## Output + +Return: + +- Verdict +- Trigger assessment +- Actionability issues +- Overlap risks +- Suggested frontmatter rewrite +- Required edits before merge diff --git a/skills/openclaw-native/openclaw-config-advisor/SKILL.md b/skills/openclaw-native/openclaw-config-advisor/SKILL.md new file mode 100644 index 0000000..46a5b44 --- /dev/null +++ b/skills/openclaw-native/openclaw-config-advisor/SKILL.md @@ -0,0 +1,59 @@ +--- +name: openclaw-config-advisor +version: "1.0" +category: openclaw-native +description: Diagnoses OpenClaw provider, fallback, channel, MCP, and gateway config issues with read-only scans and stateful summaries. +stateful: true +--- + +# OpenClaw Config Advisor + +Config problems usually look like model failures, missing tools, or broken channels. Use this skill when OpenClaw behaves inconsistently and you need a read-only diagnosis before changing settings. + +## When to Invoke + +- Provider routing or fallback model behavior is wrong +- A channel cannot connect or stops receiving messages +- MCP tools are missing, stale, or unavailable +- The gateway starts but behaves differently than expected +- You need a current config summary before editing OpenClaw settings + +## Checks + +| Area | What to inspect | +|---|---| +| Providers | Provider keys, model names, default model hints | +| Fallback | Fallback or backup model settings | +| Channels | Telegram, Discord, Slack, WhatsApp, Signal, or Lark entries | +| MCP | MCP server declarations and command/url hints | +| Gateway | Host, port, log, and process configuration hints | + +## How to Use + +```bash +python3 advise.py --scan +python3 advise.py --scan --config-dir ~/.openclaw +python3 advise.py --explain fallback +python3 advise.py --status +python3 advise.py --format json +``` + +## Procedure + +1. Run `python3 advise.py --scan --format json`. +2. Review findings by severity: `WARN` first, then `INFO`. +3. Inspect the named config files before editing anything. +4. Apply config changes only after the user confirms the intended routing or channel behavior. +5. Re-run the scan and compare with the prior state summary. + +## State + +Scan summaries are stored in `~/.openclaw/skill-state/openclaw-config-advisor/state.yaml`. + +Fields: `last_scan_at`, `config_dir`, `findings`, `affected_areas`, and `scan_history`. + +## Guardrails + +- This skill is read-only; it does not modify OpenClaw config. +- Missing config directories are warnings, not crashes. +- Do not print secrets. Report that secret-like values exist without echoing them. diff --git a/skills/openclaw-native/openclaw-config-advisor/STATE_SCHEMA.yaml b/skills/openclaw-native/openclaw-config-advisor/STATE_SCHEMA.yaml new file mode 100644 index 0000000..077cb03 --- /dev/null +++ b/skills/openclaw-native/openclaw-config-advisor/STATE_SCHEMA.yaml @@ -0,0 +1,17 @@ +version: "1.0" +fields: + last_scan_at: + type: string + description: ISO timestamp of the last config scan. + config_dir: + type: string + description: Directory scanned for OpenClaw config files. + findings: + type: list + description: Current findings with severity, area, file, and message. + affected_areas: + type: list + description: Config areas touched by current findings. + scan_history: + type: list + description: Recent scan summaries. diff --git a/skills/openclaw-native/openclaw-config-advisor/advise.py b/skills/openclaw-native/openclaw-config-advisor/advise.py new file mode 100755 index 0000000..a5715c5 --- /dev/null +++ b/skills/openclaw-native/openclaw-config-advisor/advise.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +""" +Read-only OpenClaw config advisor. + +Scans likely OpenClaw config files for provider, fallback, channel, MCP, and +gateway hints. Writes only its own state file under OPENCLAW_HOME. +""" + +import argparse +import json +import os +import re +from datetime import datetime +from pathlib import Path + + +OPENCLAW_HOME = Path(os.environ.get("OPENCLAW_HOME", Path.home() / ".openclaw")) +STATE_FILE = OPENCLAW_HOME / "skill-state" / "openclaw-config-advisor" / "state.yaml" +CONFIG_EXTS = {".json", ".yaml", ".yml", ".toml", ".env", ".conf"} +SECRET_RE = re.compile(r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?([^'\"\s]+)", re.I) +MAX_FILES = 80 +MAX_BYTES = 512_000 + + +def now() -> str: + return datetime.now().isoformat(timespec="seconds") + + +def default_state() -> dict: + return { + "last_scan_at": "", + "config_dir": "", + "findings": [], + "affected_areas": [], + "scan_history": [], + } + + +def load_state() -> dict: + if not STATE_FILE.exists(): + return default_state() + try: + data = json.loads(STATE_FILE.read_text()) + if isinstance(data, dict): + return {**default_state(), **data} + except Exception: + pass + return default_state() + + +def save_state(state: dict) -> None: + STATE_FILE.parent.mkdir(parents=True, exist_ok=True) + STATE_FILE.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n") + + +def finding(severity: str, area: str, message: str, file_path=None) -> dict: + return { + "severity": severity, + "area": area, + "file": str(file_path) if file_path else "", + "message": message, + } + + +def candidate_files(config_dir: Path) -> list[Path]: + if config_dir.is_file(): + return [config_dir] + files = [] + for path in config_dir.rglob("*"): + if len(files) >= MAX_FILES: + break + if path.is_file() and path.suffix.lower() in CONFIG_EXTS: + try: + if path.stat().st_size <= MAX_BYTES: + files.append(path) + except OSError: + continue + return files + + +def read_file(path: Path) -> str: + try: + return path.read_text(errors="ignore") + except Exception: + return "" + + +def scan_config(config_dir: Path) -> dict: + findings = [] + scanned_at = now() + + if not config_dir.exists(): + findings.append(finding( + "WARN", + "config", + "Config path does not exist; check OPENCLAW_HOME or pass --config-dir.", + config_dir, + )) + return build_scan_result(scanned_at, config_dir, findings) + + files = candidate_files(config_dir) + if not files: + findings.append(finding( + "WARN", + "config", + "No readable config files found under the config path.", + config_dir, + )) + return build_scan_result(scanned_at, config_dir, findings) + + combined = "\n".join(read_file(path).lower() for path in files) + by_file = [(path, read_file(path)) for path in files] + + checks = [ + ("providers", ["provider", "openai", "anthropic", "openrouter", "model"], "No provider or model routing hints found."), + ("fallback", ["fallback", "backup", "default_model", "default-model"], "No fallback model hint found."), + ("channels", ["telegram", "discord", "slack", "whatsapp", "signal", "feishu", "lark"], "No channel configuration hints found."), + ("mcp", ["mcp", "modelcontextprotocol", "server", "tool"], "No MCP/tool server hints found."), + ("gateway", ["gateway", "host", "port", "listen", "log"], "No gateway host, port, or log hints found."), + ] + + for area, keywords, warning in checks: + if any(keyword in combined for keyword in keywords): + matched_file = next((path for path, text in by_file if any(k in text.lower() for k in keywords)), files[0]) + findings.append(finding("INFO", area, f"Found {area} config hints.", matched_file)) + else: + findings.append(finding("WARN", area, warning, config_dir)) + + for path, text in by_file: + if SECRET_RE.search(text): + findings.append(finding( + "INFO", + "secrets", + "Secret-like setting found; value was not printed. Confirm permissions and storage policy.", + path, + )) + + return build_scan_result(scanned_at, config_dir, findings) + + +def build_scan_result(scanned_at: str, config_dir: Path, findings: list[dict]) -> dict: + affected = sorted({item["area"] for item in findings}) + warn_count = sum(1 for item in findings if item["severity"] == "WARN") + return { + "scanned_at": scanned_at, + "config_dir": str(config_dir), + "findings": findings, + "affected_areas": affected, + "summary": { + "finding_count": len(findings), + "warn_count": warn_count, + "info_count": len(findings) - warn_count, + }, + } + + +def record_scan(result: dict) -> dict: + state = load_state() + state["last_scan_at"] = result["scanned_at"] + state["config_dir"] = result["config_dir"] + state["findings"] = result["findings"] + state["affected_areas"] = result["affected_areas"] + history = state.get("scan_history") or [] + history.append({ + "scanned_at": result["scanned_at"], + "config_dir": result["config_dir"], + "finding_count": result["summary"]["finding_count"], + "warn_count": result["summary"]["warn_count"], + }) + state["scan_history"] = history[-10:] + save_state(state) + return state + + +def explain(topic: str) -> dict: + notes = { + "providers": "Check provider names, model identifiers, and API key references. Do not print secret values.", + "fallback": "Fallback config should name the backup provider/model and the condition that triggers it.", + "channels": "Channel config should include enabled channels, credentials by reference, and webhook or polling mode.", + "mcp": "MCP config should declare each server transport, command or URL, and expected tool availability.", + "gateway": "Gateway config should make host, port, logs, and process ownership easy to inspect.", + } + key = topic.lower() + return {"topic": topic, "guidance": notes.get(key, "Known topics: providers, fallback, channels, mcp, gateway.")} + + +def print_text(payload: dict) -> None: + if "guidance" in payload: + print(f"{payload['topic']}: {payload['guidance']}") + return + findings = payload.get("findings", []) + print("OpenClaw Config Advisor") + print(f"Config: {payload.get('config_dir', 'unknown')}") + print(f"Findings: {len(findings)}") + for item in findings: + location = f" ({item['file']})" if item.get("file") else "" + print(f"- {item['severity']} {item['area']}: {item['message']}{location}") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Read-only OpenClaw config advisor.") + parser.add_argument("--scan", action="store_true", help="Scan config files and write advisor state.") + parser.add_argument("--status", action="store_true", help="Show the last advisor state.") + parser.add_argument("--explain", metavar="TOPIC", help="Explain a config area.") + parser.add_argument("--config-dir", default=str(OPENCLAW_HOME), help="OpenClaw config path to scan.") + parser.add_argument("--format", dest="output_format", choices=["text", "json"], default="text") + args = parser.parse_args() + + if args.explain: + payload = explain(args.explain) + elif args.scan: + result = scan_config(Path(args.config_dir).expanduser()) + record_scan(result) + payload = result + else: + payload = load_state() + + if args.output_format == "json": + print(json.dumps(payload, indent=2, sort_keys=True)) + else: + print_text(payload) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/openclaw-native/openclaw-config-advisor/example-state.yaml b/skills/openclaw-native/openclaw-config-advisor/example-state.yaml new file mode 100644 index 0000000..355c7d7 --- /dev/null +++ b/skills/openclaw-native/openclaw-config-advisor/example-state.yaml @@ -0,0 +1,14 @@ +last_scan_at: "2026-04-16T09:00:00" +config_dir: "/Users/example/.openclaw" +findings: + - severity: WARN + area: fallback + file: config.yaml + message: "No fallback model hint found." +affected_areas: + - fallback +scan_history: + - scanned_at: "2026-04-16T09:00:00" + config_dir: "/Users/example/.openclaw" + finding_count: 1 + warn_count: 1 diff --git a/skills/openclaw-native/quality-gate-orchestrator/SKILL.md b/skills/openclaw-native/quality-gate-orchestrator/SKILL.md new file mode 100644 index 0000000..e1cdb43 --- /dev/null +++ b/skills/openclaw-native/quality-gate-orchestrator/SKILL.md @@ -0,0 +1,59 @@ +--- +name: quality-gate-orchestrator +version: "1.0" +category: openclaw-native +description: Tracks required validation gates, records pass/fail/waived results, and reports readiness before task completion. +stateful: true +--- + +# Quality Gate Orchestrator + +Agents often remember to run checks at the end, when it is easiest to miss a failed or skipped gate. Use this skill to track required validation throughout the task. + +## When to Invoke + +- A task has multiple test, lint, build, review, or migration checks +- The user asks for tests-first, verification-first, or completion gates +- Work spans sessions and validation state must survive handoff +- The agent is about to declare completion + +## Gate Model + +Each gate has: + +- `name` - stable identifier such as `lint`, `unit-tests`, or `migration-dry-run` +- `command` - command to run, if applicable +- `required` - required gates must pass or be waived before completion +- `last_status` - `pending`, `pass`, `fail`, or `waived` +- `note` - short reason, error summary, or waiver explanation + +## How to Use + +```bash +python3 gate.py --status +python3 gate.py --add --name lint --command "npm run lint" +python3 gate.py --add --name smoke --command "npm test" --optional +python3 gate.py --record --name lint --status pass +python3 gate.py --record --name smoke --status waived --note "Not relevant for docs-only change" +python3 gate.py --ready --format json +``` + +## Procedure + +1. At the start of risky work, add the expected gates. +2. After each check, record `pass`, `fail`, or `waived`. +3. If a required gate fails, fix the issue and record a new result. +4. Before completion, run `python3 gate.py --ready`. +5. Do not claim completion unless readiness is true or the user accepts the remaining risk. + +## State + +Gate state is stored in `~/.openclaw/skill-state/quality-gate-orchestrator/state.yaml`. + +Fields: `gates`, `last_ready_at`, `ready`, and `gate_history`. + +## Guardrails + +- Recording a gate result does not run the command for you. +- Waivers need a note. +- Required gates default to pending until recorded. diff --git a/skills/openclaw-native/quality-gate-orchestrator/STATE_SCHEMA.yaml b/skills/openclaw-native/quality-gate-orchestrator/STATE_SCHEMA.yaml new file mode 100644 index 0000000..f2914ce --- /dev/null +++ b/skills/openclaw-native/quality-gate-orchestrator/STATE_SCHEMA.yaml @@ -0,0 +1,14 @@ +version: "1.0" +fields: + gates: + type: list + description: Known gates with name, command, required flag, last status, note, and timestamp. + ready: + type: boolean + description: Whether all required gates have passed or been waived. + last_ready_at: + type: string + description: ISO timestamp of the last readiness check. + gate_history: + type: list + description: Recent add, record, and readiness events. diff --git a/skills/openclaw-native/quality-gate-orchestrator/example-state.yaml b/skills/openclaw-native/quality-gate-orchestrator/example-state.yaml new file mode 100644 index 0000000..1e5f0b4 --- /dev/null +++ b/skills/openclaw-native/quality-gate-orchestrator/example-state.yaml @@ -0,0 +1,17 @@ +gates: + - name: lint + command: "npm run lint" + required: true + last_status: pass + note: "Passed after style fix." + updated_at: "2026-04-16T09:30:00" +ready: true +last_ready_at: "2026-04-16T09:31:00" +gate_history: + - event: add + name: lint + at: "2026-04-16T09:20:00" + - event: record + name: lint + status: pass + at: "2026-04-16T09:30:00" diff --git a/skills/openclaw-native/quality-gate-orchestrator/gate.py b/skills/openclaw-native/quality-gate-orchestrator/gate.py new file mode 100755 index 0000000..226bd4e --- /dev/null +++ b/skills/openclaw-native/quality-gate-orchestrator/gate.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +Stateful quality gate tracker for OpenClaw tasks. + +Records validation gates and reports whether required gates have passed or +been waived. Writes only its own state file under OPENCLAW_HOME. +""" + +import argparse +import json +import os +from datetime import datetime +from pathlib import Path + + +OPENCLAW_HOME = Path(os.environ.get("OPENCLAW_HOME", Path.home() / ".openclaw")) +STATE_FILE = OPENCLAW_HOME / "skill-state" / "quality-gate-orchestrator" / "state.yaml" +PASSING = {"pass", "waived"} + + +def now() -> str: + return datetime.now().isoformat(timespec="seconds") + + +def default_state() -> dict: + return {"gates": [], "ready": True, "last_ready_at": "", "gate_history": []} + + +def load_state() -> dict: + if not STATE_FILE.exists(): + return default_state() + try: + data = json.loads(STATE_FILE.read_text()) + if isinstance(data, dict): + return {**default_state(), **data} + except Exception: + pass + return default_state() + + +def save_state(state: dict) -> None: + STATE_FILE.parent.mkdir(parents=True, exist_ok=True) + STATE_FILE.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n") + + +def find_gate(state: dict, name: str): + for gate in state.get("gates", []): + if gate.get("name") == name: + return gate + return None + + +def append_history(state: dict, event: dict) -> None: + history = state.get("gate_history") or [] + history.append(event) + state["gate_history"] = history[-25:] + + +def compute_ready(state: dict) -> dict: + blockers = [] + for gate in state.get("gates", []): + if gate.get("required", True) and gate.get("last_status", "pending") not in PASSING: + blockers.append({ + "name": gate.get("name", ""), + "status": gate.get("last_status", "pending"), + "command": gate.get("command", ""), + }) + ready = not blockers + state["ready"] = ready + state["last_ready_at"] = now() + return {"ready": ready, "blockers": blockers, "gates": state.get("gates", [])} + + +def add_gate(args: argparse.Namespace) -> dict: + state = load_state() + stamp = now() + gate = find_gate(state, args.name) + if gate is None: + gate = { + "name": args.name, + "command": args.command or "", + "required": not args.optional, + "last_status": "pending", + "note": args.note or "", + "updated_at": stamp, + } + state["gates"].append(gate) + else: + gate["command"] = args.command or gate.get("command", "") + gate["required"] = not args.optional + gate["note"] = args.note or gate.get("note", "") + gate["updated_at"] = stamp + append_history(state, {"event": "add", "name": args.name, "at": stamp}) + compute_ready(state) + save_state(state) + return {"action": "add", "gate": gate, "ready": state["ready"]} + + +def record_gate(args: argparse.Namespace) -> tuple[dict, int]: + state = load_state() + gate = find_gate(state, args.name) + if gate is None: + return {"error": f"Unknown gate: {args.name}", "known_gates": state.get("gates", [])}, 1 + if args.status == "waived" and not args.note: + return {"error": "Waived gates require --note."}, 1 + stamp = now() + gate["last_status"] = args.status + gate["note"] = args.note or gate.get("note", "") + gate["updated_at"] = stamp + append_history(state, {"event": "record", "name": args.name, "status": args.status, "at": stamp}) + readiness = compute_ready(state) + save_state(state) + return {"action": "record", "gate": gate, **readiness}, 0 + + +def status_payload() -> dict: + state = load_state() + readiness = compute_ready(state) + return {**state, "blockers": readiness["blockers"]} + + +def ready_payload() -> dict: + state = load_state() + readiness = compute_ready(state) + append_history(state, {"event": "ready", "ready": readiness["ready"], "at": state["last_ready_at"]}) + save_state(state) + return readiness + + +def print_text(payload: dict) -> None: + if "error" in payload: + print(f"ERROR: {payload['error']}") + return + gates = payload.get("gates", []) + ready = payload.get("ready", False) + print("Quality Gate Orchestrator") + print(f"Ready: {str(ready).lower()}") + if not gates: + print("No gates recorded.") + return + for gate in gates: + required = "required" if gate.get("required", True) else "optional" + print(f"- {gate.get('name')}: {gate.get('last_status', 'pending')} ({required})") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Track task validation gates.") + mode = parser.add_mutually_exclusive_group() + mode.add_argument("--status", action="store_true", help="Show gate state.") + mode.add_argument("--add", action="store_true", help="Add or update a gate.") + mode.add_argument("--record", action="store_true", help="Record a gate result.") + mode.add_argument("--ready", action="store_true", help="Report completion readiness.") + parser.add_argument("--name", help="Gate name.") + parser.add_argument("--command", default="", help="Command associated with a gate.") + parser.add_argument("--status-value", dest="status_alias", choices=["pass", "fail", "waived", "pending"], help=argparse.SUPPRESS) + parser.add_argument("--status-result", dest="status_result", choices=["pass", "fail", "waived", "pending"], help=argparse.SUPPRESS) + parser.add_argument("--result", choices=["pass", "fail", "waived", "pending"], help=argparse.SUPPRESS) + parser.add_argument("--optional", action="store_true", help="Mark gate optional.") + parser.add_argument("--note", default="", help="Result note or waiver reason.") + parser.add_argument("--format", dest="output_format", choices=["text", "json"], default="text") + parser.add_argument("legacy_status", nargs="?", choices=["pass", "fail", "waived", "pending"], help=argparse.SUPPRESS) + + # argparse cannot reuse --status as both a mode and a result option. Accept + # the requested interface by pre-processing "--record ... --status pass". + import sys + argv = sys.argv[1:] + normalized = [] + record_mode = "--record" in argv + i = 0 + while i < len(argv): + if record_mode and argv[i] == "--status" and i + 1 < len(argv) and argv[i + 1] in {"pass", "fail", "waived", "pending"}: + normalized.extend(["--status-result", argv[i + 1]]) + i += 2 + continue + normalized.append(argv[i]) + i += 1 + + args = parser.parse_args(normalized) + result_status = args.status_result or args.status_alias or args.result or args.legacy_status + + exit_code = 0 + if args.add: + if not args.name: + payload, exit_code = {"error": "--add requires --name."}, 1 + else: + payload = add_gate(args) + elif args.record: + if not args.name or not result_status: + payload, exit_code = {"error": "--record requires --name and --status pass|fail|waived|pending."}, 1 + else: + args.status = result_status + payload, exit_code = record_gate(args) + elif args.ready: + payload = ready_payload() + else: + payload = status_payload() + + if args.output_format == "json": + print(json.dumps(payload, indent=2, sort_keys=True)) + else: + print_text(payload) + return exit_code + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test-repo-fit-skill-pack.sh b/tests/test-repo-fit-skill-pack.sh new file mode 100755 index 0000000..77df9b6 --- /dev/null +++ b/tests/test-repo-fit-skill-pack.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +fail() { + echo "FAIL: $1" + exit 1 +} + +assert_file() { + [ -f "$1" ] || fail "missing file: $1" +} + +assert_dir() { + [ -d "$1" ] || fail "missing directory: $1" +} + +assert_json() { + python3 -m json.tool >/dev/null || fail "$1 did not emit valid JSON" +} + +assert_skill() { + local category="$1" + local name="$2" + local dir="$REPO_DIR/skills/$category/$name" + local skill="$dir/SKILL.md" + + assert_dir "$dir" + assert_file "$skill" + + local first_line + first_line="$(head -1 "$skill")" + [ "$first_line" = "---" ] || fail "$name SKILL.md missing frontmatter" + + local fm_name + fm_name="$(sed -n '2,/^---$/p' "$skill" | grep '^name:' | sed 's/^name: *//' | tr -d '[:space:]')" + [ "$fm_name" = "$name" ] || fail "$name frontmatter name mismatch: $fm_name" +} + +assert_stateful_skill() { + local name="$1" + local dir="$REPO_DIR/skills/openclaw-native/$name" + + assert_skill "openclaw-native" "$name" + assert_file "$dir/STATE_SCHEMA.yaml" + assert_file "$dir/example-state.yaml" +} + +echo "Testing repo-fit skill pack..." + +assert_skill "core" "pull-request-feedback-loop" +assert_skill "core" "skill-effectiveness-auditor" +assert_stateful_skill "openclaw-config-advisor" +assert_stateful_skill "quality-gate-orchestrator" + +python3 "$REPO_DIR/skills/openclaw-native/openclaw-config-advisor/advise.py" --help >/dev/null +python3 "$REPO_DIR/skills/openclaw-native/quality-gate-orchestrator/gate.py" --help >/dev/null + +export OPENCLAW_HOME="$TMP_DIR/openclaw-home" +MISSING_CONFIG="$TMP_DIR/missing-config" + +python3 "$REPO_DIR/skills/openclaw-native/openclaw-config-advisor/advise.py" \ + --scan --config-dir "$MISSING_CONFIG" --format json | assert_json "openclaw-config-advisor scan" + +python3 "$REPO_DIR/skills/openclaw-native/quality-gate-orchestrator/gate.py" \ + --status --format json | assert_json "quality-gate-orchestrator status" + +python3 "$REPO_DIR/skills/openclaw-native/quality-gate-orchestrator/gate.py" \ + --add --name lint --command "npm test" --format json | assert_json "quality-gate-orchestrator add" + +python3 "$REPO_DIR/skills/openclaw-native/quality-gate-orchestrator/gate.py" \ + --record --name lint --status pass --format json | assert_json "quality-gate-orchestrator record" + +READY_JSON="$TMP_DIR/ready.json" +python3 "$REPO_DIR/skills/openclaw-native/quality-gate-orchestrator/gate.py" \ + --ready --format json > "$READY_JSON" +python3 -m json.tool "$READY_JSON" >/dev/null || fail "quality-gate-orchestrator ready did not emit valid JSON" +python3 - "$READY_JSON" <<'PY' +import json +import sys + +with open(sys.argv[1]) as f: + data = json.load(f) + +if data.get("ready") is not True: + raise SystemExit("FAIL: expected ready=true after required gate passed") +PY + +echo "PASS: repo-fit skill pack"