31ce04c655
Merge the beta-ready Yao Meta Skill architecture, report, evidence gate, and release-boundary updates.\n\nRelease boundary: beta/public testing is allowed; formal world-class, fully reviewed, or superiority claims remain blocked until the pending evidence gates are accepted.
259 lines
8.7 KiB
Python
259 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import json
|
|
import shlex
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from render_adoption_drift_report import SENSITIVE_FIELDS, display_path
|
|
|
|
|
|
SCHEMA_VERSION = "1.0"
|
|
DEFAULT_RECIPES = [
|
|
{
|
|
"id": "browser-extension",
|
|
"client": "Browser extension",
|
|
"command": "browser-extension",
|
|
"event": "skill_activation",
|
|
"activation_type": "explicit",
|
|
"outcome": "accepted",
|
|
"failure_type": "none",
|
|
"trigger_points": [
|
|
"Skill is explicitly selected by the user.",
|
|
"Skill activation is accepted by the host client.",
|
|
],
|
|
},
|
|
{
|
|
"id": "chrome-extension",
|
|
"client": "Chrome extension",
|
|
"command": "chrome-extension",
|
|
"event": "skill_output",
|
|
"activation_type": "manual",
|
|
"outcome": "edited",
|
|
"failure_type": "none",
|
|
"trigger_points": [
|
|
"User keeps or edits a skill-generated artifact.",
|
|
"Extension records only the final outcome class, not page content.",
|
|
],
|
|
},
|
|
{
|
|
"id": "vscode-extension",
|
|
"client": "VS Code extension",
|
|
"command": "vscode-extension",
|
|
"event": "skill_activation",
|
|
"activation_type": "implicit",
|
|
"outcome": "accepted",
|
|
"failure_type": "none",
|
|
"trigger_points": [
|
|
"Workspace skill route chooses this skill.",
|
|
"Extension records route outcome without file paths or code snippets.",
|
|
],
|
|
},
|
|
{
|
|
"id": "cli-wrapper",
|
|
"client": "CLI wrapper",
|
|
"command": "cli-wrapper",
|
|
"event": "script_run",
|
|
"activation_type": "manual",
|
|
"outcome": "unknown",
|
|
"failure_type": "none",
|
|
"trigger_points": [
|
|
"Wrapper starts or finishes a known skill workflow command.",
|
|
"Wrapper records command family only, never command arguments.",
|
|
],
|
|
},
|
|
{
|
|
"id": "provider-adapter",
|
|
"client": "Provider adapter",
|
|
"command": "provider-adapter",
|
|
"event": "skill_output",
|
|
"activation_type": "manual",
|
|
"outcome": "accepted",
|
|
"failure_type": "none",
|
|
"trigger_points": [
|
|
"Provider adapter receives an accepted or rejected skill output signal.",
|
|
"Adapter records only normalized outcome metadata.",
|
|
],
|
|
},
|
|
]
|
|
|
|
|
|
def default_spool(skill_dir: Path) -> Path:
|
|
return skill_dir / ".yao" / "telemetry_spool" / "external_events.jsonl"
|
|
|
|
|
|
def command_text(argv: list[str]) -> str:
|
|
return shlex.join(argv)
|
|
|
|
|
|
def emit_argv(skill_dir: Path, output_jsonl: Path, recipe: dict[str, Any], dry_run: bool = False) -> list[str]:
|
|
argv = [
|
|
"python3",
|
|
"scripts/yao.py",
|
|
"telemetry-emit",
|
|
display_path(skill_dir),
|
|
"--output-jsonl",
|
|
display_path(output_jsonl),
|
|
"--event",
|
|
recipe["event"],
|
|
"--activation-type",
|
|
recipe["activation_type"],
|
|
"--outcome",
|
|
recipe["outcome"],
|
|
"--failure-type",
|
|
recipe["failure_type"],
|
|
"--command",
|
|
recipe["command"],
|
|
]
|
|
if dry_run:
|
|
argv.append("--dry-run")
|
|
return argv
|
|
|
|
|
|
def import_argv(skill_dir: Path, output_jsonl: Path) -> list[str]:
|
|
return [
|
|
"python3",
|
|
"scripts/yao.py",
|
|
"telemetry-import",
|
|
display_path(skill_dir),
|
|
"--input-jsonl",
|
|
display_path(output_jsonl),
|
|
]
|
|
|
|
|
|
def build_recipe(skill_dir: Path, output_jsonl: Path, recipe: dict[str, Any]) -> dict[str, Any]:
|
|
emit = emit_argv(skill_dir, output_jsonl, recipe)
|
|
dry_run = emit_argv(skill_dir, output_jsonl, recipe, dry_run=True)
|
|
return {
|
|
**recipe,
|
|
"source": "external",
|
|
"native_auto_capture": False,
|
|
"integration_status": "client-hook-recipe",
|
|
"metadata_only": True,
|
|
"emit_argv": emit,
|
|
"emit_command": command_text(emit),
|
|
"dry_run_argv": dry_run,
|
|
"dry_run_command": command_text(dry_run),
|
|
}
|
|
|
|
|
|
def render_markdown(report: dict[str, Any]) -> str:
|
|
lines = [
|
|
"# Telemetry Hook Recipes",
|
|
"",
|
|
"These recipes show how Browser, Chrome, IDE, wrapper, or provider-side integrations can call `telemetry-emit` without collecting raw prompts, outputs, transcripts, messages, notes, arguments, or file content.",
|
|
"",
|
|
"They are client hook recipes, not proof that a host client is already natively integrated.",
|
|
"",
|
|
"## Summary",
|
|
"",
|
|
f"- Recipe count: `{report['summary']['recipe_count']}`",
|
|
f"- Native auto-capture integrations claimed: `{report['summary']['native_auto_capture_count']}`",
|
|
f"- Default spool: `{report['artifacts']['spool_jsonl']}`",
|
|
"",
|
|
"## Recipes",
|
|
"",
|
|
"| Client | Command | Event | Outcome | Dry run |",
|
|
"| --- | --- | --- | --- | --- |",
|
|
]
|
|
for recipe in report["recipes"]:
|
|
lines.append(
|
|
f"| {recipe['client']} | `{recipe['command']}` | `{recipe['event']}` | `{recipe['outcome']}` | `{recipe['dry_run_command']}` |"
|
|
)
|
|
lines.extend(
|
|
[
|
|
"",
|
|
"## Import",
|
|
"",
|
|
"After a client finishes a batch, import the local spool:",
|
|
"",
|
|
"```bash",
|
|
report["artifacts"]["import_command"],
|
|
"```",
|
|
"",
|
|
"## Privacy Contract",
|
|
"",
|
|
"- Allowed fields: `event`, `skill`, `version`, `source`, `command`, `activation_type`, `outcome`, `failure_type`, `timestamp`.",
|
|
f"- Blocked raw-content fields: `{', '.join(report['privacy_contract']['blocked_fields'])}`.",
|
|
"- Client integrations must map local state to normalized outcome classes before calling the hook.",
|
|
]
|
|
)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def render_recipes(
|
|
skill_dir: Path,
|
|
output_json: Path | None = None,
|
|
output_md: Path | None = None,
|
|
output_jsonl: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
skill_dir = skill_dir.resolve()
|
|
reports_dir = skill_dir / "reports"
|
|
output_json = (output_json or reports_dir / "telemetry_hook_recipes.json").resolve()
|
|
output_md = (output_md or reports_dir / "telemetry_hook_recipes.md").resolve()
|
|
output_jsonl = (output_jsonl or default_spool(skill_dir)).resolve()
|
|
recipes = [build_recipe(skill_dir, output_jsonl, recipe) for recipe in DEFAULT_RECIPES]
|
|
import_cmd = import_argv(skill_dir, output_jsonl)
|
|
report = {
|
|
"ok": True,
|
|
"schema_version": SCHEMA_VERSION,
|
|
"skill_dir": display_path(skill_dir),
|
|
"summary": {
|
|
"recipe_count": len(recipes),
|
|
"native_auto_capture_count": sum(1 for recipe in recipes if recipe["native_auto_capture"]),
|
|
"metadata_only_recipe_count": sum(1 for recipe in recipes if recipe["metadata_only"]),
|
|
},
|
|
"privacy_contract": {
|
|
"raw_content_allowed": False,
|
|
"blocked_fields": sorted(SENSITIVE_FIELDS),
|
|
"allowed_fields": [
|
|
"event",
|
|
"skill",
|
|
"version",
|
|
"source",
|
|
"command",
|
|
"activation_type",
|
|
"outcome",
|
|
"failure_type",
|
|
"timestamp",
|
|
],
|
|
},
|
|
"recipes": recipes,
|
|
"artifacts": {
|
|
"json": display_path(output_json),
|
|
"markdown": display_path(output_md),
|
|
"spool_jsonl": display_path(output_jsonl),
|
|
"import_command": command_text(import_cmd),
|
|
"import_argv": import_cmd,
|
|
},
|
|
"warnings": [
|
|
"These recipes do not prove native host-client integration; they define the local metadata-only hook contract for those integrations."
|
|
],
|
|
"failures": [],
|
|
}
|
|
output_json.parent.mkdir(parents=True, exist_ok=True)
|
|
output_md.parent.mkdir(parents=True, exist_ok=True)
|
|
output_json.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
output_md.write_text(render_markdown(report), encoding="utf-8")
|
|
return report
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Render metadata-only telemetry client hook recipes.")
|
|
parser.add_argument("skill_dir", nargs="?", default=".")
|
|
parser.add_argument("--output-json")
|
|
parser.add_argument("--output-md")
|
|
parser.add_argument("--output-jsonl")
|
|
args = parser.parse_args()
|
|
report = render_recipes(
|
|
Path(args.skill_dir),
|
|
output_json=Path(args.output_json) if args.output_json else None,
|
|
output_md=Path(args.output_md) if args.output_md else None,
|
|
output_jsonl=Path(args.output_jsonl) if args.output_jsonl else None,
|
|
)
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|