diff --git a/Makefile b/Makefile index 13a8b2e..53b2c10 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PYTHON ?= python3 LOCAL_SKILL_INSTALL_DIR ?= $(HOME)/.agents/skills.disabled/yao-meta-skill ACTIVE_SKILL_INSTALL_DIR ?= $(HOME)/.agents/skills/yao-meta-skill -.PHONY: eval eval-suite route-scorecard route-confusion-check description-optimization judge-blind-eval description-optimization-check promotion-check python-compat-check architecture-maintainability-check yao-cli-check skill-overview-check skill-interpretation-check skill-report-metrics-check skill-report-charts-check html-rendering-check skill-ir-check compiler-check output-eval-check output-execution-check output-review-kit-check output-review-adjudication-check runtime-conformance-check runtime-permission-check trust-check skill-atlas-check registry-audit-check package-verify-check install-simulation-check upgrade-check review-viewer-check review-studio-check skill-os2-audit-check skill-os2-coverage-check world-class-evidence-check world-class-ledger-check world-class-intake-check world-class-preflight-check world-class-submission-review-check world-class-runbook-check world-class-claim-guard-check benchmark-reproducibility-check evidence-consistency-check feedback-check adaptation-safety-check skillops-opportunity-check daily-skillops-check adoption-drift-check telemetry-import-check telemetry-emit-check telemetry-hooks-check telemetry-native-host-check review-waivers-check review-annotations-check baseline-compare-check reference-scan-check github-benchmark-scan-check intent-confidence-check reference-synthesis-check output-risk-profile-check artifact-design-profile-check prompt-quality-profile-check system-model-check iteration-directions-check description-drift-history iteration-ledger results-panel regression-history context-reports portability-report portability-check failure-regression-check package-check package-failure-check security-boundary-check local-install-sync-check snapshot-check validate lint governance-check resource-boundary-check quality-check sync-local-install sync-active-install test ci-test clean +.PHONY: eval eval-suite route-scorecard route-confusion-check description-optimization judge-blind-eval description-optimization-check promotion-check python-compat-check architecture-maintainability-check yao-cli-check skill-overview-check skill-interpretation-check skill-report-metrics-check skill-report-charts-check html-rendering-check skill-ir-check compiler-check output-eval-check output-execution-check output-review-kit-check output-review-adjudication-check runtime-conformance-check runtime-permission-check trust-check skill-atlas-check registry-audit-check package-verify-check install-simulation-check upgrade-check review-viewer-check review-studio-check skill-os2-audit-check skill-os2-coverage-check world-class-evidence-check world-class-ledger-check world-class-intake-check world-class-preflight-check world-class-submission-review-check world-class-runbook-check world-class-claim-guard-check benchmark-reproducibility-check evidence-consistency-check feedback-check adaptation-safety-check skillops-opportunity-check daily-skillops-check weekly-curator-check adoption-drift-check telemetry-import-check telemetry-emit-check telemetry-hooks-check telemetry-native-host-check review-waivers-check review-annotations-check baseline-compare-check reference-scan-check github-benchmark-scan-check intent-confidence-check reference-synthesis-check output-risk-profile-check artifact-design-profile-check prompt-quality-profile-check system-model-check iteration-directions-check description-drift-history iteration-ledger results-panel regression-history context-reports portability-report portability-check failure-regression-check package-check package-failure-check security-boundary-check local-install-sync-check snapshot-check validate lint governance-check resource-boundary-check quality-check sync-local-install sync-active-install test ci-test clean eval: $(PYTHON) scripts/trigger_eval.py --description-file evals/improved_description.txt --cases evals/trigger_cases.json --baseline-description-file evals/baseline_description.txt @@ -146,6 +146,9 @@ skillops-opportunity-check: daily-skillops-check: $(PYTHON) tests/verify_daily_skillops.py +weekly-curator-check: + $(PYTHON) tests/verify_weekly_curator.py + adoption-drift-check: $(PYTHON) tests/verify_adoption_drift.py @@ -257,11 +260,11 @@ sync-local-install: package-check sync-active-install: package-check $(PYTHON) scripts/sync_local_install.py --install-dir "$(ACTIVE_SKILL_INSTALL_DIR)" -test: eval eval-suite route-scorecard route-confusion-check description-optimization description-optimization-check promotion-check python-compat-check architecture-maintainability-check yao-cli-check skill-overview-check skill-interpretation-check skill-report-metrics-check skill-report-charts-check html-rendering-check skill-ir-check compiler-check output-eval-check output-execution-check output-review-adjudication-check runtime-conformance-check runtime-permission-check trust-check skill-atlas-check registry-audit-check package-verify-check install-simulation-check upgrade-check review-viewer-check review-studio-check skill-os2-audit-check skill-os2-coverage-check world-class-evidence-check world-class-ledger-check world-class-intake-check world-class-preflight-check world-class-submission-review-check world-class-runbook-check world-class-claim-guard-check benchmark-reproducibility-check evidence-consistency-check feedback-check adaptation-safety-check skillops-opportunity-check daily-skillops-check adoption-drift-check telemetry-import-check telemetry-emit-check telemetry-hooks-check telemetry-native-host-check review-waivers-check review-annotations-check baseline-compare-check reference-scan-check github-benchmark-scan-check intent-confidence-check reference-synthesis-check output-risk-profile-check artifact-design-profile-check prompt-quality-profile-check system-model-check iteration-directions-check description-drift-history iteration-ledger regression-history context-reports portability-report portability-check failure-regression-check package-check package-failure-check security-boundary-check local-install-sync-check snapshot-check validate lint governance-check resource-boundary-check quality-check +test: eval eval-suite route-scorecard route-confusion-check description-optimization description-optimization-check promotion-check python-compat-check architecture-maintainability-check yao-cli-check skill-overview-check skill-interpretation-check skill-report-metrics-check skill-report-charts-check html-rendering-check skill-ir-check compiler-check output-eval-check output-execution-check output-review-adjudication-check runtime-conformance-check runtime-permission-check trust-check skill-atlas-check registry-audit-check package-verify-check install-simulation-check upgrade-check review-viewer-check review-studio-check skill-os2-audit-check skill-os2-coverage-check world-class-evidence-check world-class-ledger-check world-class-intake-check world-class-preflight-check world-class-submission-review-check world-class-runbook-check world-class-claim-guard-check benchmark-reproducibility-check evidence-consistency-check feedback-check adaptation-safety-check skillops-opportunity-check daily-skillops-check weekly-curator-check adoption-drift-check telemetry-import-check telemetry-emit-check telemetry-hooks-check telemetry-native-host-check review-waivers-check review-annotations-check baseline-compare-check reference-scan-check github-benchmark-scan-check intent-confidence-check reference-synthesis-check output-risk-profile-check artifact-design-profile-check prompt-quality-profile-check system-model-check iteration-directions-check description-drift-history iteration-ledger regression-history context-reports portability-report portability-check failure-regression-check package-check package-failure-check security-boundary-check local-install-sync-check snapshot-check validate lint governance-check resource-boundary-check quality-check ci-test: $(PYTHON) scripts/ci_test.py clean: - rm -rf dist tests/tmp tests/tmp_snapshot tests/tmp_cli tests/tmp_python_compat tests/tmp_architecture_maintainability tests/tmp_skill_overview tests/tmp_skill_interpretation tests/tmp_skill_report_metrics tests/tmp_skill_report_charts tests/tmp_skill_ir tests/tmp_compile_skill tests/tmp_output_eval tests/tmp_output_execution tests/tmp_output_review_adjudication tests/tmp_conformance tests/tmp_runtime_permission tests/tmp_trust tests/tmp_skill_atlas tests/tmp_registry tests/tmp_package_verification tests/tmp_install_simulation tests/tmp_upgrade_check tests/tmp_reference_scan tests/tmp_iteration_directions tests/tmp_review_viewer tests/tmp_review_studio tests/tmp_skill_os2_audit tests/tmp_skill_os2_coverage tests/tmp_world_class_evidence tests/tmp_world_class_evidence_ledger tests/tmp_world_class_evidence_intake tests/tmp_world_class_submission_review tests/tmp_world_class_operator_runbook tests/tmp_world_class_claim_guard tests/tmp_benchmark_reproducibility tests/tmp_evidence_consistency tests/tmp_feedback tests/tmp_daily_skillops tests/tmp_adoption_drift tests/tmp_telemetry_import tests/tmp_telemetry_emit tests/tmp_telemetry_hooks tests/tmp_telemetry_native_host tests/tmp_review_waivers tests/tmp_review_annotations tests/tmp_github_benchmark_scan tests/tmp_intent_confidence tests/tmp_reference_synthesis tests/tmp_output_risk_profile tests/tmp_artifact_design_profile tests/tmp_prompt_quality_profile tests/tmp_system_model tests/tmp_security tests/tmp_baseline_compare.json tests/tmp_baseline_compare.md + rm -rf dist tests/tmp tests/tmp_snapshot tests/tmp_cli tests/tmp_python_compat tests/tmp_architecture_maintainability tests/tmp_skill_overview tests/tmp_skill_interpretation tests/tmp_skill_report_metrics tests/tmp_skill_report_charts tests/tmp_skill_ir tests/tmp_compile_skill tests/tmp_output_eval tests/tmp_output_execution tests/tmp_output_review_adjudication tests/tmp_conformance tests/tmp_runtime_permission tests/tmp_trust tests/tmp_skill_atlas tests/tmp_registry tests/tmp_package_verification tests/tmp_install_simulation tests/tmp_upgrade_check tests/tmp_reference_scan tests/tmp_iteration_directions tests/tmp_review_viewer tests/tmp_review_studio tests/tmp_skill_os2_audit tests/tmp_skill_os2_coverage tests/tmp_world_class_evidence tests/tmp_world_class_evidence_ledger tests/tmp_world_class_evidence_intake tests/tmp_world_class_submission_review tests/tmp_world_class_operator_runbook tests/tmp_world_class_claim_guard tests/tmp_benchmark_reproducibility tests/tmp_evidence_consistency tests/tmp_feedback tests/tmp_daily_skillops tests/tmp_weekly_curator tests/tmp_adoption_drift tests/tmp_telemetry_import tests/tmp_telemetry_emit tests/tmp_telemetry_hooks tests/tmp_telemetry_native_host tests/tmp_review_waivers tests/tmp_review_annotations tests/tmp_github_benchmark_scan tests/tmp_intent_confidence tests/tmp_reference_synthesis tests/tmp_output_risk_profile tests/tmp_artifact_design_profile tests/tmp_prompt_quality_profile tests/tmp_system_model tests/tmp_security tests/tmp_baseline_compare.json tests/tmp_baseline_compare.md find . -type d -name __pycache__ -prune -exec rm -rf {} + diff --git a/README.md b/README.md index af84064..2367431 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ It turns rough workflows, transcripts, prompts, notes, and runbooks into reusabl - a local-first metadata-only adoption and drift report that turns real usage signals into next iteration candidates, with optional `yao.py` CLI run capture, external client event emit hooks, hook recipes, and JSONL import that record command names and outcomes without arguments or raw content - an explicit-source adaptive proposal loop that summarizes redacted repeated user preferences and generates approval-gated adaptation proposals without scanning private logs or writing source files - a SkillOps opportunity scorer and decision policy that ranks redacted repeated signals, maps them to report-only, AGENTS update, existing-skill patch, or eval-addition actions, and keeps every durable write approval-gated +- a weekly SkillOps curator report that aggregates daily opportunities, Skill Atlas portfolio signals, release lock state, and world-class evidence gaps into a proposal-only maintenance queue - a Browser/Chrome Native Messaging telemetry host that can receive length-prefixed metadata-only client events and generate a local launcher plus manifest without storing raw content - a Skill Atlas drift layer that reads aggregate adoption reports and surfaces portfolio-level drift signals without packaging raw telemetry logs - a baseline compare report for with-skill vs baseline review @@ -126,6 +127,7 @@ python3 scripts/yao.py feedback my-skill --note "Tighten exclusions before addin python3 scripts/yao.py adapt-scan my-skill --source ./curated-user-signals.jsonl python3 scripts/yao.py adapt-propose my-skill python3 scripts/yao.py daily-skillops my-skill --source ./curated-user-signals.jsonl +python3 scripts/yao.py weekly-curator my-skill python3 scripts/yao.py adoption-drift my-skill --record-event skill_activation --activation-type explicit --outcome accepted YAO_CLI_TELEMETRY=1 python3 scripts/yao.py validate my-skill python3 scripts/yao.py telemetry-emit my-skill --event skill_activation --activation-type explicit --outcome accepted --command browser-extension @@ -434,7 +436,7 @@ Utility scripts that make the meta-skill operational: - `run_description_optimization_suite.py`: runs description optimization across the root skill and governed examples, then writes reusable reports and optional drift snapshots with calibration and family summaries - `promotion_checker.py`: applies promotion policy to current description candidates, writes promotion decisions, builds candidate registries, and emits iteration bundles with review stubs - `create_iteration_snapshot.py`: freezes the current promotion decision into a versioned release snapshot with review, route, and context evidence -- `yao.py`: unified authoring CLI that exposes init, validate, optimize-description, promote-check, python-compat, review, release-snapshot, workspace-flow, report, skill-report, skill-interpretation, skill-ir, compile-skill, output-exec, output-review, skill-os2-audit, skill-os2-coverage, world-class-evidence, world-class-ledger, world-class-intake, world-class-preflight, world-class-submission-kit, world-class-submission-review, world-class-runbook, world-class-claim-guard, benchmark-reproducibility, evidence-consistency, adapt-scan, adapt-propose, adapt-apply, daily-skillops, telemetry-emit, telemetry-hooks, telemetry-import, package, registry-audit, package-verify, install-simulate, upgrade-check, review-waivers, and test as one entrypoint +- `yao.py`: unified authoring CLI that exposes init, validate, optimize-description, promote-check, python-compat, review, release-snapshot, workspace-flow, report, skill-report, skill-interpretation, skill-ir, compile-skill, output-exec, output-review, skill-os2-audit, skill-os2-coverage, world-class-evidence, world-class-ledger, world-class-intake, world-class-preflight, world-class-submission-kit, world-class-submission-review, world-class-runbook, world-class-claim-guard, benchmark-reproducibility, evidence-consistency, adapt-scan, adapt-propose, adapt-apply, daily-skillops, weekly-curator, telemetry-emit, telemetry-hooks, telemetry-import, package, registry-audit, package-verify, install-simulate, upgrade-check, review-waivers, and test as one entrypoint - `render_description_drift_history.py`: turns description-optimization snapshots into a readable drift-history report - `build_confusion_matrix.py`: scores route confusion across tracked sibling skills and `no_route` cases, then writes a route scorecard and optional milestone snapshot - `render_iteration_ledger.py`: compresses regression milestones, description optimization drift, and route scorecards into one iteration-facing ledger @@ -446,6 +448,7 @@ Utility scripts that make the meta-skill operational: - `render_skill_os2_audit.py`: renders a requirement-by-requirement Skill OS 2.0 audit that separates landed local evidence from human-required and external-required gaps - `render_skill_os2_coverage.py`: maps the Skill OS 2.0 upgrade blueprint to local artifacts, commands, tests, and remaining evidence boundaries - `render_daily_skillops_report.py`: renders an explicit-source Daily SkillOps operations report that summarizes redacted user patterns, proposal-only adaptations, approval state, release evidence, and world-class evidence gaps without scanning private logs or applying patches +- `render_weekly_curator_report.py`: renders a weekly SkillOps curator report from generated daily reports, Skill Atlas, benchmark lock, evidence consistency, and world-class ledger state without scanning private logs or applying patches - `skillops_opportunity.py`: scores redacted SkillOps opportunities and maps them to approval-gated action types such as report-only, AGENTS update, existing-skill patch, or eval addition - `render_world_class_evidence_plan.py`: renders executable evidence tasks for remaining world-class gaps without treating planned external work as completed evidence - `render_world_class_evidence_ledger.py`: renders a machine-checkable ledger for current world-class evidence acceptance, anti-overclaim guards, provenance requirements, and privacy contracts diff --git a/scripts/ci_test.py b/scripts/ci_test.py index 8e7a03b..0e779d4 100644 --- a/scripts/ci_test.py +++ b/scripts/ci_test.py @@ -69,6 +69,7 @@ DEFAULT_TARGETS = [ "adaptation-safety-check", "skillops-opportunity-check", "daily-skillops-check", + "weekly-curator-check", "adoption-drift-check", "telemetry-import-check", "telemetry-emit-check", diff --git a/scripts/render_skill_os2_coverage.py b/scripts/render_skill_os2_coverage.py index 76a96cf..a030f62 100644 --- a/scripts/render_skill_os2_coverage.py +++ b/scripts/render_skill_os2_coverage.py @@ -73,6 +73,24 @@ def latest_daily_skillops_paths(skill_dir: Path) -> list[str]: return [*base_paths, rel_path(latest_json, skill_dir), rel_path(latest_md, skill_dir)] +def latest_weekly_curator_paths(skill_dir: Path) -> list[str]: + base_paths = [ + "scripts/render_weekly_curator_report.py", + "tests/verify_weekly_curator.py", + ] + weekly_dir = skill_dir / "reports" / "skillops" / "weekly" + weekly_json_reports = sorted(weekly_dir.glob("*.json")) if weekly_dir.exists() else [] + if not weekly_json_reports: + return [ + *base_paths, + "reports/skillops/weekly/YYYY-WNN.json", + "reports/skillops/weekly/YYYY-WNN.md", + ] + latest_json = weekly_json_reports[-1] + latest_md = latest_json.with_suffix(".md") + return [*base_paths, rel_path(latest_json, skill_dir), rel_path(latest_md, skill_dir)] + + def condition_status(condition: bool, evidence_items: list[dict[str, Any]]) -> str: if not all_exist(evidence_items): return "missing" @@ -425,6 +443,26 @@ def build_coverage(skill_dir: Path, generated_at: str) -> dict[str, Any]: skill_dir=skill_dir, ) ) + weekly_curator_paths = latest_weekly_curator_paths(skill_dir) + weekly_curator_ready = paths_exist(skill_dir, weekly_curator_paths) + weekly_curator_status = "covered" if weekly_curator_ready else "partial" + extension_tracks.append( + build_extension_track( + key="weekly-curator-report", + label="Weekly Curator Report", + status=weekly_curator_status, + objective="Weekly curator layer aggregates Daily SkillOps opportunities, Skill Atlas portfolio signals, release locks, and world-class evidence gaps into a maintenance queue.", + current=( + "Weekly curator report is CLI-backed, proposal-only, and backed by committed weekly JSON/Markdown evidence." + if weekly_curator_ready + else "Daily SkillOps and Skill Atlas exist, but weekly curator aggregation is not fully wired into scripts, tests, and evidence artifacts." + ), + target="Generate reports/skillops/weekly/YYYY-WNN.* without private log scanning, source writes, auto-patching, or world-class overclaiming.", + artifact_paths=weekly_curator_paths, + next_action="Use weekly curator output as the Skill Librarian maintenance queue before approving any durable skill-library changes.", + skill_dir=skill_dir, + ) + ) extension_counts: dict[str, int] = {} for item in extension_tracks: extension_counts[item["status"]] = extension_counts.get(item["status"], 0) + 1 diff --git a/scripts/render_weekly_curator_report.py b/scripts/render_weekly_curator_report.py new file mode 100644 index 0000000..06b4092 --- /dev/null +++ b/scripts/render_weekly_curator_report.py @@ -0,0 +1,433 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from collections import Counter +from datetime import date, datetime, timezone +from pathlib import Path +from typing import Any + + +SCRIPT_INTERFACE = "cli" +SCRIPT_INTERFACE_REASON = "Renders a weekly SkillOps curator report from generated reports without scanning private logs or writing source files." + +SUMMARY_FIELDS = [ + "decision", + "week_id", + "daily_report_count", + "opportunity_count", + "unique_opportunity_count", + "ready_for_approval_review_count", + "proposal_review_count", + "observe_more_evidence_count", + "report_only_count", + "top_score", + "skill_count", + "actionable_portfolio_issue_count", + "release_lock_ready", + "evidence_consistency_ok", + "public_world_class_ready", + "world_class_pending_count", + "writes_source_files", + "auto_patch_enabled", + "failure_count", +] + + +def utc_now() -> str: + return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + + +def parse_date(value: str) -> date: + candidate = value[:10] if len(value) >= 10 else value + try: + return date.fromisoformat(candidate) + except ValueError: + return date.today() + + +def week_id(generated_at: str) -> str: + iso = parse_date(generated_at).isocalendar() + return f"{iso.year}-W{iso.week:02d}" + + +def display_path(path: Path, root: Path) -> str: + try: + return str(path.resolve().relative_to(root.resolve())) + except ValueError: + return f"[external-explicit-source]/{path.name}" + + +def resolve_output(skill_dir: Path, value: str) -> Path: + path = Path(value) + return path if path.is_absolute() else skill_dir / path + + +def load_json(path: Path) -> dict[str, Any]: + if not path.exists(): + return {} + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + return payload if isinstance(payload, dict) else {} + + +def write_json(path: Path, payload: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def write_text(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + + +def default_weekly_path(skill_dir: Path, generated_at: str, suffix: str) -> Path: + return skill_dir / "reports" / "skillops" / "weekly" / f"{week_id(generated_at)}.{suffix}" + + +def daily_report_paths(skill_dir: Path, explicit_paths: list[Path]) -> list[Path]: + if explicit_paths: + return [path if path.is_absolute() else skill_dir / path for path in explicit_paths] + daily_dir = skill_dir / "reports" / "skillops" / "daily" + if not daily_dir.exists(): + return [] + return sorted(daily_dir.glob("*.json")) + + +def _as_int(value: Any, default: int = 0) -> int: + if isinstance(value, bool): + return default + try: + return int(value) + except (TypeError, ValueError): + return default + + +def _dedupe_opportunities(daily_reports: list[dict[str, Any]]) -> list[dict[str, Any]]: + by_id: dict[str, dict[str, Any]] = {} + for report in daily_reports: + report_date = str(report.get("generated_at") or "")[:10] + for item in report.get("opportunities", []): + if not isinstance(item, dict): + continue + opportunity_id = str(item.get("opportunity_id") or "") + if not opportunity_id: + continue + current = {**item, "daily_report_date": report_date} + existing = by_id.get(opportunity_id) + if existing is None or _as_int(current.get("score")) > _as_int(existing.get("score")): + by_id[opportunity_id] = current + return sorted(by_id.values(), key=lambda item: (-_as_int(item.get("score")), str(item.get("opportunity_id")))) + + +def _counter(items: list[dict[str, Any]], key: str) -> dict[str, int]: + values = Counter(str(item.get(key) or "") for item in items) + values.pop("", None) + return dict(sorted(values.items())) + + +def _portfolio_issue_count(atlas_summary: dict[str, Any]) -> int: + keys = [ + "actionable_route_collision_count", + "actionable_owner_gap_count", + "actionable_stale_count", + "actionable_drift_signal_count", + "no_route_opportunity_count", + ] + return sum(_as_int(atlas_summary.get(key)) for key in keys) + + +def build_actions(summary: dict[str, Any], opportunities: list[dict[str, Any]], atlas_summary: dict[str, Any]) -> list[dict[str, str]]: + actions: list[dict[str, str]] = [] + if summary["ready_for_approval_review_count"]: + actions.append( + { + "key": "review-ready-opportunities", + "priority": "high", + "action": "Review the top ready-for-approval SkillOps opportunities before preparing any approval ledger entry.", + } + ) + elif opportunities: + actions.append( + { + "key": "review-proposal-queue", + "priority": "medium", + "action": "Review proposal-level SkillOps opportunities and keep low-evidence items in observation.", + } + ) + if summary["actionable_portfolio_issue_count"]: + actions.append( + { + "key": "triage-skill-library", + "priority": "high", + "action": "Triage actionable route collisions, owner gaps, stale skills, drift signals, or no-route opportunities from Skill Atlas.", + } + ) + if summary["world_class_pending_count"]: + actions.append( + { + "key": "close-world-class-evidence", + "priority": "high", + "action": "Collect accepted external or human evidence for pending world-class ledger entries before public claims.", + } + ) + if summary["evidence_consistency_ok"] is not True: + actions.append( + { + "key": "refresh-evidence-consistency", + "priority": "high", + "action": "Regenerate evidence consistency before using this curator report for release decisions.", + } + ) + if not atlas_summary: + actions.append( + { + "key": "refresh-skill-atlas", + "priority": "medium", + "action": "Generate Skill Atlas so weekly curation can see portfolio-level drift and stale-skill signals.", + } + ) + if not actions: + actions.append({"key": "monitor", "priority": "low", "action": "No curation action required beyond routine monitoring."}) + return actions + + +def build_report(skill_dir: Path, generated_at: str, daily_json: list[Path] | None = None) -> dict[str, Any]: + skill_dir = skill_dir.resolve() + reports_dir = skill_dir / "reports" + failures: list[str] = [] + + daily_paths = daily_report_paths(skill_dir, daily_json or []) + daily_reports = [] + for path in daily_paths: + payload = load_json(path) + if payload.get("ok") is True: + daily_reports.append(payload) + else: + failures.append(f"Daily SkillOps report is missing or invalid: {display_path(path, skill_dir)}") + + opportunities = _dedupe_opportunities(daily_reports) + decision_counts = _counter(opportunities, "decision") + action_counts = _counter(opportunities, "action_type") + risk_counts = _counter(opportunities, "risk_level") + + atlas = load_json(reports_dir / "skill_atlas.json") + atlas_summary = atlas.get("summary", {}) if isinstance(atlas.get("summary"), dict) else {} + benchmark = load_json(reports_dir / "benchmark_reproducibility.json") + benchmark_summary = benchmark.get("summary", {}) if isinstance(benchmark.get("summary"), dict) else {} + consistency = load_json(reports_dir / "evidence_consistency.json") + ledger = load_json(reports_dir / "world_class_evidence_ledger.json") + ledger_summary = ledger.get("summary", {}) if isinstance(ledger.get("summary"), dict) else {} + + portfolio_issue_count = _portfolio_issue_count(atlas_summary) + failure_count = len(failures) + summary = { + "decision": "blocked" + if failure_count + else "curator-review" + if opportunities or portfolio_issue_count or _as_int(ledger_summary.get("pending_count")) + else "monitor", + "week_id": week_id(generated_at), + "daily_report_count": len(daily_reports), + "opportunity_count": sum(_as_int(report.get("opportunity_summary", {}).get("opportunity_count")) for report in daily_reports), + "unique_opportunity_count": len(opportunities), + "ready_for_approval_review_count": decision_counts.get("ready_for_approval_review", 0), + "proposal_review_count": decision_counts.get("proposal_review", 0), + "observe_more_evidence_count": decision_counts.get("observe_more_evidence", 0), + "report_only_count": decision_counts.get("report_only", 0), + "top_score": _as_int(opportunities[0].get("score")) if opportunities else 0, + "skill_count": _as_int(atlas_summary.get("skill_count")), + "actionable_portfolio_issue_count": portfolio_issue_count, + "release_lock_ready": benchmark_summary.get("release_lock_ready") is True, + "evidence_consistency_ok": consistency.get("ok") is True, + "public_world_class_ready": ledger_summary.get("ready_to_claim_world_class") is True, + "world_class_pending_count": _as_int(ledger_summary.get("pending_count")), + "writes_source_files": False, + "auto_patch_enabled": False, + "failure_count": failure_count, + } + actions = build_actions(summary, opportunities, atlas_summary) + curator_contract = { + "schema_version": "1.0", + "contract": "weekly-skillops-curator-report", + "cadence": "weekly", + "source_of_truth": [ + "reports/skillops/daily/*.json", + "reports/skill_atlas.json", + "reports/benchmark_reproducibility.json", + "reports/evidence_consistency.json", + "reports/world_class_evidence_ledger.json", + ], + "raw_content_stored": False, + "redacted_or_generated_evidence_only": True, + "proposal_only": True, + "writes_source_files": False, + "auto_patch_enabled": False, + "approval_required_for_writes": True, + "counts_as_world_class_evidence": False, + } + report = { + "schema_version": "1.0", + "ok": failure_count == 0, + "generated_at": generated_at, + "skill_dir": display_path(skill_dir, skill_dir), + **{key: summary[key] for key in SUMMARY_FIELDS}, + "summary": summary, + "curator_contract": curator_contract, + "report_contract": { + "schema_version": "1.0", + "contract": "weekly-skillops-curator-report", + "top_level_mirrors_summary": True, + "summary_fields": SUMMARY_FIELDS, + }, + "daily_reports": [display_path(path, skill_dir) for path in daily_paths], + "opportunity_summary": { + "action_type_counts": action_counts, + "decision_counts": decision_counts, + "risk_level_counts": risk_counts, + }, + "curator_queue": opportunities[:20], + "portfolio": { + "skill_count": summary["skill_count"], + "actionable_issue_count": portfolio_issue_count, + "route_collision_count": _as_int(atlas_summary.get("route_collision_count")), + "actionable_route_collision_count": _as_int(atlas_summary.get("actionable_route_collision_count")), + "owner_gap_count": _as_int(atlas_summary.get("owner_gap_count")), + "actionable_owner_gap_count": _as_int(atlas_summary.get("actionable_owner_gap_count")), + "stale_count": _as_int(atlas_summary.get("stale_count")), + "actionable_stale_count": _as_int(atlas_summary.get("actionable_stale_count")), + "drift_signal_count": _as_int(atlas_summary.get("drift_signal_count")), + "actionable_drift_signal_count": _as_int(atlas_summary.get("actionable_drift_signal_count")), + "no_route_opportunity_count": _as_int(atlas_summary.get("no_route_opportunity_count")), + }, + "release_state": { + "release_lock_ready": summary["release_lock_ready"], + "evidence_consistency_ok": summary["evidence_consistency_ok"], + "public_world_class_ready": summary["public_world_class_ready"], + "world_class_pending_count": summary["world_class_pending_count"], + }, + "actions": actions, + "failures": failures, + "source_reports": { + "daily_dir": "reports/skillops/daily", + "skill_atlas": "reports/skill_atlas.json", + "benchmark_reproducibility": "reports/benchmark_reproducibility.json", + "evidence_consistency": "reports/evidence_consistency.json", + "world_class_ledger": "reports/world_class_evidence_ledger.json", + }, + "artifacts": { + "json": str(default_weekly_path(skill_dir, generated_at, "json").relative_to(skill_dir)), + "markdown": str(default_weekly_path(skill_dir, generated_at, "md").relative_to(skill_dir)), + }, + } + return report + + +def render_markdown(report: dict[str, Any]) -> str: + summary = report["summary"] + lines = [ + "# Weekly SkillOps Curator Report", + "", + f"Generated at: `{report['generated_at']}`", + f"Week: `{summary['week_id']}`", + "", + "## Summary", + "", + ] + for key in [ + "decision", + "daily_report_count", + "unique_opportunity_count", + "ready_for_approval_review_count", + "proposal_review_count", + "top_score", + "skill_count", + "actionable_portfolio_issue_count", + "release_lock_ready", + "evidence_consistency_ok", + "public_world_class_ready", + "world_class_pending_count", + ]: + lines.append(f"- {key}: `{summary[key]}`") + lines.extend( + [ + "", + "This report is a weekly curator cockpit for generated SkillOps evidence. It does not scan private logs, write source files, apply patches, or count as world-class evidence.", + "", + "## Curator Boundary", + "", + ] + ) + contract = report["curator_contract"] + for key in [ + "raw_content_stored", + "redacted_or_generated_evidence_only", + "proposal_only", + "writes_source_files", + "auto_patch_enabled", + "approval_required_for_writes", + "counts_as_world_class_evidence", + ]: + lines.append(f"- {key}: `{str(contract[key]).lower()}`") + lines.extend(["", "## Actions", ""]) + for action in report["actions"]: + lines.append(f"- `{action['priority']}` {action['action']}") + lines.extend(["", "## Curator Queue", ""]) + if not report["curator_queue"]: + lines.append("- No weekly curator opportunities.") + for item in report["curator_queue"]: + lines.extend( + [ + f"### {item.get('title', '')}", + "", + f"- ID: `{item.get('opportunity_id', '')}`", + f"- Action: `{item.get('action_type', '')}`", + f"- Decision: `{item.get('decision', '')}`", + f"- Score: `{item.get('score', 0)}`", + f"- Risk: `{item.get('risk_level', '')}`", + f"- Source day: `{item.get('daily_report_date', '')}`", + f"- Policy: {item.get('policy_reason', '')}", + "", + ] + ) + lines.extend(["## Portfolio Signals", ""]) + for key, value in report["portfolio"].items(): + lines.append(f"- {key}: `{value}`") + lines.extend(["", "## Evidence", ""]) + for label, path in report["source_reports"].items(): + lines.append(f"- {label}: `{path}`") + if report["failures"]: + lines.extend(["", "## Failures", ""]) + lines.extend(f"- {failure}" for failure in report["failures"]) + return "\n".join(lines).rstrip() + "\n" + + +def main() -> None: + parser = argparse.ArgumentParser(description="Render a weekly SkillOps curator report from generated SkillOps evidence.") + parser.add_argument("skill_dir", nargs="?", default=".") + parser.add_argument("--daily-json", action="append", default=[], help="Explicit Daily SkillOps JSON report to include.") + parser.add_argument("--output-json") + parser.add_argument("--output-md") + parser.add_argument("--generated-at", default=utc_now()) + args = parser.parse_args() + + skill_dir = Path(args.skill_dir).resolve() + report = build_report(skill_dir, args.generated_at, [Path(item) for item in args.daily_json]) + output_json = resolve_output(skill_dir, args.output_json) if args.output_json else default_weekly_path(skill_dir, args.generated_at, "json") + output_md = resolve_output(skill_dir, args.output_md) if args.output_md else default_weekly_path(skill_dir, args.generated_at, "md") + report["artifacts"] = { + "json": display_path(output_json, skill_dir), + "markdown": display_path(output_md, skill_dir), + } + write_json(output_json, report) + write_text(output_md, render_markdown(report)) + print(json.dumps(report, ensure_ascii=False, indent=2)) + if not report["ok"]: + raise SystemExit(2) + + +if __name__ == "__main__": + main() diff --git a/scripts/yao.py b/scripts/yao.py index 5dd33f9..f909d4c 100644 --- a/scripts/yao.py +++ b/scripts/yao.py @@ -10,7 +10,13 @@ from yao_cli_config import ( resolve_promotion_target, resolve_target, ) -from yao_cli_adaptation_commands import command_adapt_apply, command_adapt_propose, command_adapt_scan, command_daily_skillops +from yao_cli_adaptation_commands import ( + command_adapt_apply, + command_adapt_propose, + command_adapt_scan, + command_daily_skillops, + command_weekly_curator, +) from yao_cli_create_commands import command_init, command_quickstart from yao_cli_distribution_commands import ( command_compile_skill, diff --git a/scripts/yao_cli_adaptation_commands.py b/scripts/yao_cli_adaptation_commands.py index 9450f46..184e553 100644 --- a/scripts/yao_cli_adaptation_commands.py +++ b/scripts/yao_cli_adaptation_commands.py @@ -103,3 +103,19 @@ def command_daily_skillops(args: argparse.Namespace) -> int: result = run_script("render_daily_skillops_report.py", cmd) print(json.dumps(result["payload"] if result["payload"] is not None else result, ensure_ascii=False, indent=2)) return 0 if result["ok"] else 2 + + +def command_weekly_curator(args: argparse.Namespace) -> int: + skill_dir = str(Path(args.skill_dir).resolve()) + cmd = [skill_dir] + for path in args.daily_json: + cmd.extend(["--daily-json", path]) + if args.output_json: + cmd.extend(["--output-json", args.output_json]) + if args.output_md: + cmd.extend(["--output-md", args.output_md]) + if args.generated_at: + cmd.extend(["--generated-at", args.generated_at]) + result = run_script("render_weekly_curator_report.py", cmd) + print(json.dumps(result["payload"] if result["payload"] is not None else result, ensure_ascii=False, indent=2)) + return 0 if result["ok"] else 2 diff --git a/scripts/yao_cli_parser.py b/scripts/yao_cli_parser.py index 3c69114..430c542 100644 --- a/scripts/yao_cli_parser.py +++ b/scripts/yao_cli_parser.py @@ -475,6 +475,17 @@ def build_parser(command_handlers: dict[str, Callable[[argparse.Namespace], int] daily_skillops_cmd.add_argument("--no-refresh-source-reports", action="store_true") daily_skillops_cmd.set_defaults(func=_handler(command_handlers, "command_daily_skillops")) + weekly_curator_cmd = subparsers.add_parser( + "weekly-curator", + help="Render a weekly SkillOps curator report from generated daily reports and portfolio evidence.", + ) + weekly_curator_cmd.add_argument("skill_dir", nargs="?", default=".") + weekly_curator_cmd.add_argument("--daily-json", action="append", default=[]) + weekly_curator_cmd.add_argument("--output-json") + weekly_curator_cmd.add_argument("--output-md") + weekly_curator_cmd.add_argument("--generated-at") + weekly_curator_cmd.set_defaults(func=_handler(command_handlers, "command_weekly_curator")) + adoption_drift_cmd = subparsers.add_parser( "adoption-drift", help="Render local-first metadata-only adoption and drift telemetry for a skill package.", diff --git a/tests/verify_review_studio.py b/tests/verify_review_studio.py index 0e0c325..5fd0eb7 100644 --- a/tests/verify_review_studio.py +++ b/tests/verify_review_studio.py @@ -551,10 +551,10 @@ def main() -> None: if full_payload["data"]["skill_os2_coverage"]: assert full_payload["data"]["skill_os2_coverage"]["summary"]["local_blueprint_ready"] is True, full_payload["data"]["skill_os2_coverage"] assert full_payload["data"]["skill_os2_coverage"]["summary"]["public_world_class_ready"] is False, full_payload["data"]["skill_os2_coverage"] - assert full_payload["data"]["skill_os2_coverage"]["summary"]["extension_track_count"] == 3, full_payload["data"]["skill_os2_coverage"] + assert full_payload["data"]["skill_os2_coverage"]["summary"]["extension_track_count"] == 4, full_payload["data"]["skill_os2_coverage"] assert full_payload["data"]["skill_os2_coverage"]["summary"]["extension_partial_count"] == 0, full_payload["data"]["skill_os2_coverage"] assert full_payload["data"]["skill_os2_coverage"]["summary"]["extension_planned_count"] == 0, full_payload["data"]["skill_os2_coverage"] - assert full_payload["data"]["skill_os2_coverage"]["summary"]["extension_covered_count"] == 3, full_payload["data"]["skill_os2_coverage"] + assert full_payload["data"]["skill_os2_coverage"]["summary"]["extension_covered_count"] == 4, full_payload["data"]["skill_os2_coverage"] assert full_payload["data"]["skill_os2_coverage"]["summary"]["adaptive_extension_ready"] is True, full_payload["data"]["skill_os2_coverage"] extension_tracks = { item["key"]: item @@ -563,6 +563,7 @@ def main() -> None: assert extension_tracks["skill-interpretation-report"]["status"] == "covered", extension_tracks assert extension_tracks["adaptive-self-iteration"]["status"] == "covered", extension_tracks assert extension_tracks["daily-skillops-report"]["status"] == "covered", extension_tracks + assert extension_tracks["weekly-curator-report"]["status"] == "covered", extension_tracks world_class_entries = full_payload["data"]["world_class_evidence_ledger"]["entries"] assert len(world_class_entries) == 4, world_class_entries assert {item["key"] for item in world_class_entries} == { @@ -673,7 +674,7 @@ def main() -> None: assert "Extension Track Count" in html, html assert "Adaptive Extension Ready" in html, html assert "