"""squish/squash/cli.py — Standalone ``squash`` CLI entry point.

Provides the ``squash attest`` sub-command that CI/CD integrations call:

    squash attest ./my-model --policy eu-ai-act --policy enterprise-strict

Exit codes (per project CLI standard):
    0  Success — attestation passed
    1  User / input error (bad path, unknown policy, missing flag)
    2  Runtime error (I/O failure, scan error, attestation violation)

Usage::

    squash attest MODEL_PATH [options]

Options::

    --policy, -p     Policy name to evaluate (repeatable, default: enterprise-strict)
    --output-dir     Artifact output directory (default: model dir)
    --sign           Sign the CycloneDX BOM via Sigstore keyless signing
    --fail-on-violation  Exit 2 if any policy error-severity finding is raised
    --skip-scan      Skip the security scanner
    --json-result    Path to write the master attestation record as JSON
    --model-id       Override the model ID in the SBOM
    --hf-repo        HuggingFace repository ID for provenance metadata
    --quant-format   Quantization format label (e.g. INT4, BF16)
    --quiet, -q      Suppress informational output (errors still go to stderr)
    --help           Show this message and exit

"""

from __future__ import annotations

import argparse
import json
import logging
import os
import sys
import tempfile
from pathlib import Path


def _ib_profile_args(p: argparse.ArgumentParser) -> None:
    """Shared profile-source arguments for industry-benchmark subcommands."""
    grp = p.add_mutually_exclusive_group()
    grp.add_argument(
        "--scores", default=None,
        help="Comma-separated compliance scores (e.g. 71,74,68,72)",
    )
    grp.add_argument(
        "--registry", default=None, dest="registry_path",
        help="Path to squash attestation_registry.db for automatic profile build",
    )
    p.add_argument("--frameworks", default="", dest="frameworks",
                   help="Comma-separated framework names (e.g. eu-ai-act,gdpr)")
    p.add_argument("--period", type=int, default=90, dest="period_days",
                   help="History window in days (default: 90)")
    p.add_argument("--model-filter", default="", dest="model_filter",
                   help="Filter attestations by model ID substring (registry mode)")


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="squash",
        description="AI-SBOM attestation for ML models (Squish Squash)",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Examples:\n"
            "  squash attest ./llama-3.1-8b-q4 --policy eu-ai-act\n"
            "  squash attest ./model --sign --fail-on-violation --json-result ./result.json\n"
            "  squash policies              # list available policy templates\n"
        ),
    )
    parser.add_argument("--quiet", "-q", action="store_true", help="Suppress info output")

    sub = parser.add_subparsers(dest="command", metavar="COMMAND")

    # ── squash attest ──────────────────────────────────────────────────────────
    attest = sub.add_parser("attest", help="Run full attestation pipeline on a model artifact")
    attest.add_argument(
        "model_path",
        help="Path to model directory or file (e.g. ./llama-3.1-8b-q4 or ./model.gguf)",
    )
    attest.add_argument(
        "--policy", "-p",
        dest="policies",
        action="append",
        default=[],
        metavar="POLICY",
        help="Policy name to evaluate (repeatable). Default: enterprise-strict. "
             "Also: eu-cra, fedramp, cmmc, eu-ai-act, nist-ai-rmf, owasp-llm-top10, iso-42001 "
             "(run 'squash policies' to list all)",
    )
    attest.add_argument("--output-dir", default=None, help="Artifact destination directory")
    attest.add_argument("--sign", action="store_true", help="Sign BOM via Sigstore keyless")
    attest.add_argument(
        "--fail-on-violation",
        action="store_true",
        help="Exit 2 if any error-severity policy finding exists or scan is unsafe",
    )
    attest.add_argument("--skip-scan", action="store_true", help="Skip security scanner")
    attest.add_argument(
        "--json-result",
        default=None,
        metavar="PATH",
        help="Write master attestation record JSON to this path",
    )
    attest.add_argument("--model-id", default="", help="Override model ID in SBOM")
    attest.add_argument("--hf-repo", default="", help="HuggingFace repo ID for provenance")
    attest.add_argument(
        "--quant-format",
        default="unknown",
        help="Quantization format label (e.g. INT4, BF16)",
    )
    # ── SPDX AI Profile enrichment ────────────────────────────────────────────
    attest.add_argument(
        "--spdx-type",
        default=None,
        metavar="TYPE",
        dest="spdx_type",
        help="SPDX AI Profile: type_of_model (e.g. text-generation, text-classification, "
             "translation, summarization, question-answering). Default: text-generation",
    )
    attest.add_argument(
        "--spdx-safety-risk",
        default=None,
        choices=["high", "medium", "low", "unspecified"],
        dest="spdx_safety_risk",
        help="SPDX AI Profile: safetyRiskAssessment tier. Default: unspecified",
    )
    attest.add_argument(
        "--spdx-dataset",
        action="append",
        default=[],
        dest="spdx_datasets",
        metavar="DATASET_ID",
        help="Training dataset HF ID or URI (repeatable; e.g. --spdx-dataset wikipedia "
             "--spdx-dataset c4). Embedded in the SPDX AI Profile",
    )
    attest.add_argument(
        "--spdx-training-info",
        default=None,
        dest="spdx_training_info",
        metavar="TEXT",
        help="SPDX AI Profile: informationAboutTraining free-text. "
             "Default: see-model-card",
    )
    attest.add_argument(
        "--spdx-sensitive-data",
        default=None,
        choices=["absent", "present", "unknown"],
        dest="spdx_sensitive_data",
        help="SPDX AI Profile: sensitivePIIInTrainingData. Default: absent",
    )
    # ── W49: offline / air-gapped mode ────────────────────────────────────────
    attest.add_argument(
        "--offline",
        action="store_true",
        default=False,
        help="Air-gapped mode: disable all OIDC/network calls (also set by SQUASH_OFFLINE=1)",
    )
    attest.add_argument(
        "--offline-key",
        metavar="PATH",
        default=None,
        dest="offline_key",
        help="Path to Ed25519 .priv.pem for offline signing (requires --sign --offline)",
    )

    # ── squash keygen ─────────────────────────────────────────────────────────
    keygen_cmd = sub.add_parser(
        "keygen",
        help="Generate a local Ed25519 keypair for offline signing",
        description=(
            "Generate an Ed25519 keypair for offline BOM signing.\n\n"
            "Example: squash keygen mykey\n"
            "Example: squash keygen ci-key --key-dir ~/.squash/keys"
        ),
    )
    keygen_cmd.add_argument("name", help="Base filename for the keypair (no extension)")
    keygen_cmd.add_argument(
        "--key-dir",
        metavar="DIR",
        default=".",
        dest="key_dir",
        help="Directory to write <name>.priv.pem and <name>.pub.pem (default: current dir)",
    )
    keygen_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── squash verify-local ───────────────────────────────────────────────────
    verify_local_cmd = sub.add_parser(
        "verify-local",
        help="Verify a BOM's Ed25519 offline signature against a local public key",
        description=(
            "Verify the local Ed25519 signature for a CycloneDX BOM.\n\n"
            "Example: squash verify-local ./model/cyclonedx-mlbom.json "
            "--key mykey.pub.pem\n"
            "Example: squash verify-local bom.json --key ci-key.pub.pem --sig bom.sig"
        ),
    )
    verify_local_cmd.add_argument("bom_path", help="Path to the CycloneDX BOM file to verify")
    verify_local_cmd.add_argument(
        "--key",
        required=True,
        metavar="PATH",
        dest="pub_key",
        help="Path to the Ed25519 .pub.pem public key",
    )
    verify_local_cmd.add_argument(
        "--sig",
        default=None,
        metavar="PATH",
        dest="sig_file",
        help="Explicit .sig file path (default: <bom_path>.sig)",
    )
    verify_local_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── squash pack-offline ───────────────────────────────────────────────────
    pack_offline_cmd = sub.add_parser(
        "pack-offline",
        help="Bundle a model directory and squash artefacts into a .squash-bundle.tar.gz",
        description=(
            "Archive a model directory (weights + BOM + signatures + chain) into a "
            "portable, self-contained tarball for air-gapped deployment.\n\n"
            "Example: squash pack-offline ./llama-3.1-8b-q4\n"
            "Example: squash pack-offline ./model --output /tmp/bundle.squash-bundle.tar.gz"
        ),
    )
    pack_offline_cmd.add_argument("model_dir", help="Path to the model directory to bundle")
    pack_offline_cmd.add_argument(
        "--output",
        metavar="PATH",
        default=None,
        dest="output_path",
        help="Output .squash-bundle.tar.gz path (default: <model_dir>-<timestamp>.squash-bundle.tar.gz)",
    )
    pack_offline_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── squash policies ────────────────────────────────────────────────────────
    policies_cmd = sub.add_parser("policies", help="List available built-in policy templates")
    policies_cmd.add_argument(
        "--validate",
        metavar="PATH",
        default=None,
        help="Validate a custom YAML rules file (exit 0 = valid, 1 = user error, 2 = invalid rules)",
    )

    # ── squash scan ────────────────────────────────────────────────────────────
    scan_cmd = sub.add_parser(
        "scan",
        help="Run security scanner only (no SBOM generation). Accepts a local "
             "path or an hf://owner/model URI for the public HF scanner.",
        description=(
            "Run the security scanner against a local model directory or a "
            "public HuggingFace model.\n\n"
            "Examples:\n"
            "  squash scan ./my-model\n"
            "  squash scan hf://microsoft/phi-3-mini-4k-instruct\n"
            "  squash scan hf://meta-llama/Llama-3.1-8B-Instruct@main \\\n"
            "        --policy enterprise-strict --output-dir ./out\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    scan_cmd.add_argument(
        "model_path",
        help="Path to model directory/file, OR `hf://owner/model[@revision]` "
             "for the public HF scanner.",
    )
    scan_cmd.add_argument("--json-result", default=None, metavar="PATH")
    scan_cmd.add_argument(
        "--sarif",
        default=None,
        metavar="PATH",
        help="Write SARIF 2.1.0 output to PATH",
    )
    scan_cmd.add_argument(
        "--exit-2-on-unsafe",
        action="store_true",
        default=False,
        help="Exit 2 on critical/high findings; exit 1 on other unsafe statuses",
    )

    # ── B1 (Sprint 14, W205) — hf:// public scanner flags ─────────────────────
    scan_cmd.add_argument(
        "--policy", action="append", default=None, dest="hf_policies",
        help="(hf:// only) Policy preview to evaluate (repeatable).",
    )
    scan_cmd.add_argument(
        "--output-dir", default=None, dest="hf_output_dir",
        help="(hf:// only) Directory to write `squash-hf-scan.{json,md}`.",
    )
    scan_cmd.add_argument(
        "--download-weights", action="store_true", dest="hf_download_weights",
        help="(hf:// only) Fetch full weight files. Default fetches small "
             "artefacts only — keeps the public scanner fast & cheap.",
    )
    scan_cmd.add_argument(
        "--keep-download", action="store_true", dest="hf_keep_download",
        help="(hf:// only) Retain the downloaded snapshot directory after scan.",
    )
    scan_cmd.add_argument(
        "--hf-token", default="", dest="hf_token",
        help="(hf:// only) HF Hub token for private/gated repos. Falls back "
             "to HUGGING_FACE_HUB_TOKEN / HF_TOKEN env.",
    )
    scan_cmd.add_argument(
        "--quiet", action="store_true", dest="hf_quiet",
        help="(hf:// only) Suppress non-essential output.",
    )

    # ── squash sbom-diff ──────────────────────────────────────────────────────
    sbom_diff_cmd = sub.add_parser(
        "sbom-diff",
        help="Compare two CycloneDX SBOM snapshots and report differences",
    )
    sbom_diff_cmd.add_argument("sbom_a", metavar="SBOM_A", help="Older (baseline) SBOM JSON file")
    sbom_diff_cmd.add_argument("sbom_b", metavar="SBOM_B", help="Newer SBOM JSON file")
    sbom_diff_cmd.add_argument(
        "--exit-1-on-regression",
        action="store_true",
        default=False,
        help="Exit 1 when new vulnerabilities are introduced or policy status worsens",
    )

    # ── squash verify ──────────────────────────────────────────────────────────
    verify_cmd = sub.add_parser(
        "verify",
        help="Verify the Sigstore bundle for a model's CycloneDX BOM",
    )
    verify_cmd.add_argument(
        "model_path",
        help="Path to model directory (must contain cyclonedx-mlbom.json)",
    )
    verify_cmd.add_argument(
        "--bundle",
        default=None,
        metavar="PATH",
        help="Explicit path to the .sig.json bundle (default: <bom>.sig.json)",
    )
    verify_cmd.add_argument(
        "--strict",
        action="store_true",
        default=False,
        help="Exit 2 when no bundle is found (treat unsigned BOMs as failures)",
    )
    verify_cmd.add_argument(
        "--check-timestamp",
        action="store_true",
        default=False,
        help="Phase G.3: also verify the RFC 3161 TSA timestamp token "
             "(requires tsa_token.json in the attestation dir).",
    )

    # ── squash self-verify ─────────────────────────────────────────────────────
    # Phase G.3 — full chain walker. Validates input_manifest, canonical
    # body, Ed25519 signature, RFC 3161 timestamp, and SLSA provenance
    # for an attestation directory. Exit 0 only when every link verifies.
    self_verify_cmd = sub.add_parser(
        "self-verify",
        help="Walk the full crypto chain (input manifest → canonical body → "
             "Ed25519 → RFC 3161 → SLSA) and exit 0 only on success",
    )
    self_verify_cmd.add_argument(
        "--attestation-dir", "-d",
        default=".",
        help="Directory containing the squash attestation artefacts (default: cwd).",
    )
    self_verify_cmd.add_argument(
        "--offline",
        action="store_true",
        help="Skip network-dependent checks (TSA, Rekor).",
    )
    self_verify_cmd.add_argument(
        "--json",
        action="store_true",
        help="Emit machine-readable JSON instead of text.",
    )

    # ── squash report ──────────────────────────────────────────────────────────
    report_cmd = sub.add_parser(
        "report",
        help="Generate an HTML or JSON compliance report from attestation artifacts",
        description="squash report MODEL_DIR  # writes squash-report.html into model dir",
    )
    report_cmd.add_argument(
        "model_path",
        help="Path to model directory containing attestation artifacts",
    )
    report_cmd.add_argument(
        "--output",
        default=None,
        metavar="PATH",
        help="Output file path (default: <model_dir>/squash-report.html)",
    )
    report_cmd.add_argument(
        "--format",
        choices=["html", "json"],
        default="html",
        help="Output format (default: html)",
    )

    # ── squash vex ─────────────────────────────────────────────────────────────
    vex_cmd = sub.add_parser(
        "vex",
        help="VEX feed cache management",
    )
    vex_sub = vex_cmd.add_subparsers(dest="vex_command")
    vex_update = vex_sub.add_parser("update", help="Refresh the local VEX feed cache")
    vex_update.add_argument(
        "--url",
        default=None,
        help="Override VEX feed URL (default: SQUASH_VEX_URL env or built-in)",
    )
    vex_update.add_argument(
        "--timeout",
        type=float,
        default=10.0,
        help="HTTP timeout in seconds (default: 10)",
    )
    vex_sub.add_parser("status", help="Show VEX cache status and freshness")

    # Wave 52 — subscribe / unsubscribe / list-subscriptions
    vex_subscribe = vex_sub.add_parser(
        "subscribe",
        help="Register a remote VEX feed URL for periodic polling",
        description=(
            "squash vex subscribe URL [--alias NAME] [--api-key-env VAR] [--polling-hours N]\n\n"
            "Example: squash vex subscribe https://vex.example.com/feed.json --alias corp-feed\n"
            "Example: squash vex subscribe https://api.example.com/vex --api-key-env CORP_VEX_KEY"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    vex_subscribe.add_argument(
        "url",
        metavar="URL",
        help="HTTPS endpoint returning an OpenVEX JSON feed",
    )
    vex_subscribe.add_argument(
        "--alias",
        default="",
        metavar="NAME",
        help="Short human-readable name for this subscription (optional)",
    )
    vex_subscribe.add_argument(
        "--api-key-env",
        default="SQUASH_VEX_API_KEY",
        metavar="VAR",
        help="Environment variable name that holds the API key (default: SQUASH_VEX_API_KEY)",
    )
    vex_subscribe.add_argument(
        "--polling-hours",
        type=int,
        default=24,
        metavar="N",
        help="Refresh interval in hours used by 'squash vex update --all' (default: 24)",
    )
    vex_subscribe.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    vex_unsubscribe = vex_sub.add_parser(
        "unsubscribe",
        help="Remove a registered VEX feed subscription",
        description=(
            "squash vex unsubscribe URL_OR_ALIAS\n\n"
            "Example: squash vex unsubscribe corp-feed\n"
            "Example: squash vex unsubscribe https://vex.example.com/feed.json"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    vex_unsubscribe.add_argument(
        "url_or_alias",
        metavar="URL_OR_ALIAS",
        help="URL or alias of the subscription to remove",
    )
    vex_unsubscribe.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    vex_sub.add_parser("list-subscriptions", help="List all registered VEX feed subscriptions")

    # ── squash attest-composed ────────────────────────────────────────────────
    ac_cmd = sub.add_parser(
        "attest-composed",
        help="Attest multiple models and produce a parent composite BOM",
        description="squash attest-composed MODEL_A MODEL_B ...  [--output-dir DIR]",
    )
    ac_cmd.add_argument(
        "model_paths",
        nargs="+",
        metavar="MODEL_PATH",
        help="Two or more model directories to attest",
    )
    ac_cmd.add_argument(
        "--output-dir",
        default=None,
        metavar="DIR",
        help="Write parent BOM and component results here (default: first model dir)",
    )
    ac_cmd.add_argument(
        "--policy",
        dest="policies",
        action="append",
        default=None,
        metavar="NAME",
        help="Policy name(s) to evaluate (repeatable; default: enterprise-strict; "
             "also: eu-cra, fedramp, cmmc — run 'squash policies' to list all)",
    )
    ac_cmd.add_argument(
        "--sign",
        action="store_true",
        default=False,
        help="Sign each component BOM with Sigstore after attestation",
    )

    # ── squash push ───────────────────────────────────────────────────────────
    push_cmd = sub.add_parser(
        "push",
        help="Push a CycloneDX SBOM to a supported registry (Dependency-Track, GUAC, Squash)",
        description="squash push MODEL_DIR --registry-url URL  [options]",
    )
    push_cmd.add_argument(
        "model_path",
        help="Model directory containing cyclonedx-mlbom.json",
    )
    push_cmd.add_argument(
        "--registry-url",
        required=True,
        metavar="URL",
        help="Registry endpoint URL",
    )
    push_cmd.add_argument(
        "--api-key",
        default=None,
        metavar="KEY",
        help="API key or token (or set SQUASH_REGISTRY_KEY env var)",
    )
    push_cmd.add_argument(
        "--registry-type",
        choices=["dtrack", "guac", "squash"],
        default="dtrack",
        help="Registry protocol (default: dtrack)",
    )

    # ── Wave 20 — NTIA minimum elements check ─────────────────────────────────
    ntia_cmd = sub.add_parser(
        "ntia-check",
        help="Validate NTIA minimum elements in a CycloneDX BOM",
        description=(
            "Check a CycloneDX BOM for the NTIA Minimum Elements for SBOM "
            "compliance (Nov 2021).\n\n"
            "Example: squash ntia-check model/cyclonedx-mlbom.json"
        ),
    )
    ntia_cmd.add_argument("bom_path", help="Path to the CycloneDX BOM JSON file")
    ntia_cmd.add_argument(
        "--strict",
        action="store_true",
        help="Require non-empty dependsOn fields (stricter NTIA compliance)",
    )
    ntia_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── Wave 21 — SLSA provenance attestation ─────────────────────────────────
    slsa_cmd = sub.add_parser(
        "slsa-attest",
        help="Generate SLSA provenance statement for a model directory",
        description=(
            "Build a SLSA 1.0 Build Provenance statement for the artefacts in "
            "MODEL_DIR and (optionally) sign it.\n\n"
            "Example: squash slsa-attest ./my-model --level 2"
        ),
    )
    slsa_cmd.add_argument("model_dir", help="Path to the squash model directory")
    slsa_cmd.add_argument(
        "--level",
        type=int,
        choices=[1, 2, 3],
        default=1,
        help="SLSA build track level (default: 1)",
    )
    slsa_cmd.add_argument(
        "--builder-id",
        default="https://squish.local/squash/builder",
        help="URI identifying the build system",
    )
    slsa_cmd.add_argument("--sign", action="store_true", help="Force signing even at L1")
    slsa_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── Wave 22 — BOM merge ────────────────────────────────────────────────────
    merge_cmd = sub.add_parser(
        "merge",
        help="Merge multiple CycloneDX BOMs into one canonical BOM",
        description=(
            "Deduplicate components by PURL and union vulnerabilities across "
            "multiple CycloneDX BOMs.\n\n"
            "Example: squash merge a/cyclonedx-mlbom.json b/cyclonedx-mlbom.json "
            "--output merged/cyclonedx-mlbom.json"
        ),
    )
    merge_cmd.add_argument(
        "bom_paths", nargs="+", help="Two or more CycloneDX BOM JSON files to merge"
    )
    merge_cmd.add_argument(
        "--output", required=True, help="Destination path for the merged BOM"
    )
    merge_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── Wave 23 — AI risk assessment ──────────────────────────────────────────
    risk_cmd = sub.add_parser(
        "risk-assess",
        help="Assess AI risk per EU AI Act and/or NIST AI RMF",
        description=(
            "Evaluate the BOM in MODEL_DIR against the EU AI Act (2024/1689) "
            "risk tiers and the NIST AI Risk Management Framework.\n\n"
            "Example: squash risk-assess ./my-model --framework eu-ai-act"
        ),
    )
    risk_cmd.add_argument("model_dir", help="Path to the squash model directory")
    risk_cmd.add_argument(
        "--framework",
        choices=["eu-ai-act", "nist-rmf", "both"],
        default="both",
        help="Risk framework to run (default: both)",
    )
    risk_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── Wave 24 — Drift monitoring ────────────────────────────────────────────
    monitor_cmd = sub.add_parser(
        "monitor",
        help="Detect drift in a squash model directory",
        description=(
            "Snapshot the attestation state of MODEL_DIR and compare against a "
            "previous snapshot to detect BOM changes, new CVEs, or policy "
            "regressions.\n\n"
            "Example: squash monitor ./my-model --once"
        ),
    )
    monitor_cmd.add_argument("model_dir", help="Path to the squash model directory")
    monitor_cmd.add_argument(
        "--baseline",
        default=None,
        help="SHA-256 baseline snapshot string to compare against (omit to just snapshot)",
    )
    monitor_cmd.add_argument(
        "--interval",
        type=float,
        default=3600.0,
        help="Poll interval in seconds for continuous monitoring (default: 3600)",
    )
    monitor_cmd.add_argument(
        "--once",
        action="store_true",
        help="Snapshot once (or compare against --baseline) then exit",
    )
    monitor_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── Wave 25 — CI/CD integration ───────────────────────────────────────────
    ci_cmd = sub.add_parser(
        "ci-run",
        help="Run the full squash check pipeline in CI",
        description=(
            "Execute NTIA validation, AI risk assessment, and drift detection "
            "for MODEL_DIR, then emit native CI annotations.\n\n"
            "Example: squash ci-run ./my-model --report-format github"
        ),
    )
    ci_cmd.add_argument("model_dir", help="Path to the squash model directory")
    ci_cmd.add_argument(
        "--report-format",
        choices=["github", "jenkins", "gitlab", "text"],
        default="text",
        help="CI annotation format (default: text; auto-detected if not set)",
    )
    ci_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── Wave 27 — Kubernetes admission webhook ─────────────────────────────────
    webhook_cmd = sub.add_parser(
        "k8s-webhook",
        help="Start the Kubernetes admission webhook server",
        description=(
            "Run an HTTPS validating admission webhook that enforces Squash BOM "
            "attestation policy.  Pods annotated with "
            "squash.ai/attestation-required=true must carry a valid "
            "squash.ai/bom-digest annotation whose digest is present in the "
            "configured policy store.\n\n"
            "Example: squash k8s-webhook --port 8443 --tls-cert /tls/tls.crt "
            "--tls-key /tls/tls.key --policy-store /var/squash/policy-store.json"
        ),
    )
    webhook_cmd.add_argument(
        "--port",
        type=int,
        default=8443,
        help="TCP port for the webhook server (default: 8443)",
    )
    webhook_cmd.add_argument(
        "--tls-cert",
        metavar="PATH",
        default=None,
        help="Path to PEM-encoded TLS certificate (omit for dev HTTP mode)",
    )
    webhook_cmd.add_argument(
        "--tls-key",
        metavar="PATH",
        default=None,
        help="Path to PEM-encoded TLS private key",
    )
    webhook_cmd.add_argument(
        "--policy-store",
        metavar="PATH",
        default=None,
        help="Path to JSON policy store file: {digest: bool}",
    )
    webhook_cmd.add_argument(
        "--default-deny",
        action="store_true",
        default=False,
        help="Deny pods that lack the attestation-required annotation (default: allow)",
    )
    webhook_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── Wave 50 — Shadow AI detection ─────────────────────────────────────────
    shadow_ai_cmd = sub.add_parser(
        "shadow-ai",
        help="Detect shadow AI model files running inside Kubernetes pods",
        description=(
            "Scan a Kubernetes pod list for shadow AI model file references "
            "(*.gguf, *.safetensors, *.bin, *.pt, etc.) in volume mounts, "
            "environment variables, and container arguments.\n\n"
            "Example: squash shadow-ai scan pods.json\n"
            "Example: kubectl get pods -o json | squash shadow-ai scan -\n"
            "Example: squash shadow-ai scan pods.json --fail-on-hits"
        ),
    )
    shadow_ai_sub = shadow_ai_cmd.add_subparsers(dest="shadow_ai_cmd", metavar="SUBCOMMAND")
    shadow_ai_sub.required = True
    shadow_ai_scan_cmd = shadow_ai_sub.add_parser(
        "scan",
        help="Scan a pod list JSON for shadow AI model file references",
        description=(
            "Read a Kubernetes pod list (kubectl get pods -o json) from a file or stdin "
            "and report any container that references shadow AI model files.\n\n"
            "Exit codes: 0 = clean, 1 = error, 2 = shadow AI hits found (with --fail-on-hits)"
        ),
    )
    shadow_ai_scan_cmd.add_argument(
        "pod_list",
        metavar="POD_LIST_JSON",
        help="Path to pod list JSON file, or '-' to read from stdin",
    )
    shadow_ai_scan_cmd.add_argument(
        "--namespace",
        metavar="NS",
        action="append",
        dest="namespaces",
        default=[],
        help="Only scan pods in this namespace (repeatable; default: all namespaces)",
    )
    shadow_ai_scan_cmd.add_argument(
        "--extensions",
        nargs="+",
        metavar="EXT",
        default=None,
        help="Override the set of file extensions to flag (e.g. --extensions .gguf .pt)",
    )
    shadow_ai_scan_cmd.add_argument(
        "--output-json",
        metavar="PATH",
        default=None,
        help="Write the full scan result as JSON to this path",
    )
    shadow_ai_scan_cmd.add_argument(
        "--fail-on-hits",
        action="store_true",
        default=False,
        help="Exit with code 2 if any shadow AI model files are detected",
    )
    shadow_ai_scan_cmd.add_argument(
        "--quiet", action="store_true", help="Suppress non-error output"
    )

    # ── Wave 51 — SBOM drift detection ────────────────────────────────────────
    drift_check_cmd = sub.add_parser(
        "drift-check",
        help="Verify a model directory against its CycloneDX BOM (SHA-256 digest check)",
        description=(
            "Compare the SHA-256 digests of every weight file in MODEL_DIR against "
            "the digests recorded in the squish CycloneDX BOM sidecar.  Reports "
            "missing or tampered files and optionally exits non-zero on drift.\n\n"
            "Example: squash drift-check ./my-model --bom ./my-model/cyclonedx-mlbom.json\n"
            "Example: squash drift-check ./my-model --bom bom.json --fail-on-drift\n"
            "Exit codes: 0 = clean, 1 = error, 2 = drift found (with --fail-on-drift)"
        ),
    )
    drift_check_cmd.add_argument(
        "model_dir",
        metavar="MODEL_DIR",
        help="Path to the squish compressed model directory",
    )
    drift_check_cmd.add_argument(
        "--bom",
        metavar="BOM_PATH",
        required=True,
        help="Path to the CycloneDX BOM JSON file (cyclonedx-mlbom.json)",
    )
    drift_check_cmd.add_argument(
        "--fail-on-drift",
        action="store_true",
        default=False,
        help="Exit with code 2 when drift is detected (default: exit 0 and report only)",
    )
    drift_check_cmd.add_argument(
        "--output-json",
        metavar="PATH",
        default=None,
        help="Write the full drift result as JSON to this path",
    )
    drift_check_cmd.add_argument(
        "--quiet", action="store_true", help="Suppress non-error output"
    )

    # ── Wave 29 — VEX publish + integration CLI shims ─────────────────────────
    vex_pub_cmd = sub.add_parser(
        "vex-publish",
        help="Generate and write a static OpenVEX 0.2.0 feed JSON file",
        description=(
            "Build an OpenVEX 0.2.0 document from a list of statement entries and "
            "write it to a configurable output path.  Entries are read from a JSON "
            "file, stdin ('-'), or an inline JSON string.\n\n"
            "Example: squash vex-publish --output feed.json --entries entries.json\n"
            "Example: squash vex-publish --output feed.json --entries '[]'"
        ),
    )
    vex_pub_cmd.add_argument(
        "--output",
        metavar="PATH",
        required=True,
        help="Destination path to write the OpenVEX JSON file",
    )
    vex_pub_cmd.add_argument(
        "--entries",
        metavar="PATH_OR_JSON",
        default="[]",
        help=(
            "Statement entries as a JSON file path, '-' for stdin, or inline JSON "
            "array string (default: '[]')"
        ),
    )
    vex_pub_cmd.add_argument(
        "--author",
        default="squash",
        help="Author field in the VEX document (default: squash)",
    )
    vex_pub_cmd.add_argument(
        "--doc-id",
        metavar="URL",
        default=None,
        help="Optional @id URI for the document; auto-generated if omitted",
    )
    vex_pub_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    attest_mlflow_cmd = sub.add_parser(
        "attest-mlflow",
        help="Run attestation pipeline and emit result as JSON (MLflow-compatible)",
        description=(
            "Execute the full Squash attestation pipeline on MODEL_PATH and write "
            "the result JSON to stdout (or --output-dir).  Designed for piping into "
            "MLflow artifact upload scripts or CI steps that wrap mlflow.log_artifact.\n\n"
            "Example: squash attest-mlflow ./my-model --policies enterprise-strict"
        ),
    )
    attest_mlflow_cmd.add_argument("model_path", help="Path to the model directory or file")
    attest_mlflow_cmd.add_argument(
        "--output-dir",
        metavar="PATH",
        default=None,
        help="Directory to write attestation artifacts (default: <model_path>/../squash)",
    )
    attest_mlflow_cmd.add_argument(
        "--policies",
        nargs="*",
        metavar="POLICY",
        default=None,
        help="Policy templates to evaluate (default: enterprise-strict; "
             "also: eu-cra, fedramp, cmmc — run 'squash policies' for all)",
    )
    attest_mlflow_cmd.add_argument(
        "--sign", action="store_true", help="Sign BOM via Sigstore keyless"
    )
    attest_mlflow_cmd.add_argument(
        "--fail-on-violation",
        action="store_true",
        help="Exit 1 if any policy violation is found",
    )
    attest_mlflow_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    attest_wandb_cmd = sub.add_parser(
        "attest-wandb",
        help="Run attestation pipeline and emit result as JSON (W&B-compatible)",
        description=(
            "Execute the full Squash attestation pipeline on MODEL_PATH and write "
            "the result JSON to stdout (or --output-dir).  Designed for piping into "
            "W&B artifact upload scripts or run-summary steps.\n\n"
            "Example: squash attest-wandb ./my-model --policies enterprise-strict"
        ),
    )
    attest_wandb_cmd.add_argument("model_path", help="Path to the model directory or file")
    attest_wandb_cmd.add_argument(
        "--output-dir",
        metavar="PATH",
        default=None,
        help="Directory to write attestation artifacts (default: <model_path>/../squash)",
    )
    attest_wandb_cmd.add_argument(
        "--policies",
        nargs="*",
        metavar="POLICY",
        default=None,
        help="Policy templates to evaluate (default: enterprise-strict; "
             "also: eu-cra, fedramp, cmmc — run 'squash policies' for all)",
    )
    attest_wandb_cmd.add_argument(
        "--sign", action="store_true", help="Sign BOM via Sigstore keyless"
    )
    attest_wandb_cmd.add_argument(
        "--fail-on-violation",
        action="store_true",
        help="Exit 1 if any policy violation is found",
    )
    attest_wandb_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    attest_hf_cmd = sub.add_parser(
        "attest-huggingface",
        help="Attest a model and push artifacts to a HuggingFace Hub repository",
        description=(
            "Run the Squash attestation pipeline on MODEL_PATH and upload the "
            "resulting artifacts to --repo-id on the HuggingFace Hub.\n\n"
            "Example: squash attest-huggingface ./my-model --repo-id myorg/llama-3-8b"
        ),
    )
    attest_hf_cmd.add_argument("model_path", help="Path to the local model directory")
    attest_hf_cmd.add_argument(
        "--repo-id",
        metavar="ORG/REPO",
        default=None,
        help="HuggingFace Hub repo ID to push artifacts to (skip push if omitted)",
    )
    attest_hf_cmd.add_argument(
        "--hf-token",
        metavar="TOKEN",
        default=None,
        help="HuggingFace API token; falls back to HF_TOKEN env var",
    )
    attest_hf_cmd.add_argument(
        "--output-dir",
        metavar="PATH",
        default=None,
        help="Local artifact output directory (default: <model_path>/../squash)",
    )
    attest_hf_cmd.add_argument(
        "--policies",
        nargs="*",
        metavar="POLICY",
        default=None,
        help="Policy templates to evaluate (default: enterprise-strict; "
             "also: eu-cra, fedramp, cmmc — run 'squash policies' for all)",
    )
    attest_hf_cmd.add_argument(
        "--sign", action="store_true", help="Sign BOM via Sigstore keyless"
    )
    attest_hf_cmd.add_argument(
        "--fail-on-violation",
        action="store_true",
        help="Exit 1 if any policy violation is found",
    )
    attest_hf_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    attest_lc_cmd = sub.add_parser(
        "attest-langchain",
        help="Run a one-shot attestation pass on a model (LangChain-compatible)",
        description=(
            "Run the Squash attestation pipeline on MODEL_PATH and write the "
            "result JSON to stdout.  Mirrors the behaviour of SquashCallback on "
            "first LLM invocation, allowing offline pre-validation before deploying "
            "a LangChain agent.\n\n"
            "Example: squash attest-langchain ./my-model --policies enterprise-strict"
        ),
    )
    attest_lc_cmd.add_argument("model_path", help="Path to the model directory or file")
    attest_lc_cmd.add_argument(
        "--output-dir",
        metavar="PATH",
        default=None,
        help="Directory for attestation artifacts (default: <model_path>/../squash)",
    )
    attest_lc_cmd.add_argument(
        "--policies",
        nargs="*",
        metavar="POLICY",
        default=None,
        help="Policy templates to evaluate (default: enterprise-strict; "
             "also: eu-cra, fedramp, cmmc — run 'squash policies' for all)",
    )
    attest_lc_cmd.add_argument(
        "--sign", action="store_true", help="Sign BOM via Sigstore keyless"
    )
    attest_lc_cmd.add_argument(
        "--fail-on-violation",
        action="store_true",
        help="Exit 1 if any policy violation is found",
    )
    attest_lc_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    attest_mcp_cmd = sub.add_parser(
        "attest-mcp",
        help="Scan an MCP tool manifest catalog for supply-chain threats",
        description=(
            "Scan a Model Context Protocol (MCP) tools/list JSON catalog for six "
            "threat classes: prompt injection, SSRF vectors, tool shadowing, "
            "integrity gaps, data exfiltration patterns, and permission over-reach.\n\n"
            "Addresses EU AI Act Art. 9(2)(d): adversarial input resilience for "
            "agentic AI systems that invoke MCP tools at runtime.\n\n"
            "Example: squash attest-mcp ./mcp_catalog.json --policy mcp-strict"
        ),
    )
    attest_mcp_cmd.add_argument("catalog_path", help="Path to the MCP tool catalog JSON file")
    attest_mcp_cmd.add_argument(
        "--policy",
        metavar="POLICY",
        default="mcp-strict",
        help="Policy template to apply (default: mcp-strict)",
    )
    attest_mcp_cmd.add_argument(
        "--sign",
        action="store_true",
        help="Sign the catalog with Sigstore keyless signing after attestation",
    )
    attest_mcp_cmd.add_argument(
        "--fail-on-violation",
        action="store_true",
        help="Exit 1 if any error-severity finding is present",
    )
    attest_mcp_cmd.add_argument(
        "--json-result",
        metavar="PATH",
        default=None,
        help="Write scan result JSON to this file (default: stdout only)",
    )
    attest_mcp_cmd.add_argument(
        "--output-dir",
        metavar="PATH",
        default=None,
        help="Directory for attestation artifacts (default: catalog directory)",
    )
    attest_mcp_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── Wave 46 — Agent audit trail ───────────────────────────────────────────
    audit_cmd = sub.add_parser(
        "audit",
        help="Agent audit trail management (show / verify)",
        description=(
            "Manage the squash agent audit trail (append-only JSONL with hash chain).\n\n"
            "Examples:\n"
            "  squash audit show --n 20\n"
            "  squash audit verify --log /var/log/squash/audit.jsonl"
        ),
    )
    audit_sub = audit_cmd.add_subparsers(dest="audit_command")

    audit_show = audit_sub.add_parser(
        "show",
        help="Print the last N entries from the audit log",
    )
    audit_show.add_argument(
        "--n",
        type=int,
        default=20,
        help="Number of entries to show (default: 20)",
    )
    audit_show.add_argument(
        "--log",
        metavar="PATH",
        default=None,
        help="Audit log file path (default: $SQUASH_AUDIT_LOG or ~/.squash/audit.jsonl)",
    )
    audit_show.add_argument(
        "--json",
        dest="json_output",
        action="store_true",
        help="Output entries as a JSON array instead of pretty-printed lines",
    )
    audit_show.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    audit_verify = audit_sub.add_parser(
        "verify",
        help="Verify the hash chain integrity of the audit log (exit 0=intact, 2=tampered)",
    )
    audit_verify.add_argument(
        "--log",
        metavar="PATH",
        default=None,
        help="Audit log file path (default: $SQUASH_AUDIT_LOG or ~/.squash/audit.jsonl)",
    )
    audit_verify.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── Wave 47 — RAG knowledge base integrity ────────────────────────────────
    scan_rag_cmd = sub.add_parser(
        "scan-rag",
        help="RAG knowledge base integrity scanner — index a corpus and detect drift",
    )
    scan_rag_sub = scan_rag_cmd.add_subparsers(dest="scan_rag_command")

    scan_rag_index = scan_rag_sub.add_parser(
        "index", help="Hash every document in a corpus and write a signed manifest"
    )
    scan_rag_index.add_argument("corpus_dir", help="Corpus directory to index")
    scan_rag_index.add_argument(
        "--glob",
        default="**/*",
        metavar="PATTERN",
        help='File glob (default "**/*")',
    )
    scan_rag_index.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    scan_rag_verify = scan_rag_sub.add_parser(
        "verify",
        help="Verify live corpus against manifest (exit 0=intact, 2=drift detected)",
    )
    scan_rag_verify.add_argument("corpus_dir", help="Corpus directory to verify")
    scan_rag_verify.add_argument(
        "--json",
        dest="json_output",
        action="store_true",
        help="Print drift report as JSON",
    )
    scan_rag_verify.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── Wave 48 — Model transformation lineage ────────────────────────────────────
    lineage_cmd = sub.add_parser(
        "lineage",
        help="Model transformation lineage chain — record, show, and verify (EU AI Act Annex IV)",
        description=(
            "Manage the Merkle-chained transformation ledger for a model artefact.\n\n"
            "Addresses EU AI Act Annex IV technical documentation requirements (Art. 11)\n"
            "and NIST AI RMF GOVERN 1.7 (supply-chain provenance).  The chain file\n"
            "(.lineage_chain.json) travels with the model so provenance is available\n"
            "after transfer or M&A.\n\n"
            "Examples:\n"
            "  squash lineage record ./my-model --operation compress --params format=INT4 awq=true\n"
            "  squash lineage show   ./my-model\n"
            "  squash lineage verify ./my-model"
        ),
    )
    lineage_sub = lineage_cmd.add_subparsers(dest="lineage_command")

    lineage_record = lineage_sub.add_parser(
        "record", help="Append a transformation event to the lineage chain"
    )
    lineage_record.add_argument("model_dir", help="Model artefact directory")
    lineage_record.add_argument(
        "--operation",
        required=True,
        metavar="OP",
        help="Operation label (e.g. compress, quantize, sign, verify, export)",
    )
    lineage_record.add_argument(
        "--model-id",
        default="",
        dest="model_id",
        help="Model identifier (default: directory name)",
    )
    lineage_record.add_argument(
        "--input-dir",
        default="",
        dest="input_dir",
        help="Source model directory (default: model_dir)",
    )
    lineage_record.add_argument(
        "--params",
        nargs="*",
        metavar="KEY=VALUE",
        default=[],
        help="Arbitrary key=value operation parameters (repeatable)",
    )
    lineage_record.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    lineage_show = lineage_sub.add_parser(
        "show", help="Print the transformation lineage chain for a model directory"
    )
    lineage_show.add_argument("model_dir", help="Model artefact directory")
    lineage_show.add_argument(
        "--json",
        dest="json_output",
        action="store_true",
        help="Output events as a JSON array",
    )
    lineage_show.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    lineage_verify = lineage_sub.add_parser(
        "verify",
        help="Verify the Merkle chain integrity (exit 0=intact, 2=tampered/missing)",
    )
    lineage_verify.add_argument("model_dir", help="Model artefact directory")
    lineage_verify.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── remediate ──────────────────────────────────────────────────────────
    remediate_cmd = sub.add_parser(
        "remediate",
        help="Convert unsafe .bin/.pt/.pth pickle files to .safetensors",
    )
    remediate_cmd.add_argument("model_path", help="Model directory or single file to remediate")
    remediate_cmd.add_argument(
        "--convert-to",
        default="safetensors",
        dest="target_format",
        choices=["safetensors"],
        help="Target format (default: safetensors)",
    )
    remediate_cmd.add_argument("--output-dir", default=None, dest="output_dir",
                                help="Where to write converted files (default: alongside originals)")
    remediate_cmd.add_argument("--dry-run", action="store_true",
                                help="Analyse files but do not write converted output")
    remediate_cmd.add_argument("--overwrite", action="store_true",
                                help="Overwrite existing .safetensors files at the destination")
    remediate_cmd.add_argument("--sbom", default=None,
                                help="CycloneDX BOM to update with new hashes after conversion")
    remediate_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── evaluate ───────────────────────────────────────────────────────────
    evaluate_cmd = sub.add_parser(
        "evaluate",
        help="Dynamic behavioural safety red-team evaluation against an inference endpoint",
    )
    evaluate_cmd.add_argument(
        "endpoint",
        help="OpenAI-compatible base URL (e.g. http://localhost:11434/v1) or 'auto' to use squish serve",
    )
    evaluate_cmd.add_argument("--model", default="llama3",
                               help="Model name to pass to the endpoint (default: llama3)")
    evaluate_cmd.add_argument("--api-key", default=None, dest="api_key",
                               help="Bearer API key (optional for local endpoints)")
    evaluate_cmd.add_argument("--output-dir", default=None, dest="output_dir",
                               help="Directory for squash-eval-report.json (default: cwd)")
    evaluate_cmd.add_argument("--bom", default=None,
                               help="CycloneDX BOM to annotate with evaluation metrics")
    evaluate_cmd.add_argument("--fail-on-critical", action="store_true", dest="fail_on_critical",
                               help="Exit 2 if any critical probe fails")
    evaluate_cmd.add_argument("--timeout", type=float, default=30.0,
                               help="Seconds to wait per probe request (default: 30)")
    evaluate_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── edge-scan ──────────────────────────────────────────────────────────
    edge_scan_cmd = sub.add_parser(
        "edge-scan",
        help="Parse and security-scan TFLite (.tflite) or CoreML (.mlpackage) edge AI models",
    )
    edge_scan_cmd.add_argument("model_path",
                                help="Path to a .tflite file or .mlpackage directory")
    edge_scan_cmd.add_argument("--json-result", default=None, dest="json_result",
                                help="Write structured scan result to this JSON file")
    edge_scan_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    # ── chat ───────────────────────────────────────────────────────────────
    chat_cmd = sub.add_parser(
        "chat",
        help="Interactive RAG compliance auditor — ask plain-English questions about squash artifacts",
    )
    chat_cmd.add_argument("model_dir", help="Model directory containing squash attestation artifacts")
    chat_cmd.add_argument("--backend", choices=["ollama", "openai"], default="ollama",
                           help="LLM backend to use (default: ollama)")
    chat_cmd.add_argument("--model", default=None,
                           help="Model name (default: llama3 for ollama, gpt-4o-mini for openai)")
    chat_cmd.add_argument("--api-key", default=None, dest="api_key",
                           help="API key (required for openai backend)")
    chat_cmd.add_argument("--top-k", type=int, default=5, dest="top_k",
                           help="Number of chunks to retrieve per question (default: 5)")
    chat_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    mc_cmd = sub.add_parser(
        "model-card",
        help="Generate regulation-compliant model cards from squash attestation artifacts",
        description=(
            "Generate AI regulation–compliant model cards from squash attestation "
            "artifacts (ML-BOM, scan results, policy reports, VEX report).\n\n"
            "Example:\n"
            "  squash model-card ./my-model --format all"
        ),
    )
    mc_cmd.add_argument(
        "model_dir",
        help="Model directory containing squash attestation artifacts",
    )
    mc_cmd.add_argument(
        "--format",
        choices=["hf", "eu-ai-act", "iso-42001", "all"],
        default="hf",
        dest="mc_format",
        help="Output format: hf (HuggingFace), eu-ai-act (EU AI Act Art. 13), "
             "iso-42001 (ISO/IEC 42001:2023), all (write all three). Default: hf",
    )
    mc_cmd.add_argument(
        "--output-dir",
        default=None,
        dest="mc_output_dir",
        help="Directory to write model card file(s). Defaults to model_dir.",
    )
    mc_cmd.add_argument(
        "--model-id",
        default="",
        dest="mc_model_id",
        help="Override model identifier used in card metadata.",
    )
    mc_cmd.add_argument(
        "--license",
        default="apache-2.0",
        dest="mc_license",
        help="SPDX licence identifier for the card (default: apache-2.0).",
    )
    # W194 (Sprint 10) — first-class CLI: validate + push subflags
    mc_cmd.add_argument(
        "--validate",
        action="store_true",
        dest="mc_validate",
        help="Validate generated card(s) against the HuggingFace model-card schema. "
             "Exits non-zero on errors.",
    )
    mc_cmd.add_argument(
        "--validate-only",
        action="store_true",
        dest="mc_validate_only",
        help="Skip generation and validate an existing card file at "
             "model_dir/squash-model-card-hf.md.",
    )
    mc_cmd.add_argument(
        "--push-to-hub",
        default=None,
        dest="mc_push_repo",
        metavar="REPO_ID",
        help="After generating, push squash-model-card-hf.md to the given HF repo "
             "(e.g. user/model). Requires `huggingface_hub` to be installed.",
    )
    mc_cmd.add_argument(
        "--hub-token",
        default=None,
        dest="mc_hub_token",
        help="HuggingFace token for --push-to-hub. Falls back to HUGGING_FACE_HUB_TOKEN.",
    )
    mc_cmd.add_argument(
        "--json",
        action="store_true",
        dest="mc_json",
        help="With --validate or --validate-only, emit structured JSON report.",
    )
    mc_cmd.add_argument(
        "--quiet",
        action="store_true",
        help="Suppress non-error output",
    )

    # ── Wave 77 — Cloud CLI commands ──────────────────────────────────────────
    cloud_status_cmd = sub.add_parser(
        "cloud-status",
        help="Show EU AI Act conformance status for a single tenant",
        description=(
            "Query the in-memory cloud dashboard for EU AI Act conformance status "
            "of the specified tenant.  Exits 0 if conformant, 2 if non-conformant.\n\n"
            "Example: squash cloud-status acme-tenant-id"
        ),
    )
    cloud_status_cmd.add_argument(
        "tenant_id",
        help="Tenant identifier to inspect",
    )
    cloud_status_cmd.add_argument(
        "--json",
        action="store_true",
        dest="output_json",
        help="Also dump full conformance dict as JSON to stdout",
    )
    cloud_status_cmd.add_argument(
        "--quiet",
        action="store_true",
        help="Suppress non-error output",
    )

    cloud_report_cmd = sub.add_parser(
        "cloud-report",
        help="Print a platform-wide EU AI Act conformance report",
        description=(
            "Print a summary table of EU AI Act conformance for all registered "
            "tenants.  Exits 0 if all tenants are conformant, 2 if any are not.\n\n"
            "Example: squash cloud-report"
        ),
    )
    cloud_report_cmd.add_argument(
        "--json",
        action="store_true",
        dest="output_json",
        help="Dump full conformance report dict as JSON to stdout",
    )
    cloud_report_cmd.add_argument(
        "--quiet",
        action="store_true",
        help="Suppress non-error output",
    )

    cloud_export_cmd = sub.add_parser(
        "cloud-export",
        help="Export a complete compliance audit bundle for a tenant",
        description=(
            "Compose and export a complete compliance audit bundle for the "
            "specified tenant.  Scope is gated by SQUASH_PLAN "
            "(community/professional/enterprise).\n\n"
            "Example: squash cloud-export acme-tenant-id --output report.json"
        ),
    )
    cloud_export_cmd.add_argument(
        "tenant_id",
        help="Tenant identifier to export",
    )
    cloud_export_cmd.add_argument(
        "--output",
        metavar="PATH",
        default=None,
        dest="output_path",
        help="Write JSON to PATH (default: stdout; use - for stdout explicitly)",
    )
    cloud_export_cmd.add_argument(
        "--quiet",
        action="store_true",
        help="Suppress non-error output",
    )

    # ── Wave 79 — Cloud Attestation + Cloud VEX CLI ───────────────────────────
    cloud_attest_cmd = sub.add_parser(
        "cloud-attest",
        help="Attest a model for a tenant and register it in the cloud inventory",
        description=(
            "Run the full attestation pipeline against MODEL_PATH and register"
            " the result in the cloud dashboard inventory for TENANT_ID.\n\n"
            "Example: squash cloud-attest acme-corp ./models/llama-3.1-8b"
        ),
    )
    cloud_attest_cmd.add_argument(
        "tenant_id",
        help="Tenant identifier that owns this model",
    )
    cloud_attest_cmd.add_argument(
        "model_path",
        help="Path to the model directory or file to attest",
    )
    cloud_attest_cmd.add_argument(
        "--policy",
        metavar="POLICY",
        default="enterprise-strict",
        dest="policy",
        help="Policy to evaluate (default: enterprise-strict)",
    )
    cloud_attest_cmd.add_argument(
        "--output-path",
        metavar="PATH",
        default=None,
        dest="output_path",
        help="Directory for attestation artifacts (default: model_path directory)",
    )
    cloud_attest_cmd.add_argument(
        "--json",
        action="store_true",
        dest="output_json",
        help="Dump attestation result as JSON to stdout",
    )
    cloud_attest_cmd.add_argument(
        "--quiet",
        action="store_true",
        help="Suppress non-error output",
    )

    cloud_vex_cmd = sub.add_parser(
        "cloud-vex",
        help="List open VEX/CVE alerts for a tenant",
        description=(
            "Retrieve VEX alerts from the cloud dashboard for TENANT_ID.\n\n"
            "Example: squash cloud-vex acme-corp --limit 20"
        ),
    )
    cloud_vex_cmd.add_argument(
        "tenant_id",
        help="Tenant identifier to inspect",
    )
    cloud_vex_cmd.add_argument(
        "--limit",
        metavar="N",
        type=int,
        default=50,
        dest="limit",
        help="Maximum number of alerts to return (default: 50)",
    )
    cloud_vex_cmd.add_argument(
        "--status",
        metavar="STATUS",
        default=None,
        dest="vex_status",
        help="Filter by alert status: open | acknowledged | resolved",
    )
    cloud_vex_cmd.add_argument(
        "--severity",
        metavar="SEVERITY",
        default=None,
        dest="severity",
        help="Filter by severity: critical | high | medium | low | unknown",
    )
    cloud_vex_cmd.add_argument(
        "--json",
        action="store_true",
        dest="output_json",
        help="Dump alerts as JSON to stdout",
    )
    cloud_vex_cmd.add_argument(
        "--quiet",
        action="store_true",
        help="Suppress non-error output",
    )

    # ── Wave 80 — Cloud Risk Profile CLI ─────────────────────────────────────
    cloud_risk_cmd = sub.add_parser(
        "cloud-risk",
        help="Show EU AI Act risk profile for a tenant or the entire platform",
        description=(
            "Compute and display the EU AI Act risk tier for each model in a tenant's "
            "inventory, or a platform-wide risk overview when --overview is used. "
            "Risk tiers: UNACCEPTABLE > HIGH > LIMITED > MINIMAL (Art. 6/9).\n\n"
            "Example: squash cloud-risk acme-corp\n"
            "Example: squash cloud-risk --overview"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    cloud_risk_cmd.add_argument(
        "tenant_id",
        nargs="?",
        default=None,
        help="Tenant ID to inspect (omit when using --overview)",
    )
    cloud_risk_cmd.add_argument(
        "--overview",
        action="store_true",
        help="Show platform-wide risk summary across all tenants",
    )
    cloud_risk_cmd.add_argument(
        "--json",
        dest="output_json",
        action="store_true",
        help="Dump risk profile as JSON to stdout",
    )
    cloud_risk_cmd.add_argument(
        "--quiet",
        action="store_true",
        help="Suppress non-error output",
    )

    # ── Wave 81 — Cloud Remediation Plan CLI ─────────────────────────────────
    cloud_remediate_cmd = sub.add_parser(
        "cloud-remediate",
        help="Generate a prioritised EU AI Act remediation plan for a tenant",
        description=(
            "Produce a step-by-step remediation plan for a cloud tenant based on "
            "its EU AI Act risk tier.  Steps are ordered by priority "
            "(1 = critical, 2 = high, 3 = medium).\n\n"
            "Example: squash cloud-remediate acme-corp\n"
            "Example: squash cloud-remediate acme-corp --json"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    cloud_remediate_cmd.add_argument(
        "tenant_id",
        help="Tenant ID to generate a remediation plan for",
    )
    cloud_remediate_cmd.add_argument(
        "--json",
        dest="output_json",
        action="store_true",
        help="Dump remediation plan as JSON to stdout",
    )
    cloud_remediate_cmd.add_argument(
        "--quiet",
        action="store_true",
        help="Suppress non-error output",
    )

    # ── W170 — ISO 42001 Readiness Assessment ────────────────────────────────
    iso42001_cmd = sub.add_parser(
        "iso42001",
        help="ISO/IEC 42001:2023 AI Management System readiness assessment",
        description=(
            "Assess a model directory against the 38 controls of ISO/IEC 42001:2023 "
            "and generate a gap analysis with remediation roadmap.\n\n"
            "Example: squash iso42001 ./my-model\n"
            "Example: squash iso42001 ./my-model --output iso42001-report.json --format json\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    iso42001_cmd.add_argument("model_path", help="Path to model directory")
    iso42001_cmd.add_argument("--output", "-o", default=None, help="Output file path (default: model_path/iso42001_report.json)")
    iso42001_cmd.add_argument("--format", default="text", choices=["text", "json"], help="Output format (default: text)")
    iso42001_cmd.add_argument("--fail-below", type=float, default=None, metavar="SCORE",
                              help="Exit 2 if readiness score below this percentage")

    # ── W171 — Trust Package ──────────────────────────────────────────────────
    trust_pkg_cmd = sub.add_parser(
        "trust-package",
        help="Export a signed vendor attestation bundle (eliminates questionnaire process)",
        description=(
            "Bundle all compliance artifacts into a signed, verifiable trust package ZIP. "
            "Buyers verify it in <10 seconds instead of reviewing a 40-page questionnaire.\n\n"
            "Example: squash trust-package ./my-model --output vendor-package.zip\n"
            "Example: squash trust-package ./my-model --sign --model-id acme-llm-v2\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    trust_pkg_cmd.add_argument("model_path", help="Path to model directory containing squash artifacts")
    trust_pkg_cmd.add_argument("--output", "-o", default=None, help="Output ZIP path (default: <model_id>-trust-package.zip)")
    trust_pkg_cmd.add_argument("--model-id", default=None, help="Override model ID in package")
    trust_pkg_cmd.add_argument("--sign", action="store_true", help="Sign manifest via Sigstore")
    trust_pkg_cmd.add_argument("--verification-url", default="", help="URL for online verification")

    verify_trust_cmd = sub.add_parser(
        "verify-trust-package",
        help="Verify integrity and compliance posture of a trust package ZIP",
        description=(
            "Verify a vendor trust package: check SHA-256 integrity of all artifacts, "
            "parse the compliance summary, and report pass/fail.\n\n"
            "Example: squash verify-trust-package vendor-package.zip\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    verify_trust_cmd.add_argument("package_path", help="Path to trust package ZIP file")
    verify_trust_cmd.add_argument("--json", dest="output_json", action="store_true", help="Output JSON result")
    verify_trust_cmd.add_argument("--fail-on-error", action="store_true", help="Exit 2 if verification fails")

    # ── W172 — Agent Audit (OWASP Agentic AI Top 10) ─────────────────────────
    agent_audit_cmd = sub.add_parser(
        "agent-audit",
        help="OWASP Agentic AI Top 10 compliance audit for AI agents",
        description=(
            "Audit an AI agent manifest against the OWASP Agentic AI Top 10 (December 2025): "
            "goal hijacking, unsafe tools, identity abuse, memory poisoning, cascading failure, "
            "rogue agents, auditability, excessive autonomy, data exfiltration, human oversight.\n\n"
            "Example: squash agent-audit ./agent.json\n"
            "Example: squash agent-audit ./agent.json --output agent_audit.json --fail-on-critical\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    agent_audit_cmd.add_argument("manifest_path", help="Path to agent manifest JSON file")
    agent_audit_cmd.add_argument("--output", "-o", default=None, help="Output path for audit report JSON")
    agent_audit_cmd.add_argument("--fail-on-critical", action="store_true", help="Exit 2 if any CRITICAL risk found")
    agent_audit_cmd.add_argument("--fail-on-high", action="store_true", help="Exit 2 if any HIGH risk found")
    agent_audit_cmd.add_argument("--format", default="text", choices=["text", "json"], help="Output format")

    # ── W173 — Incident Response ──────────────────────────────────────────────
    incident_cmd = sub.add_parser(
        "incident",
        help="Generate AI incident response package (EU AI Act Article 73 disclosure)",
        description=(
            "Generate a structured incident response package including the model attestation "
            "snapshot, EU AI Act Article 73 disclosure document, drift delta, and remediation plan.\n\n"
            "Example: squash incident ./my-model --description 'Model output exposed PII'\n"
            "Example: squash incident ./my-model --severity serious --affected-persons 150\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    incident_cmd.add_argument("model_path", help="Path to model directory")
    incident_cmd.add_argument("--description", "-d", required=True, help="Incident description")
    incident_cmd.add_argument("--timestamp", default=None, help="Incident timestamp (ISO8601, default: now)")
    incident_cmd.add_argument("--severity", default="serious",
                              choices=["critical", "serious", "moderate", "minor"],
                              help="Incident severity (default: serious)")
    incident_cmd.add_argument("--category", default="other",
                              choices=["bias_discrimination", "pii_exposure", "harmful_output",
                                       "model_failure", "security_breach", "accuracy_regression",
                                       "policy_violation", "data_poisoning", "prompt_injection", "other"],
                              help="Incident category")
    incident_cmd.add_argument("--affected-persons", type=int, default=0, dest="affected_persons",
                              help="Number of affected persons")
    incident_cmd.add_argument("--output-dir", default=None, dest="output_dir",
                              help="Output directory for incident package")
    incident_cmd.add_argument("--model-id", default=None, dest="model_id", help="Override model ID")

    # ── W174 — Board Report ───────────────────────────────────────────────────
    board_report_cmd = sub.add_parser(
        "board-report",
        help="Generate executive AI compliance board report",
        description=(
            "Generate a quarterly AI compliance board report: compliance scorecard, "
            "model portfolio status, violations, CVEs, regulatory deadlines, and remediation roadmap.\n\n"
            "Example: squash board-report --models-dir ./models --quarter Q2-2026\n"
            "Example: squash board-report --model ./my-model --output-dir ./reports\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    board_report_cmd.add_argument("--models-dir", default=None, dest="models_dir",
                                  help="Directory containing model subdirectories")
    board_report_cmd.add_argument("--model", default=None, dest="model_path",
                                  help="Single model directory path")
    board_report_cmd.add_argument("--quarter", "-q", default=None,
                                  help="Quarter identifier, e.g. Q2-2026 (default: current quarter)")
    board_report_cmd.add_argument("--output-dir", default=None, dest="output_dir",
                                  help="Output directory (default: ./board-report-<quarter>)")
    board_report_cmd.add_argument("--format", default="all", choices=["all", "json", "md", "text"],
                                  help="Output format (default: all)")
    board_report_cmd.add_argument("--json", dest="output_json", action="store_true",
                                  help="Print JSON summary to stdout")

    # ── W182 — Annual Review ──────────────────────────────────────────────────
    annual_review_cmd = sub.add_parser(
        "annual-review",
        help="Generate annual AI system compliance review",
        description=(
            "Generate a full annual AI compliance review: model portfolio audit, "
            "compliance score trend, incident log, regulatory changes, and next-year objectives.\n\n"
            "Examples:\n"
            "  squash annual-review --year 2025 --models-dir ./models\n"
            "  squash annual-review --model ./my-model --output-dir ./annual-review-2025\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    annual_review_cmd.add_argument("--year", type=int, default=None, help="Review year (default: last year)")
    annual_review_cmd.add_argument("--models-dir", default=None, dest="models_dir")
    annual_review_cmd.add_argument("--model", default=None, dest="model_path")
    annual_review_cmd.add_argument("--output-dir", default=None, dest="output_dir")
    annual_review_cmd.add_argument("--json", dest="output_json", action="store_true")

    # ── W183 — Attestation Registry ───────────────────────────────────────────
    pub_cmd = sub.add_parser(
        "publish",
        help="Publish attestation to the squash public registry",
        description=(
            "Publish a signed attestation to the squash attestation registry "
            "(att://attestations.getsquash.dev). Buyers can verify your compliance posture "
            "in <10 seconds without questionnaires.\n\n"
            "Examples:\n"
            "  squash publish ./my-model --org acme-corp\n"
            "  squash publish ./my-model --org acme-corp --private\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    pub_cmd.add_argument("model_path", help="Path to model directory with squash artifacts")
    pub_cmd.add_argument("--org", default="default", help="Organization name")
    pub_cmd.add_argument("--model-id", default=None, dest="model_id")
    pub_cmd.add_argument("--private", action="store_true", help="Publish as private (not queryable)")
    pub_cmd.add_argument("--db", default=None)

    lookup_cmd = sub.add_parser(
        "lookup",
        help="Query the squash attestation registry",
        description="Look up published attestations by model ID or organization.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    lookup_cmd.add_argument("--model-id", default=None, dest="model_id")
    lookup_cmd.add_argument("--org", default=None)
    lookup_cmd.add_argument("--entry-id", default=None, dest="entry_id")
    lookup_cmd.add_argument("--json", dest="output_json", action="store_true")
    lookup_cmd.add_argument("--db", default=None)

    reg_verify_cmd = sub.add_parser(
        "verify-entry",
        help="Verify integrity of a published registry entry",
    )
    reg_verify_cmd.add_argument("entry_id", help="Registry entry ID")
    reg_verify_cmd.add_argument("--db", default=None)

    # ── W184 — CISO Dashboard ─────────────────────────────────────────────────
    dashboard_cmd = sub.add_parser(
        "dashboard",
        help="CISO/Executive AI compliance dashboard",
        description=(
            "Render a terminal compliance dashboard: portfolio score, violations, CVEs, "
            "regulatory deadline countdown, and model risk heat-map.\n\n"
            "Examples:\n"
            "  squash dashboard --models-dir ./models\n"
            "  squash dashboard --model ./my-model --json\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    dashboard_cmd.add_argument("--models-dir", default=None, dest="models_dir")
    dashboard_cmd.add_argument("--model", default=None, dest="model_path")
    dashboard_cmd.add_argument("--json", dest="output_json", action="store_true")
    dashboard_cmd.add_argument("--no-color", action="store_true", dest="no_color")

    # ── W185 — Regulatory Intelligence Feed ───────────────────────────────────
    regulatory_cmd = sub.add_parser(
        "regulatory",
        help="Regulatory intelligence feed — AI regulation tracking and deadline monitoring",
        description=(
            "Track AI regulations across all major jurisdictions, monitor enforcement deadlines, "
            "and check which regulations affect your AI portfolio.\n\n"
            "Examples:\n"
            "  squash regulatory status\n"
            "  squash regulatory list --jurisdiction eu\n"
            "  squash regulatory updates --since 2026-01-01\n"
            "  squash regulatory deadlines --days 180\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    reg_sub = regulatory_cmd.add_subparsers(dest="regulatory_command", metavar="SUBCOMMAND")

    reg_status = reg_sub.add_parser("status", help="Overall regulatory status summary")
    reg_status.add_argument("--json", dest="output_json", action="store_true")

    reg_list = reg_sub.add_parser("list", help="List all tracked regulations")
    reg_list.add_argument("--jurisdiction", default=None, help="Filter by jurisdiction (eu, us_federal, us_state, global)")
    reg_list.add_argument("--industry", default=None, help="Filter by industry")
    reg_list.add_argument("--json", dest="output_json", action="store_true")

    reg_updates = reg_sub.add_parser("updates", help="Show regulatory changes since a date")
    reg_updates.add_argument("--since", default=None, help="ISO date filter, e.g. 2026-01-01")
    reg_updates.add_argument("--json", dest="output_json", action="store_true")

    reg_deadlines = reg_sub.add_parser("deadlines", help="Upcoming enforcement deadlines")
    reg_deadlines.add_argument("--days", type=int, default=365, help="Look-ahead window in days")
    reg_deadlines.add_argument("--json", dest="output_json", action="store_true")

    # ── W186 — M&A Due Diligence ──────────────────────────────────────────────
    dd_cmd = sub.add_parser(
        "due-diligence",
        help="M&A / investment AI due diligence package",
        description=(
            "Generate a comprehensive AI compliance package for M&A review: model inventory, "
            "security exposure, regulatory compliance matrix, training data provenance, "
            "bias audit results, liability flags, and R&W guidance.\n\n"
            "Examples:\n"
            "  squash due-diligence --models-dir ./models --company AcmeCorp\n"
            "  squash due-diligence --model ./my-model --deal-type investment\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    dd_cmd.add_argument("--models-dir", default=None, dest="models_dir")
    dd_cmd.add_argument("--model", default=None, dest="model_path")
    dd_cmd.add_argument("--company", default="Target Company", help="Target company name")
    dd_cmd.add_argument("--deal-type", default="acquisition", dest="deal_type",
                        choices=["acquisition", "investment", "partnership"])
    dd_cmd.add_argument("--output-dir", default=None, dest="output_dir")
    dd_cmd.add_argument("--json", dest="output_json", action="store_true")

    # ── W178 — AI Vendor Risk Register ───────────────────────────────────────
    vendor_cmd = sub.add_parser(
        "vendor",
        help="AI Vendor Risk Register — track and assess third-party AI vendors",
        description=(
            "Manage the AI vendor risk register: add vendors, generate due-diligence "
            "questionnaires, import Trust Packages, and monitor assessment status.\n\n"
            "Examples:\n"
            "  squash vendor add --name OpenAI --risk-tier high --use-case 'Customer chat'\n"
            "  squash vendor list\n"
            "  squash vendor questionnaire VENDOR_ID\n"
            "  squash vendor import-trust-package VENDOR_ID ./vendor.zip\n"
            "  squash vendor summary\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    vendor_sub = vendor_cmd.add_subparsers(dest="vendor_command", metavar="SUBCOMMAND")

    vendor_add = vendor_sub.add_parser("add", help="Add a new AI vendor to the register")
    vendor_add.add_argument("--name", required=True, help="Vendor name")
    vendor_add.add_argument("--website", default="", help="Vendor website URL")
    vendor_add.add_argument("--risk-tier", default="medium",
                            choices=["critical","high","medium","low"], dest="risk_tier",
                            help="Risk tier (default: medium)")
    vendor_add.add_argument("--use-case", default="", dest="use_case", help="Use case description")
    vendor_add.add_argument("--data-access", default="none", dest="data_access",
                            help="Data accessed (e.g. PII, financial, none)")
    vendor_add.add_argument("--notes", default="", help="Additional notes")
    vendor_add.add_argument("--db", default=None, help="Registry database path")

    vendor_list = vendor_sub.add_parser("list", help="List registered vendors")
    vendor_list.add_argument("--tier", default=None, help="Filter by risk tier")
    vendor_list.add_argument("--json", dest="output_json", action="store_true")
    vendor_list.add_argument("--db", default=None)

    vendor_q = vendor_sub.add_parser("questionnaire", help="Generate due-diligence questionnaire for a vendor")
    vendor_q.add_argument("vendor_id", help="Vendor ID")
    vendor_q.add_argument("--output", "-o", default=None, help="Output file (.json or .txt)")
    vendor_q.add_argument("--db", default=None)

    vendor_import = vendor_sub.add_parser("import-trust-package", help="Import and verify a vendor Trust Package")
    vendor_import.add_argument("vendor_id", help="Vendor ID")
    vendor_import.add_argument("package_path", help="Path to trust package ZIP")
    vendor_import.add_argument("--db", default=None)

    vendor_summary = vendor_sub.add_parser("summary", help="Show vendor risk register summary")
    vendor_summary.add_argument("--json", dest="output_json", action="store_true")
    vendor_summary.add_argument("--db", default=None)

    # ── W179 — AI Asset Registry ──────────────────────────────────────────────
    registry_cmd = sub.add_parser(
        "registry",
        help="AI Asset Registry — inventory of all AI models in the organization",
        description=(
            "Maintain a continuously updated inventory of every AI model the organization owns.\n\n"
            "Examples:\n"
            "  squash registry add --model-id gpt4-ft-v2 --environment production\n"
            "  squash registry sync ./my-model\n"
            "  squash registry list\n"
            "  squash registry summary\n"
            "  squash registry export --format md\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    registry_sub = registry_cmd.add_subparsers(dest="registry_command", metavar="SUBCOMMAND")

    registry_add = registry_sub.add_parser("add", help="Register an AI asset")
    registry_add.add_argument("--model-id", required=True, dest="model_id")
    registry_add.add_argument("--model-path", default="", dest="model_path")
    registry_add.add_argument("--environment", default="development",
                              choices=["production","staging","development","research","retired"])
    registry_add.add_argument("--owner", default="")
    registry_add.add_argument("--team", default="")
    registry_add.add_argument("--risk-tier", default="unknown", dest="risk_tier")
    registry_add.add_argument("--notes", default="")
    registry_add.add_argument("--shadow", action="store_true", help="Flag as shadow AI")
    registry_add.add_argument("--db", default=None)

    registry_sync = registry_sub.add_parser("sync", help="Sync an asset from squash attestation artifacts")
    registry_sync.add_argument("model_path", help="Path to model directory with squash artifacts")
    registry_sync.add_argument("--db", default=None)

    registry_list = registry_sub.add_parser("list", help="List all registered assets")
    registry_list.add_argument("--environment", default=None)
    registry_list.add_argument("--risk-tier", default=None, dest="risk_tier")
    registry_list.add_argument("--shadow-only", action="store_true", dest="shadow_only")
    registry_list.add_argument("--json", dest="output_json", action="store_true")
    registry_list.add_argument("--db", default=None)

    registry_summary = registry_sub.add_parser("summary", help="Show asset registry summary")
    registry_summary.add_argument("--json", dest="output_json", action="store_true")
    registry_summary.add_argument("--db", default=None)

    registry_export = registry_sub.add_parser("export", help="Export registry to JSON or Markdown")
    registry_export.add_argument("--format", default="json", choices=["json","md"])
    registry_export.add_argument("--output", "-o", default=None)
    registry_export.add_argument("--db", default=None)

    # ── W180 — Training Data Lineage ──────────────────────────────────────────
    data_lineage_cmd = sub.add_parser(
        "data-lineage",
        help="Training Data Lineage Certificate — license check, PII risk, GDPR assessment",
        description=(
            "Trace training datasets from a model directory, check licenses against the "
            "SPDX database, flag PII risks, and generate a signed lineage certificate.\n\n"
            "Examples:\n"
            "  squash data-lineage ./my-model\n"
            "  squash data-lineage ./my-model --config train_config.json\n"
            "  squash data-lineage ./my-model --datasets wikipedia,common_crawl\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    data_lineage_cmd.add_argument("model_path", help="Path to model directory")
    data_lineage_cmd.add_argument("--config", default=None, dest="config_path",
                                  help="Training config file path")
    data_lineage_cmd.add_argument("--datasets", default=None,
                                  help="Comma-separated list of dataset names (supplements auto-detection)")
    data_lineage_cmd.add_argument("--model-id", default=None, dest="model_id")
    data_lineage_cmd.add_argument("--output", "-o", default=None,
                                  help="Output path (default: model_path/data_lineage_certificate.json)")
    data_lineage_cmd.add_argument("--format", default="text", choices=["text", "json"])
    data_lineage_cmd.add_argument("--fail-on-pii", action="store_true", dest="fail_on_pii",
                                  help="Exit 2 if HIGH or CRITICAL PII risk detected")
    data_lineage_cmd.add_argument("--fail-on-license", action="store_true", dest="fail_on_license",
                                  help="Exit 2 if any license issues detected")

    # ── W181 — Bias Audit ─────────────────────────────────────────────────────
    bias_audit_cmd = sub.add_parser(
        "bias-audit",
        help="Algorithmic bias audit (NYC Local Law 144, EU AI Act Annex III, ECOA)",
        description=(
            "Audit model predictions for bias across protected attributes.\n"
            "Metrics: Demographic Parity Difference, Disparate Impact Ratio (4/5ths rule), "
            "Equalized Odds Difference, Predictive Equality Difference.\n\n"
            "Examples:\n"
            "  squash bias-audit --predictions pred.csv --protected age_group,gender\n"
            "  squash bias-audit --predictions pred.csv --standard nyc_local_law_144 "
            "--label-col hired --pred-col model_output --fail-on-fail\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    bias_audit_cmd.add_argument("--predictions", required=True, dest="predictions_path",
                                help="Path to CSV with predictions and protected attributes")
    bias_audit_cmd.add_argument("--protected", required=True,
                                help="Comma-separated protected attribute column names")
    bias_audit_cmd.add_argument("--label-col", default="label", dest="label_col",
                                help="Ground truth label column name (default: label)")
    bias_audit_cmd.add_argument("--pred-col", default="prediction", dest="pred_col",
                                help="Prediction column name (default: prediction)")
    bias_audit_cmd.add_argument("--standard", default="generic", dest="standard",
                                choices=["nyc_local_law_144","eu_ai_act_annex_iii",
                                         "ecoa_4_5ths_rule","fair_housing","generic"],
                                help="Regulatory standard for thresholds (default: generic)")
    bias_audit_cmd.add_argument("--model-id", default="model", dest="model_id")
    bias_audit_cmd.add_argument("--output", "-o", default=None,
                                help="Output path for audit report JSON")
    bias_audit_cmd.add_argument("--format", default="text", choices=["text","json"])
    bias_audit_cmd.add_argument("--fail-on-fail", action="store_true", dest="fail_on_fail",
                                help="Exit 2 if overall verdict is FAIL")
    bias_audit_cmd.add_argument("--fail-on-warn", action="store_true", dest="fail_on_warn",
                                help="Exit 2 if overall verdict is WARN or FAIL")

    # ── W191 — SBOM diff ──────────────────────────────────────────────────────
    diff_cmd = sub.add_parser(
        "diff",
        help="Compare two squash attestation files and show compliance delta",
        description=(
            "Compare two squash attestation JSON files. Shows compliance score movement,\n"
            "component changes, policy drift, and vulnerability lifecycle.\n\n"
            "Examples:\n"
            "  squash diff v1.json v2.json\n"
            "  squash diff v1.json v2.json --format json\n"
            "  squash diff v1.json v2.json --format html --output delta.html\n"
            "  squash diff v1.json v2.json --fail-on-regression\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    diff_cmd.add_argument("before", help="Path to the older (before) attestation JSON")
    diff_cmd.add_argument("after", help="Path to the newer (after) attestation JSON")
    diff_cmd.add_argument(
        "--format", default="table",
        choices=["table", "json", "html", "summary"],
        help="Output format (default: table)",
    )
    diff_cmd.add_argument("--output", "-o", default=None, help="Write output to file instead of stdout")
    diff_cmd.add_argument(
        "--fail-on-regression", action="store_true", dest="fail_on_regression",
        help="Exit 2 if compliance regression is detected",
    )

    # ── W190 — Webhook management ─────────────────────────────────────────────
    webhook_cmd = sub.add_parser(
        "webhook",
        help="Manage outbound webhook endpoints for squash compliance events",
        description=(
            "Register, list, test, and remove outbound webhook endpoints.\n"
            "Squash POSTs signed JSON events to registered endpoints on attestation,\n"
            "violation, drift, and VEX alert events.\n\n"
            "Examples:\n"
            "  squash webhook add --url https://hooks.example.com/squash --events attestation.complete\n"
            "  squash webhook list\n"
            "  squash webhook test --url https://hooks.example.com/squash\n"
            "  squash webhook remove <id>\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    webhook_sub = webhook_cmd.add_subparsers(dest="webhook_command")
    wh_add = webhook_sub.add_parser("add", help="Register a new webhook endpoint")
    wh_add.add_argument("--url", required=True, help="HTTPS endpoint URL")
    wh_add.add_argument(
        "--events", default=None,
        help="Comma-separated event types (default: all). Options: attestation.complete,violation.detected,drift.detected,vex.alert,score.changed",
    )
    wh_add.add_argument("--secret", default=None, help="HMAC-SHA256 signing secret (auto-generated if omitted)")
    wh_list = webhook_sub.add_parser("list", help="List registered webhook endpoints")
    wh_list.add_argument("--all", action="store_true", dest="show_all", help="Include inactive endpoints")
    wh_test = webhook_sub.add_parser("test", help="Send a test event to a URL")
    wh_test.add_argument("--url", required=True, help="URL to test")
    wh_rm = webhook_sub.add_parser("remove", help="Deactivate a webhook endpoint")
    wh_rm.add_argument("id", help="Endpoint ID to remove")

    # ── W188 — Telemetry ──────────────────────────────────────────────────────
    telemetry_cmd = sub.add_parser(
        "telemetry",
        help="Configure and test OpenTelemetry integration",
        description=(
            "Configure squash to emit OpenTelemetry spans for every attestation run.\n"
            "Integrates with Datadog, Honeycomb, Jaeger, and any OTLP-compatible backend.\n\n"
            "Examples:\n"
            "  squash telemetry status\n"
            "  squash telemetry test --endpoint http://localhost:4317\n"
            "  squash telemetry configure --endpoint http://otelcollector:4317 --service squash-prod\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    telemetry_sub = telemetry_cmd.add_subparsers(dest="telemetry_command")
    tel_status = telemetry_sub.add_parser("status", help="Show current telemetry configuration")
    tel_test = telemetry_sub.add_parser("test", help="Send a test span to verify connectivity")
    tel_test.add_argument("--endpoint", default=None, help="OTLP gRPC endpoint (overrides env var)")
    tel_test.add_argument("--http-endpoint", default=None, dest="http_endpoint", help="OTLP HTTP endpoint")
    tel_configure = telemetry_sub.add_parser("configure", help="Show configuration instructions")
    tel_configure.add_argument("--endpoint", default=None, help="OTLP gRPC endpoint")
    tel_configure.add_argument("--service", default="squash", help="Service name (default: squash)")

    # ── W189 — GitOps ─────────────────────────────────────────────────────────
    gitops_cmd = sub.add_parser(
        "gitops",
        help="ArgoCD / Flux GitOps enforcement gate for Kubernetes deployments",
        description=(
            "Enforce squash compliance in Kubernetes GitOps pipelines.\n"
            "Blocks deployment of AI models that lack valid attestations or\n"
            "fall below the minimum compliance score.\n\n"
            "Examples:\n"
            "  squash gitops check --manifest deployment.yaml --min-score 80\n"
            "  squash gitops webhook-manifest --url https://squash.example.com\n"
            "  squash gitops annotate --deployment my-model --attestation att://myorg/v1 --score 87.5\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    gitops_sub = gitops_cmd.add_subparsers(dest="gitops_command")
    go_check = gitops_sub.add_parser("check", help="Check a K8s manifest for squash compliance annotations")
    go_check.add_argument("--manifest", required=True, dest="manifest_path", help="Path to K8s manifest YAML")
    go_check.add_argument("--min-score", type=float, default=80.0, dest="min_score", help="Minimum compliance score (default: 80)")
    go_check.add_argument("--require-attestation", action="store_true", default=True, dest="require_attestation")
    go_check.add_argument("--json", action="store_true", dest="output_json", help="Output JSON")
    go_manifest = gitops_sub.add_parser("webhook-manifest", help="Generate K8s ValidatingWebhookConfiguration YAML")
    go_manifest.add_argument("--url", required=True, help="HTTPS URL where squash webhook is hosted")
    go_manifest.add_argument("--namespace", default="squash-system", help="Kubernetes namespace (default: squash-system)")
    go_manifest.add_argument("--failure-policy", default="Fail", choices=["Fail", "Ignore"], dest="failure_policy")
    go_annotate = gitops_sub.add_parser("annotate", help="Print kubectl annotate command for a deployment")
    go_annotate.add_argument("--deployment", required=True, help="Deployment name")
    go_annotate.add_argument("--attestation", required=True, dest="attestation_id", help="Attestation ID (att:// URI)")
    go_annotate.add_argument("--score", type=float, required=True, dest="compliance_score", help="Compliance score")
    go_annotate.add_argument("--policy", default="eu-ai-act", help="Policy name (default: eu-ai-act)")
    go_annotate.add_argument("--passed", action="store_true", default=True)

    # ── W249-W250 / D5 — Industry Compliance Benchmarking ─────────────────────
    ib_cmd = sub.add_parser(
        "industry-benchmark",
        help="Industry compliance benchmarking — see how you compare to sector peers",
        description=(
            "Compare your attestation posture against published sector baselines.\n"
            "8 sectors: financial-services, healthcare, legal, technology,\n"
            "           manufacturing, retail, government, education\n\n"
            "Examples:\n"
            "  squash industry-benchmark report --sector financial-services --scores 71,74,68\n"
            "  squash industry-benchmark report --sector technology --registry ~/.squash/attestation_registry.db\n"
            "  squash industry-benchmark compare --sectors technology,financial-services --scores 71,74\n"
            "  squash industry-benchmark list-sectors\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    ib_sub = ib_cmd.add_subparsers(dest="ib_command")

    ib_report = ib_sub.add_parser("report", help="Generate a QBR-ready sector benchmark report")
    ib_report.add_argument("--sector", required=True, dest="sector_id",
                            help="Industry sector (e.g. financial-services, technology)")
    _ib_profile_args(ib_report)
    ib_report.add_argument("--format", default="text", choices=["text", "json", "md"], dest="ib_format")
    ib_report.add_argument("--out", default=None, help="Write report to PATH")

    ib_compare = ib_sub.add_parser("compare", help="Compare your score across multiple sectors")
    ib_compare.add_argument("--sectors", required=True,
                             help="Comma-separated sector IDs (e.g. technology,financial-services)")
    _ib_profile_args(ib_compare)
    ib_compare.add_argument("--format", default="text", choices=["text", "json", "md"], dest="ib_format")

    ib_list = ib_sub.add_parser("list-sectors", help="List all available benchmark sectors")
    ib_list.add_argument("--json", action="store_true", dest="output_json")

    # ── W226-W228 / D2 — AI Identity Attestation ──────────────────────────────
    ai_cmd = sub.add_parser(
        "attest-identity",
        help="AI identity attestation — least-privilege analysis for AI agents (92% lack visibility)",
        description=(
            "Attest the identity configuration of an AI agent or service account.\n"
            "Verifies scopes, rotation age, MFA, and least-privilege policy compliance.\n"
            "Providers: AWS IAM · Azure AD · Okta\n\n"
            "Examples:\n"
            "  squash attest-identity attest --provider aws-iam --principal ai-agent-prod\n"
            "  squash attest-identity attest --provider okta --domain acme.okta.com --principal ai-bot\n"
            "  squash attest-identity attest --principal-file principal.json --policy policy.json\n"
            "  squash attest-identity list-principals --provider aws-iam\n"
            "  squash attest-identity policy-init --principal ai-agent-prod\n"
            "  squash attest-identity verify cert.json\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    ai_sub = ai_cmd.add_subparsers(dest="ai_command")

    ai_attest = ai_sub.add_parser("attest", help="Attest an AI principal's identity configuration")
    ai_attest.add_argument("--provider", choices=["aws-iam", "azure-ad", "okta", "file"],
                            default="file", help="Identity provider (default: file)")
    ai_attest.add_argument("--principal", default="", dest="principal_name",
                            help="Principal name / role ARN / app label to attest")
    ai_attest.add_argument("--principal-file", default=None, dest="principal_file",
                            help="JSON file containing a pre-built IdentityPrincipal")
    ai_attest.add_argument("--policy", default=None, dest="policy_file",
                            help="Least-privilege policy JSON file")
    ai_attest.add_argument("--priv-key", default=None, dest="priv_key",
                            help="Ed25519 .priv.pem for signing")
    ai_attest.add_argument("--out", default=None, help="Write certificate to PATH")
    ai_attest.add_argument("--format", default="json", choices=["json", "md", "text"],
                            dest="ai_format")
    ai_attest.add_argument("--fail-on-violation", action="store_true", dest="fail_on_violation",
                            help="Exit 2 if any CRITICAL violation found")
    # Provider-specific
    ai_attest.add_argument("--domain", default="", help="Okta domain (e.g. acme.okta.com)")
    ai_attest.add_argument("--token", default="", dest="api_token",
                            help="API token (or set OKTA_API_TOKEN / AZURE_ACCESS_TOKEN)")
    ai_attest.add_argument("--tenant-id", default="", dest="tenant_id",
                            help="Azure AD tenant ID")
    ai_attest.add_argument("--region", default="us-east-1", dest="aws_region")

    ai_verify = ai_sub.add_parser("verify", help="Verify an attestation certificate's signature")
    ai_verify.add_argument("cert_path")
    ai_verify.add_argument("--json", action="store_true", dest="output_json")

    ai_list = ai_sub.add_parser("list-principals", help="List AI principals from a provider")
    ai_list.add_argument("--provider", required=True, choices=["aws-iam", "azure-ad", "okta"])
    ai_list.add_argument("--filter", default="", dest="filter_tag",
                          help="Filter by tag value / label substring")
    ai_list.add_argument("--domain", default="", help="Okta domain")
    ai_list.add_argument("--token", default="", dest="api_token")
    ai_list.add_argument("--tenant-id", default="", dest="tenant_id")
    ai_list.add_argument("--region", default="us-east-1", dest="aws_region")
    ai_list.add_argument("--json", action="store_true", dest="output_json")

    ai_policy = ai_sub.add_parser("policy-init", help="Scaffold a least-privilege policy JSON file")
    ai_policy.add_argument("--principal", default="ai-agent-prod", dest="principal_name")
    ai_policy.add_argument("--out", default=None)

    # ── W267-W269 / C10 — Runtime Hallucination Monitor ──────────────────────
    # Extends the existing `squash monitor` command with --mode hallucination.
    # We also register a standalone `squash hallucination-monitor` alias for
    # discoverability.
    for _mon_name in ("hallucination-monitor",):
        _hm_cmd = sub.add_parser(
            _mon_name,
            help="Runtime hallucination monitor — EU AI Act Art. 9 post-market monitoring",
            description=(
                "18% production hallucination rate · 39% of chatbots reworked in 2024.\n"
                "Continuous post-market monitoring required by EU AI Act Article 9.\n\n"
                "Examples:\n"
                "  squash hallucination-monitor run --endpoint http://model:8080\n"
                "  squash hallucination-monitor run --endpoint mock://test --once\n"
                "  squash hallucination-monitor score --response 'Paris is the capital' --context 'Paris is the capital of France'\n"
                "  squash hallucination-monitor status\n"
                "  squash hallucination-monitor batch --requests-file ./requests.json\n"
            ),
            formatter_class=argparse.RawDescriptionHelpFormatter,
        )
        _hm_sub = _hm_cmd.add_subparsers(dest="hm_command")

        _hm_run = _hm_sub.add_parser("run", help="Start monitor daemon or single-shot poll")
        _hm_run.add_argument("--endpoint", required=True, dest="endpoint")
        _hm_run.add_argument("--model-id", default="", dest="model_id")
        _hm_run.add_argument("--sample-rate", type=float, default=0.05, dest="sample_rate")
        _hm_run.add_argument("--threshold", type=float, default=0.10, dest="threshold")
        _hm_run.add_argument("--window", type=int, default=60, dest="window_minutes", help="Rolling window in minutes (default: 60)")
        _hm_run.add_argument("--poll-interval", type=float, default=30.0, dest="poll_interval", help="Seconds between polls (default: 30)")
        _hm_run.add_argument("--once", action="store_true", dest="once", help="Score one request and exit (cron mode)")
        _hm_run.add_argument("--state-dir", default=None, dest="state_dir")
        _hm_run.add_argument("--format", default="text", choices=["text", "json"], dest="hm_format")

        _hm_score = _hm_sub.add_parser("score", help="Score a single response for hallucination risk")
        _hm_score.add_argument("--response", required=True, dest="response")
        _hm_score.add_argument("--context", default="", dest="context")
        _hm_score.add_argument("--ground-truth", default="", dest="ground_truth")
        _hm_score.add_argument("--json", action="store_true", dest="output_json")

        _hm_status = _hm_sub.add_parser("status", help="Show current monitor state and rolling rate")
        _hm_status.add_argument("--state-dir", default=None, dest="state_dir")
        _hm_status.add_argument("--threshold", type=float, default=0.10, dest="threshold")
        _hm_status.add_argument("--window", type=int, default=60, dest="window_minutes")
        _hm_status.add_argument("--json", action="store_true", dest="output_json")

        _hm_batch = _hm_sub.add_parser("batch", help="Score a batch of offline request/response pairs")
        _hm_batch.add_argument("--requests-file", required=True, dest="requests_file",
                                help="JSON file: [{prompt, response, context?, ground_truth?}]")
        _hm_batch.add_argument("--model-id", default="", dest="model_id")
        _hm_batch.add_argument("--threshold", type=float, default=0.10, dest="threshold")
        _hm_batch.add_argument("--fail-on-breach", action="store_true", dest="fail_on_breach")
        _hm_batch.add_argument("--json", action="store_true", dest="output_json")

    # ── W251-W252 / C7 — Hallucination Rate Attestation ──────────────────────
    ha_cmd = sub.add_parser(
        "hallucination-attest",
        help="Hallucination rate attestation — signed domain-calibrated certificate ($67.4B stat)",
        description=(
            "Produce a signed certificate of your model's hallucination rate on a\n"
            "domain-specific probe set. Five domains with calibrated thresholds:\n"
            "  legal/medical 2% · financial 3% · code 5% · general 10%\n\n"
            "Examples:\n"
            "  squash hallucination-attest attest --model http://localhost:8080 --domain legal\n"
            "  squash hallucination-attest attest --model mock://test --domain general\n"
            "  squash hallucination-attest verify ./cert.json\n"
            "  squash hallucination-attest list-probes --domain medical\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    ha_sub = ha_cmd.add_subparsers(dest="ha_command")

    ha_attest = ha_sub.add_parser("attest", help="Run hallucination probe set and issue certificate")
    ha_attest.add_argument("--model", required=True, dest="model_endpoint")
    ha_attest.add_argument("--domain", required=True, choices=["legal", "medical", "financial", "code", "general"])
    ha_attest.add_argument("--model-id", default="", dest="model_id")
    ha_attest.add_argument("--max-rate", type=float, default=None, dest="max_rate")
    ha_attest.add_argument("--probes-file", default=None, dest="probes_file")
    ha_attest.add_argument("--probe-limit", type=int, default=None, dest="probe_limit")
    ha_attest.add_argument("--priv-key", default=None, dest="priv_key")
    ha_attest.add_argument("--out", default=None)
    ha_attest.add_argument("--format", default="json", choices=["json", "md", "text"], dest="ha_format")
    ha_attest.add_argument("--fail-on-exceed", action="store_true", dest="fail_on_exceed")

    ha_verify = ha_sub.add_parser("verify", help="Verify a certificate's Ed25519 signature")
    ha_verify.add_argument("cert_path")
    ha_verify.add_argument("--json", action="store_true", dest="output_json")

    ha_show = ha_sub.add_parser("show", help="Render a certificate as Markdown")
    ha_show.add_argument("cert_path")

    ha_probes = ha_sub.add_parser("list-probes", help="List built-in probes for a domain")
    ha_probes.add_argument("--domain", default="general", choices=["legal", "medical", "financial", "code", "general"])
    ha_probes.add_argument("--json", action="store_true", dest="output_json")

    # ── D4 — Multi-Jurisdiction Compliance Matrix ─────────────────────────────
    cm_cmd = sub.add_parser(
        "compliance-matrix",
        help="Multi-jurisdiction compliance matrix — cross-reference frameworks",
        description=(
            "Build a 2-D matrix of (requirement × jurisdiction) → status across\n"
            "11 frameworks and 11 jurisdictions. Replaces a 1-week legal-mapping\n"
            "consult per multinational deployment.\n\n"
            "Examples:\n"
            "  squash compliance-matrix --regions eu,us,uk\n"
            "  squash compliance-matrix --regions eu,us,uk,sg,ca \\\n"
            "      --models ./model --output matrix.html --format html\n"
            "  squash compliance-matrix --regions eu --models ./model \\\n"
            "      --format json --remediation\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    cm_cmd.add_argument("--regions", required=True, dest="cm_regions",
                         help="Comma-separated jurisdictions (eu,us,uk,sg,ca,au,...)")
    cm_cmd.add_argument("--models", default=None, dest="cm_models",
                         help="Path to model directory (squash artifacts read from here)")
    cm_cmd.add_argument("--attestation", default=None, dest="cm_attestation",
                         help="Path to a JSON attestation document")
    cm_cmd.add_argument("--model-id", default="", dest="cm_model_id")
    cm_cmd.add_argument("--output", "-o", default=None, dest="cm_output",
                         help="Write to file (default: stdout)")
    cm_cmd.add_argument("--format", default="text",
                         choices=["text", "json", "md", "html"], dest="cm_format")
    cm_cmd.add_argument("--remediation", action="store_true", dest="cm_remediation",
                         help="Append a sequenced remediation plan to output")
    cm_cmd.add_argument("--fail-on-gap", action="store_true", dest="cm_fail_on_gap",
                         help="Exit 1 if any cell is FAIL or PARTIAL")
    cm_cmd.add_argument("--list-requirements", action="store_true",
                         dest="cm_list_reqs",
                         help="Print the built-in requirement catalogue and exit")
    cm_cmd.add_argument("--list-jurisdictions", action="store_true",
                         dest="cm_list_jurs",
                         help="Print supported jurisdictions and exit")

    # ── D1 — squash GitHub App ────────────────────────────────────────────────
    gha_cmd = sub.add_parser(
        "github-app",
        help="GitHub App — auto-attest PRs/commits as Check Runs",
        description=(
            "Run the squash GitHub App: webhook-driven Check Runs on PRs and\n"
            "pushes that touch model artefacts.\n\n"
            "Examples:\n"
            "  squash github-app config --init ./squash-github-app.yaml\n"
            "  squash github-app config --check ./squash-github-app.yaml\n"
            "  squash github-app serve --config ./squash-github-app.yaml\n"
            "  squash github-app attest --config app.yaml --installation-id 1 \\\n"
            "      --repo octo/repo --sha abc123 --paths model.safetensors\n"
            "  squash github-app verify-webhook --secret S3CRET --body-file body.json \\\n"
            "      --signature 'sha256=...'\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    gha_sub = gha_cmd.add_subparsers(dest="gha_command")

    gha_serve = gha_sub.add_parser("serve", help="Start the webhook receiver")
    gha_serve.add_argument("--config", required=True, dest="gha_config")
    gha_serve.add_argument("--host", default=None, dest="gha_host")
    gha_serve.add_argument("--port", type=int, default=None, dest="gha_port")

    gha_attest = gha_sub.add_parser("attest", help="Run an attestation manually")
    gha_attest.add_argument("--config", required=True, dest="gha_config")
    gha_attest.add_argument("--installation-id", type=int, required=True, dest="gha_install")
    gha_attest.add_argument("--repo", required=True, dest="gha_repo",
                             help="owner/name (e.g. octo/repo)")
    gha_attest.add_argument("--sha", required=True, dest="gha_sha")
    gha_attest.add_argument("--clone-url", default="", dest="gha_clone_url")
    gha_attest.add_argument("--paths", nargs="*", default=[], dest="gha_paths",
                             help="Changed paths (defaults to scanning the clone)")
    gha_attest.add_argument("--no-clone", action="store_true", dest="gha_no_clone",
                             help="Treat --workdir as the already-checked-out tree")
    gha_attest.add_argument("--workdir", default=None, dest="gha_workdir")
    gha_attest.add_argument("--dry-run", action="store_true", dest="gha_dry_run",
                             help="Skip posting Check Runs to GitHub")
    gha_attest.add_argument("--json", action="store_true", dest="output_json")

    gha_cfg = gha_sub.add_parser("config", help="Render or validate config")
    gha_cfg_grp = gha_cfg.add_mutually_exclusive_group(required=True)
    gha_cfg_grp.add_argument("--init", metavar="PATH", default=None, dest="gha_cfg_init")
    gha_cfg_grp.add_argument("--check", metavar="PATH", default=None, dest="gha_cfg_check")
    gha_cfg_grp.add_argument("--show-defaults", action="store_true", dest="gha_cfg_show")

    gha_verify = gha_sub.add_parser(
        "verify-webhook", help="Verify an X-Hub-Signature-256 header"
    )
    gha_verify.add_argument("--secret", required=True, dest="gha_v_secret")
    gha_verify.add_argument("--signature", required=True, dest="gha_v_sig",
                             help="Header value, e.g. 'sha256=...'")
    gha_verify_body = gha_verify.add_mutually_exclusive_group(required=True)
    gha_verify_body.add_argument("--body", default=None, dest="gha_v_body")
    gha_verify_body.add_argument("--body-file", default=None, dest="gha_v_body_file")

    gha_install = gha_sub.add_parser(
        "install", help="Print the GitHub App install URL and emit a config template",
    )
    gha_install.add_argument("--app-id", type=int, default=0, dest="gha_install_app_id",
                              help="GitHub App numeric ID (optional — shown in install URL)")
    gha_install.add_argument("--out", default=None, dest="gha_install_out",
                              help="Write config template to this path (default: stdout)")

    gha_sub.add_parser(
        "config-template", help="Dump the YAML config template to stdout",
    )

    # ── W221-W222 / C1 ★ — squash freeze (Emergency Response Orchestrator) ────
    fz_cmd = sub.add_parser(
        "freeze",
        help="EMERGENCY: revoke + broadcast + log + notify + incident package",
        description=(
            "The Red Button. One command, in <10 s, atomically:\n"
            "  1. Revokes every live attestation for a model in the registry\n"
            "  2. Broadcasts attestation.frozen webhook to every subscriber\n"
            "  3. Writes a signed freeze ledger entry (Ed25519 audit trail)\n"
            "  4. Dispatches a notifications.notify(event=attestation.frozen)\n"
            "  5. Builds an Incident Package (Article 73 disclosure draft)\n\n"
            "Examples:\n"
            "  squash freeze --attestation-id att://acme/llm-v2/abc123 \\\n"
            "      --reason 'CVE-2026-1234 — RCE in tokenizer'\n"
            "  squash freeze --model-path ./model.safetensors \\\n"
            "      --reason 'data poisoning detected' --severity critical\n"
            "  squash freeze --attestation-id att://... --priv-key ~/.squash/freeze.priv.pem\n"
            "  squash freeze ledger --limit 20\n"
            "  squash freeze verify ./freeze_receipt.json\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    fz_sub = fz_cmd.add_subparsers(dest="fz_command")

    fz_run = fz_sub.add_parser(
        "run",
        help="(default) Execute a freeze. May be omitted — `squash freeze --attestation-id …` works.",
    )
    for _p in (fz_cmd, fz_run):
        _p.add_argument("--attestation-id", default=None, dest="fz_attestation_id")
        _p.add_argument("--model-path", default=None, dest="fz_model_path")
        _p.add_argument("--model-id", default="", dest="fz_model_id")
        _p.add_argument("--reason", default="", dest="fz_reason")
        _p.add_argument("--actor", default="", dest="fz_actor")
        _p.add_argument(
            "--severity", default="critical",
            choices=["critical", "serious", "moderate", "minor"], dest="fz_severity",
        )
        _p.add_argument(
            "--category", default="other",
            choices=[
                "fundamental_rights", "health_safety", "discrimination",
                "infrastructure", "other",
            ],
            dest="fz_category",
        )
        _p.add_argument("--affected-persons", type=int, default=0, dest="fz_affected")
        _p.add_argument("--incident-dir", default=None, dest="fz_incident_dir")
        _p.add_argument("--state-dir", default=None, dest="fz_state_dir")
        _p.add_argument("--priv-key", default=None, dest="fz_priv_key")
        _p.add_argument("--no-incident", action="store_true", dest="fz_no_incident")
        _p.add_argument("--webhook-timeout", type=float, default=10.0, dest="fz_webhook_timeout")
        _p.add_argument("--out", default=None, dest="fz_out")
        _p.add_argument(
            "--format", default="text",
            choices=["text", "json", "md"], dest="fz_format",
        )
        _p.add_argument("--quiet", action="store_true", dest="fz_quiet")

    fz_ledger = fz_sub.add_parser("ledger", help="Show freeze ledger entries (newest last)")
    fz_ledger.add_argument("--state-dir", default=None, dest="fz_state_dir")
    fz_ledger.add_argument("--limit", type=int, default=20, dest="fz_limit")
    fz_ledger.add_argument("--json", action="store_true", dest="output_json")

    fz_verify = fz_sub.add_parser("verify", help="Verify a freeze receipt's Ed25519 signature")
    fz_verify.add_argument("receipt_path")
    fz_verify.add_argument("--json", action="store_true", dest="output_json")

    # ── W223-W225 / C2 — AI Washing Detection ─────────────────────────────────
    aw_cmd = sub.add_parser(
        "detect-washing",
        help="AI washing detection — scan marketing collateral for unsupported capability claims",
        description=(
            "Scan marketing docs, investor decks, model cards, and landing pages\n"
            "for AI capability claims; cross-reference against squash attestation\n"
            "evidence; flag every divergence. SEC Operation AI Comply top priority.\n\n"
            "Examples:\n"
            "  squash detect-washing scan ./investor-deck.md\n"
            "  squash detect-washing scan ./docs/ --master-record ./out/master_record.json\n"
            "  squash detect-washing scan ./landing.md --bias-audit ./bias.json\n"
            "  squash detect-washing scan ./docs/ --fail-on high --format json --out r.json\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    aw_sub = aw_cmd.add_subparsers(dest="aw_command")

    aw_scan = aw_sub.add_parser("scan", help="Scan documents for AI washing indicators")
    aw_scan.add_argument("doc_paths", nargs="+", help="Document paths or directories to scan")
    aw_scan.add_argument("--model-id", default="", dest="model_id")
    aw_scan.add_argument("--master-record", default=None, dest="master_record", help="Path to master_record.json")
    aw_scan.add_argument("--bias-audit", default=None, dest="bias_audit", help="Path to bias_audit.json")
    aw_scan.add_argument("--data-lineage", default=None, dest="data_lineage", help="Path to data_lineage.json")
    aw_scan.add_argument("--format", default="text", choices=["text", "json", "md"], dest="aw_format")
    aw_scan.add_argument("--out", default=None)
    aw_scan.add_argument(
        "--fail-on", default="high", choices=["low", "medium", "high", "critical"], dest="fail_on",
    )

    aw_report = aw_sub.add_parser("report", help="Render a previously saved AI washing report")
    aw_report.add_argument("report_path")
    aw_report.add_argument("--format", default="text", choices=["text", "json", "md"], dest="aw_format")

    # ── W196 / B10 — License Conflict Detection ───────────────────────────────
    lc_cmd = sub.add_parser(
        "license-check",
        help="License conflict detection — SPDX compatibility matrix + AI model licences",
        description=(
            "Scan a project for licence conflicts across model weights, training\n"
            "datasets, and code dependencies. Checks 12 conflict rules covering\n"
            "copyleft, NC restrictions, AGPL SaaS triggers, ShareAlike contamination,\n"
            "and AI model custom licences (LLaMA, Gemma, Mistral, BLOOM/RAIL).\n\n"
            "Examples:\n"
            "  squash license-check scan ./my-model-project\n"
            "  squash license-check scan ./project --use-case commercial --format md\n"
            "  squash license-check scan ./project --use-case saas_api --fail-on medium\n"
            "  squash license-check explain Apache-2.0\n"
            "  squash license-check report ./license-report.json\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    lc_sub = lc_cmd.add_subparsers(dest="lc_command")

    lc_scan = lc_sub.add_parser("scan", help="Scan a project directory for licence conflicts")
    lc_scan.add_argument("project_path", help="Path to project directory")
    lc_scan.add_argument(
        "--use-case", default="commercial",
        choices=["research", "commercial", "open_source", "saas_api", "internal", "government"],
        dest="use_case", help="Intended deployment scenario (default: commercial)",
    )
    lc_scan.add_argument("--format", default="text", choices=["text", "json", "md"], dest="lc_format")
    lc_scan.add_argument("--out", default=None, help="Write report to PATH instead of stdout")
    lc_scan.add_argument(
        "--fail-on", default="high",
        choices=["low", "medium", "high", "critical"],
        dest="fail_on",
        help="Exit non-zero if overall risk meets or exceeds this level (default: high)",
    )

    lc_explain = lc_sub.add_parser("explain", help="Explain a single SPDX licence identifier")
    lc_explain.add_argument("spdx_id", help="SPDX licence identifier (e.g. Apache-2.0, CC-BY-NC-4.0)")

    lc_report = lc_sub.add_parser("report", help="Render a previously saved licence conflict report")
    lc_report.add_argument("report_path", help="Path to licence-conflict report JSON")
    lc_report.add_argument("--format", default="text", choices=["text", "json", "md"], dest="lc_format")

    # ── W195 / B9 — Data Poisoning Detection ──────────────────────────────────
    dp_cmd = sub.add_parser(
        "data-poison",
        help="Training data poisoning detection — six-layer scanner",
        description=(
            "Scan training datasets for data poisoning indicators across six\n"
            "detection layers: threat intelligence, label integrity, duplicate\n"
            "injection, statistical outliers, backdoor trigger patterns, and\n"
            "provenance chain integrity.\n\n"
            "Examples:\n"
            "  squash data-poison scan ./datasets/training\n"
            "  squash data-poison scan ./datasets/training --format json --out report.json\n"
            "  squash data-poison scan ./datasets/training --fail-on HIGH\n"
            "  squash data-poison report ./report.json\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    dp_sub = dp_cmd.add_subparsers(dest="dp_command")

    dp_scan = dp_sub.add_parser("scan", help="Scan a dataset directory or file for poisoning indicators")
    dp_scan.add_argument("dataset_path", help="Path to dataset directory or file")
    dp_scan.add_argument("--format", default="text", choices=["text", "json", "md"], dest="scan_format", help="Output format (default: text)")
    dp_scan.add_argument("--out", default=None, help="Write report to PATH instead of stdout")
    dp_scan.add_argument(
        "--fail-on",
        default="high",
        choices=["low", "medium", "high", "critical"],
        dest="fail_on",
        help="Exit non-zero if risk level meets or exceeds this threshold (default: high)",
    )
    dp_scan.add_argument("--provenance", default=None, dest="provenance_path", help="Path to provenance JSON to cross-reference")

    dp_report = dp_sub.add_parser("report", help="Render a previously saved data-poison report")
    dp_report.add_argument("report_path", help="Path to data-poison report JSON")
    dp_report.add_argument("--format", default="text", choices=["text", "json", "md"], dest="report_format")

    # ── W194 / B7 — Drift SLA Certificate ────────────────────────────────────
    dc_cmd = sub.add_parser(
        "drift-cert",
        help="Drift SLA Certificate — prove sustained compliance over a time window",
        description=(
            "Issue and verify Drift SLA Certificates: signed, time-windowed\n"
            "assertions that a model maintained a compliance score above a\n"
            "threshold for a defined SLA window.\n\n"
            "Examples:\n"
            "  squash drift-cert ingest ./out/master_record.json\n"
            "  squash drift-cert issue --model phi-3 --framework eu-ai-act --min-score 80 --window 90\n"
            "  squash drift-cert issue --model phi-3 --priv-key ./squash.priv.pem --out cert.json\n"
            "  squash drift-cert verify cert.json\n"
            "  squash drift-cert show cert.json\n"
            "  squash drift-cert export cert.json --format html\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    dc_sub = dc_cmd.add_subparsers(dest="dc_command")

    dc_ingest = dc_sub.add_parser("ingest", help="Add a master_record.json snapshot to the score ledger")
    dc_ingest.add_argument("master_record_path", help="Path to master_record.json")
    dc_ingest.add_argument("--ledger", default=None, dest="ledger_path", help="Score ledger path (default: ~/.squash/drift/score_ledger.jsonl)")

    dc_issue = dc_sub.add_parser("issue", help="Evaluate SLA and issue a signed certificate")
    dc_issue.add_argument("--model", required=True, dest="model_id", help="Model ID")
    dc_issue.add_argument("--framework", default="eu-ai-act", help="Compliance framework (default: eu-ai-act)")
    dc_issue.add_argument("--min-score", type=float, default=80.0, dest="min_score", help="Minimum passing score (default: 80)")
    dc_issue.add_argument("--window", type=int, default=90, dest="window_days", help="Evaluation window in days (default: 90)")
    dc_issue.add_argument("--max-violation-rate", type=float, default=0.05, dest="max_violation_rate", help="Max fraction of failing snapshots (default: 0.05)")
    dc_issue.add_argument("--min-snapshots", type=int, default=3, dest="min_snapshots", help="Minimum snapshots required (default: 3)")
    dc_issue.add_argument("--org", default="", help="Organisation name embedded in certificate")
    dc_issue.add_argument("--priv-key", default=None, dest="priv_key", help="Ed25519 .priv.pem for signing")
    dc_issue.add_argument("--ledger", default=None, dest="ledger_path", help="Score ledger path")
    dc_issue.add_argument("--out", default=None, help="Write certificate JSON to PATH (default: stdout)")
    dc_issue.add_argument("--format", default="json", choices=["json", "md", "html"], dest="issue_format", help="Output format (default: json)")
    dc_issue.add_argument("--json", action="store_true", dest="output_json")

    dc_verify = dc_sub.add_parser("verify", help="Verify a certificate's Ed25519 signature and self-consistency")
    dc_verify.add_argument("cert_path", help="Path to certificate JSON file")
    dc_verify.add_argument("--json", action="store_true", dest="output_json")

    dc_show = dc_sub.add_parser("show", help="Render a certificate as human-readable Markdown")
    dc_show.add_argument("cert_path")

    dc_export = dc_sub.add_parser("export", help="Export a certificate to Markdown, HTML, or PDF")
    dc_export.add_argument("cert_path")
    dc_export.add_argument("--format", default="html", choices=["md", "html", "pdf"], dest="export_format")
    dc_export.add_argument("--out", default=None, help="Output path (default: <cert_id>.<format>)")

    # ── W193 / B6 — Audit-trail blockchain anchoring ──────────────────────────
    anchor_cmd = sub.add_parser(
        "anchor",
        help="Audit-trail blockchain anchoring (Merkle batch + multi-backend)",
        description=(
            "Stage attestations into a batch, build a Merkle commitment, and\n"
            "anchor the root using a local Ed25519 witness, OpenTimestamps\n"
            "(Bitcoin), or an Ethereum-class chain.\n\n"
            "Examples:\n"
            "  squash anchor add ./out/master_record.json\n"
            "  squash anchor commit --backend local --priv-key ./squash.priv.pem\n"
            "  squash anchor commit --backend opentimestamps\n"
            "  squash anchor verify att-abc123\n"
            "  squash anchor proof  att-abc123 --out proof.json\n"
            "  squash anchor list   --json\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    anchor_sub = anchor_cmd.add_subparsers(dest="anchor_command")

    an_add = anchor_sub.add_parser("add", help="Stage a master attestation record into the pending batch")
    an_add.add_argument("master_record_path", help="Path to master_record.json")
    an_add.add_argument("--ledger-dir", default=None, help="Ledger root directory (default: ~/.squash/anchor)")

    an_commit = anchor_sub.add_parser("commit", help="Build Merkle root and anchor the pending batch")
    an_commit.add_argument("--backend", default="local", choices=["local", "opentimestamps", "ethereum"])
    an_commit.add_argument("--priv-key", dest="priv_key", default=None, help="Ed25519 .priv.pem (local backend)")
    an_commit.add_argument("--pub-key", dest="pub_key", default=None, help="Ed25519 .pub.pem (local backend; embedded in anchor)")
    an_commit.add_argument("--rpc-url", dest="rpc_url", default=None, help="EVM RPC URL (ethereum backend)")
    an_commit.add_argument("--eth-key", dest="eth_key", default=None, help="0x-prefixed hex private key (ethereum backend; or set $SQUASH_ETH_KEY)")
    an_commit.add_argument("--ledger-dir", default=None, help="Ledger root directory (default: ~/.squash/anchor)")
    an_commit.add_argument("--json", action="store_true", dest="output_json")

    an_verify = anchor_sub.add_parser("verify", help="Verify Merkle inclusion + anchor witness for an attestation")
    an_verify.add_argument("attestation_id")
    an_verify.add_argument("--ledger-dir", default=None)
    an_verify.add_argument("--json", action="store_true", dest="output_json")

    an_proof = anchor_sub.add_parser("proof", help="Emit a portable inclusion-proof JSON document")
    an_proof.add_argument("attestation_id")
    an_proof.add_argument("--ledger-dir", default=None)
    an_proof.add_argument("--out", default=None, help="Write to PATH instead of stdout")

    an_list = anchor_sub.add_parser("list", help="List committed anchor entries")
    an_list.add_argument("--ledger-dir", default=None)
    an_list.add_argument("--json", action="store_true", dest="output_json")

    an_status = anchor_sub.add_parser("status", help="Show staged batch + last anchor")
    an_status.add_argument("--ledger-dir", default=None)
    an_status.add_argument("--json", action="store_true", dest="output_json")
    # ── B3 (Sprint 15 W209/W210) — compliance digest ──────────────────────────
    digest_cmd = sub.add_parser(
        "digest",
        help="Compose + send the weekly/monthly compliance portfolio digest",
        description=(
            "Render and (optionally) email a portfolio compliance digest:\n"
            "  · 5-metric summary panel\n"
            "  · top-5 risk movers (score, violations, CVEs, drift, Δ vs prior)\n"
            "  · regulatory deadline countdown (EU Aug 2 · CO Jun 1 · ISO 42001)\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    digest_sub = digest_cmd.add_subparsers(dest="digest_command", metavar="SUBCOMMAND")
    digest_sub.required = True

    def _add_common_digest_args(p: argparse.ArgumentParser) -> None:
        p.add_argument("--period", default="weekly",
                       choices=["weekly", "monthly"],
                       help="Digest period. Default: weekly.")
        p.add_argument("--models-dir", default=None, dest="digest_models_dir",
                       help="Directory containing per-model attestation subdirs.")
        p.add_argument("--org", default="", dest="digest_org",
                       help="Org name shown in the digest header.")
        p.add_argument("--dashboard-url", default="", dest="digest_dashboard_url",
                       help="URL to the live dashboard (footer link).")
        p.add_argument("--score-history", default=None,
                       dest="digest_score_history", metavar="JSON_FILE",
                       help="JSON file mapping model_id → previous score.")
        p.add_argument("--quiet", action="store_true",
                       help="Suppress non-essential output")

    digest_preview = digest_sub.add_parser(
        "preview", help="Render the digest and print to stdout. No email.",
    )
    _add_common_digest_args(digest_preview)
    digest_preview.add_argument(
        "--format", default="text", choices=["text", "html", "json"],
        dest="digest_preview_format",
        help="Render text body, HTML body, or JSON. Default: text.",
    )
    digest_preview.add_argument(
        "--output", default=None, dest="digest_preview_output",
        help="Write to FILE instead of stdout.",
    )

    digest_send = digest_sub.add_parser(
        "send", help="Render + email the digest via SMTP.",
    )
    _add_common_digest_args(digest_send)
    digest_send.add_argument(
        "--recipients", "--recipient", action="append", default=None,
        dest="digest_recipients",
        help="Email recipient (repeatable). Required unless --dry-run.",
    )
    digest_send.add_argument(
        "--dry-run", action="store_true", dest="digest_dry_run",
        help="Render and print both bodies; skip the SMTP send.",
    )
    digest_send.add_argument(
        "--smtp-host", default="", dest="digest_smtp_host",
        help="SMTP host. Default: SQUASH_SMTP_HOST env var.",
    )
    digest_send.add_argument(
        "--smtp-port", default=0, type=int, dest="digest_smtp_port",
        help="SMTP port. Default: 587 (or SQUASH_SMTP_PORT env).",
    )
    digest_send.add_argument(
        "--smtp-from", default="", dest="digest_smtp_from",
        help="From address. Default: SQUASH_SMTP_FROM env var.",
    )
    digest_send.add_argument(
        "--no-tls", action="store_true", dest="digest_no_tls",
        help="Disable STARTTLS (use only on localhost test relays).",
    )

    # ── B5 (Track B) — gateway-config: Kong + AWS API Gateway runtime gate ───
    gw_cmd = sub.add_parser(
        "gateway-config",
        help="Emit runtime API gateway gate config (Kong / AWS API Gateway)",
        description=(
            "Generate runtime gate configurations for Kong and AWS API Gateway.\n"
            "Examples:\n"
            "  squash gateway-config kong --min-score 0.8 > kong-config.yaml\n"
            "  squash gateway-config kong --emit-plugin --output ./squash-attest/\n"
            "  squash gateway-config aws-apigw --min-score 0.8 > template.yaml\n"
            "  squash gateway-config aws-apigw --emit-handler > handler.py\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    gw_sub = gw_cmd.add_subparsers(dest="gateway_target", metavar="TARGET")
    gw_kong = gw_sub.add_parser("kong", help="Emit Kong declarative config or plugin source")
    gw_kong.add_argument("--min-score", type=float, default=0.8, dest="min_score")
    gw_kong.add_argument("--header", default="X-Squash-Attestation", dest="header_name")
    gw_kong.add_argument("--squash-api-url", default="https://api.getsquash.dev", dest="squash_api_url")
    gw_kong.add_argument("--service-name", default="ai-inference", dest="service_name")
    gw_kong.add_argument("--upstream-url", default="http://upstream-inference:8080", dest="upstream_url")
    gw_kong.add_argument("--path", action="append", dest="route_paths")
    gw_kong.add_argument("--max-age-days", type=int, default=30, dest="max_age_days")
    gw_kong.add_argument("--require-framework", action="append", dest="required_frameworks")
    gw_kong.add_argument("--emit-plugin", action="store_true", dest="emit_plugin")
    gw_kong.add_argument("--output", dest="output")
    gw_aws = gw_sub.add_parser("aws-apigw", help="Emit AWS SAM template or Lambda authorizer source")
    gw_aws.add_argument("--min-score", type=float, default=0.8, dest="min_score")
    gw_aws.add_argument("--header", default="X-Squash-Attestation", dest="header_name")
    gw_aws.add_argument("--squash-api-url", default="https://api.getsquash.dev", dest="squash_api_url")
    gw_aws.add_argument("--function-name", default="SquashAttestAuthorizer", dest="function_name")
    gw_aws.add_argument("--max-age-days", type=int, default=30, dest="max_age_days")
    gw_aws.add_argument("--require-framework", action="append", dest="required_frameworks")
    gw_aws.add_argument("--runtime", default="python3.11", dest="runtime")
    gw_aws.add_argument("--emit-handler", action="store_true", dest="emit_handler")
    gw_aws.add_argument("--emit-authorizer-dir", action="store_true", dest="emit_authorizer_dir")
    gw_aws.add_argument("--output", dest="output")



    # ── B8 (Track B) — scan-adapter: LoRA / Adapter poisoning detection ─────
    sa_cmd = sub.add_parser(
        "scan-adapter",
        help="Scan a LoRA / adapter file for poisoning, backdoors, and format risks",
        description=(
            "Analyse a LoRA or fine-tuning adapter file for indicators of\n"
            "backdoor injection, weight-delta anomalies, and unsafe serialisation.\n\n"
            "Examples:\n"
            "  squash scan-adapter --lora ./adapter.safetensors\n"
            "  squash scan-adapter --lora ./adapter.safetensors --require-safetensors\n"
            "  squash scan-adapter --lora ./adapter.pt --sign --output report.json\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    sa_cmd.add_argument("--lora", required=True, dest="lora_path",
                        help="Path to the LoRA / adapter file to scan")
    sa_cmd.add_argument("--require-safetensors", action="store_true",
                        dest="require_safetensors",
                        help="Fail (rc=2) if adapter is not in safetensors format")
    sa_cmd.add_argument("--sign", action="store_true", dest="sign_cert",
                        help="Embed HMAC-SHA256 signature in the certificate JSON")
    sa_cmd.add_argument("--output", dest="cert_output",
                        help="Path to write signed certificate JSON (default: <adapter>-squash-adapter-scan.json)")
    sa_cmd.add_argument("--json", action="store_true", dest="output_json",
                        help="Print full report JSON to stdout")


    # ── W197 (Sprint 11) — chain-attest: composite chain / pipeline attest ────
    chain_cmd = sub.add_parser(
        "chain-attest",
        help="Attest an entire RAG / agent / multi-LLM pipeline as a composite",
        description=(
            "Run composite chain attestation. The chain is defined either as a "
            "JSON / YAML spec or as a Python module path that exposes a "
            "LangChain Runnable.\n\n"
            "Examples:\n"
            "  squash chain-attest ./chain-spec.json --json\n"
            "  squash chain-attest ./chain-spec.yaml --fail-on-component-violation\n"
            "  squash chain-attest --verify ./chain-attest.json\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    chain_cmd.add_argument(
        "spec", metavar="SPEC",
        help="Path to JSON/YAML chain spec, or 'module.path:attr' for a LangChain Runnable",
    )
    chain_cmd.add_argument("--verify", action="store_true", dest="chain_verify_only",
                           help="Verify an existing chain attestation file (SPEC is the file to verify)")
    chain_cmd.add_argument("--chain-id", dest="chain_id_override", default=None,
                           help="Override the chain_id in the spec")
    chain_cmd.add_argument("--policy", action="append", dest="chain_policies", metavar="POLICY",
                           help="Policy to evaluate (repeatable; default: enterprise-strict)")
    chain_cmd.add_argument("--output-dir", dest="chain_output_dir", default=None,
                           help="Directory to write chain-attest.json and chain-attest.md")
    chain_cmd.add_argument("--fail-on-component-violation", action="store_true",
                           dest="chain_fail_on_violation",
                           help="Exit 1 if any component fails policy")
    chain_cmd.add_argument("--sign-components", action="store_true",
                           dest="chain_sign_components",
                           help="HMAC-sign each component attestation")
    chain_cmd.add_argument("--json", action="store_true", dest="chain_json",
                           help="Print full attestation JSON to stdout")
    chain_cmd.add_argument("--quiet", "-q", action="store_true",
                           help="Suppress informational output")

    # ── W201 (Sprint 12) — registry-gate: unified pre-registration policy gate ─
    rg_cmd = sub.add_parser(
        "registry-gate",
        help="Pre-registration policy gate for MLflow / W&B / SageMaker / local",
        description=(
            "Attest a local model before promoting it to a model registry.\n"
            "Writes a structured gate decision to registry-gate.json.\n\n"
            "Examples:\n"
            "  squash registry-gate --backend local --model-path ./model\n"
            "  squash registry-gate --backend mlflow --uri models:/MyModel/Staging --model-path ./model\n"
            "  squash registry-gate --backend sagemaker --uri arn:aws:sagemaker:us-east-1:123:model/v1 --model-path ./model\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    rg_cmd.add_argument("--backend", default="local",
                        choices=["local", "mlflow", "wandb", "sagemaker"],
                        help="Registry backend (default: local)")
    rg_cmd.add_argument("--model-path", required=True, dest="rg_model_path",
                        help="Path to the local model directory")
    rg_cmd.add_argument("--uri", dest="rg_uri", default=None,
                        help="Registry URI (e.g. models:/Name/Stage for MLflow)")
    rg_cmd.add_argument("--name", dest="rg_name", default=None,
                        help="Human-readable model name to embed in the gate record")
    rg_cmd.add_argument("--policy", action="append", dest="rg_policies", metavar="POLICY",
                        help="Policy to evaluate (repeatable; default: enterprise-strict)")
    rg_cmd.add_argument("--output-dir", dest="rg_output_dir", default=None,
                        help="Directory for gate JSON (default: <model-path>/squash/)")
    rg_cmd.add_argument("--allow-on-fail", action="store_true", dest="rg_allow_on_fail",
                        help="Record the failure but exit 0 (non-blocking)")
    rg_cmd.add_argument("--json", action="store_true", dest="rg_json",
                        help="Print full gate JSON to stdout")
    rg_cmd.add_argument("--sign", action="store_true", dest="rg_sign",
                        help="Sigstore-sign the underlying attestation")
    rg_cmd.add_argument("--quiet", "-q", action="store_true",
                        help="Suppress informational output")



    # ── C3 (Sprint 23) — Approval Workflow (W232–W234) ───────────────────────

    # squash request-approval
    req_appr_cmd = sub.add_parser(
        "request-approval",
        help="Create a signed human-oversight approval request for a model deployment",
        description=(
            "Create a multi-reviewer approval request for a model attestation.\n"
            "Required by EU AI Act Article 9 and NIST AI RMF GOVERN pillar.\n\n"
            "Examples:\n"
            "  squash request-approval --attestation att://sha256:a3f1... --reviewers ciso@acme.com,vp-eng@acme.com\n"
            "  squash request-approval --attestation att://sha256:a3f1... --threshold 2 --require-role COMPLIANCE,ENGINEERING\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    req_appr_cmd.add_argument("--attestation", required=True, dest="appr_attestation_id",
                              help="Attestation ID (att:// URI or bare entry_id)")
    req_appr_cmd.add_argument("--model-id", dest="appr_model_id", default="",
                              help="Human-readable model identifier")
    req_appr_cmd.add_argument("--reviewers", dest="appr_reviewers", default="",
                              help="Comma-separated reviewer email addresses")
    req_appr_cmd.add_argument("--threshold", dest="appr_threshold", type=int, default=1,
                              help="Minimum APPROVED responses needed (default: 1)")
    req_appr_cmd.add_argument("--require-role", dest="appr_required_roles", default="",
                              help="Comma-separated roles required (COMPLIANCE,ENGINEERING,SECURITY,LEGAL,EXECUTIVE)")
    req_appr_cmd.add_argument("--requestor", dest="appr_requestor", default="",
                              help="Email of the requestor")
    req_appr_cmd.add_argument("--notes", dest="appr_notes", default="",
                              help="Context notes for reviewers")
    req_appr_cmd.add_argument("--attestation-hash", dest="appr_hash", default="",
                              help="SHA-256 of the attestation payload (snapshot integrity)")
    req_appr_cmd.add_argument("--ttl-days", dest="appr_ttl", type=int, default=30,
                              help="Days before the request expires (default: 30)")
    req_appr_cmd.add_argument("--json", action="store_true", dest="appr_json",
                              help="Print full request JSON")
    req_appr_cmd.add_argument("--quiet", "-q", action="store_true")

    # squash approve
    approve_cmd = sub.add_parser(
        "approve",
        help="Submit a reviewer decision on an approval request",
        description=(
            "Record your APPROVED / REJECTED / APPROVED_WITH_CONDITIONS decision.\n"
            "Each record is HMAC-SHA256 signed and written to the approval store.\n\n"
            "Examples:\n"
            "  squash approve appr-abc123 --decision APPROVED --rationale \"Bias audit clean\"\n"
            "  squash approve appr-abc123 --decision APPROVED_WITH_CONDITIONS --condition \"Retrain by 2026-08-01\"\n"
            "  squash approve appr-abc123 --decision REJECTED --rationale \"Drift exceeds threshold\"\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    approve_cmd.add_argument("request_id", help="Approval request ID (appr-...)")
    approve_cmd.add_argument("--decision", required=True,
                             choices=["APPROVED", "REJECTED", "APPROVED_WITH_CONDITIONS"],
                             dest="appr_decision",
                             help="Your decision on this request")
    approve_cmd.add_argument("--rationale", required=True, dest="appr_rationale",
                             help="Documented basis for your decision (required for Article 9)")
    approve_cmd.add_argument("--reviewer-email", dest="appr_reviewer_email", default="",
                             help="Your email address (defaults to SQUASH_REVIEWER_EMAIL env)")
    approve_cmd.add_argument("--reviewer-name", dest="appr_reviewer_name", default="",
                             help="Your display name")
    approve_cmd.add_argument("--role", dest="appr_reviewer_role", default="ANY",
                             choices=["COMPLIANCE", "ENGINEERING", "SECURITY", "LEGAL", "EXECUTIVE", "ANY"],
                             help="Your reviewer role (default: ANY)")
    approve_cmd.add_argument("--condition", action="append", dest="appr_conditions",
                             metavar="CONDITION",
                             help="Attach a condition to an APPROVED_WITH_CONDITIONS decision (repeatable)")
    approve_cmd.add_argument("--json", action="store_true", dest="appr_json",
                             help="Print full record JSON")
    approve_cmd.add_argument("--quiet", "-q", action="store_true")

    # squash approval-status
    appr_status_cmd = sub.add_parser(
        "approval-status",
        help="Show the current status of an approval request",
        description="Displays request details, collected records, and overall decision.\n\n"
                    "Example:\n  squash approval-status appr-abc123",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    appr_status_cmd.add_argument("request_id", help="Approval request ID (appr-...)")
    appr_status_cmd.add_argument("--json", action="store_true", dest="appr_json",
                                 help="Print full JSON")
    appr_status_cmd.add_argument("--quiet", "-q", action="store_true")

    # squash approval-list
    appr_list_cmd = sub.add_parser(
        "approval-list",
        help="List approval requests",
        description="List pending or recent approval requests, optionally filtered by reviewer.\n\n"
                    "Example:\n  squash approval-list --reviewer ciso@acme.com --pending-only",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    appr_list_cmd.add_argument("--reviewer", dest="appr_reviewer_filter", default="",
                               help="Filter by reviewer email")
    appr_list_cmd.add_argument("--pending-only", action="store_true", dest="appr_pending_only",
                               help="Show only PENDING requests")
    appr_list_cmd.add_argument("--limit", type=int, default=20, dest="appr_limit",
                               help="Max results (default: 20)")
    appr_list_cmd.add_argument("--json", action="store_true", dest="appr_json",
                               help="Print full JSON list")
    appr_list_cmd.add_argument("--quiet", "-q", action="store_true")

    # squash approval-export
    appr_export_cmd = sub.add_parser(
        "approval-export",
        help="Export a signed Article 9 evidence bundle for a completed approval",
        description="Exports a regulator-ready evidence bundle: request JSON, per-record\n"
                    "signature verification, regulatory mapping, and audit summary.\n\n"
                    "Example:\n  squash approval-export appr-abc123 --output evidence.json",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    appr_export_cmd.add_argument("request_id", help="Approval request ID (appr-...)")
    appr_export_cmd.add_argument("--output", dest="appr_output", default=None,
                                 help="Write evidence to this file (default: stdout)")
    appr_export_cmd.add_argument("--json", action="store_true", dest="appr_json",
                                 help="Pretty-print JSON (default when no --output)")
    appr_export_cmd.add_argument("--quiet", "-q", action="store_true")


    # ── Sprint 22 W229-W231 (Track C / C5) — simulate-audit ───────────────────
    sa_cmd = sub.add_parser(
        "simulate-audit",
        help="Run a mock regulatory examination — pulls answers from squash attestation data",
        description=(
            "Simulate a regulatory examination from the examiner's perspective.\n"
            "78% of executives can't pass an AI governance audit in 90 days.\n"
            "This command closes that gap in 60 seconds.\n\n"
            "Supported regulators:\n"
            "  EU-AI-Act   38 questions (Art. 9-15, 17, 73, Annex IV)\n"
            "  NIST-RMF    30 questions (GOVERN, MAP, MEASURE, MANAGE)\n"
            "  SEC         22 questions (AI disclosure, OMB M-26-04)\n"
            "  FDA         20 questions (SaMD, clinical validation)\n\n"
            "Examples:\n"
            "  squash simulate-audit --regulator EU-AI-Act --models-dir ./model\n"
            "  squash simulate-audit --regulator NIST-RMF --json\n"
            "  squash simulate-audit --regulator SEC --output-dir ./compliance/\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    sa_cmd.add_argument(
        "--regulator", "-r",
        default="EU-AI-Act",
        choices=["EU-AI-Act", "NIST-RMF", "SEC", "FDA"],
        dest="sa_regulator",
        help="Regulatory framework to simulate. Default: EU-AI-Act",
    )
    sa_cmd.add_argument(
        "--models-dir", "--model-path",
        default=".",
        dest="sa_models_dir",
        help="Path to model directory containing squash attestation artefacts. "
             "Default: current directory.",
    )
    sa_cmd.add_argument(
        "--output-dir",
        default=None,
        dest="sa_output_dir",
        help="Directory to write audit-readiness.json + audit-readiness.md. "
             "Default: write to --models-dir.",
    )
    sa_cmd.add_argument(
        "--json",
        action="store_true",
        dest="sa_json",
        help="Print structured JSON report to stdout.",
    )
    sa_cmd.add_argument(
        "--fail-below",
        type=int,
        default=0,
        dest="sa_fail_below",
        help="Exit non-zero if readiness score is below this threshold (0–100).",
    )
    sa_cmd.add_argument(
        "--quiet",
        action="store_true",
        help="Suppress non-essential output.",
    )

    # ── Sprint 24 W235-W237 (Track C / C6) — insurance-package ────────────────
    ip_cmd = sub.add_parser(
        "insurance-package",
        help="Generate AI cyber insurance risk package (Munich Re / Coalition format)",
        description=(
            "Produce a standardised AI cyber insurance underwriting package:\n"
            "  · Per-model risk profile (tier, compliance score, CVE exposure,\n"
            "    drift events, incidents, bias audit status)\n"
            "  · Aggregate organisation risk score (0–100)\n"
            "  · Munich Re AI cyber format (5 control domains, maturity level 1–4)\n"
            "  · Coalition AI risk format (5 categories, weighted score)\n"
            "  · Generic JSON adapter for any other underwriter\n"
            "  · Signed ZIP bundle with integrity.sha256 manifest\n\n"
            "New buyer motion: Chief Risk Officer + insurance procurement.\n\n"
            "Examples:\n"
            "  squash insurance-package --models-dir ./models --org 'Acme Corp'\n"
            "  squash insurance-package --models-dir ./models --zip ./bundle.zip\n"
            "  squash insurance-package --models-dir ./models --json\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    ip_cmd.add_argument(
        "--models-dir", "--model-path",
        default=".",
        dest="ip_models_dir",
        help="Model directory (or parent of model subdirectories). Default: cwd.",
    )
    ip_cmd.add_argument(
        "--org",
        default="",
        dest="ip_org",
        help="Organisation name shown in the package header.",
    )
    ip_cmd.add_argument(
        "--output-dir",
        default=None,
        dest="ip_output_dir",
        help="Directory to write insurance-package.{json,md}. Default: --models-dir.",
    )
    ip_cmd.add_argument(
        "--zip",
        default=None,
        dest="ip_zip",
        metavar="PATH",
        help="Also write a signed ZIP bundle to PATH (e.g. ./insurance-bundle.zip).",
    )
    ip_cmd.add_argument(
        "--json",
        action="store_true",
        dest="ip_json",
        help="Print structured JSON report to stdout.",
    )
    ip_cmd.add_argument(
        "--underwriter",
        default=None,
        choices=["munich-re", "coalition", "generic"],
        dest="ip_underwriter",
        help="Print only the specified underwriter format to stdout (with --json).",
    )
    ip_cmd.add_argument(
        "--quiet",
        action="store_true",
        help="Suppress non-essential output.",
    )

    # ── Sprint 39 W272-W274 (Track C / C11) — genealogy + copyright-check ──────
    geo_cmd = sub.add_parser(
        "genealogy",
        help="Trace model derivation chain + copyright contamination cert",
        description=(
            "Trace the model's derivation chain (base → fine-tune → adapter),\n"
            "flag copyright-heavy training sources, and optionally run\n"
            "memorisation probes. Produces a signed certificate for the GC.\n\n"
            "Examples:\n"
            "  squash genealogy --model ./model --deployment-domain legal-drafting\n"
            "  squash genealogy --model ./model --endpoint http://localhost:8080/v1/complete\n"
            "  squash genealogy --model ./model --block-on-contamination\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    geo_cmd.add_argument(
        "--model", "--model-path", "--models-dir",
        default=".", dest="geo_model_path",
        help="Model directory containing squash artefacts. Default: cwd.",
    )
    geo_cmd.add_argument(
        "--deployment-domain",
        default="default",
        dest="geo_domain",
        choices=["content-generation", "legal-drafting", "code-assistance",
                 "customer-support", "internal-summarization", "research", "default"],
        help="Deployment domain (determines copyright threshold). Default: default.",
    )
    geo_cmd.add_argument(
        "--endpoint", default="", dest="geo_endpoint",
        help="Optional inference endpoint URL for live memorisation probing.",
    )
    geo_cmd.add_argument(
        "--probe-file", default=None, dest="geo_probe_file",
        metavar="PATH",
        help="JSON file with extra probe passages (public-domain only).",
    )
    geo_cmd.add_argument(
        "--output-dir", default=None, dest="geo_output_dir",
        help="Directory to write squash-genealogy.{json,md}.",
    )
    geo_cmd.add_argument(
        "--json", action="store_true", dest="geo_json",
        help="Print structured JSON to stdout.",
    )
    geo_cmd.add_argument(
        "--block-on-contamination",
        action="store_true", dest="geo_block",
        help="Exit 1 if verdict is BLOCKED; exit 2 if WARNING.",
    )
    geo_cmd.add_argument(
        "--quiet", action="store_true",
        help="Suppress non-essential output.",
    )

    cc_cmd = sub.add_parser(
        "copyright-check",
        help="SPDX licence detection + copyright risk assessment for a model",
        description=(
            "Detect SPDX licences for model weights and training data,\n"
            "check licence compatibility, track copyright holders,\n"
            "and produce a signed certificate for the General Counsel.\n\n"
            "Examples:\n"
            "  squash copyright-check --model ./model --deployment-use commercial\n"
            "  squash copyright-check --model ./model --json\n"
            "  squash copyright-check --model ./model --deployment-use research\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    cc_cmd.add_argument(
        "--model", "--model-path", "--models-dir",
        default=".", dest="cc_model_path",
        help="Model directory containing squash artefacts. Default: cwd.",
    )
    cc_cmd.add_argument(
        "--deployment-use",
        default="commercial",
        dest="cc_use",
        choices=["commercial", "research", "internal"],
        help="Intended deployment use. Default: commercial.",
    )
    cc_cmd.add_argument(
        "--output-dir", default=None, dest="cc_output_dir",
        help="Directory to write squash-copyright.{json,md}.",
    )
    cc_cmd.add_argument(
        "--json", action="store_true", dest="cc_json",
        help="Print structured JSON to stdout.",
    )
    cc_cmd.add_argument(
        "--fail-on-incompatible",
        action="store_true", dest="cc_fail",
        help="Exit 1 if compatible=False; exit 2 if compatible=None (uncertain).",
    )
    cc_cmd.add_argument(
        "--quiet", action="store_true",
        help="Suppress non-essential output.",
    )

    # ── Sprint 27 W243-W245 (Track C / C4) — watch-regulatory daemon ──────────
    wr_cmd = sub.add_parser(
        "watch-regulatory",
        help="Continuous regulatory watch daemon — polls SEC, NIST, EUR-Lex for AI governance updates",
        description=(
            "Poll live regulatory sources for new AI governance requirements, "
            "map them to squash policy controls, and surface gap analysis "
            "against your attested model portfolio.\n\n"
            "Examples:\n"
            "  # One-shot poll (cron-friendly)\n"
            "  squash watch-regulatory --once --models-dir ./models\n"
            "\n"
            "  # Continuous daemon (every 6 hours)\n"
            "  squash watch-regulatory --interval 6h --alert-channel slack\n"
            "\n"
            "  # Dry-run: show what would be fetched without persisting\n"
            "  squash watch-regulatory --once --dry-run\n"
            "\n"
            "  # Add custom RSS feed\n"
            "  squash watch-regulatory --once \\\n"
            "      --extra-feed name=legiscan,url=https://example.com/rss,keywords=artificial+intelligence\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    wr_cmd.add_argument(
        "--once", action="store_true", dest="wr_once",
        help="Poll once, print results, and exit (default behaviour; use without --interval).",
    )
    wr_cmd.add_argument(
        "--interval", default="0", dest="wr_interval",
        metavar="INTERVAL",
        help="Polling interval as integer hours or string '6h', '1d', etc. "
             "When set, runs continuously until interrupted. Default: poll once.",
    )
    wr_cmd.add_argument(
        "--sources", action="append", default=None, dest="wr_sources",
        choices=["sec", "nist", "eurlex"],
        help="Sources to poll (repeatable). Default: sec nist eurlex.",
    )
    wr_cmd.add_argument(
        "--extra-feed", action="append", default=None, dest="wr_extra_feeds",
        metavar="KEY=VAL,...",
        help="Add a custom RSS feed. Format: name=NAME,url=URL[,keywords=k1+k2]. "
             "Repeatable.",
    )
    wr_cmd.add_argument(
        "--models-dir", default=None, dest="wr_models_dir",
        help="Directory of attested model subdirectories for gap analysis.",
    )
    wr_cmd.add_argument(
        "--alert-channel", default="stdout",
        choices=["stdout", "slack", "teams", "webhook"],
        dest="wr_alert_channel",
        help="Where to send new-event alerts. Default: stdout.",
    )
    wr_cmd.add_argument(
        "--db-path", default=None, dest="wr_db_path",
        help="SQLite store path for seen events. Default: ~/.squash/regulatory_events.db",
    )
    wr_cmd.add_argument(
        "--dry-run", action="store_true", dest="wr_dry_run",
        help="Fetch events but skip persistence and alert dispatch.",
    )
    wr_cmd.add_argument(
        "--json", action="store_true", dest="wr_json",
        help="Emit structured JSON output (gap analysis results).",
    )
    wr_cmd.add_argument(
        "--max-events", type=int, default=50, dest="wr_max_events",
        help="Maximum new events to process per poll cycle (default: 50).",
    )
    wr_cmd.add_argument(
        "--quiet", action="store_true", help="Suppress non-error output",
    )


    # ── C8 (Sprint 35) — deprecation-watch: model sunset cross-reference ──────
    dw_cmd = sub.add_parser(
        "deprecation-watch",
        help="Cross-reference registered models against provider deprecation schedules",
        description=(
            "Detect deployed models approaching end-of-life across OpenAI, Anthropic,\n"
            "Google, Meta, and Mistral. Fires alerts with configurable lead time,\n"
            "migration effort estimates, and re-attestation checklists.\n\n"
            "Examples:\n"
            "  squash deprecation-watch\n"
            "  squash deprecation-watch --lead-time 60 --provider openai,anthropic\n"
            "  squash deprecation-watch --check gpt-4-0613\n"
            "  squash deprecation-watch --list --provider google\n"
            "  squash deprecation-watch --json --all\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    dw_cmd.add_argument("--lead-time", type=int, default=30, dest="dw_lead_time",
                        metavar="DAYS",
                        help="Alert when sunset is within DAYS days (default: 30)")
    dw_cmd.add_argument("--provider", dest="dw_providers", default="",
                        help="Comma-separated provider filter: openai,anthropic,google,meta,mistral")
    dw_cmd.add_argument("--check", dest="dw_check_model", default=None,
                        metavar="MODEL_ID",
                        help="Check a specific model ID (bypasses registry scan)")
    dw_cmd.add_argument("--list", action="store_true", dest="dw_list_all",
                        help="List all known deprecation entries (no registry scan)")
    dw_cmd.add_argument("--all", action="store_true", dest="dw_include_all",
                        help="Include entries beyond the lead-time window")
    dw_cmd.add_argument("--include-informational", action="store_true",
                        dest="dw_informational",
                        help="Include INFORMATIONAL entries (no hard sunset)")
    dw_cmd.add_argument("--include-sunsetted", action="store_true",
                        dest="dw_sunsetted",
                        help="Include already-sunsetted models")
    dw_cmd.add_argument("--model-ids", dest="dw_model_ids", default="",
                        help="Comma-separated model IDs to check (bypasses AssetRegistry)")
    dw_cmd.add_argument("--registry-db", dest="dw_registry_db", default=None,
                        help="Path to asset_registry.db (default: ~/.squash/asset_registry.db)")
    dw_cmd.add_argument("--alert-channel", dest="dw_channel",
                        default="stdout", choices=["stdout", "slack", "json"],
                        help="Alert routing channel (default: stdout)")
    dw_cmd.add_argument("--checklist", action="store_true", dest="dw_checklist",
                        help="Print re-attestation checklist for each alert")
    dw_cmd.add_argument("--json", action="store_true", dest="dw_json",
                        help="Emit full JSON output")
    dw_cmd.add_argument("--fail-on-alert", action="store_true", dest="dw_fail",
                        help="Exit 1 if any deprecation alerts are found")
    dw_cmd.add_argument("--quiet", "-q", action="store_true")



    # ── C9 (Sprint 36) — attest-carbon: Carbon / Energy Attestation ──────────
    ac_cmd = sub.add_parser(
        "attest-carbon",
        help="Compute and attest the carbon / energy footprint of a deployed model",
        description=(
            "Generate a CSRD-mappable, cryptographically signed carbon + energy\n"
            "attestation certificate. Covers CSRD Scope 2/3, EU AI Act Annex IV §4,\n"
            "CSDDD, UK PRA SS1/23, and OMB/DOE data-centre reporting.\n\n"
            "Examples:\n"
            "  squash attest-carbon --model-id bert-base --params 110M --region eu-west-1\n"
            "  squash attest-carbon --model-id gpt-3 --params 175B --region us-east-1 --hardware h100\n"
            "  squash attest-carbon --model-id mymodel --params 7B --bom ./mlbom.json --csrd\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    ac_cmd.add_argument("--model-id", dest="ac_model_id", required=True,
                        help="Model identifier")
    ac_cmd.add_argument("--params", dest="ac_params", required=True,
                        help="Parameter count: integer or shorthand (110M, 7B, 1.5T)")
    ac_cmd.add_argument("--region", dest="ac_region", default="us-east-1",
                        help="Deployment region (AWS/GCP/Azure region or ISO country code; default: us-east-1)")
    ac_cmd.add_argument("--architecture", dest="ac_arch", default="transformer",
                        choices=["transformer", "cnn", "rnn", "moe", "diffusion", "embedding", "unknown"],
                        help="Model architecture family (default: transformer)")
    ac_cmd.add_argument("--hardware", dest="ac_hw", default="a100",
                        choices=["a100", "h100", "h200", "tpu_v4", "tpu_v5", "rtx4090", "cpu", "unknown"],
                        help="Inference hardware (default: a100)")
    ac_cmd.add_argument("--inferences-per-day", type=int, default=10000,
                        dest="ac_inf_per_day",
                        help="Expected daily inference volume (default: 10 000)")
    ac_cmd.add_argument("--tokens-per-inference", type=int, default=512,
                        dest="ac_tokens",
                        help="Average tokens per inference (default: 512)")
    ac_cmd.add_argument("--seq-len", type=int, default=512, dest="ac_seq_len",
                        help="Sequence length for FLOP calculation (default: 512)")
    ac_cmd.add_argument("--utilization", type=float, default=0.45, dest="ac_util",
                        help="GPU/TPU utilization fraction 0–1 (default: 0.45)")
    ac_cmd.add_argument("--pue", type=float, default=None, dest="ac_pue",
                        help="Power Usage Effectiveness override (default: 1.20)")
    ac_cmd.add_argument("--renewable-fraction", type=float, default=0.0,
                        dest="ac_renewable",
                        help="Fraction of electricity from renewables/RECs 0–1 (default: 0)")
    ac_cmd.add_argument("--live-intensity", action="store_true", dest="ac_live",
                        help="Attempt live Electricity Maps API fetch (requires SQUASH_ELECTRICITY_MAPS_KEY)")
    ac_cmd.add_argument("--sign", action="store_true", dest="ac_sign",
                        help="HMAC-SHA256 sign the certificate")
    ac_cmd.add_argument("--output", dest="ac_output",
                        help="Write certificate JSON to this file (default: <model-id>-carbon-attest.json)")
    ac_cmd.add_argument("--bom", dest="ac_bom",
                        help="Path to CycloneDX ML-BOM JSON — enrich with energy fields")
    ac_cmd.add_argument("--csrd", action="store_true", dest="ac_csrd",
                        help="Also write a CSRD ESRS E1 mapping JSON alongside the certificate")
    ac_cmd.add_argument("--framework", dest="ac_framework", default="csrd",
                        choices=["csrd", "csddd", "uk_pra_ss1_23", "omb_doe", "eu_ai_act"],
                        help="Regulatory framework for --csrd output (default: csrd)")
    ac_cmd.add_argument("--json", action="store_true", dest="ac_json",
                        help="Print full certificate JSON to stdout")
    ac_cmd.add_argument("--quiet", "-q", action="store_true")



    # ── D3 (Sprint 28) — score: AI Procurement Scoring API ───────────────────
    score_cmd = sub.add_parser(
        "score",
        help="Query the AI compliance score for a vendor (Procurement Scoring API)",
        description=(
            "The credit-score API for AI compliance. Returns a vendor's\n"
            "compliance score, tier, and attestation metadata.\n\n"
            "Examples:\n"
            "  squash score acme-corp\n"
            "  squash score acme-corp --breakdown\n"
            "  squash score acme-corp --history --json\n"
            "  squash score acme-corp --api-url https://squash.works\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    score_cmd.add_argument("vendor", help="Vendor name or identifier to look up")
    score_cmd.add_argument("--breakdown", action="store_true", dest="score_breakdown",
                           help="Show per-component score breakdown (requires Pro plan)")
    score_cmd.add_argument("--history", action="store_true", dest="score_history",
                           help="Show 12-month score history (requires Enterprise plan)")
    score_cmd.add_argument("--months", type=int, default=12, dest="score_months",
                           help="History window in months (default: 12)")
    score_cmd.add_argument("--api-url", dest="score_api_url", default=None,
                           help="Override the squash API base URL")
    score_cmd.add_argument("--local", action="store_true", dest="score_local",
                           help="Query local registry databases instead of the live API")
    score_cmd.add_argument("--json", action="store_true", dest="score_json",
                           help="Emit full JSON response")
    score_cmd.add_argument("--quiet", "-q", action="store_true")



    # ── D6 (Sprint 18) — soc2: SOC 2 Type II Readiness ──────────────────────
    soc2_cmd = sub.add_parser(
        "soc2",
        help="SOC 2 Type II readiness assessment and auditor-ready evidence bundle",
        description=(
            "SOC 2 Type II is the most-requested item in enterprise procurement.\n"
            "squash soc2 maps the Trust Services Criteria (all 65 controls) to squash\n"
            "building blocks and produces an auditor-ready evidence bundle.\n\n"
            "Examples:\n"
            "  squash soc2 readiness\n"
            "  squash soc2 readiness --json --window 365\n"
            "  squash soc2 evidence --output ./evidence/\n"
            "  squash soc2 evidence --output ./evidence/ --window 365\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    soc2_sub = soc2_cmd.add_subparsers(dest="soc2_command", metavar="SUBCOMMAND")
    soc2_sub.required = True

    soc2_readiness = soc2_sub.add_parser(
        "readiness",
        help="Print coverage report across all 65 TSC controls",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    soc2_readiness.add_argument("--window", type=int, default=365, dest="soc2_window",
                                help="Evidence collection window in days (default: 365)")
    soc2_readiness.add_argument("--json", action="store_true", dest="soc2_json",
                                help="Emit full JSON report")
    soc2_readiness.add_argument("--category", dest="soc2_category", default=None,
                                choices=["CC", "A", "PI", "C", "P"],
                                help="Filter to one TSC category")
    soc2_readiness.add_argument("--status", dest="soc2_status", default=None,
                                choices=["COVERED", "PARTIAL", "GAP", "NOT_APPLICABLE"],
                                help="Filter by control status")
    soc2_readiness.add_argument("--quiet", "-q", action="store_true")

    soc2_evidence = soc2_sub.add_parser(
        "evidence",
        help="Build auditor-ready evidence ZIP bundle",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    soc2_evidence.add_argument("--output", dest="soc2_output", default=None,
                               help="Output directory for the ZIP bundle (default: ./)")
    soc2_evidence.add_argument("--window", type=int, default=365, dest="soc2_window",
                               help="Evidence collection window in days (default: 365)")
    soc2_evidence.add_argument("--no-attestations", action="store_true",
                               dest="soc2_no_attest",
                               help="Exclude attestation artifacts from bundle")
    soc2_evidence.add_argument("--quiet", "-q", action="store_true")


    # ── W135 / W136 — Annex IV generate + validate ────────────────────────────
    annex_iv_cmd = sub.add_parser(
        "annex-iv",
        help="EU AI Act Annex IV technical documentation (generate / validate)",
        description=(
            "Generate or validate EU AI Act Annex IV technical documentation.\n\n"
            "Examples:\n"
            "  squash annex-iv generate --root ./my-training-run --system-name \"BERT Classifier\"\n"
            "  squash annex-iv generate --root . --format md html json pdf --output-dir ./docs\n"
            "  squash annex-iv validate ./docs/annex_iv.json\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    annex_iv_sub = annex_iv_cmd.add_subparsers(dest="annex_iv_command", metavar="SUBCOMMAND")
    annex_iv_sub.required = True

    # squash annex-iv generate
    aiv_gen = annex_iv_sub.add_parser(
        "generate",
        help="Generate Annex IV documentation from a training run directory",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    aiv_gen.add_argument(
        "--root", "-r",
        required=True,
        metavar="DIR",
        help="Training run directory to scan (TensorBoard logs, configs, .py files)",
    )
    aiv_gen.add_argument(
        "--output-dir", "-o",
        default=None,
        metavar="DIR",
        help="Directory to write Annex IV artifacts. Defaults to --root.",
    )
    aiv_gen.add_argument(
        "--format", "-f",
        dest="formats",
        nargs="+",
        default=["md", "json"],
        choices=["md", "html", "json", "pdf"],
        metavar="FMT",
        help="Output formats: md html json pdf (default: md json)",
    )
    aiv_gen.add_argument("--system-name",    default="AI System",  help="Human-readable AI system name (§1(a))")
    aiv_gen.add_argument("--version",        default="1.0.0",      help="System version string (§1(a))")
    aiv_gen.add_argument("--intended-purpose", default=None,        help="§1(b) — what this system is designed to do")
    aiv_gen.add_argument("--risk-level",     default=None,
                         choices=["minimal", "limited", "high", "unacceptable"],
                         help="EU AI Act risk classification (§4)")
    aiv_gen.add_argument("--general-description", default=None,    help="§1(a) — free-text system overview")
    aiv_gen.add_argument("--hardware",       dest="hardware_requirements", default=None,
                         help="§1(a) — compute / hardware requirements")
    aiv_gen.add_argument("--deployment-context", default=None,     help="§1(b) — production environment description")
    aiv_gen.add_argument("--risk-management", default=None,        help="§4 — risk management system description")
    aiv_gen.add_argument("--oversight",      dest="oversight_description", default=None,
                         help="§5 — human oversight description")
    aiv_gen.add_argument("--model-type",     default=None,         help="§3(a) — architecture family (e.g. transformer)")
    aiv_gen.add_argument("--lifecycle-plan", default=None,         help="§7 — lifecycle management description")
    aiv_gen.add_argument("--monitoring-plan", default=None,        help="§7 — post-deployment monitoring")
    aiv_gen.add_argument(
        "--mlflow-run",
        default=None,
        metavar="RUN_ID",
        help="Augment with MLflow run metrics and params (requires mlflow)",
    )
    aiv_gen.add_argument(
        "--mlflow-uri",
        default="http://localhost:5000",
        metavar="URI",
        help="MLflow tracking URI (default: http://localhost:5000)",
    )
    aiv_gen.add_argument(
        "--wandb-run",
        default=None,
        metavar="ENTITY/PROJECT/RUN_ID",
        help="Augment with Weights & Biases run (requires wandb)",
    )
    aiv_gen.add_argument(
        "--hf-dataset",
        dest="hf_datasets",
        action="append",
        default=[],
        metavar="DATASET_ID",
        help="Augment with HuggingFace dataset provenance (repeatable)",
    )
    aiv_gen.add_argument(
        "--hf-token",
        default=None,
        metavar="TOKEN",
        help="HuggingFace API token for private datasets",
    )
    aiv_gen.add_argument(
        "--stem",
        default="annex_iv",
        metavar="NAME",
        help="Output filename stem (default: annex_iv → annex_iv.md, annex_iv.json, …)",
    )
    aiv_gen.add_argument("--no-validate", action="store_true", help="Skip post-generation validation report")

    # ── B2 (Sprint 15 W208) — branded PDF flags ────────────────────────────
    aiv_gen.add_argument(
        "--branded",
        action="store_true",
        dest="annex_iv_branded",
        help=(
            "When --format pdf is specified, produce a branded executive PDF "
            "with cover page, KPI scorecard, and signature block in addition "
            "to the plain PDF. Requires: pip install weasyprint"
        ),
    )
    aiv_gen.add_argument(
        "--org",
        default="",
        dest="annex_iv_org",
        help="Organisation name shown on the cover page (branded PDF).",
    )
    aiv_gen.add_argument(
        "--author",
        default="",
        dest="annex_iv_author",
        help="Preparer name / role shown on the cover page (branded PDF).",
    )
    aiv_gen.add_argument(
        "--logo",
        default=None,
        dest="annex_iv_logo",
        metavar="PATH",
        help="Custom logo file (SVG or PNG) to embed on the cover page.",
    )
    aiv_gen.add_argument(
        "--accent",
        default="#22c55e",
        dest="annex_iv_accent",
        help="Brand accent colour (hex) for the branded PDF. Default: #22c55e (Squash green).",
    )
    aiv_gen.add_argument("--fail-on-warning", action="store_true", help="Exit 1 if validation produces warnings")
    aiv_gen.add_argument("--quiet", action="store_true", help="Suppress informational output")

    # squash annex-iv validate
    aiv_val = annex_iv_sub.add_parser(
        "validate",
        help="Validate an existing Annex IV JSON document against EU AI Act requirements",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    aiv_val.add_argument(
        "document",
        metavar="PATH",
        help="Path to an annex_iv.json file produced by 'squash annex-iv generate'",
    )
    aiv_val.add_argument("--fail-on-warning", action="store_true", help="Exit 1 if validation produces warnings")
    aiv_val.add_argument("--quiet", action="store_true", help="Suppress informational output")

    # ── W160 — squash demo ────────────────────────────────────────────────────
    demo_cmd = sub.add_parser(
        "demo",
        help="Konjo Edition demo — animated, slick, sales-ready.",
        description=(
            "Runs a complete attestation pipeline on a bundled BERT model.\n"
            "Generates an HTML compliance report, opens it in your browser,\n"
            "and opens the output folder in Finder — all in under 30 seconds.\n\n"
            "Examples:\n"
            "  squash demo                        # sales mode (default)\n"
            "  squash demo --output-dir ./out     # custom output location\n"
            "  squash demo --policy nist-ai-rmf   # different policy\n"
            "  squash demo --no-open              # skip auto-open\n"
            "  squash demo --explore              # 10-section technical walkthrough\n"
            "  squash demo --server               # interactive web demo\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    demo_cmd.add_argument(
        "--output-dir",
        metavar="DIR",
        default="",
        help="Write artifacts here (default: ~/Desktop/squash-demo/TIMESTAMP/).",
    )
    demo_cmd.add_argument(
        "--policy",
        metavar="POLICY",
        default="eu-ai-act",
        help="Policy to evaluate (default: eu-ai-act).",
    )
    demo_cmd.add_argument(
        "--no-open",
        action="store_true",
        dest="no_open",
        help="Skip auto-opening report in browser and folder in Finder/Explorer.",
    )
    demo_cmd.add_argument(
        "--no-color",
        action="store_true",
        dest="no_color",
        help="Plain text output — no Rich colors or animations.",
    )
    demo_cmd.add_argument("--quiet", action="store_true", help="Suppress all output.")
    demo_cmd.add_argument(
        "--explore",
        action="store_true",
        help="10-section technical walkthrough: RFC 8785, chain, genealogy, copyright.",
    )
    demo_cmd.add_argument(
        "--walkthrough",
        action="store_true",
        help=argparse.SUPPRESS,  # legacy alias for --explore
    )
    demo_cmd.add_argument(
        "--server",
        action="store_true",
        help="Launch the interactive web demo at localhost:8002.",
    )
    demo_cmd.add_argument(
        "--port",
        type=int,
        default=8002,
        metavar="PORT",
        help="Port for --server (default 8002).",
    )

    # ── W162 — squash init ────────────────────────────────────────────────────
    init_cmd = sub.add_parser(
        "init",
        help="Scaffold a .squash.yml config for the current project.",
        description=(
            "Auto-detects the ML framework in the current directory, generates a "
            ".squash.yml configuration scaffold with sensible policy defaults, and "
            "runs a first dry-run attestation to show what will be produced.\n\n"
            "Examples:\n"
            "  squash init\n"
            "  squash init --dir ./models/llama-3\n"
            "  squash init --framework pytorch --policy eu-ai-act nist-ai-rmf\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    init_cmd.add_argument(
        "--dir",
        metavar="DIR",
        default=".",
        help="Project directory to inspect (default: current directory).",
    )
    init_cmd.add_argument(
        "--framework",
        metavar="FRAMEWORK",
        default="",
        help="Override framework detection (pytorch, tensorflow, jax, mlx, huggingface).",
    )
    init_cmd.add_argument(
        "--policy",
        metavar="POLICY",
        nargs="*",
        default=None,
        help="Policy templates to include in the scaffold (default: eu-ai-act).",
    )
    init_cmd.add_argument(
        "--dry-run",
        action="store_true",
        default=True,
        help="Run a dry-run attestation after scaffolding (default: true).",
    )
    init_cmd.add_argument("--no-dry-run", action="store_false", dest="dry_run")
    init_cmd.add_argument("--quiet", action="store_true", help="Suppress output")

    # ── W167 — squash watch ───────────────────────────────────────────────────
    watch_cmd = sub.add_parser(
        "watch",
        help="Watch a model directory and re-attest on file changes.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=(
            "Watch a model directory for changes and automatically re-run\n"
            "attestation whenever model files are modified. Press Ctrl+C to stop.\n\n"
            "Examples::\n\n"
            "  squash watch ./models\n"
            "  squash watch ./models --policy eu-ai-act --interval 10\n"
            "  squash watch ./models --on-fail notify\n"
        ),
    )
    watch_cmd.add_argument(
        "path",
        nargs="?",
        default=".",
        help="Model directory to watch (default: current directory).",
    )
    watch_cmd.add_argument(
        "--policy",
        nargs="+",
        default=["eu-ai-act"],
        metavar="POLICY",
        help="Policy framework(s) to enforce (default: eu-ai-act).",
    )
    watch_cmd.add_argument(
        "--interval",
        type=int,
        default=5,
        metavar="SECONDS",
        help="Polling interval in seconds (default: 5).",
    )
    watch_cmd.add_argument(
        "--on-fail",
        choices=["log", "notify", "exit"],
        default="log",
        dest="on_fail",
        help="Action on attestation failure (default: log).",
    )
    watch_cmd.add_argument(
        "--output-dir",
        default=None,
        metavar="DIR",
        help="Directory to write attestation artifacts (default: <path>/attestation).",
    )
    watch_cmd.add_argument("--quiet", action="store_true", help="Suppress output")

    # ── W168 — squash install-hook ────────────────────────────────────────────
    hook_cmd = sub.add_parser(
        "install-hook",
        help="Install squash as a git pre-push hook.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=(
            "Install a git pre-push hook that runs squash attest before every push.\n"
            "Blocks the push if attestation fails.\n\n"
            "Examples::\n\n"
            "  squash install-hook\n"
            "  squash install-hook --dir ./my-repo\n"
            "  squash install-hook --hook-type pre-commit\n"
        ),
    )
    hook_cmd.add_argument(
        "--dir",
        default=".",
        metavar="DIR",
        help="Git repository root (default: current directory).",
    )
    hook_cmd.add_argument(
        "--hook-type",
        choices=["pre-push", "pre-commit"],
        default="pre-push",
        dest="hook_type",
        help="Git hook type to install (default: pre-push).",
    )
    hook_cmd.add_argument(
        "--policy",
        nargs="+",
        default=["eu-ai-act"],
        metavar="POLICY",
        help="Policy framework(s) to enforce in the hook.",
    )
    hook_cmd.add_argument("--quiet", action="store_true", help="Suppress output")

    # ── OWASP Agentic Top 10 2026 ─────────────────────────────────────────────
    agentic_cmd = sub.add_parser(
        "scan-agentic",
        help="Scan an agentic system config against OWASP Agentic Top 10 2026",
        description=(
            "Load a YAML or JSON agentic system specification and run all ten "
            "OWASP Agentic Top 10 2026 risk checks.  Prints a findings table "
            "and exits 0 on pass (no critical/high findings) or 2 on fail.\n\n"
            "Example: squash scan-agentic --config my-agent.yaml\n"
            "Example: squash scan-agentic --config my-agent.json --json-result out.json"
        ),
    )
    agentic_cmd.add_argument(
        "--config",
        required=True,
        metavar="PATH",
        dest="agentic_config",
        help="Path to YAML or JSON agentic system specification file.",
    )
    agentic_cmd.add_argument(
        "--json-result",
        default=None,
        metavar="PATH",
        dest="json_result",
        help="Write the full scan result as JSON to this path.",
    )
    agentic_cmd.add_argument("--quiet", action="store_true", help="Suppress non-error output")

    return parser


def _cmd_policies(args: argparse.Namespace, quiet: bool) -> int:
    try:
        from squash.policy import AVAILABLE_POLICIES, PolicyRegistry
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    validate_path: str | None = getattr(args, "validate", None)

    if validate_path is not None:
        rules_path = Path(validate_path)
        if not rules_path.exists():
            print(f"error: path does not exist: {rules_path}", file=sys.stderr)
            return 1
        try:
            rules = PolicyRegistry.load_rules_from_yaml(rules_path)
        except ImportError as e:
            print(f"error: {e}", file=sys.stderr)
            return 2
        except (OSError, ValueError) as e:
            print(f"error loading rules: {e}", file=sys.stderr)
            return 1

        raw_errors = PolicyRegistry.validate_rules(rules)
        if raw_errors:
            if not quiet:
                print(f"✗ {len(raw_errors)} validation error(s):", file=sys.stderr)
                for err in raw_errors:
                    print(f"  {err}", file=sys.stderr)
            return 2

        if not quiet:
            print(f"✓ {len(rules)} rule(s) valid: {rules_path}")
        return 0

    if not quiet:
        print("Available policy templates:")
    for name in sorted(AVAILABLE_POLICIES):
        print(f"  {name}")
    return 0


def _cmd_scan(args: argparse.Namespace, quiet: bool) -> int:
    # B1 (Sprint 14, W205) — `squash scan hf://owner/model` public scanner
    if isinstance(args.model_path, str) and args.model_path.startswith("hf://"):
        return _cmd_scan_hf(args, quiet)

    try:
        from squash.scanner import ModelScanner
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    model_path = Path(args.model_path)
    if not model_path.exists():
        print(f"error: path does not exist: {model_path}", file=sys.stderr)
        return 1

    scan_dir = model_path if model_path.is_dir() else model_path.parent
    result = ModelScanner.scan_directory(scan_dir)

    if not quiet:
        icon = "✓" if result.is_safe else "✗"
        print(f"{icon} Scan {result.status}: {scan_dir}")
        for f in result.findings:
            print(f"  [{f.severity.upper()}] {f.title} — {f.detail}")

    if args.json_result:
        data = {
            "status": result.status,
            "is_safe": result.is_safe,
            "critical": result.critical_count,
            "high": result.high_count,
            "findings": [
                {"severity": f.severity, "title": f.title, "file": f.file_path}
                for f in result.findings
            ],
        }
        Path(args.json_result).write_text(json.dumps(data, indent=2))

    if args.sarif:
        try:
            from squash.sarif import SarifBuilder
        except ImportError as e:  # pragma: no cover
            print(f"sarif export unavailable: {e}", file=sys.stderr)
            return 2
        SarifBuilder.write(result, Path(args.sarif))
        if not quiet:
            print(f"SARIF written to {args.sarif}")

    if getattr(args, "exit_2_on_unsafe", False):
        if result.critical_count > 0 or result.high_count > 0:
            return 2
        if not result.is_safe:
            return 1
        return 0

    return 0 if result.is_safe else 2


def _cmd_scan_hf(args: argparse.Namespace, quiet: bool) -> int:
    """B1 (Sprint 14, W205) — public `squash scan hf://...` handler.

    Routes from `_cmd_scan` when the positional argument is an hf:// URI.
    Builds an HFScanReport and writes squash-hf-scan.{json,md} to
    --output-dir (default: cwd).

    Exit codes:
      0  scan clean
      1  scan unsafe
      2  configuration / dependency error
    """
    try:
        from squash.hf_scanner import HFScanner
    except ImportError as e:
        print(f"squash hf scanner unavailable: {e}", file=sys.stderr)
        return 2

    quiet = quiet or getattr(args, "hf_quiet", False)

    import os as _os
    token = (
        getattr(args, "hf_token", "")
        or _os.environ.get("HUGGING_FACE_HUB_TOKEN", "")
        or _os.environ.get("HF_TOKEN", "")
    )

    output_dir = Path(args.hf_output_dir) if getattr(args, "hf_output_dir", None) \
        else Path.cwd()
    policies = getattr(args, "hf_policies", None)
    download_weights = bool(getattr(args, "hf_download_weights", False))
    keep_download = bool(getattr(args, "hf_keep_download", False))

    scanner = HFScanner()
    try:
        report = scanner.scan(
            uri=args.model_path,
            policies=policies,
            download_weights=download_weights,
            keep_download=keep_download,
            token=token,
        )
    except ValueError as exc:
        print(f"squash scan: {exc}", file=sys.stderr)
        return 2
    except ImportError as exc:
        print(f"squash scan: {exc}", file=sys.stderr)
        return 2
    except Exception as exc:  # noqa: BLE001 — surface fetch errors to user
        print(f"squash scan: hf fetch failed: {exc}", file=sys.stderr)
        return 1

    # Write artefacts
    paths = report.save(output_dir)

    # ── Optional pass-through outputs honoured for hf:// path ────────────────
    if getattr(args, "json_result", None):
        Path(args.json_result).write_text(report.to_json(), encoding="utf-8")
    if getattr(args, "sarif", None):
        try:
            from squash.sarif import SarifBuilder
            from squash.scanner import ScanFinding, ScanResult
        except ImportError:
            pass
        else:
            findings = [
                ScanFinding(
                    severity=f.get("severity", "info"),
                    finding_id=f.get("finding_id", ""),
                    title=f.get("title", ""),
                    detail=f.get("detail", ""),
                    file_path=f.get("file_path", ""),
                    cve=f.get("cve", ""),
                )
                for f in report.findings
            ]
            sr = ScanResult(
                scanned_path=f"hf://{report.metadata.repo_id}",
                status=report.scan_status,
                findings=findings,
            )
            SarifBuilder.write(sr, Path(args.sarif))

    # ── Console output ───────────────────────────────────────────────────────
    if not quiet:
        emoji = "✅" if report.is_safe else "❌"
        print(f"{emoji} hf://{report.metadata.repo_id} — scan {report.scan_status}"
              f" ({len(report.findings)} findings, {report.file_count} files)")
        if report.metadata.license:
            print(f"   license: {report.metadata.license}  "
                  f"downloads: {report.metadata.downloads:,}")
        for w in report.license_warnings:
            print(f"   ⚠️  {w}")
        for fmt, p in paths.items():
            print(f"   {fmt}: {p}")

    if getattr(args, "exit_2_on_unsafe", False):
        critical = sum(1 for f in report.findings
                       if f.get("severity") == "critical")
        high = sum(1 for f in report.findings if f.get("severity") == "high")
        if critical or high:
            return 2
        if not report.is_safe:
            return 1
        return 0
    return 0 if report.is_safe else 1


def _cmd_sbom_diff(args: argparse.Namespace, quiet: bool) -> int:
    try:
        from squash.sbom_builder import SbomDiff
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    path_a = Path(args.sbom_a)
    path_b = Path(args.sbom_b)
    for p in (path_a, path_b):
        if not p.exists():
            print(f"error: path does not exist: {p}", file=sys.stderr)
            return 1

    try:
        bom_a = json.loads(path_a.read_text(encoding="utf-8"))
        bom_b = json.loads(path_b.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as e:
        print(f"error reading SBOM: {e}", file=sys.stderr)
        return 1

    diff = SbomDiff.compare(bom_a, bom_b)

    if not quiet:
        print(f"hash changed:          {diff.hash_changed}")
        print(f"score delta:           {diff.score_delta}")
        print(f"policy status changed: {diff.policy_status_changed}")
        if diff.new_findings:
            print(f"new findings ({len(diff.new_findings)}):")
            for fid in diff.new_findings:
                print(f"  + {fid}")
        if diff.resolved_findings:
            print(f"resolved findings ({len(diff.resolved_findings)}):")
            for fid in diff.resolved_findings:
                print(f"  - {fid}")
        if diff.metadata_changes:
            print("metadata changes:")
            for key, (old, new) in diff.metadata_changes.items():
                print(f"  {key}: {old!r} → {new!r}")

    if getattr(args, "exit_1_on_regression", False) and diff.has_regressions:
        return 1
    return 0


def _cmd_verify(args: argparse.Namespace, quiet: bool) -> int:
    try:
        from squash.oms_signer import OmsVerifier
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    model_path = Path(args.model_path)
    if not model_path.exists():
        print(f"error: path does not exist: {model_path}", file=sys.stderr)
        return 1

    bom_path = model_path / "cyclonedx-mlbom.json" if model_path.is_dir() else model_path
    if not bom_path.exists():
        print(f"error: CycloneDX BOM not found: {bom_path}", file=sys.stderr)
        return 1

    bundle_path = Path(args.bundle) if args.bundle else None
    result = OmsVerifier.verify(bom_path, bundle_path)

    if result is None:
        if args.strict:
            if not quiet:
                print("✗ no bundle found (strict mode)", file=sys.stderr)
            return 2
        if not quiet:
            print("— verification skipped (no bundle)")
        return 0

    if result:
        if not quiet:
            print(f"✓ verified: {bom_path}")
        # Phase G.3: opt-in TSA roundtrip check.
        if getattr(args, "check_timestamp", False):
            from squash.self_verify import check_tsa_timestamp

            att_dir = bom_path.parent
            tsa_check = check_tsa_timestamp(att_dir)
            if not quiet:
                icon = "✓" if tsa_check.passed else "✗"
                print(f"{icon} timestamp: {tsa_check.detail}")
            if not tsa_check.passed and args.strict:
                return 2
        return 0

    print(f"✗ verification FAILED: {bom_path}", file=sys.stderr)
    return 2


# ────────────────────────────────────────────────────────────────────────────
# Wave 49 — air-gapped / offline signing helpers
# ────────────────────────────────────────────────────────────────────────────

def _cmd_keygen(args: argparse.Namespace, quiet: bool) -> int:
    """Generate an Ed25519 keypair for offline BOM signing."""
    try:
        from squash.oms_signer import OmsSigner
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    try:
        priv_path, pub_path = OmsSigner.keygen(args.name, args.key_dir)
    except ImportError as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    except Exception as e:
        print(f"runtime error: {e}", file=sys.stderr)
        return 2

    if not quiet:
        print(f"✓ keypair generated:")
        print(f"  Private : {priv_path}")
        print(f"  Public  : {pub_path}")
        print()
        print("Keep the private key secret.  Share the public key for verification.")
        print(f"  Sign  : squash attest <model> --sign --offline --offline-key {priv_path}")
        print(f"  Verify: squash verify-local <bom> --key {pub_path}")
    return 0


def _cmd_verify_local(args: argparse.Namespace, quiet: bool) -> int:
    """Verify a BOM's Ed25519 offline signature against a local public key."""
    try:
        from squash.oms_signer import OmsVerifier
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    bom_path = Path(args.bom_path)
    if not bom_path.exists():
        print(f"error: BOM not found: {bom_path}", file=sys.stderr)
        return 1

    pub_key_path = Path(args.pub_key)
    if not pub_key_path.exists():
        print(f"error: public key not found: {pub_key_path}", file=sys.stderr)
        return 1

    sig_path = Path(args.sig_file) if args.sig_file else None

    try:
        ok = OmsVerifier.verify_local(bom_path, pub_key_path, sig_path)
    except ImportError as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    except Exception as e:
        print(f"runtime error: {e}", file=sys.stderr)
        return 2

    if ok:
        if not quiet:
            print(f"✓ verified (offline): {bom_path}")
        return 0

    print(f"✗ verification FAILED (offline): {bom_path}", file=sys.stderr)
    return 2


def _cmd_pack_offline(args: argparse.Namespace, quiet: bool) -> int:
    """Bundle a model directory into a portable .squash-bundle.tar.gz archive."""
    try:
        from squash.oms_signer import OmsSigner
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    model_dir = Path(args.model_dir)
    if not model_dir.exists():
        print(f"error: model_dir not found: {model_dir}", file=sys.stderr)
        return 1

    output_path = Path(args.output_path) if args.output_path else None

    try:
        bundle_path = OmsSigner.pack_offline(model_dir, output_path)
    except FileNotFoundError as e:
        print(f"error: {e}", file=sys.stderr)
        return 1
    except Exception as e:
        print(f"runtime error: {e}", file=sys.stderr)
        return 2

    size_mb = bundle_path.stat().st_size / (1024 * 1024)
    if not quiet:
        print(f"✓ bundle created: {bundle_path} ({size_mb:.1f} MB)")
    return 0


def _cmd_attest(args: argparse.Namespace, quiet: bool) -> int:
    try:
        from squash.attest import (
            AttestConfig,
            AttestPipeline,
            AttestationViolationError,
        )
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    model_path = Path(args.model_path)
    if not model_path.exists():
        print(f"error: path does not exist: {model_path}", file=sys.stderr)
        return 1

    policies = args.policies if args.policies else ["enterprise-strict"]

    # Build SpdxOptions only when the user supplied at least one SPDX flag.
    spdx_options = None
    if any([
        args.spdx_type,
        args.spdx_safety_risk,
        args.spdx_datasets,
        args.spdx_training_info,
        args.spdx_sensitive_data,
    ]):
        from squash.spdx_builder import SpdxOptions
        spdx_options = SpdxOptions(
            type_of_model=args.spdx_type or "text-generation",
            safety_risk_assessment=args.spdx_safety_risk or "unspecified",
            dataset_ids=list(args.spdx_datasets),
            information_about_training=args.spdx_training_info or "see-model-card",
            sensitive_personal_information=args.spdx_sensitive_data or "absent",
        )

    config = AttestConfig(
        model_path=model_path,
        output_dir=Path(args.output_dir) if args.output_dir else None,
        model_id=args.model_id,
        hf_repo=args.hf_repo,
        quant_format=args.quant_format,
        policies=policies,
        sign=args.sign,
        offline=args.offline,
        local_signing_key=Path(args.offline_key) if args.offline_key else None,
        fail_on_violation=False,  # handle ourselves below for clean exit codes
        skip_scan=args.skip_scan,
        spdx_options=spdx_options,
    )

    try:
        result = AttestPipeline.run(config)
    except FileNotFoundError as e:
        print(f"error: {e}", file=sys.stderr)
        return 1
    except Exception as e:
        print(f"runtime error: {e}", file=sys.stderr)
        return 2

    if not quiet:
        icon = "✓" if result.passed else "✗"
        print(f"{icon} {result.summary()}")
        if result.cyclonedx_path:
            print(f"   CycloneDX : {result.cyclonedx_path}")
        if result.spdx_json_path:
            print(f"   SPDX JSON : {result.spdx_json_path}")
        if result.master_record_path:
            print(f"   Master    : {result.master_record_path}")
        if result.signature_path:
            print(f"   Signature : {result.signature_path}")

    if args.json_result and result.master_record_path and result.master_record_path.exists():
        import shutil
        shutil.copy2(result.master_record_path, args.json_result)

    if args.fail_on_violation and not result.passed:
        if not quiet:
            print("error: attestation failed (fail-on-violation set)", file=sys.stderr)
        return 2

    return 0 if result.passed else 2


# ────────────────────────────────────────────────────────────────────────────
# Wave 15  — HTML / JSON compliance report
# ────────────────────────────────────────────────────────────────────────────

def _cmd_report(args: argparse.Namespace, quiet: bool) -> int:
    try:
        from squash.report import ComplianceReporter
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    model_path = Path(args.model_path)
    if not model_path.exists():
        print(f"error: path does not exist: {model_path}", file=sys.stderr)
        return 1

    output = Path(args.output) if args.output else None
    fmt: str = getattr(args, "format", "html")

    if fmt == "json":
        # Emit a raw JSON summary of all artifacts (no HTML rendering)
        import json as _json
        from squash.report import _load_artifacts  # type: ignore[attr-defined]
        ctx = _load_artifacts(model_path)
        payload = {
            "model_dir": str(ctx["model_dir"]),
            "has_attest": ctx.get("attest") is not None,
            "has_cdx": ctx.get("cdx") is not None,
            "has_scan": ctx.get("scan") is not None,
            "has_vex": ctx.get("vex") is not None,
            "policy_count": len(ctx.get("policies", {})),
            "bundle_present": ctx.get("bundle_present", False),
        }
        dest = output or (model_path / "squash-report.json")
        dest.write_text(_json.dumps(payload, indent=2), encoding="utf-8")
        if not quiet:
            print(f"Report written to {dest}")
        return 0

    try:
        dest = ComplianceReporter.write(model_path, output)
    except Exception as e:
        print(f"error generating report: {e}", file=sys.stderr)
        return 2

    if not quiet:
        print(f"Report written to {dest}")
    return 0


# ────────────────────────────────────────────────────────────────────────────
# Wave 16  — VEX feed cache management
# ────────────────────────────────────────────────────────────────────────────

def _cmd_vex(args: argparse.Namespace, quiet: bool) -> int:
    try:
        from squash.vex import VexCache
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    vex_cmd = getattr(args, "vex_command", None)
    if vex_cmd == "update":
        import os
        url = args.url or os.environ.get("SQUASH_VEX_URL", VexCache.DEFAULT_URL)
        timeout = float(args.timeout)
        api_key = os.environ.get("SQUASH_VEX_API_KEY") or None
        try:
            cache = VexCache()
            feed = cache.load_or_fetch(url, timeout=timeout, force=True, api_key=api_key)
            if not quiet:
                print(f"VEX cache updated: {len(feed.statements)} statements from {url}")
        except Exception as e:
            print(f"error updating VEX cache: {e}", file=sys.stderr)
            return 2
        return 0

    if vex_cmd == "status":
        cache = VexCache()
        manifest = cache.manifest()
        if not manifest:
            if not quiet:
                print("VEX cache: empty (run 'squash vex update' to populate)")
            return 0
        if not quiet:
            print(f"URL         : {manifest.get('url', 'unknown')}")
            print(f"Fetched at  : {manifest.get('last_fetched', 'unknown')}")
            print(f"Statements  : {manifest.get('statement_count', 'unknown')}")
            stale = cache.is_stale()
            print(f"Stale       : {'yes' if stale else 'no'}")
        return 0

    # ── Wave 52: subscribe  ───────────────────────────────────────────────────
    if vex_cmd == "subscribe":
        from squash.vex import VexSubscription, VexSubscriptionStore
        import os
        url = args.url
        if not url.startswith("http"):
            print(f"error: URL must begin with http(s)://: {url!r}", file=sys.stderr)
            return 1
        sub = VexSubscription(
            url=url,
            alias=args.alias or "",
            api_key_env_var=args.api_key_env,
            polling_hours=args.polling_hours,
        )
        _store_dir = os.environ.get("SQUISH_SQUASH_STORE_DIR")
        store = VexSubscriptionStore(Path(_store_dir) if _store_dir else None)
        store.add(sub)
        _q = getattr(args, "quiet", False) or quiet
        if not _q:
            label = f" (alias: {sub.alias})" if sub.alias else ""
            print(f"Subscribed to {url}{label}")
            print(f"  API key env : {sub.api_key_env_var}")
            print(f"  Polling     : every {sub.polling_hours}h")
        return 0

    if vex_cmd == "unsubscribe":
        from squash.vex import VexSubscriptionStore
        import os as _os
        _store_dir = _os.environ.get("SQUISH_SQUASH_STORE_DIR")
        store = VexSubscriptionStore(Path(_store_dir) if _store_dir else None)
        removed = store.remove(args.url_or_alias)
        _q = getattr(args, "quiet", False) or quiet
        if not removed:
            print(f"error: no subscription found for {args.url_or_alias!r}", file=sys.stderr)
            return 1
        if not _q:
            print(f"Unsubscribed from {args.url_or_alias}")
        return 0

    if vex_cmd == "list-subscriptions":
        from squash.vex import VexSubscriptionStore
        import os as _os
        _store_dir = _os.environ.get("SQUISH_SQUASH_STORE_DIR")
        store = VexSubscriptionStore(Path(_store_dir) if _store_dir else None)
        subs = store.list()
        if not subs:
            if not quiet:
                print("No VEX feed subscriptions registered.")
            return 0
        if not quiet:
            for sub in subs:
                alias_part = f" [{sub.alias}]" if sub.alias else ""
                polled = sub.last_polled or "never"
                print(f"  {sub.url}{alias_part}")
                print(f"    api-key-env={sub.api_key_env_var}  polling={sub.polling_hours}h  last-polled={polled}")
        return 0

    # No sub-command — print help
    print("usage: squash vex {update,status,subscribe,unsubscribe,list-subscriptions} [options]", file=sys.stderr)
    return 1


# ────────────────────────────────────────────────────────────────────────────
# Wave 18  — Composite multi-model attestation
# ────────────────────────────────────────────────────────────────────────────

def _cmd_attest_composed(args: argparse.Namespace, quiet: bool) -> int:
    try:
        from squash.attest import CompositeAttestConfig, CompositeAttestPipeline
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    model_paths = [Path(p) for p in args.model_paths]
    for mp in model_paths:
        if not mp.exists():
            print(f"error: path does not exist: {mp}", file=sys.stderr)
            return 1

    if len(model_paths) < 2:
        print("error: attest-composed requires at least two model paths", file=sys.stderr)
        return 1

    config = CompositeAttestConfig(
        model_paths=model_paths,
        output_dir=Path(args.output_dir) if args.output_dir else None,
        policies=args.policies or ["enterprise-strict"],
        sign=args.sign,
    )

    try:
        result = CompositeAttestPipeline.run(config)
    except Exception as e:
        print(f"runtime error: {e}", file=sys.stderr)
        return 2

    if not quiet:
        icon = "✓" if result.passed else "✗"
        print(f"{icon} composite attestation {'passed' if result.passed else 'FAILED'}")
        for cr in result.component_results:
            sub_icon = "✓" if cr.passed else "✗"
            print(f"  {sub_icon} {cr.model_path}")
        if result.parent_bom_path:
            print(f"  parent BOM: {result.parent_bom_path}")

    return 0 if result.passed else 2


# ────────────────────────────────────────────────────────────────────────────
# Wave 19  — SBOM registry push
# ────────────────────────────────────────────────────────────────────────────

def _cmd_push(args: argparse.Namespace, quiet: bool) -> int:
    import os

    try:
        from squash.sbom_builder import SbomRegistry
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    model_path = Path(args.model_path)
    if not model_path.exists():
        print(f"error: path does not exist: {model_path}", file=sys.stderr)
        return 1

    bom_path = model_path / "cyclonedx-mlbom.json"
    if not bom_path.exists():
        print(f"error: CycloneDX BOM not found: {bom_path}", file=sys.stderr)
        return 1

    api_key = args.api_key or os.environ.get("SQUASH_REGISTRY_KEY", "")
    registry_url: str = args.registry_url
    registry_type: str = getattr(args, "registry_type", "dtrack")

    try:
        if registry_type == "dtrack":
            pushed_url = SbomRegistry.push_dtrack(bom_path, registry_url, api_key)
        elif registry_type == "guac":
            pushed_url = SbomRegistry.push_guac(bom_path, registry_url)
        else:
            pushed_url = SbomRegistry.push_squash(bom_path, registry_url, api_key)
    except Exception as e:
        print(f"error pushing SBOM: {e}", file=sys.stderr)
        return 2

    if not quiet:
        print(f"✓ SBOM pushed to {pushed_url}")
    return 0


# ── Wave 20 — NTIA check handler ───────────────────────────────────────────────

def _cmd_ntia_check(args: argparse.Namespace, quiet: bool) -> int:
    from squash.policy import NtiaValidator

    bom_path = Path(args.bom_path)
    if not bom_path.exists():
        print(f"error: BOM file not found: {bom_path}", file=sys.stderr)
        return 1
    try:
        result = NtiaValidator.check(bom_path, strict=getattr(args, "strict", False))
    except Exception as e:
        print(f"error: NTIA check failed: {e}", file=sys.stderr)
        return 2
    if not quiet:
        status = "PASS" if result.passed else "FAIL"
        print(f"NTIA minimum elements: {status}")
        print(f"  completeness: {result.completeness_score:.1%}  "
              f"({len(result.present_fields)}/{len(result.present_fields) + len(result.missing_fields)} fields)")
        if result.missing_fields:
            print(f"  missing: {', '.join(result.missing_fields)}")
    return 0 if result.passed else 1


# ── Wave 21 — SLSA attest handler ─────────────────────────────────────────────

def _cmd_slsa_attest(args: argparse.Namespace, quiet: bool) -> int:
    from squash.slsa import SlsaLevel, SlsaProvenanceBuilder

    model_dir = Path(args.model_dir)
    if not model_dir.exists():
        print(f"error: model directory not found: {model_dir}", file=sys.stderr)
        return 1
    level_int = getattr(args, "level", 1)
    level = SlsaLevel(level_int)
    builder_id = getattr(args, "builder_id", "https://squish.local/squash/builder")
    try:
        attest = SlsaProvenanceBuilder.build(
            model_dir,
            level=level,
            builder_id=builder_id,
        )
    except Exception as e:
        print(f"error: SLSA attestation failed: {e}", file=sys.stderr)
        return 2
    if not quiet:
        print(f"✓ SLSA L{level.value} provenance written to {attest.output_path}")
        print(f"  subject: {attest.subject_name}")
        print(f"  digest:  sha256:{attest.subject_sha256}")
    return 0


# ── Wave 22 — BOM merge handler ───────────────────────────────────────────────

def _cmd_merge(args: argparse.Namespace, quiet: bool) -> int:
    from squash.sbom_builder import BomMerger

    bom_paths = [Path(p) for p in args.bom_paths]
    output_path = Path(args.output)
    for p in bom_paths:
        if not p.exists():
            print(f"error: BOM file not found: {p}", file=sys.stderr)
            return 1
    try:
        merged = BomMerger.merge(bom_paths, output_path)
    except Exception as e:
        print(f"error: BOM merge failed: {e}", file=sys.stderr)
        return 2
    if not quiet:
        n_comp = len(merged.get("components", []))
        print(f"✓ Merged {len(bom_paths)} BOMs → {output_path}  ({n_comp} components)")
    return 0


# ── Wave 23 — Risk assess handler ─────────────────────────────────────────────

def _cmd_risk_assess(args: argparse.Namespace, quiet: bool) -> int:
    from squash.risk import AiRiskAssessor

    model_dir = Path(args.model_dir)
    bom_path = model_dir / "cyclonedx-mlbom.json"
    if not bom_path.exists():
        print(f"error: CycloneDX BOM not found: {bom_path}", file=sys.stderr)
        return 1
    framework = getattr(args, "framework", "both")
    overall_passed = True
    try:
        if framework in ("eu-ai-act", "both"):
            eu = AiRiskAssessor.assess_eu_ai_act(bom_path)
            if not quiet:
                print(f"EU AI Act: {eu.category.value.upper()}  "
                      f"({'PASS' if eu.passed else 'FAIL'})")
                for r in eu.rationale:
                    print(f"  • {r}")
            if not eu.passed:
                overall_passed = False
        if framework in ("nist-rmf", "both"):
            rmf = AiRiskAssessor.assess_nist_rmf(bom_path)
            if not quiet:
                print(f"NIST RMF:  {rmf.category.value.upper()}  "
                      f"({'PASS' if rmf.passed else 'FAIL'})")
                for r in rmf.rationale:
                    print(f"  • {r}")
            if not rmf.passed:
                overall_passed = False
    except Exception as e:
        print(f"error: risk assessment failed: {e}", file=sys.stderr)
        return 2
    return 0 if overall_passed else 1


# ── Wave 24 — Drift monitor handler ───────────────────────────────────────────

def _cmd_monitor(args: argparse.Namespace, quiet: bool) -> int:
    from squash.governor import DriftMonitor

    model_dir = Path(args.model_dir)
    if not model_dir.exists():
        print(f"error: model directory not found: {model_dir}", file=sys.stderr)
        return 1
    baseline = getattr(args, "baseline", None)
    once = getattr(args, "once", False)

    try:
        if baseline is None:
            snap = DriftMonitor.snapshot(model_dir)
            if not quiet:
                print(f"✓ Snapshot: {snap}")
            return 0
        events = DriftMonitor.compare(model_dir, baseline)
    except Exception as e:
        print(f"error: drift monitor failed: {e}", file=sys.stderr)
        return 2

    if not events:
        if not quiet:
            print("✓ No drift detected")
        return 0

    for evt in events:
        print(f"[{evt.event_type}] {evt.component}: {evt.old_value!r} → {evt.new_value!r}")
    return 1


# ── Wave 25 — CI run handler ───────────────────────────────────────────────────

def _cmd_ci_run(args: argparse.Namespace, quiet: bool) -> int:
    from squash.cicd import CicdAdapter

    model_dir = Path(args.model_dir)
    if not model_dir.exists():
        print(f"error: model directory not found: {model_dir}", file=sys.stderr)
        return 1
    report_format = getattr(args, "report_format", "text")
    try:
        report = CicdAdapter.run_pipeline(model_dir, report_format=report_format)
    except Exception as e:
        print(f"error: CI pipeline failed: {e}", file=sys.stderr)
        return 2
    if not quiet and report_format in ("github", "text"):
        print(CicdAdapter.job_summary(report))
    return 0 if report.passed else 1


# ── Wave 27 — Kubernetes admission webhook handler ─────────────────────────────

def _cmd_k8s_webhook(args: argparse.Namespace, quiet: bool) -> int:
    from squash.integrations.kubernetes import (
        KubernetesWebhookHandler,
        WebhookConfig,
        serve_webhook,
    )

    policy_store_path = Path(args.policy_store) if getattr(args, "policy_store", None) else None
    config = WebhookConfig(
        policy_store_path=policy_store_path,
        default_allow=not getattr(args, "default_deny", False),
    )
    handler = KubernetesWebhookHandler(config)
    port: int = getattr(args, "port", 8443)
    tls_cert: str | None = getattr(args, "tls_cert", None)
    tls_key: str | None = getattr(args, "tls_key", None)

    if not quiet:
        mode = "HTTPS" if tls_cert else "HTTP (dev)"
        print(f"squash webhook: starting {mode} server on port {port}")
        if policy_store_path:
            print(f"squash webhook: policy store → {policy_store_path}")

    try:
        serve_webhook(handler, port=port, tls_cert=tls_cert, tls_key=tls_key)
    except Exception as e:
        print(f"error: webhook server failed: {e}", file=sys.stderr)
        return 2
    return 0


# ── Wave 50 — Shadow AI detection ─────────────────────────────────────────────

def _cmd_shadow_ai(args: argparse.Namespace, quiet: bool) -> int:  # noqa: C901
    """Run the shadow-ai scan subcommand."""
    import json as _json

    from squash.integrations.kubernetes import (
        ShadowAiConfig,
        ShadowAiScanner,
        SHADOW_AI_MODEL_EXTENSIONS,
    )

    subcommand = getattr(args, "shadow_ai_cmd", None)
    if subcommand != "scan":
        print("error: unknown shadow-ai subcommand", file=sys.stderr)
        return 1

    # ─ Load pod list JSON ──────────────────────────────────────────────────────
    pod_list_path: str = args.pod_list
    try:
        if pod_list_path == "-":
            raw = sys.stdin.read()
        else:
            raw = Path(pod_list_path).read_text(encoding="utf-8")
        pod_list = _json.loads(raw)
    except (OSError, _json.JSONDecodeError) as exc:
        print(f"error: could not read pod list: {exc}", file=sys.stderr)
        return 1

    # ─ Build config ───────────────────────────────────────────────────────────
    extensions: frozenset[str] | None = None
    raw_exts: list[str] | None = getattr(args, "extensions", None)
    if raw_exts:
        extensions = frozenset(e if e.startswith(".") else f".{e}" for e in raw_exts)

    cfg = ShadowAiConfig(
        scan_extensions=extensions if extensions is not None else SHADOW_AI_MODEL_EXTENSIONS,
        namespaces_include=list(getattr(args, "namespaces", None) or []),
    )

    # ─ Scan ───────────────────────────────────────────────────────────────────
    scanner = ShadowAiScanner(cfg)
    result = scanner.scan_pod_list(pod_list)

    # ─ Output ─────────────────────────────────────────────────────────────────
    if not quiet:
        print(result.summary)
        for hit in result.hits:
            print(
                f"  [{hit.location_type}] {hit.namespace}/{hit.pod_name}"
                f" container={hit.container_name!r}"
                f" value={hit.matched_value!r} ({hit.extension})"
            )

    output_json_path: str | None = getattr(args, "output_json", None)
    if output_json_path:
        import dataclasses
        try:
            Path(output_json_path).write_text(
                _json.dumps(
                    {
                        "ok": result.ok,
                        "pods_scanned": result.pods_scanned,
                        "summary": result.summary,
                        "hits": [dataclasses.asdict(h) for h in result.hits],
                    },
                    indent=2,
                ),
                encoding="utf-8",
            )
        except OSError as exc:
            print(f"error: could not write output JSON: {exc}", file=sys.stderr)
            return 1

    fail_on_hits: bool = getattr(args, "fail_on_hits", False)
    if not result.ok and fail_on_hits:
        return 2
    return 0


# ── Wave 51 — SBOM drift detection handler ───────────────────────────────────

def _cmd_drift_check(args: argparse.Namespace, quiet: bool) -> int:
    """Run the drift-check subcommand (W51)."""
    import json as _json
    import dataclasses
    from squash.drift import DriftConfig, check_drift

    bom_path = Path(getattr(args, "bom", "") or "")
    model_dir = Path(args.model_dir)

    if not bom_path or not bom_path.exists():
        print(f"error: BOM file not found: {bom_path}", file=sys.stderr)
        return 1

    if not model_dir.exists():
        print(f"error: model directory not found: {model_dir}", file=sys.stderr)
        return 1

    try:
        config = DriftConfig(bom_path=bom_path, model_dir=model_dir)
        result = check_drift(config)
    except (OSError, _json.JSONDecodeError, ValueError) as exc:
        print(f"error: drift check failed: {exc}", file=sys.stderr)
        return 1

    if not quiet:
        print(result.summary)
        for hit in result.hits:
            if hit.missing:
                print(f"  [MISSING]  {hit.path}")
            else:
                print(f"  [TAMPERED] {hit.path}")
                print(f"             expected: {hit.expected_digest}")
                print(f"             actual:   {hit.actual_digest}")

    output_json_path: str | None = getattr(args, "output_json", None)
    if output_json_path:
        try:
            Path(output_json_path).write_text(
                _json.dumps(
                    {
                        "ok": result.ok,
                        "files_checked": result.files_checked,
                        "summary": result.summary,
                        "hits": [dataclasses.asdict(h) for h in result.hits],
                    },
                    indent=2,
                ),
                encoding="utf-8",
            )
        except OSError as exc:
            print(f"error: could not write output JSON: {exc}", file=sys.stderr)
            return 1

    fail_on_drift: bool = getattr(args, "fail_on_drift", False)
    if not result.ok and fail_on_drift:
        return 2
    return 0


# ── Wave 29 — VEX publish + integration CLI shims ─────────────────────────────

def _cmd_vex_publish(args: argparse.Namespace, quiet: bool) -> int:
    """Generate an OpenVEX 0.2.0 feed JSON file from statement entries."""
    import json as _json
    import sys as _sys

    from squash.vex import VexFeedManifest

    # Resolve entries: inline JSON string, '-' for stdin, or file path
    entries_raw: str = args.entries
    if entries_raw == "-":
        entries_raw = _sys.stdin.read()

    try:
        p = Path(entries_raw)
        if p.exists():
            entries_raw = p.read_text()
    except (OSError, ValueError):
        pass  # not a valid path — treat as inline JSON

    try:
        entries: list[dict] = _json.loads(entries_raw)
    except _json.JSONDecodeError as e:
        print(f"error: could not parse entries JSON: {e}", file=sys.stderr)
        return 1

    if not isinstance(entries, list):
        print("error: --entries must be a JSON array", file=sys.stderr)
        return 1

    doc = VexFeedManifest.generate(
        entries,
        author=args.author,
        doc_id=getattr(args, "doc_id", None),
    )

    errors = VexFeedManifest.validate(doc)
    if errors:
        for err in errors:
            print(f"validation error: {err}", file=sys.stderr)
        return 1

    output_path = Path(args.output)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(_json.dumps(doc, indent=2))

    if not quiet:
        print(
            f"✓ VEX feed written to {output_path} "
            f"({len(entries)} statement(s), spec {VexFeedManifest.SPEC_VERSION})"
        )
    return 0


def _cmd_attest_mlflow(args: argparse.Namespace, quiet: bool) -> int:
    """Run the attestation pipeline and emit result JSON (MLflow-compatible offline shim)."""
    import json as _json

    from squash.attest import AttestConfig, AttestPipeline

    model_path = Path(args.model_path)
    if not model_path.exists():
        print(f"error: model path not found: {model_path}", file=sys.stderr)
        return 1

    out_dir = Path(args.output_dir) if getattr(args, "output_dir", None) else None
    config = AttestConfig(
        model_path=model_path,
        output_dir=out_dir or (model_path.parent / "squash"),
        policies=args.policies or ["enterprise-strict"],
        sign=getattr(args, "sign", False),
        fail_on_violation=getattr(args, "fail_on_violation", False),
    )

    try:
        result = AttestPipeline.run(config)
    except Exception as e:
        print(f"error: attestation failed: {e}", file=sys.stderr)
        return 2

    if not quiet:
        icon = "✓" if result.passed else "✗"
        print(f"{icon} mlflow attestation {'passed' if result.passed else 'FAILED'}: {model_path}")
        print(f"  artifacts  : {result.output_dir}")
        print(f"  bom_path   : {result.bom_path}")

    # Emit JSON to stdout for pipe-friendly consumption
    print(_json.dumps(result.to_dict() if hasattr(result, "to_dict") else {
        "passed": result.passed,
        "bom_path": str(result.bom_path) if result.bom_path else None,
        "output_dir": str(result.output_dir) if result.output_dir else None,
    }))
    return 0 if result.passed else 1


def _cmd_attest_wandb(args: argparse.Namespace, quiet: bool) -> int:
    """Run the attestation pipeline and emit result JSON (W&B-compatible offline shim)."""
    import json as _json

    from squash.attest import AttestConfig, AttestPipeline

    model_path = Path(args.model_path)
    if not model_path.exists():
        print(f"error: model path not found: {model_path}", file=sys.stderr)
        return 1

    out_dir = Path(args.output_dir) if getattr(args, "output_dir", None) else None
    config = AttestConfig(
        model_path=model_path,
        output_dir=out_dir or (model_path.parent / "squash"),
        policies=args.policies or ["enterprise-strict"],
        sign=getattr(args, "sign", False),
        fail_on_violation=getattr(args, "fail_on_violation", False),
    )

    try:
        result = AttestPipeline.run(config)
    except Exception as e:
        print(f"error: attestation failed: {e}", file=sys.stderr)
        return 2

    if not quiet:
        icon = "✓" if result.passed else "✗"
        print(f"{icon} wandb attestation {'passed' if result.passed else 'FAILED'}: {model_path}")
        print(f"  artifacts  : {result.output_dir}")
        print(f"  bom_path   : {result.bom_path}")

    print(_json.dumps(result.to_dict() if hasattr(result, "to_dict") else {
        "passed": result.passed,
        "bom_path": str(result.bom_path) if result.bom_path else None,
        "output_dir": str(result.output_dir) if result.output_dir else None,
    }))
    return 0 if result.passed else 1


def _cmd_attest_huggingface(args: argparse.Namespace, quiet: bool) -> int:
    """Attest a model and optionally push artifacts to HuggingFace Hub."""
    import os

    model_path = Path(args.model_path)
    if not model_path.exists():
        print(f"error: model path not found: {model_path}", file=sys.stderr)
        return 1

    repo_id: str | None = getattr(args, "repo_id", None)
    hf_token: str | None = getattr(args, "hf_token", None) or os.environ.get("HF_TOKEN")
    policies = getattr(args, "policies", None) or ["enterprise-strict"]
    sign = getattr(args, "sign", False)
    fail_on_violation = getattr(args, "fail_on_violation", False)
    out_dir = Path(args.output_dir) if getattr(args, "output_dir", None) else None

    if repo_id:
        # Full push via HFSquash
        try:
            from squash.integrations.huggingface import HFSquash
        except ImportError as e:
            print(f"error: HFSquash not available: {e}", file=sys.stderr)
            return 2
        try:
            result = HFSquash.attest_and_push(
                repo_id,
                model_path,
                hf_token=hf_token or "",
                policies=policies,
                sign=sign,
                fail_on_violation=fail_on_violation,
            )
        except Exception as e:
            print(f"error: HuggingFace attestation failed: {e}", file=sys.stderr)
            return 2
    else:
        # Offline attestation only (no push)
        from squash.attest import AttestConfig, AttestPipeline

        config = AttestConfig(
            model_path=model_path,
            output_dir=out_dir or (model_path.parent / "squash"),
            policies=policies,
            sign=sign,
            fail_on_violation=fail_on_violation,
        )
        try:
            result = AttestPipeline.run(config)
        except Exception as e:
            print(f"error: attestation failed: {e}", file=sys.stderr)
            return 2

    if not quiet:
        icon = "✓" if result.passed else "✗"
        label = f"→ {repo_id}" if repo_id else "(local only)"
        print(f"{icon} huggingface attestation {'passed' if result.passed else 'FAILED'} {label}")
        print(f"  bom_path   : {result.bom_path}")

    return 0 if result.passed else 1


def _cmd_attest_langchain(args: argparse.Namespace, quiet: bool) -> int:
    """Run a one-shot attestation pass on a model (matches SquashCallback first-run behaviour)."""
    import json as _json

    from squash.attest import AttestConfig, AttestPipeline

    model_path = Path(args.model_path)
    if not model_path.exists():
        print(f"error: model path not found: {model_path}", file=sys.stderr)
        return 1

    out_dir = Path(args.output_dir) if getattr(args, "output_dir", None) else None
    config = AttestConfig(
        model_path=model_path,
        output_dir=out_dir or (model_path.parent / "squash"),
        policies=getattr(args, "policies", None) or ["enterprise-strict"],
        sign=getattr(args, "sign", False),
        fail_on_violation=getattr(args, "fail_on_violation", False),
    )

    try:
        result = AttestPipeline.run(config)
    except Exception as e:
        print(f"error: attestation failed: {e}", file=sys.stderr)
        return 2

    if not quiet:
        icon = "✓" if result.passed else "✗"
        print(f"{icon} langchain attestation {'passed' if result.passed else 'FAILED'}: {model_path}")
        print(f"  artifacts  : {result.output_dir}")
        print(f"  bom_path   : {result.bom_path}")

    print(_json.dumps(result.to_dict() if hasattr(result, "to_dict") else {
        "passed": result.passed,
        "bom_path": str(result.bom_path) if result.bom_path else None,
        "output_dir": str(result.output_dir) if result.output_dir else None,
    }))
    return 0 if result.passed else 1


def _cmd_attest_mcp(args: argparse.Namespace, quiet: bool) -> int:
    """Scan an MCP tool manifest catalog for supply-chain threats."""
    import json as _json

    from squash.mcp import McpScanner, McpSigner

    catalog_path = Path(args.catalog_path)
    if not catalog_path.exists():
        print(f"error: catalog not found: {catalog_path}", file=sys.stderr)
        return 1

    result = McpScanner.scan_file(catalog_path, getattr(args, "policy", "mcp-strict"))

    if not quiet:
        icon = "✓" if result.status == "safe" else ("⚠" if result.status == "warn" else "✗")
        label = {"safe": "SAFE", "warn": "WARNINGS", "unsafe": "UNSAFE"}.get(result.status, result.status.upper())
        print(f"{icon} MCP attestation {label}: {catalog_path}")
        print(f"  tools      : {result.tool_count}")
        print(f"  catalog_sha: {result.catalog_hash[:16]}…")
        errors = sum(1 for f in result.findings if f.severity == "error")
        warnings = sum(1 for f in result.findings if f.severity == "warning")
        if errors or warnings:
            print(f"  findings   : {errors} error(s), {warnings} warning(s)")
            for finding in result.findings:
                prefix = "  ✗" if finding.severity == "error" else "  ⚠"
                print(f"{prefix} [{finding.rule_id}] {finding.tool_name}: {finding.detail}")

    result_dict = result.to_dict()

    json_result_path = getattr(args, "json_result", None)
    if json_result_path:
        try:
            out_path = Path(json_result_path)
            out_path.parent.mkdir(parents=True, exist_ok=True)
            out_path.write_text(_json.dumps(result_dict, indent=2), encoding="utf-8")
            if not quiet:
                print(f"  result     : {out_path}")
        except Exception as exc:
            print(f"error: could not write result file: {exc}", file=sys.stderr)
            return 2

    sign = getattr(args, "sign", False)
    if sign:
        sig_path = McpSigner.sign(catalog_path)
        if sig_path and not quiet:
            print(f"  signed     : {sig_path}")
        elif not sig_path and not quiet:
            print("  signing    : unavailable (sigstore not installed)", file=sys.stderr)

    fail_on_violation = getattr(args, "fail_on_violation", False)
    if fail_on_violation and result.status == "unsafe":
        return 1
    return 0


def _cmd_audit(args: argparse.Namespace, quiet: bool) -> int:
    """Handler for ``squash audit show`` and ``squash audit verify``."""
    audit_command = getattr(args, "audit_command", None)
    if not audit_command:
        print("usage: squash audit <show|verify>", file=sys.stderr)
        return 1

    try:
        from squash.governor import AgentAuditLogger
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    log_path = getattr(args, "log", None)
    logger = AgentAuditLogger(log_path=log_path)

    if audit_command == "show":
        n = getattr(args, "n", 20)
        entries = logger.read_tail(n)
        if not entries:
            if not quiet:
                print("(audit log is empty or does not exist)")
            return 0
        json_output = getattr(args, "json_output", False)
        if json_output:
            print(json.dumps(entries, indent=2))
        else:
            for e in entries:
                ts = e.get("ts", "?")
                seq = e.get("seq", "?")
                etype = e.get("event_type", "?")
                model = e.get("model_id", "")
                session = e.get("session_id", "")
                latency = e.get("latency_ms", -1)
                lat_str = f" {latency:.1f}ms" if latency >= 0 else ""
                sid_str = f" [{session}]" if session else ""
                mod_str = f" model={model}" if model else ""
                print(f"#{seq} {ts} {etype}{sid_str}{mod_str}{lat_str}")
        return 0

    if audit_command == "verify":
        ok, msg = logger.verify_chain()
        if ok:
            if not quiet:
                path_str = str(logger.path)
                print(f"✓ audit chain intact: {path_str}")
            return 0
        print(f"✗ audit chain TAMPERED: {msg}", file=sys.stderr)
        return 2

    print(f"unknown audit subcommand: {audit_command}", file=sys.stderr)
    return 1


def _cmd_lineage(args: argparse.Namespace, quiet: bool) -> int:
    """Handler for ``squash lineage record``, ``show``, and ``verify``."""
    lineage_command = getattr(args, "lineage_command", None)
    if not lineage_command:
        print("usage: squash lineage {record,show,verify} -- use --help for details", file=sys.stderr)
        return 1

    try:
        from squash.lineage import LineageChain  # lazy — keeps cli.py import-fast
    except ImportError as e:
        print(f"squash is not installed: {e}", file=sys.stderr)
        return 2

    model_dir = Path(args.model_dir)

    if lineage_command == "record":
        operation = args.operation
        model_id = getattr(args, "model_id", "") or model_dir.name
        input_dir = getattr(args, "input_dir", "") or str(model_dir)
        raw_params = getattr(args, "params", []) or []
        params: dict = {}
        for kv in raw_params:
            if "=" in kv:
                k, _, v = kv.partition("=")
                params[k.strip()] = v.strip()
        try:
            model_dir.mkdir(parents=True, exist_ok=True)
            evt = LineageChain.create_event(
                operation=operation,
                model_id=model_id,
                input_dir=input_dir,
                output_dir=str(model_dir),
                params=params,
            )
            event_hash = LineageChain.record(model_dir, evt)
        except Exception as exc:
            print(f"error recording lineage event: {exc}", file=sys.stderr)
            return 2
        if not quiet:
            print(
                f"✓ lineage event recorded\n"
                f"  model_dir  : {model_dir}\n"
                f"  operation  : {operation}\n"
                f"  event_hash : {event_hash}"
            )
        return 0

    if lineage_command == "show":
        if not model_dir.exists():
            print(f"error: directory not found: {model_dir}", file=sys.stderr)
            return 1
        try:
            events = LineageChain.load(model_dir)
        except Exception as exc:
            print(f"error loading lineage: {exc}", file=sys.stderr)
            return 2
        json_output = getattr(args, "json_output", False)
        if json_output:
            print(json.dumps([e.to_dict() for e in events], indent=2))
        else:
            if not events:
                if not quiet:
                    print("(no lineage events recorded)")
                return 0
            for i, e in enumerate(events):
                prev = e.prev_hash[:32] + "\u2026" if e.prev_hash else "(genesis)"
                print(f"#{i + 1} {e.timestamp}  [{e.operation}]  {e.model_id}")
                print(f"     operator  : {e.operator}")
                print(f"     input_dir : {e.input_dir}")
                print(f"     output_dir: {e.output_dir}")
                if e.params:
                    pstr = "  ".join(f"{k}={v}" for k, v in e.params.items())
                    print(f"     params    : {pstr}")
                print(f"     event_hash: {e.event_hash[:32]}\u2026")
                print(f"     prev_hash : {prev}")
        return 0

    if lineage_command == "verify":
        if not model_dir.exists():
            print(f"error: directory not found: {model_dir}", file=sys.stderr)
            return 1
        try:
            result = LineageChain.verify(model_dir)
        except Exception as exc:
            print(f"error verifying chain: {exc}", file=sys.stderr)
            return 2
        if not quiet:
            icon = "\u2713" if result.ok else "\u2717"
            print(f"{icon} lineage chain: {result.message}")
            print(f"   model_dir  : {result.model_dir}")
            print(f"   event_count: {result.event_count}")
            if result.broken_at is not None:
                print(f"   broken_at  : event index {result.broken_at}", file=sys.stderr)
        return 0 if result.ok else 2

    print(f"unknown lineage subcommand: {lineage_command}", file=sys.stderr)
    return 1


def _cmd_scan_rag(args: argparse.Namespace, quiet: bool) -> int:
    """Handler for ``squash scan-rag index`` and ``squash scan-rag verify``."""
    from squash.rag import RagScanner  # lazy — keeps cli.py import-fast

    sub = getattr(args, "scan_rag_command", None)
    if sub is None:
        print("usage: squash scan-rag {index,verify} -- use --help for details", file=sys.stderr)
        return 1

    if sub == "index":
        corpus_dir = args.corpus_dir
        try:
            manifest = RagScanner.index(corpus_dir, glob=args.glob)
        except NotADirectoryError as exc:
            print(f"error: {exc}", file=sys.stderr)
            return 1
        except Exception as exc:  # noqa: BLE001
            print(f"error indexing corpus: {exc}", file=sys.stderr)
            return 2
        if not quiet:
            print(
                f"✓ indexed {manifest.file_count} files\n"
                f"  corpus:        {manifest.corpus_dir}\n"
                f"  manifest_hash: {manifest.manifest_hash}\n"
                f"  indexed_at:    {manifest.indexed_at}"
            )
        return 0

    if sub == "verify":
        corpus_dir = args.corpus_dir
        try:
            result = RagScanner.verify(corpus_dir)
        except Exception as exc:  # noqa: BLE001
            print(f"error verifying corpus: {exc}", file=sys.stderr)
            return 2
        if getattr(args, "json_output", False):
            import json as _json
            print(_json.dumps(result.to_dict(), indent=2))
        elif not quiet:
            if result.ok:
                print(f"✓ corpus intact — {result.total_files} files, no drift")
            else:
                print(
                    f"✗ drift detected — {result.drift_count} change(s) in {result.corpus_dir}",
                    file=sys.stderr,
                )
                for item in result.drift:
                    print(f"  [{item.status:8s}] {item.path}", file=sys.stderr)
        return 0 if result.ok else 2

    print(f"unknown scan-rag subcommand: {sub}", file=sys.stderr)
    return 1


# ── Wave 54–56: remediate / evaluate / edge-scan / chat ──────────────────────


def _cmd_remediate(args: argparse.Namespace, quiet: bool) -> int:
    try:
        from squash.remediate import Remediator
    except ImportError as exc:
        print(f"squash remediate requires torch and safetensors: {exc}", file=sys.stderr)
        return 2

    model_path = Path(args.model_path)
    output_dir = Path(args.output_dir) if args.output_dir else None

    result = Remediator.convert(
        model_path,
        target_format=args.target_format,
        output_dir=output_dir,
        dry_run=args.dry_run,
        overwrite=args.overwrite,
    )

    if not quiet:
        print(result.summary())

    if args.sbom and result.sbom_patch:
        sbom_path = Path(args.sbom)
        patched = Remediator.patch_sbom(sbom_path, result.sbom_patch)
        if not quiet:
            if patched:
                print(f"✓ Updated hashes in {sbom_path}")
            else:
                print(f"! Could not patch {sbom_path} (not found or invalid JSON)", file=sys.stderr)

    if result.failed and not args.dry_run:
        for f in result.failed:
            print(f"  ✗ {f.source.name}: {f.reason}", file=sys.stderr)
        return 1

    return 0


def _cmd_evaluate(args: argparse.Namespace, quiet: bool) -> int:
    try:
        from squash.evaluator import EvalEngine
    except ImportError as exc:
        print(f"squash evaluate unavailable: {exc}", file=sys.stderr)
        return 2

    engine = EvalEngine(
        endpoint=args.endpoint,
        model=args.model,
        api_key=args.api_key,
        timeout_s=args.timeout,
    )

    if not quiet:
        print(f"Running {len(engine._extra_probes or []) + 8} probes against {args.endpoint} …")

    report = engine.run()

    output_dir = Path(args.output_dir) if args.output_dir else Path.cwd()
    output_dir.mkdir(parents=True, exist_ok=True)
    report_path = output_dir / "squash-eval-report.json"
    report.save(report_path)

    if not quiet:
        print(report.summary_text())
        print(f"Report saved → {report_path}")

    if args.bom:
        patched = engine.patch_bom(Path(args.bom), report)
        if not quiet:
            if patched:
                print(f"✓ BOM annotated → {args.bom}")
            else:
                print(f"! Could not patch BOM {args.bom}", file=sys.stderr)

    if args.fail_on_critical and report.critical_failures:
        print(f"✗ {report.critical_failures} critical probe(s) failed", file=sys.stderr)
        return 2

    return 0


def _cmd_edge_scan(args: argparse.Namespace, quiet: bool) -> int:
    try:
        from squash.edge_formats import (
            TFLiteParser,
            CoreMLParser,
            EdgeSecurityScanner,
        )
    except ImportError as exc:
        print(f"squash edge-scan unavailable: {exc}", file=sys.stderr)
        return 2

    target = Path(args.model_path)
    if not target.exists():
        print(f"Path not found: {target}", file=sys.stderr)
        return 1

    suffix = target.suffix.lower()
    if suffix == ".tflite":
        meta = TFLiteParser.parse(target)
        findings = EdgeSecurityScanner.scan(target)
        result_dict: dict = {
            "format": "tflite",
            "file": str(target),
            "sha256": meta.sha256,
            "schema_version": meta.schema_version,
            "operator_count": meta.operator_count,
            "subgraph_count": meta.subgraph_count,
            "quant_level": meta.quant_level,
            "custom_ops": meta.custom_ops,
            "parse_error": meta.parse_error,
            "findings": [
                {
                    "severity": f.severity,
                    "id": f.finding_id,
                    "title": f.title,
                    "detail": f.detail,
                }
                for f in findings
            ],
        }
        if not quiet:
            print(f"TFLite model: {target.name}")
            print(f"  Schema version : {meta.schema_version}")
            print(f"  Operators      : {meta.operator_count}")
            print(f"  Quantisation   : {meta.quant_level}")
            if meta.custom_ops:
                print(f"  Custom ops     : {', '.join(meta.custom_ops)}")
            if meta.parse_error:
                print(f"  ⚠ parse error  : {meta.parse_error}", file=sys.stderr)
    elif target.is_dir() and target.name.endswith(".mlpackage"):
        meta = CoreMLParser.parse(target)
        findings = EdgeSecurityScanner.scan(target)
        result_dict = {
            "format": "coreml",
            "package": str(target),
            "sha256": meta.sha256,
            "model_version": meta.model_version,
            "spec_version": meta.spec_version,
            "short_description": meta.short_description,
            "quant_level": meta.quant_level,
            "pipeline_stages": meta.pipeline_stages,
            "findings": [
                {
                    "severity": f.severity,
                    "id": f.finding_id,
                    "title": f.title,
                    "detail": f.detail,
                }
                for f in findings
            ],
        }
        if not quiet:
            print(f"CoreML package: {target.name}")
            print(f"  Spec version   : {meta.spec_version}")
            print(f"  Quantisation   : {meta.quant_level}")
    else:
        print(
            "Unsupported format. Provide a .tflite file or a .mlpackage directory.",
            file=sys.stderr,
        )
        return 1

    critical = [f for f in findings if f.severity == "critical"]
    high = [f for f in findings if f.severity == "high"]
    if not quiet:
        if findings:
            print(f"\n{len(findings)} finding(s): {len(critical)} critical, {len(high)} high")
            for f in findings:
                print(f"  [{f.severity.upper():8s}] {f.finding_id} — {f.title}")
        else:
            print("✓ No security findings")

    if args.json_result:
        import json as _json
        json_path = Path(args.json_result)
        json_path.parent.mkdir(parents=True, exist_ok=True)
        json_path.write_text(_json.dumps(result_dict, indent=2))
        if not quiet:
            print(f"Result saved → {json_path}")

    return 2 if critical else (1 if high else 0)


def _cmd_chat(args: argparse.Namespace, quiet: bool) -> int:
    try:
        from squash.chat import ChatSession
    except ImportError as exc:
        print(f"squash chat unavailable: {exc}", file=sys.stderr)
        return 2

    model_dir = Path(args.model_dir)
    if not model_dir.exists():
        print(f"Model directory not found: {model_dir}", file=sys.stderr)
        return 1

    backend_defaults = {
        "ollama": ("http://localhost:11434/v1", "llama3"),
        "openai": ("https://api.openai.com/v1", "gpt-4o-mini"),
    }
    base_url, default_model = backend_defaults[args.backend]
    model_name = args.model or default_model

    session = ChatSession.from_model_dir(
        model_dir,
        endpoint=base_url,
        model=model_name,
        api_key=args.api_key,
        top_k=args.top_k,
    )

    if not quiet:
        print(f"squash chat — {model_name} via {args.backend} ({base_url})")
        print(f"Loaded {len(session._retriever._chunks)} document chunks from {model_dir.name}")

    session.repl()
    return 0


def _cmd_model_card(args: argparse.Namespace, quiet: bool) -> int:
    """Generate regulation-compliant model cards from squash attestation artifacts.

    W194 (Sprint 10): added --validate, --validate-only, and --push-to-hub flows.
    """
    try:
        from squash.model_card import ModelCardConfig, ModelCardGenerator
    except ImportError as exc:
        print(f"squash model-card unavailable: {exc}", file=sys.stderr)
        return 2

    model_dir = Path(args.model_dir)
    if not model_dir.exists():
        print(f"Model directory not found: {model_dir}", file=sys.stderr)
        return 1

    output_dir = Path(args.mc_output_dir) if args.mc_output_dir else None

    # ── Validate-only short-circuit ──────────────────────────────────────────
    if getattr(args, "mc_validate_only", False):
        return _model_card_validate(
            card_path=(output_dir or model_dir) / "squash-model-card-hf.md",
            json_out=getattr(args, "mc_json", False),
            quiet=quiet,
        )

    config = ModelCardConfig(
        model_dir=model_dir,
        model_id=args.mc_model_id or "",
        license=args.mc_license or "apache-2.0",
        output_dir=output_dir,
    )
    gen = ModelCardGenerator(model_dir=model_dir, config=config)

    try:
        paths = gen.generate(fmt=args.mc_format, output_dir=output_dir)
    except ValueError as exc:
        print(str(exc), file=sys.stderr)
        return 1

    if not quiet:
        for p in paths:
            print(f"✓ {p}")

    # ── Optional validate after generation ───────────────────────────────────
    if getattr(args, "mc_validate", False):
        hf_card = next(
            (p for p in paths if p.name == "squash-model-card-hf.md"),
            (output_dir or model_dir) / "squash-model-card-hf.md",
        )
        rc = _model_card_validate(
            card_path=hf_card,
            json_out=getattr(args, "mc_json", False),
            quiet=quiet,
        )
        if rc != 0:
            return rc

    # ── Optional push to HuggingFace Hub ─────────────────────────────────────
    push_repo = getattr(args, "mc_push_repo", None)
    if push_repo:
        hf_card = next(
            (p for p in paths if p.name == "squash-model-card-hf.md"), None
        )
        if hf_card is None:
            print(
                "--push-to-hub requires --format hf or --format all to produce "
                "squash-model-card-hf.md",
                file=sys.stderr,
            )
            return 1
        return _model_card_push(
            card_path=hf_card,
            repo_id=push_repo,
            token=getattr(args, "mc_hub_token", None),
            quiet=quiet,
        )

    return 0


def _model_card_validate(card_path: Path, json_out: bool, quiet: bool) -> int:
    """Validate an HF model card; print report. Exit non-zero on errors."""
    try:
        from squash.model_card_validator import ModelCardValidator
    except ImportError as exc:
        print(f"squash model-card validator unavailable: {exc}", file=sys.stderr)
        return 2

    report = ModelCardValidator().validate(card_path)

    if json_out:
        print(json.dumps(report.to_dict(), indent=2))
    elif not quiet:
        print(report.summary())
        for f in report.errors + report.warnings + report.infos:
            print(f"  {f.render()}")

    return 0 if report.is_valid else 1


def _cmd_soc2(args: argparse.Namespace, quiet: bool) -> int:
    """D6 — SOC 2 Type II readiness / evidence."""
    from squash.soc2 import (
        Soc2CoverageReport, Soc2EvidenceBundle,
        TscCategory, ControlStatus,
    )

    cmd = getattr(args, "soc2_command", None)
    window = getattr(args, "soc2_window", 365)
    as_json = getattr(args, "soc2_json", False)

    if cmd == "readiness":
        report = Soc2CoverageReport.build(window_days=window)

        # Category / status filter
        cat_filter = getattr(args, "soc2_category", None)
        stat_filter = getattr(args, "soc2_status", None)
        controls = report.controls
        if cat_filter:
            controls = [c for c in controls if c.category.value == cat_filter]
        if stat_filter:
            controls = [c for c in controls if c.status.value == stat_filter]

        if as_json:
            out_report = report.to_dict()
            if cat_filter or stat_filter:
                out_report["controls"] = [c.to_dict() for c in controls]
            print(json.dumps(out_report, indent=2))
        elif not quiet:
            print(report.summary_text())
            if cat_filter or stat_filter:
                print(f"\n  Filtered: {len(controls)} control(s)")
                for c in controls:
                    icon = {"COVERED": "✅", "PARTIAL": "⚠️", "GAP": "❌",
                            "NOT_APPLICABLE": "➖"}.get(c.status.value, "•")
                    print(f"    {icon} {c.id}  {c.title}")
        return 0

    if cmd == "evidence":
        out_dir = Path(args.soc2_output) if args.soc2_output else Path.cwd()
        include_att = not getattr(args, "soc2_no_attest", False)
        if not quiet:
            print(f"[squash soc2 evidence] Building evidence bundle "
                  f"({window}-day window)…")
        bundle = Soc2EvidenceBundle.build(
            output_dir=out_dir,
            window_days=window,
            include_attestations=include_att,
        )
        if not quiet:
            import zipfile
            with zipfile.ZipFile(bundle) as zf:
                file_count = len(zf.namelist())
            size_kb = bundle.stat().st_size // 1024
            print(f"[squash soc2 evidence] ✅ Bundle written: {bundle}")
            print(f"  Files: {file_count}  Size: {size_kb} KB")
        return 0

    print("squash soc2: specify a subcommand — readiness | evidence", file=sys.stderr)
    return 1



def _cmd_score(args: argparse.Namespace, quiet: bool) -> int:
    """D3 — Procurement Scoring API CLI."""
    vendor = args.vendor
    use_local = getattr(args, "score_local", False)
    api_url   = getattr(args, "score_api_url", None) or os.environ.get(
        "SQUASH_API_URL", "https://squash.works")
    as_json   = getattr(args, "score_json", False)
    breakdown = getattr(args, "score_breakdown", False)
    show_hist = getattr(args, "score_history", False)
    months    = getattr(args, "score_months", 12)

    # ── Local mode: query registry directly ──────────────────────────────────
    if use_local:
        from squash.procurement_scoring import ProcurementScorer
        scorer = ProcurementScorer(base_url=api_url)
        vs = scorer.score_vendor(vendor)
        if show_hist:
            vs.history = scorer.score_history(vendor, months=months)

        if as_json:
            print(json.dumps(vs.to_dict(
                include_breakdown=breakdown,
                include_history=show_hist,
            ), indent=2))
        elif not quiet:
            tier_icon = {
                "CERTIFIED": "🏆", "VERIFIED": "✅",
                "BASIC": "🔵", "UNVERIFIED": "⚪",
            }.get(vs.tier, "•")
            print(f"[squash score] {tier_icon} {vendor}")
            print(f"  Score:       {vs.score:.1f} / 100  ({vs.tier})")
            print(f"  Attestations: {vs.attestation_count}")
            if vs.last_attested:
                print(f"  Last attested: {vs.last_attested[:10]}")
            if vs.frameworks:
                print(f"  Frameworks:  {', '.join(vs.frameworks)}")
            print(f"  Trust pkg:   {'yes' if vs.has_trust_package else 'no'}")
            if breakdown and vs.breakdown:
                print("  Breakdown:")
                for k, v in vs.breakdown.to_dict().items():
                    print(f"    {k:27s} {v:5.1f}")
            if show_hist and vs.history:
                print("  History (monthly):")
                for m in vs.history[-6:]:
                    bar = "█" * int(m["score"] / 10)
                    print(f"    {m['month']}  {m['score']:5.1f}  {bar}")
        return 0

    # ── Live API mode ─────────────────────────────────────────────────────────
    import urllib.request
    import urllib.error

    url = f"{api_url.rstrip('/')}/v1/score/{vendor}"
    if show_hist:
        url += "/history"

    headers = {}
    api_key = os.environ.get("SQUASH_API_KEY", "")
    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"

    try:
        req = urllib.request.Request(url, headers=headers)
        with urllib.request.urlopen(req, timeout=5) as resp:
            data = json.loads(resp.read())
    except urllib.error.HTTPError as exc:
        if exc.code == 402:
            print(f"score: plan upgrade required — {exc.read().decode()[:120]}",
                  file=sys.stderr)
            return 2
        print(f"score: HTTP {exc.code} from {url}", file=sys.stderr)
        return 1
    except (urllib.error.URLError, OSError) as exc:
        if not quiet:
            print(f"score: API unreachable ({exc}). Use --local for offline scoring.",
                  file=sys.stderr)
        return 1

    if as_json:
        print(json.dumps(data, indent=2))
        return 0

    if show_hist:
        if not quiet:
            print(f"[squash score] {vendor} — {months}-month history")
            for m in data.get("history", [])[-6:]:
                bar = "█" * int(m.get("score", 0) / 10)
                print(f"  {m['month']}  {m.get('score', 0):5.1f}  {bar}")
        return 0

    tier_icon = {
        "CERTIFIED": "🏆", "VERIFIED": "✅",
        "BASIC": "🔵", "UNVERIFIED": "⚪",
    }.get(data.get("tier", ""), "•")
    if not quiet:
        print(f"[squash score] {tier_icon} {vendor}")
        print(f"  Score:       {data.get('score', 0):.1f} / 100  ({data.get('tier', '?')})")
        if data.get("last_attested"):
            print(f"  Last attested: {data['last_attested'][:10]}")
        if data.get("frameworks"):
            print(f"  Frameworks:  {', '.join(data['frameworks'])}")
        if data.get("breakdown"):
            print("  Breakdown:")
            for k, v in data["breakdown"].items():
                print(f"    {k:27s} {v:5.1f}")
    return 0



def _cmd_attest_carbon(args: argparse.Namespace, quiet: bool) -> int:
    """C9 — Carbon / Energy Attestation."""
    from squash.carbon_attest import (
        CarbonAttestation, CarbonIntensityCache, ModelArchitecture,
        HardwareType, enrich_mlbom, format_summary,
    )

    # Parse parameter count shorthand (110M, 7B, 1.5T)
    params_str = str(args.ac_params).strip().upper()
    try:
        multipliers = {"K": 1_000, "M": 1_000_000, "B": 1_000_000_000,
                       "T": 1_000_000_000_000}
        if params_str[-1] in multipliers:
            param_count = int(float(params_str[:-1]) * multipliers[params_str[-1]])
        else:
            param_count = int(params_str)
    except (ValueError, IndexError):
        print(f"attest-carbon: cannot parse --params {args.ac_params!r}. "
              "Use an integer or shorthand (110M, 7B, 1.5T).", file=sys.stderr)
        return 2

    arch = ModelArchitecture(args.ac_arch)
    hw   = HardwareType(args.ac_hw)

    cache = CarbonIntensityCache() if getattr(args, "ac_live", False) else None
    try:
        cert = CarbonAttestation.compute(
            model_id=args.ac_model_id,
            param_count=param_count,
            deployment_region=args.ac_region,
            architecture=arch,
            hardware=hw,
            inferences_per_day=args.ac_inf_per_day,
            tokens_per_inference=args.ac_tokens,
            seq_len=args.ac_seq_len,
            utilization=args.ac_util,
            pue_override=args.ac_pue,
            renewable_fraction=args.ac_renewable,
            live_intensity=getattr(args, "ac_live", False),
            cache=cache,
            sign=getattr(args, "ac_sign", False),
        )
    finally:
        if cache:
            cache.close()

    # Write certificate
    out_path = Path(args.ac_output) if args.ac_output else (
        Path(f"{args.ac_model_id.replace('/', '_')}-carbon-attest.json")
    )
    out_path.write_text(json.dumps(cert.to_dict(), indent=2), encoding="utf-8")

    # CSRD report
    if getattr(args, "ac_csrd", False):
        csrd_path = out_path.with_suffix("").with_name(
            out_path.stem.replace("-carbon-attest", "") + "-csrd.json"
        )
        fw = getattr(args, "ac_framework", "csrd")
        csrd_report = cert.to_regulatory(fw)
        csrd_path.write_text(json.dumps(csrd_report, indent=2), encoding="utf-8")
        if not quiet:
            print(f"[squash attest-carbon] CSRD report: {csrd_path}")

    # ML-BOM enrichment
    if getattr(args, "ac_bom", None):
        bom_path = Path(args.ac_bom)
        if bom_path.exists():
            enrich_mlbom(bom_path, cert)
            if not quiet:
                print(f"[squash attest-carbon] ML-BOM enriched: {bom_path}")
        else:
            print(f"attest-carbon: --bom path not found: {bom_path}", file=sys.stderr)

    if getattr(args, "ac_json", False):
        print(json.dumps(cert.to_dict(), indent=2))
    elif not quiet:
        print(format_summary(cert))
        print(f"  Certificate: {out_path}")
    return 0



def _cmd_deprecation_watch(args: argparse.Namespace, quiet: bool) -> int:
    """C8 — model deprecation cross-reference engine."""
    from squash.deprecation_watch import (
        DeprecationWatcher, route_alerts,
    )

    providers = [p.strip() for p in args.dw_providers.split(",") if p.strip()] or None
    registry_db = Path(args.dw_registry_db) if args.dw_registry_db else None
    model_ids = [m.strip() for m in args.dw_model_ids.split(",") if m.strip()] or None
    lead_time = args.dw_lead_time

    with DeprecationWatcher() as watcher:
        watcher.load_feeds(
            providers=providers,
            include_informational=getattr(args, "dw_informational", False),
        )

        # ── --list: show all known entries (no registry scan) ────────────────
        if getattr(args, "dw_list_all", False):
            entries = watcher.list_entries(
                providers=providers,
                include_informational=True,   # --list shows everything
                include_sunsetted=True,       # --list shows everything
            )
            if args.dw_json:
                print(json.dumps([e.to_dict() for e in entries], indent=2))
            elif not quiet:
                print(f"[squash deprecation-watch] {len(entries)} known deprecations")
                for e in entries:
                    days = e.days_until_sunset
                    days_str = f"{days}d" if days is not None else "unknown/already sunsetted"
                    status = "⛔ SUNSETTED" if e.is_sunsetted else f"⚠ sunset in {days_str}"
                    print(f"  [{e.provider}] {e.model_id} → {e.successor_model or '(no successor)'} — {status}")
            return 0

        # ── --check: single model lookup ──────────────────────────────────────
        if args.dw_check_model:
            alert = watcher.check_model(args.dw_check_model, providers=providers)
            if alert is None:
                if not quiet:
                    print(f"[squash deprecation-watch] ✓ {args.dw_check_model} — not in deprecation feed")
                return 0
            if args.dw_json:
                print(json.dumps(alert.to_dict(), indent=2))
            else:
                print(alert.summary(lead_time))
                if getattr(args, "dw_checklist", False):
                    print("\n  Re-attestation checklist:")
                    for item in alert.re_attestation_checklist:
                        print(f"  {item}")
            return 1 if getattr(args, "dw_fail", False) else 0

        # ── Scan registry / explicit model list ───────────────────────────────
        effective_lead = 36500 if getattr(args, "dw_include_all", False) else lead_time
        alerts = watcher.scan(
            lead_time_days=effective_lead,
            providers=providers,
            registry_db=registry_db,
            model_ids=model_ids,
        )

        if args.dw_json or args.dw_channel == "json":
            print(json.dumps([a.to_dict() for a in alerts], indent=2))
        elif not quiet:
            print(f"[squash deprecation-watch] {len(alerts)} alert(s) "
                  f"(lead-time: {lead_time}d, provider: {args.dw_providers or 'all'})")
            for alert in alerts:
                print(f"  {alert.summary(lead_time)}")
                if getattr(args, "dw_checklist", False):
                    print("  Re-attestation checklist:")
                    for item in alert.re_attestation_checklist[:5]:
                        print(f"    {item}")

        if args.dw_channel == "slack":
            route_alerts(alerts, channel="slack", lead_time_days=lead_time)

        if getattr(args, "dw_fail", False) and alerts:
            return 1
        return 0



def _cmd_request_approval(args: argparse.Namespace, quiet: bool) -> int:
    """C3 — create a multi-reviewer approval request."""
    from squash.approval_workflow import ApprovalWorkflow, ReviewerRole

    reviewers = [e.strip() for e in args.appr_reviewers.split(",") if e.strip()]
    required_roles_raw = [r.strip() for r in args.appr_required_roles.split(",") if r.strip()]
    try:
        required_roles = [ReviewerRole(r) for r in required_roles_raw]
    except ValueError as exc:
        print(f"request-approval: invalid role — {exc}", file=sys.stderr)
        return 2

    with ApprovalWorkflow() as wf:
        req = wf.request(
            attestation_id=args.appr_attestation_id,
            model_id=args.appr_model_id or args.appr_attestation_id,
            reviewers=reviewers,
            threshold=args.appr_threshold,
            required_roles=required_roles,
            requestor_email=args.appr_requestor or os.environ.get("SQUASH_REQUESTOR_EMAIL", ""),
            notes=args.appr_notes,
            attestation_hash=args.appr_hash,
            ttl_days=args.appr_ttl,
        )

    if getattr(args, "appr_json", False):
        print(json.dumps(req.to_dict(), indent=2))
    elif not quiet:
        print(f"[squash request-approval] Created {req.request_id}")
        print(f"  Model:      {req.model_id}")
        print(f"  Attestation: {req.attestation_id}")
        print(f"  Reviewers:  {', '.join(req.reviewers) or '(open)'}")
        print(f"  Threshold:  {req.threshold}")
        if req.required_roles:
            print(f"  Roles:      {', '.join(r.value for r in req.required_roles)}")
        print(f"  Expires:    {req.expires_at}")
    return 0


def _cmd_approve(args: argparse.Namespace, quiet: bool) -> int:
    """C3 — record a reviewer's decision on an approval request."""
    from squash.approval_workflow import (
        ApprovalDecision, ApprovalWorkflow, ApproverIdentity, ReviewerRole,
    )

    email = (args.appr_reviewer_email
             or os.environ.get("SQUASH_REVIEWER_EMAIL", "")
             or os.environ.get("GIT_AUTHOR_EMAIL", ""))
    if not email:
        print("approve: --reviewer-email is required (or set SQUASH_REVIEWER_EMAIL)", file=sys.stderr)
        return 2

    try:
        decision = ApprovalDecision(args.appr_decision)
        role = ReviewerRole(args.appr_reviewer_role)
    except ValueError as exc:
        print(f"approve: invalid value — {exc}", file=sys.stderr)
        return 2

    reviewer = ApproverIdentity(
        email=email,
        name=args.appr_reviewer_name or os.environ.get("SQUASH_REVIEWER_NAME", email),
        role=role,
    )

    try:
        with ApprovalWorkflow() as wf:
            record = wf.approve(
                request_id=args.request_id,
                reviewer=reviewer,
                decision=decision,
                rationale=args.appr_rationale,
                conditions=getattr(args, "appr_conditions", None) or [],
            )
            req = wf.status(args.request_id)
    except ValueError as exc:
        print(f"approve: {exc}", file=sys.stderr)
        return 1

    if getattr(args, "appr_json", False):
        print(json.dumps(record.to_dict(), indent=2))
    elif not quiet:
        icons = {
            "APPROVED": "✅", "REJECTED": "❌",
            "APPROVED_WITH_CONDITIONS": "✅⚠",
        }
        icon = icons.get(record.decision.value, "•")
        print(f"[squash approve] {icon} {record.decision.value} — {args.request_id}")
        print(f"  Reviewer:  {email} ({role.value})")
        print(f"  Rationale: {record.rationale[:80]}")
        if record.conditions:
            for c in record.conditions:
                print(f"  Condition: {c}")
        print(f"  Sig:       {record.signature[:24]}...")
        print(f"  Overall:   {req.overall_status.value}  "
              f"({req.approved_count}/{req.threshold} approved)")
    return 0


def _cmd_approval_status(args: argparse.Namespace, quiet: bool) -> int:
    """C3 — show status of an approval request."""
    from squash.approval_workflow import ApprovalWorkflow, RequestStatus

    try:
        with ApprovalWorkflow() as wf:
            req = wf.status(args.request_id)
    except ValueError as exc:
        print(f"approval-status: {exc}", file=sys.stderr)
        return 1

    if getattr(args, "appr_json", False):
        print(json.dumps(req.to_dict(), indent=2))
    elif not quiet:
        status_icon = {
            RequestStatus.APPROVED: "✅",
            RequestStatus.REJECTED: "❌",
            RequestStatus.PENDING:  "⏳",
            RequestStatus.EXPIRED:  "⌛",
        }.get(req.overall_status, "•")
        print(f"[squash approval-status] {status_icon} {req.request_id}")
        print(f"  Model:        {req.model_id}")
        print(f"  Attestation:  {req.attestation_id}")
        print(f"  Status:       {req.overall_status.value}")
        print(f"  Threshold:    {req.approved_count}/{req.threshold} approved")
        print(f"  Requested:    {req.requested_at}")
        print(f"  Expires:      {req.expires_at}")
        if req.pending_reviewers:
            print(f"  Awaiting:     {', '.join(req.pending_reviewers)}")
        if req.all_conditions:
            print("  Conditions:")
            for c in req.all_conditions:
                print(f"    · {c}")
        if req.records:
            print("  Records:")
            for r in req.records:
                valid = "✓sig" if r.verify() else "✗sig"
                print(f"    {r.reviewer.email} [{r.reviewer.role.value}] → "
                      f"{r.decision.value} {valid}")
    return 0 if req.overall_status.value != "REJECTED" else 1


def _cmd_approval_list(args: argparse.Namespace, quiet: bool) -> int:
    """C3 — list approval requests."""
    from squash.approval_workflow import ApprovalWorkflow

    reviewer_filter = getattr(args, "appr_reviewer_filter", "") or ""
    pending_only = getattr(args, "appr_pending_only", False)
    limit = getattr(args, "appr_limit", 20)

    with ApprovalWorkflow() as wf:
        if pending_only:
            requests = wf.list_pending(reviewer_email=reviewer_filter or None)
        else:
            requests = wf.list_all(limit=limit)
            if reviewer_filter:
                requests = [r for r in requests if reviewer_filter in r.reviewers]

    if getattr(args, "appr_json", False):
        print(json.dumps([r.to_dict() for r in requests], indent=2))
    elif not quiet:
        if not requests:
            print("[squash approval-list] No requests found")
        else:
            for req in requests:
                status_icon = {"APPROVED": "✅", "REJECTED": "❌", "PENDING": "⏳"}.get(
                    req.overall_status.value, "•")
                print(f"  {status_icon} {req.request_id}  {req.model_id}  "
                      f"{req.overall_status.value}  {req.requested_at[:10]}  "
                      f"({req.approved_count}/{req.threshold})")
    return 0


def _cmd_approval_export(args: argparse.Namespace, quiet: bool) -> int:
    """C3 — export Article 9 evidence bundle."""
    from squash.approval_workflow import ApprovalWorkflow

    try:
        with ApprovalWorkflow() as wf:
            evidence = wf.export_evidence(args.request_id)
    except ValueError as exc:
        print(f"approval-export: {exc}", file=sys.stderr)
        return 1

    blob = json.dumps(evidence, indent=2)
    output = getattr(args, "appr_output", None)
    if output:
        Path(output).write_text(blob)
        if not quiet:
            print(f"[squash approval-export] Written to {output}")
            print(f"  Signatures valid: {evidence['all_signatures_valid']}")
    else:
        print(blob)
    return 0



def _cmd_chain_attest(args: argparse.Namespace, quiet: bool) -> int:
    """W197 — `squash chain-attest`. Composite chain / pipeline attestation."""
    try:
        from squash.chain_attest import (
            ChainAttestConfig, ChainAttestPipeline,
            attestation_from_dict, load_chain_spec, verify_signature,
        )
    except ImportError as exc:
        print(f"squash chain-attest unavailable: {exc}", file=sys.stderr)
        return 2

    spec_arg: str = args.spec
    output_dir = Path(args.chain_output_dir) if args.chain_output_dir else None
    policies = args.chain_policies or ["enterprise-strict"]

    # ── Verify-only short-circuit ────────────────────────────────────────────
    if getattr(args, "chain_verify_only", False):
        target = Path(spec_arg)
        if not target.exists():
            print(f"chain-attest: file not found: {target}", file=sys.stderr)
            return 1
        try:
            raw = json.loads(target.read_text(encoding="utf-8"))
        except json.JSONDecodeError as exc:
            print(f"chain-attest: not a valid JSON file: {exc}", file=sys.stderr)
            return 1
        try:
            attestation = attestation_from_dict(raw)
        except KeyError as exc:
            print(f"chain-attest: file missing required field: {exc}", file=sys.stderr)
            return 1
        ok = verify_signature(attestation)
        if not quiet:
            status = "✅ VALID" if ok else "❌ TAMPERED"
            print(f"{status} {target} — chain_id={attestation.chain_id}")
        return 0 if ok else 1

    # ── Resolve spec: file path or 'module.path:variable' ───────────────────
    if ":" in spec_arg and not Path(spec_arg).exists():
        # Python import form: chain attestation via attest_chain()
        try:
            chain_obj = _resolve_python_chain(spec_arg)
        except Exception as exc:  # noqa: BLE001 — surface import errors verbatim
            print(f"chain-attest: could not resolve {spec_arg!r}: {exc}",
                  file=sys.stderr)
            return 1
        try:
            from squash.integrations.langchain import attest_chain
        except ImportError as exc:
            print(f"chain-attest: langchain integration unavailable: {exc}",
                  file=sys.stderr)
            return 2
        attestation = attest_chain(
            chain_obj,
            chain_id=args.chain_id_override or "",
            policies=policies,
            output_dir=output_dir,
            fail_on_component_violation=args.chain_fail_on_violation,
            sign_components=args.chain_sign_components,
        )
    else:
        # File path: JSON or YAML chain spec
        spec_path = Path(spec_arg)
        if not spec_path.exists():
            print(f"chain-attest: spec not found: {spec_path}", file=sys.stderr)
            return 1
        try:
            spec = load_chain_spec(spec_path)
        except (ValueError, ImportError, json.JSONDecodeError) as exc:
            print(f"chain-attest: could not load spec {spec_path}: {exc}",
                  file=sys.stderr)
            return 1
        if args.chain_id_override:
            spec.chain_id = args.chain_id_override
        cfg = ChainAttestConfig(
            spec=spec,
            policies=policies,
            output_dir=output_dir,
            fail_on_component_violation=args.chain_fail_on_violation,
            sign_components=args.chain_sign_components,
        )
        attestation = ChainAttestPipeline.run(cfg)

    # ── Output ───────────────────────────────────────────────────────────────
    if getattr(args, "chain_json", False):
        print(attestation.to_json())
    elif not quiet:
        print(attestation.to_markdown())
        if output_dir:
            print(f"✓ chain-attest.json + chain-attest.md → {output_dir}")

    if args.chain_fail_on_violation and not attestation.composite_passed:
        return 1
    return 0


def _cmd_digest(args: argparse.Namespace, quiet: bool) -> int:
    """B3 (Sprint 15 W209/W210) — `squash digest preview|send`."""
    try:
        from squash.notifications import (
            ComplianceDigestBuilder, SmtpConfig, send_email_digest,
        )
    except ImportError as exc:
        print(f"squash digest unavailable: {exc}", file=sys.stderr)
        return 2

    score_history: dict[str, float] = {}
    sh_path = getattr(args, "digest_score_history", None)
    if sh_path:
        try:
            raw = json.loads(Path(sh_path).read_text(encoding="utf-8"))
            if isinstance(raw, dict):
                score_history = {str(k): float(v) for k, v in raw.items()}
            else:
                print(f"squash digest: --score-history must be a JSON object "
                      f"(model_id → score), got {type(raw).__name__}",
                      file=sys.stderr)
                return 2
        except (OSError, ValueError, TypeError) as exc:
            print(f"squash digest: could not load --score-history: {exc}",
                  file=sys.stderr)
            return 2

    models_dir_arg = getattr(args, "digest_models_dir", None)
    models_dir = Path(models_dir_arg) if models_dir_arg else None
    quiet = quiet or getattr(args, "quiet", False)

    try:
        digest = ComplianceDigestBuilder().build(
            period=args.period, models_dir=models_dir,
            org_name=args.digest_org,
            dashboard_url=args.digest_dashboard_url,
            score_history=score_history,
        )
    except ValueError as exc:
        print(f"squash digest: {exc}", file=sys.stderr)
        return 2
    except Exception as exc:  # noqa: BLE001
        print(f"squash digest: build failed: {exc}", file=sys.stderr)
        return 1

    sub_cmd = getattr(args, "digest_command", "")

    if sub_cmd == "preview":
        fmt = getattr(args, "digest_preview_format", "text")
        if fmt == "text":
            payload = digest.text_body
        elif fmt == "html":
            payload = digest.html_body
        else:
            payload = json.dumps(digest.to_dict(), indent=2, sort_keys=True)
        out_path = getattr(args, "digest_preview_output", None)
        if out_path:
            Path(out_path).write_text(payload, encoding="utf-8")
            if not quiet:
                print(f"✓ digest written to {out_path}")
        else:
            print(payload)
        return 0

    if sub_cmd == "send":
        recipients = list(getattr(args, "digest_recipients", None) or [])
        dry_run = bool(getattr(args, "digest_dry_run", False))
        if not recipients and not dry_run:
            print("squash digest send: --recipients required unless --dry-run",
                  file=sys.stderr)
            return 2
        if dry_run:
            if not quiet:
                print(f"Subject: {digest.subject}")
                print()
                print(digest.text_body)
                print(f"[dry-run] would email {len(recipients)} recipient(s): "
                      f"{', '.join(recipients) or '(none — preview only)'}")
            return 0
        smtp = SmtpConfig(
            host=getattr(args, "digest_smtp_host", "") or "",
            port=int(getattr(args, "digest_smtp_port", 0) or 0) or 587,
            from_addr=getattr(args, "digest_smtp_from", "") or "",
            use_tls=not bool(getattr(args, "digest_no_tls", False)),
        )
        result = send_email_digest(digest, recipients, smtp=smtp, dry_run=False)
        if not result.success:
            print(f"squash digest send: {result.error}", file=sys.stderr)
            return 1
        if not quiet:
            print(f"✓ digest emailed to {result.delivered} recipient(s) "
                  f"({', '.join(recipients)})")
        return 0

    print(f"squash digest: unknown subcommand {sub_cmd!r}", file=sys.stderr)
    return 2


def _cmd_genealogy(args: argparse.Namespace, quiet: bool) -> int:
    """Sprint 39 W274 — `squash genealogy`. Model derivation chain + copyright cert."""
    try:
        from squash.genealogy import GenealogyBuilder
    except ImportError as exc:
        print(f"squash genealogy unavailable: {exc}", file=sys.stderr)
        return 2

    model_path = Path(getattr(args, "geo_model_path", ".") or ".")
    if not model_path.exists():
        print(f"genealogy: path not found: {model_path}", file=sys.stderr)
        return 2

    quiet_mode = quiet or getattr(args, "quiet", False)
    domain     = getattr(args, "geo_domain", "default") or "default"
    endpoint   = getattr(args, "geo_endpoint", "") or ""
    probe_file_s = getattr(args, "geo_probe_file", None)
    probe_file = Path(probe_file_s) if probe_file_s else None

    try:
        report = GenealogyBuilder().build(
            model_path,
            deployment_domain=domain,
            endpoint=endpoint,
            probe_file=probe_file,
        )
    except Exception as exc:  # noqa: BLE001
        print(f"genealogy: build failed: {exc}", file=sys.stderr)
        return 1

    output_dir_s = getattr(args, "geo_output_dir", None)
    output_dir   = Path(output_dir_s) if output_dir_s else model_path
    written = report.save(output_dir)

    if getattr(args, "geo_json", False):
        print(report.to_json())
    elif not quiet_mode:
        verdict_icon = {"CLEAN": "✅", "WARNING": "⚠️", "BLOCKED": "🔴"}.get(
            report.contamination_verdict, "⚪"
        )
        print(
            f"{verdict_icon} Genealogy [{report.deployment_domain}] "
            f"{report.contamination_verdict} · "
            f"Risk: {report.copyright_risk_tier} ({report.copyright_risk_score}/100) · "
            f"Chain depth: {report.chain.depth}"
        )
        srcs = report.chain.worst_copyright_sources()
        if srcs:
            print(f"   Copyright sources: {', '.join(srcs[:3])}")
        for fmt, p in written.items():
            print(f"   [{fmt}] {p}")

    block = getattr(args, "geo_block", False)
    if block:
        if report.contamination_verdict == "BLOCKED":
            return 1
        if report.contamination_verdict == "WARNING":
            return 2
    return 0


def _cmd_copyright_check(args: argparse.Namespace, quiet: bool) -> int:
    """Sprint 39 W274 — `squash copyright-check`. SPDX + compatibility analysis."""
    try:
        from squash.copyright import CopyrightAnalyzer
    except ImportError as exc:
        print(f"squash copyright-check unavailable: {exc}", file=sys.stderr)
        return 2

    model_path = Path(getattr(args, "cc_model_path", ".") or ".")
    if not model_path.exists():
        print(f"copyright-check: path not found: {model_path}", file=sys.stderr)
        return 2

    quiet_mode = quiet or getattr(args, "quiet", False)
    use        = getattr(args, "cc_use", "commercial") or "commercial"

    try:
        report = CopyrightAnalyzer().analyze(model_path, deployment_use=use)
    except Exception as exc:  # noqa: BLE001
        print(f"copyright-check: analysis failed: {exc}", file=sys.stderr)
        return 1

    output_dir_s = getattr(args, "cc_output_dir", None)
    output_dir   = Path(output_dir_s) if output_dir_s else model_path
    written = report.save(output_dir)

    if getattr(args, "cc_json", False):
        print(report.to_json())
    elif not quiet_mode:
        compat_str = (
            "compatible" if report.compatible
            else "INCOMPATIBLE" if report.compatible is False
            else "uncertain — legal review needed"
        )
        compat_icon = "✅" if report.compatible else ("❌" if report.compatible is False else "⚠️")
        print(
            f"{compat_icon} Copyright check [{use}]: {compat_str} · "
            f"Risk: {report.risk_tier} ({report.risk_score}/100) · "
            f"Model licence: {report.model_license.spdx_id}"
        )
        for issue in report.compatibility_issues[:3]:
            sev_icon = {"CRITICAL": "🔴", "HIGH": "🟠", "MEDIUM": "🟡"}.get(issue.severity, "ℹ️")
            print(f"   {sev_icon} {issue.severity}: {issue.issue[:80]}")
        for fmt, p in written.items():
            print(f"   [{fmt}] {p}")

    fail = getattr(args, "cc_fail", False)
    if fail:
        if report.compatible is False:
            return 1
        if report.compatible is None:
            return 2
    return 0


def _cmd_insurance_package(args: argparse.Namespace, quiet: bool) -> int:
    """Sprint 24 W237 — `squash insurance-package`.

    Generate AI cyber insurance risk package for underwriting submission.

    Exit codes:
      0   success
      1   build failure (empty model directory etc.)
      2   configuration error
    """
    try:
        from squash.insurance import InsuranceBuilder, MunichReAdapter, CoalitionAdapter, GenericAdapter
    except ImportError as exc:
        print(f"squash insurance-package unavailable: {exc}", file=sys.stderr)
        return 2

    models_dir = Path(getattr(args, "ip_models_dir", ".") or ".")
    if not models_dir.exists():
        print(f"insurance-package: path not found: {models_dir}", file=sys.stderr)
        return 2

    quiet_mode = quiet or getattr(args, "quiet", False)
    org = getattr(args, "ip_org", "") or ""

    try:
        pkg = InsuranceBuilder().build(models_dir, org_name=org)
    except Exception as exc:  # noqa: BLE001
        print(f"insurance-package: build failed: {exc}", file=sys.stderr)
        return 1

    emit_json    = getattr(args, "ip_json", False)
    underwriter  = getattr(args, "ip_underwriter", None)
    output_dir_s = getattr(args, "ip_output_dir", None)
    zip_path_s   = getattr(args, "ip_zip", None)

    output_dir = Path(output_dir_s) if output_dir_s else models_dir
    written: dict[str, Path] = {}

    # Write JSON + Markdown
    written.update(pkg.save(output_dir))

    # Write ZIP if requested
    if zip_path_s:
        zip_path = Path(zip_path_s)
        pkg.save_zip(zip_path)
        written["zip"] = zip_path

    # JSON / underwriter output to stdout
    if emit_json:
        if underwriter == "munich-re":
            payload = json.dumps(MunichReAdapter().format(pkg), indent=2, sort_keys=True)
        elif underwriter == "coalition":
            payload = json.dumps(CoalitionAdapter().format(pkg), indent=2, sort_keys=True)
        elif underwriter == "generic":
            payload = json.dumps(GenericAdapter().format(pkg), indent=2, sort_keys=True)
        else:
            payload = pkg.to_json()
        print(payload)
    elif not quiet_mode:
        risk_label = (
            "🔴 HIGH" if pkg.aggregate_risk_score > 60 else
            "🟡 MEDIUM" if pkg.aggregate_risk_score > 30 else "🟢 LOW"
        )
        print(
            f"✓ AI Insurance Package — {pkg.total_models} model(s) · "
            f"Risk: {risk_label} ({pkg.aggregate_risk_score}/100) · "
            f"Compliance: {pkg.aggregate_compliance_score}/100"
        )
        if pkg.critical_cves:
            print(f"   ⚠️  {pkg.critical_cves} critical/high CVE(s) requiring remediation")
        if pkg.high_risk_count:
            print(f"   ⚠️  {pkg.high_risk_count} high-risk model(s)")
        for fmt, p in written.items():
            print(f"   [{fmt}] {p}")

    return 0


def _cmd_simulate_audit(args: argparse.Namespace, quiet: bool) -> int:
    """Sprint 22 W231 — `squash simulate-audit`. Regulatory exam simulation.

    Exit codes:
      0   success (score meets --fail-below threshold if set)
      1   score below --fail-below threshold
      2   configuration error
    """
    try:
        from squash.audit_sim import AuditSimulator
    except ImportError as exc:
        print(f"squash simulate-audit unavailable: {exc}", file=sys.stderr)
        return 2

    model_path = Path(getattr(args, "sa_models_dir", ".") or ".")
    if not model_path.exists():
        print(f"simulate-audit: path not found: {model_path}", file=sys.stderr)
        return 2

    regulator = getattr(args, "sa_regulator", "EU-AI-Act") or "EU-AI-Act"
    quiet = quiet or getattr(args, "quiet", False)

    try:
        report = AuditSimulator().simulate(model_path, regulator)
    except ValueError as exc:
        print(f"simulate-audit: {exc}", file=sys.stderr)
        return 2
    except Exception as exc:  # noqa: BLE001
        print(f"simulate-audit: failed: {exc}", file=sys.stderr)
        return 1

    output_dir_arg = getattr(args, "sa_output_dir", None)
    output_dir = Path(output_dir_arg) if output_dir_arg else model_path
    written = report.save(output_dir)

    emit_json = getattr(args, "sa_json", False)
    if emit_json:
        print(report.to_json())
    elif not quiet:
        tier_emoji = {
            "AUDIT_READY":  "✅",
            "SUBSTANTIAL":  "🟡",
            "DEVELOPING":   "🟠",
            "EARLY_STAGE":  "🔴",
        }.get(report.readiness_tier, "⚪")
        print(
            f"{tier_emoji} {regulator} readiness: "
            f"{report.overall_score}/100 · {report.readiness_tier.replace('_', ' ')}"
        )
        print(
            f"   {report.passing} PASS · {report.partial} PARTIAL · "
            f"{report.failing} FAIL ({report.critical_fails} critical)"
        )
        for fmt, p in written.items():
            print(f"   [{fmt}] {p}")

    fail_below = int(getattr(args, "sa_fail_below", 0) or 0)
    if fail_below and report.overall_score < fail_below:
        if not quiet:
            print(
                f"simulate-audit: score {report.overall_score} < "
                f"threshold {fail_below} — failing",
                file=sys.stderr,
            )
        return 1
    return 0


def _cmd_watch_regulatory(args: argparse.Namespace, quiet: bool) -> int:
    """Sprint 27 W245 — `squash watch-regulatory`.

    Poll live regulatory sources, run gap analysis, and route alerts.

    Exit codes:
      0  success (no new events, or new events processed and notified)
      1  one or more source adapters failed (partial results still shown)
      2  configuration error
    """
    try:
        from squash.regulatory_watch import (
            WatcherConfig, RegulatoryWatcher,
        )
    except ImportError as exc:
        print(f"squash watch-regulatory unavailable: {exc}", file=sys.stderr)
        return 2

    quiet = quiet or getattr(args, "quiet", False)

    # ── Build config ──────────────────────────────────────────────────────────
    sources = list(getattr(args, "wr_sources", None) or ["sec", "nist", "eurlex"])
    db_path_arg = getattr(args, "wr_db_path", None)

    # Parse extra feed configs: "name=foo,url=https://...,keywords=ai+act"
    extra_feeds: list[dict] = []
    for raw_feed in (getattr(args, "wr_extra_feeds", None) or []):
        parts = dict(item.split("=", 1) for item in raw_feed.split(",") if "=" in item)
        if "name" not in parts or "url" not in parts:
            print(
                f"watch-regulatory: --extra-feed must have name= and url= keys: {raw_feed!r}",
                file=sys.stderr,
            )
            return 2
        keywords = parts.get("keywords", "").replace("+", " ").split() or None
        extra_feeds.append({
            "name": parts["name"],
            "url": parts["url"],
            "keywords": keywords,
        })

    cfg = WatcherConfig(
        sources=sources,
        extra_feeds=extra_feeds,
        max_events=getattr(args, "wr_max_events", 50),
        alert_channel=getattr(args, "wr_alert_channel", "stdout"),
    )
    if db_path_arg:
        cfg.db_path = Path(db_path_arg)

    watcher = RegulatoryWatcher(cfg)
    models_dir = Path(args.wr_models_dir) if getattr(args, "wr_models_dir", None) else None
    dry_run = bool(getattr(args, "wr_dry_run", False))
    emit_json = bool(getattr(args, "wr_json", False))
    channel = getattr(args, "wr_alert_channel", "stdout")

    # ── Determine polling interval ────────────────────────────────────────────
    from squash.regulatory_watch import parse_interval as _parse_interval
    interval_arg = getattr(args, "wr_interval", "0") or "0"
    interval_seconds = _parse_interval(interval_arg)
    continuous = interval_seconds > 0

    # ── Main loop ─────────────────────────────────────────────────────────────
    had_error = False
    iterations = 0
    while True:
        iterations += 1
        try:
            new_events, gap_results = watcher.poll(models_dir=models_dir)
        except Exception as exc:  # noqa: BLE001
            print(f"watch-regulatory: poll failed: {exc}", file=sys.stderr)
            had_error = True
            new_events, gap_results = [], []

        if not quiet and not emit_json:
            if new_events:
                print(f"✓ {len(new_events)} new regulatory event(s) detected")
            else:
                if not continuous or iterations == 1:
                    print("✓ No new regulatory events since last poll")

        if emit_json:
            print(json.dumps(
                {
                    "new_events": len(new_events),
                    "gap_results": [g.to_dict() for g in gap_results],
                },
                indent=2, sort_keys=True,
            ))
        elif gap_results and not quiet:
            for gap in gap_results:
                print()
                print(gap.summary_text())

        if gap_results and not dry_run and channel != "stdout":
            watcher.notify(gap_results, channel=channel)

        if dry_run and new_events and not quiet:
            print(f"[dry-run] {len(new_events)} event(s) not persisted")

        if not continuous or getattr(args, "wr_once", False):
            break

        if not quiet:
            next_poll = datetime.datetime.now(datetime.timezone.utc) + \
                datetime.timedelta(seconds=interval_seconds)
            print(f"Next poll at {next_poll.strftime('%H:%M UTC')} "
                  f"({interval_seconds // 3600}h interval)")
        time.sleep(interval_seconds)

    return 1 if had_error else 0


def _cmd_freeze(args: argparse.Namespace, quiet: bool) -> int:
    """Sprint 19 W221-W222 — `squash freeze` ★ (Track C / C1).

    Emergency response orchestrator. Exit codes:
      0  freeze succeeded — every step ok
      1  freeze partial — registry revoke ok, but >=1 broadcast step failed
      2  freeze aborted — registry revoke failed (no side-effects performed)
      3  configuration / argument error
    """
    from squash import freeze as freeze_mod

    sub = getattr(args, "fz_command", None)
    quiet = quiet or bool(getattr(args, "fz_quiet", False))

    if sub == "ledger":
        entries = freeze_mod.read_ledger(
            state_dir=getattr(args, "fz_state_dir", None),
            limit=getattr(args, "fz_limit", None),
        )
        if getattr(args, "output_json", False):
            print(json.dumps(entries, indent=2, sort_keys=True))
            return 0
        if not entries:
            if not quiet:
                print("(no freeze ledger entries)")
            return 0
        for e in entries:
            print(
                f"{e.get('logged_at', '?')}  "
                f"{e.get('freeze_id', '?')}  "
                f"actor={e.get('actor', '?')}  "
                f"revoked={len(e.get('revoked_entries', []))}  "
                f"reason={e.get('reason', '')!r}"
            )
        return 0

    if sub == "verify":
        receipt_path = Path(getattr(args, "receipt_path"))
        if not receipt_path.exists():
            print(f"squash freeze verify: {receipt_path} not found", file=sys.stderr)
            return 3
        try:
            receipt_data = json.loads(receipt_path.read_text())
        except Exception as exc:
            print(f"squash freeze verify: failed to parse receipt: {exc}", file=sys.stderr)
            return 3
        ok, msg = freeze_mod.verify_receipt(receipt_data)
        if getattr(args, "output_json", False):
            print(json.dumps({"valid": ok, "message": msg}, indent=2))
            return 0 if ok else 1
        if ok:
            print(f"✓ receipt {receipt_data.get('freeze_id', '?')} verified ({msg})")
            return 0
        print(f"✗ receipt verification failed: {msg}", file=sys.stderr)
        return 1

    # Default: run a freeze.
    attestation_id = getattr(args, "fz_attestation_id", None)
    model_path = getattr(args, "fz_model_path", None)
    if not attestation_id and not model_path:
        print(
            "squash freeze: must provide --attestation-id or --model-path",
            file=sys.stderr,
        )
        return 3

    try:
        receipt = freeze_mod.freeze(
            attestation_id=attestation_id,
            model_path=model_path,
            model_id=getattr(args, "fz_model_id", "") or "",
            reason=getattr(args, "fz_reason", "") or "",
            actor=getattr(args, "fz_actor", "") or "",
            severity=getattr(args, "fz_severity", "critical") or "critical",
            category=getattr(args, "fz_category", "other") or "other",
            affected_persons=int(getattr(args, "fz_affected", 0) or 0),
            incident_dir=getattr(args, "fz_incident_dir", None),
            state_dir=getattr(args, "fz_state_dir", None),
            priv_key_pem=getattr(args, "fz_priv_key", None),
            write_incident=not bool(getattr(args, "fz_no_incident", False)),
            webhook_timeout_s=float(getattr(args, "fz_webhook_timeout", 10.0)),
        )
    except ValueError as exc:
        print(f"squash freeze: {exc}", file=sys.stderr)
        return 3

    fmt = (getattr(args, "fz_format", "text") or "text").lower()
    out_path = getattr(args, "fz_out", None)
    if fmt == "json":
        body = receipt.to_json()
    elif fmt == "md":
        body = "```\n" + receipt.summary() + "\n```\n"
    else:
        body = receipt.summary()

    if out_path:
        Path(out_path).write_text(body)
        if not quiet:
            print(f"freeze receipt written to {out_path}")
    elif not quiet:
        print(body)

    if not receipt.revoke_ok:
        return 2
    return 0 if receipt.all_ok else 1


def _cmd_compliance_matrix(args: argparse.Namespace, quiet: bool) -> int:
    """D4 — `squash compliance-matrix`. Multi-jurisdiction matrix.

    Exit codes:
      0  matrix produced (no failing cells, or --fail-on-gap not set)
      1  --fail-on-gap and at least one FAIL/PARTIAL cell exists
      2  configuration / argument error
    """
    try:
        from squash import compliance_matrix as cm
    except Exception as exc:  # noqa: BLE001
        print(f"squash compliance-matrix unavailable: {exc}", file=sys.stderr)
        return 2

    if getattr(args, "cm_list_jurs", False):
        for j in cm.Jurisdiction:
            print(f"  {j.value:<10} {j.display}")
        return 0

    if getattr(args, "cm_list_reqs", False):
        out = []
        for r in cm.builtin_requirements():
            out.append({
                "requirement_id": r.requirement_id,
                "title": r.title,
                "jurisdictions": [j.value for j in r.jurisdictions],
                "regulations": list(r.regulations),
                "squash_control": r.squash_control,
                "severity": r.severity,
            })
        print(json.dumps(out, indent=2))
        return 0

    regions_raw = getattr(args, "cm_regions", "") or ""
    if not regions_raw:
        print("squash compliance-matrix: --regions is required", file=sys.stderr)
        return 2
    try:
        regions = cm.parse_regions(regions_raw)
    except ValueError as exc:
        print(f"squash compliance-matrix: {exc}", file=sys.stderr)
        return 2

    attestation: dict = {}
    att_path = getattr(args, "cm_attestation", None)
    if att_path:
        try:
            attestation = json.loads(Path(att_path).read_text())
        except Exception as exc:  # noqa: BLE001
            print(f"failed to read attestation: {exc}", file=sys.stderr)
            return 2

    models_path = getattr(args, "cm_models", None)
    if models_path:
        loaded = cm.load_attestation_dir(models_path)
        for k, v in loaded.items():
            attestation.setdefault(k, v)

    matrix = cm.ComplianceMatrix.build(
        regions=regions,
        attestation=attestation,
        model_dir=models_path,
        model_id=getattr(args, "cm_model_id", "") or
                 (Path(models_path).name if models_path else ""),
    )

    fmt = (getattr(args, "cm_format", "text") or "text").lower()
    if fmt == "json":
        body = matrix.to_json()
        if getattr(args, "cm_remediation", False):
            doc = json.loads(body)
            doc["remediation_plan"] = [
                s.to_dict() for s in cm.GapAnalyser(matrix).plan()
            ]
            body = json.dumps(doc, indent=2, sort_keys=True)
    elif fmt == "md":
        body = matrix.to_markdown()
        if getattr(args, "cm_remediation", False):
            plan = cm.GapAnalyser(matrix).plan()
            if plan:
                body += "\n## Remediation plan\n\n"
                for i, s in enumerate(plan, 1):
                    body += f"{i}. `{s.squash_control}` — {s.detail}\n"
    elif fmt == "html":
        body = matrix.to_html()
    else:
        body = matrix.to_text()
        if getattr(args, "cm_remediation", False):
            plan = cm.GapAnalyser(matrix).plan()
            if plan:
                body += "\nRemediation plan:\n"
                for i, s in enumerate(plan, 1):
                    body += f"  {i}. {s.squash_control} — {s.detail}\n"

    out_path = getattr(args, "cm_output", None)
    if out_path:
        Path(out_path).write_text(body)
        if not quiet:
            print(f"matrix written to {out_path}")
    elif not quiet:
        print(body, end="" if body.endswith("\n") else "\n")

    if getattr(args, "cm_fail_on_gap", False):
        if matrix.summary.fail_count or matrix.summary.partial_count:
            return 1
    return 0


def _cmd_github_app(args: argparse.Namespace, quiet: bool) -> int:
    """D1 — `squash github-app`. Auto-attest PRs/commits as GitHub Check Runs.

    Subcommands:
      serve           — start the webhook receiver
      attest          — manually run an attestation against a repo @ sha
      config          — render or validate a YAML config file
      verify-webhook  — verify an X-Hub-Signature-256 header

    Exit codes:
      0  success
      1  attestation failed (CI gate)
      2  configuration / validation error
      3  GitHub API or runtime failure
    """
    sub = getattr(args, "gha_command", None)

    try:
        from squash import github_app as gh
    except Exception as exc:  # noqa: BLE001
        print(f"squash github-app unavailable: {exc}", file=sys.stderr)
        return 2

    if sub is None:
        print(
            "squash github-app: specify a subcommand — "
            "serve | attest | config | verify-webhook",
            file=sys.stderr,
        )
        return 2

    # ── config ─────────────────────────────────────────────────────────────
    if sub == "config":
        if getattr(args, "gha_cfg_show", False):
            print(gh._CONFIG_TEMPLATE)  # noqa: SLF001
            return 0
        init_path = getattr(args, "gha_cfg_init", None)
        check_path = getattr(args, "gha_cfg_check", None)
        if init_path:
            p = gh.dump_config_template(init_path)
            if not quiet:
                print(f"wrote {p}")
            return 0
        if check_path:
            try:
                cfg = gh.load_config(check_path)
            except Exception as exc:  # noqa: BLE001
                print(f"config load failed: {exc}", file=sys.stderr)
                return 2
            errors = cfg.validate()
            if errors:
                for e in errors:
                    print(f"✗ {e}", file=sys.stderr)
                return 2
            if not quiet:
                print(
                    f"✓ {check_path} — app_id={cfg.app_id} "
                    f"patterns={len(cfg.model_patterns)}"
                )
            return 0
        return 2

    # ── verify-webhook ─────────────────────────────────────────────────────
    if sub == "verify-webhook":
        secret = getattr(args, "gha_v_secret", "")
        sig = getattr(args, "gha_v_sig", "")
        body_text = getattr(args, "gha_v_body", None)
        body_file = getattr(args, "gha_v_body_file", None)
        if body_file:
            try:
                body_bytes = Path(body_file).read_bytes()
            except Exception as exc:  # noqa: BLE001
                print(f"body-file read failed: {exc}", file=sys.stderr)
                return 2
        else:
            body_bytes = (body_text or "").encode()
        verifier = gh.WebhookVerifier(secret)
        ok = verifier.verify(body_bytes, sig)
        if not quiet:
            print("✓ signature valid" if ok else "✗ signature INVALID")
        return 0 if ok else 1

    # ── install ────────────────────────────────────────────────────────────────
    if sub == "install":
        return _cmd_github_app_install(args, quiet, gh)

    # ── config-template ────────────────────────────────────────────────────────
    if sub == "config-template":
        print(gh._CONFIG_TEMPLATE, end="")  # noqa: SLF001
        return 0

    # ── serve / attest both need a config ──────────────────────────────────
    cfg_path = getattr(args, "gha_config", None)
    if not cfg_path:
        print("squash github-app: --config is required", file=sys.stderr)
        return 2
    try:
        cfg = gh.load_config(cfg_path)
    except Exception as exc:  # noqa: BLE001
        print(f"config load failed: {exc}", file=sys.stderr)
        return 2

    errors = cfg.validate()
    if errors:
        for e in errors:
            print(f"✗ {e}", file=sys.stderr)
        return 2

    # ── serve ──────────────────────────────────────────────────────────────
    if sub == "serve":
        try:
            server = gh.serve(
                cfg,
                host=getattr(args, "gha_host", None),
                port=getattr(args, "gha_port", None),
            )
        except Exception as exc:  # noqa: BLE001
            print(f"serve failed: {exc}", file=sys.stderr)
            return 3
        if not quiet:
            host, port = server.server_address[:2]
            print(f"squash-github-app listening on {host}:{port}")
        try:
            server.serve_forever()
        except KeyboardInterrupt:
            if not quiet:
                print("\nshutting down")
        finally:
            server.server_close()
        return 0

    # ── attest ─────────────────────────────────────────────────────────────
    if sub == "attest":
        repo = str(getattr(args, "gha_repo", ""))
        if "/" not in repo:
            print("--repo must be 'owner/name'", file=sys.stderr)
            return 2
        owner, name = repo.split("/", 1)
        sha = str(getattr(args, "gha_sha", ""))
        installation_id = int(getattr(args, "gha_install", 0) or 0)
        paths = list(getattr(args, "gha_paths", []) or [])
        clone_url = str(getattr(args, "gha_clone_url", "") or "")
        no_clone = bool(getattr(args, "gha_no_clone", False))
        workdir = getattr(args, "gha_workdir", None)
        dry_run = bool(getattr(args, "gha_dry_run", False))

        runner = gh.AttestationRunner(cfg)
        if no_clone:
            if not workdir:
                print("--no-clone requires --workdir", file=sys.stderr)
                return 2
            outcome = runner.run(
                workdir=workdir, changed_model_files=paths or [], model_id=repo,
            )
        else:
            if not clone_url:
                clone_url = f"https://github.com/{owner}/{name}.git"
            with tempfile.TemporaryDirectory(prefix=f"squash-app-{sha[:7]}-") as tmp:
                wd = Path(tmp) / "repo"
                try:
                    gh.clone_repo_at_sha(
                        clone_url=clone_url, sha=sha,
                        destination=wd, depth=cfg.clone_depth,
                    )
                except Exception as exc:  # noqa: BLE001
                    print(f"clone failed: {exc}", file=sys.stderr)
                    return 3
                outcome = runner.run(
                    workdir=wd, changed_model_files=paths or [], model_id=repo,
                )

        body = dict(
            passed=outcome.passed,
            conclusion=outcome.conclusion,
            model_id=outcome.model_id,
            summary=outcome.summary,
            violations=outcome.violations,
            artifacts=outcome.artifacts,
        )
        if getattr(args, "output_json", False):
            print(json.dumps(body, indent=2))
        elif not quiet:
            print(f"[{outcome.conclusion.upper()}] {outcome.summary}")
            if outcome.violations:
                for v in outcome.violations:
                    print(f"  - {v}")

        if not dry_run and installation_id:
            try:
                auth = gh.GitHubAppAuth(cfg)
                client = gh.GitHubAppClient(auth)
                check = client.create_check_run(
                    installation_id=installation_id,
                    owner=owner, repo=name, head_sha=sha,
                    status="completed", output=outcome.to_check_run_output(),
                )
                if check.get("id"):
                    client.update_check_run(
                        installation_id=installation_id,
                        owner=owner, repo=name,
                        check_run_id=int(check["id"]),
                        status="completed", conclusion=outcome.conclusion,
                        completed_at=gh._now_iso(),  # noqa: SLF001
                    )
            except gh.GitHubApiError as exc:
                print(f"GitHub API error: {exc}", file=sys.stderr)
                return 3
            except Exception as exc:  # noqa: BLE001
                print(f"check-run post failed: {exc}", file=sys.stderr)
                return 3

        return 0 if outcome.passed else 1

    print(f"unknown github-app subcommand: {sub}", file=sys.stderr)
    return 2


def _cmd_github_app_install(
    args: argparse.Namespace, quiet: bool, gh: object
) -> int:
    """Print the GitHub App install URL and optionally write a config template."""
    app_id = int(getattr(args, "gha_install_app_id", 0) or 0)
    out_path = getattr(args, "gha_install_out", None)

    if app_id:
        url = f"https://github.com/apps/squash-ai/installations/new?app_id={app_id}"
    else:
        url = "https://github.com/apps/squash-ai/installations/new"

    if not quiet:
        print(f"Install URL: {url}")
        print("")
        print("After installation, record your App ID and generate a private key,")
        print("then create a config file with:")
        print("  squash github-app config --init ./squash-github-app.yaml")

    if out_path:
        p = gh.dump_config_template(out_path)  # type: ignore[attr-defined]
        if not quiet:
            print(f"\nConfig template written to: {p}")

    return 0


def _cmd_registry_gate(args: argparse.Namespace, quiet: bool) -> int:
    """W201 — `squash registry-gate`. Pre-registration policy gate.

    Attests the local model and emits a structured gate decision. Exit 0 on
    policy pass, 1 on policy fail (unless --allow-on-fail), 2 on misconfig.
    """
    try:
        from squash.attest import AttestConfig, AttestPipeline
    except ImportError as exc:
        print(f"squash registry-gate unavailable: {exc}", file=sys.stderr)
        return 2

    model_path = Path(args.rg_model_path)
    if not model_path.exists():
        print(f"registry-gate: model not found: {model_path}", file=sys.stderr)
        return 2

    output_dir = (
        Path(args.rg_output_dir) if args.rg_output_dir
        else (model_path.parent / "squash")
    )
    output_dir.mkdir(parents=True, exist_ok=True)

    policies = args.rg_policies or ["enterprise-strict"]
    backend: str = args.backend
    uri: str = args.rg_uri or ""

    # ── Light backend-specific URI validation ────────────────────────────────
    rc_validate = _validate_registry_uri(backend, uri)
    if rc_validate != 0:
        return rc_validate

    config = AttestConfig(
        model_path=model_path,
        output_dir=output_dir,
        policies=policies,
        sign=getattr(args, "rg_sign", False),
        fail_on_violation=False,  # gate decides exit code itself
    )
    result = AttestPipeline.run(config)

    decision = "allow" if result.passed else "refuse"
    if args.rg_allow_on_fail and not result.passed:
        decision = "record-only"

    gate = {
        "squash_version": "registry_gate_v1",
        "backend": backend,
        "uri": uri,
        "name": args.rg_name or "",
        "model_path": str(model_path),
        "passed": result.passed,
        "decision": decision,
        "attestation_id": result.model_id,
        "scan_status": (
            result.scan_result.status if result.scan_result else "skipped"
        ),
        "policies": {
            n: {
                "passed": pr.passed,
                "errors": pr.error_count,
                "warnings": pr.warning_count,
            }
            for n, pr in result.policy_results.items()
        },
        "summary": result.summary(),
        "output_dir": str(output_dir),
    }

    gate_path = output_dir / "registry-gate.json"
    gate_path.write_text(json.dumps(gate, indent=2, sort_keys=True), encoding="utf-8")

    if args.rg_json:
        print(json.dumps(gate, indent=2, sort_keys=True))
    elif not quiet:
        emoji = "✅" if result.passed else "❌"
        print(f"{emoji} registry-gate [{backend}] decision={decision}")
        print(f"   model: {model_path}")
        if uri:
            print(f"   uri:   {uri}")
        print(f"   {result.summary()}")
        print(f"   gate file: {gate_path}")

    if not result.passed and not args.rg_allow_on_fail:
        return 1
    return 0


def _validate_registry_uri(backend: str, uri: str) -> int:
    """Light, opinionated URI sanity check per backend. Returns 0 or 2."""
    if not uri or backend == "local":
        return 0
    if backend == "mlflow":
        if not (uri.startswith("models:/") or uri.startswith("runs:/")):
            print(
                f"registry-gate: --uri for mlflow must start with 'models:/' "
                f"or 'runs:/' — got {uri!r}", file=sys.stderr,
            )
            return 2
    elif backend == "wandb":
        if not uri.startswith("wandb://"):
            print(
                f"registry-gate: --uri for wandb must start with 'wandb://' "
                f"— got {uri!r}", file=sys.stderr,
            )
            return 2
    elif backend == "sagemaker":
        if not uri.startswith("arn:aws:sagemaker:"):
            print(
                f"registry-gate: --uri for sagemaker must be an AWS ARN "
                f"starting with 'arn:aws:sagemaker:' — got {uri!r}",
                file=sys.stderr,
            )
            return 2
    return 0


def _resolve_python_chain(spec: str) -> Any:
    """Resolve a 'module.path:attr' or 'module.path:attr.nested' string to an object."""
    import importlib
    if ":" not in spec:
        raise ValueError(f"Expected 'module:attribute' form, got {spec!r}")
    module_path, attr_path = spec.split(":", 1)
    mod = importlib.import_module(module_path)
    obj: Any = mod
    for part in attr_path.split("."):
        if not part:
            continue
        obj = getattr(obj, part)
    if callable(obj) and not hasattr(obj, "steps") and not hasattr(obj, "tools"):
        # Treat zero-arg factories as builders — call them
        try:
            obj = obj()
        except TypeError:
            pass
    return obj


def _model_card_push(
    card_path: Path, repo_id: str, token: str | None, quiet: bool,
) -> int:
    """Upload squash-model-card-hf.md to a HuggingFace repo. Optional dep."""
    if not card_path.exists():
        print(f"Model card not found: {card_path}", file=sys.stderr)
        return 1
    try:
        from huggingface_hub import HfApi  # type: ignore
    except ImportError:
        print(
            "--push-to-hub requires `huggingface_hub`. Install with: "
            "pip install huggingface_hub",
            file=sys.stderr,
        )
        return 2

    import os as _os
    hub_token = token or _os.environ.get("HUGGING_FACE_HUB_TOKEN") \
        or _os.environ.get("HF_TOKEN")
    if not hub_token:
        print(
            "--push-to-hub requires a token. Pass --hub-token or set "
            "HUGGING_FACE_HUB_TOKEN.",
            file=sys.stderr,
        )
        return 1

    api = HfApi(token=hub_token)
    try:
        api.upload_file(
            path_or_fileobj=str(card_path),
            path_in_repo="README.md",
            repo_id=repo_id,
            repo_type="model",
            commit_message="Squash: model card auto-generated by squash model-card",
        )
    except Exception as exc:  # noqa: BLE001 — surface any HF error verbatim
        print(f"HuggingFace upload failed: {exc}", file=sys.stderr)
        return 1

    if not quiet:
        print(f"✓ pushed {card_path.name} to https://huggingface.co/{repo_id}")
    return 0


# ── Wave 77 — Cloud CLI command implementations ───────────────────────────────

def _cmd_cloud_status(args: argparse.Namespace, quiet: bool) -> int:
    """Show EU AI Act conformance status for a single tenant. Exit 0=conformant, 2=non-conformant."""
    try:
        from squash import api as _api
    except ImportError as exc:
        print(f"squash is not installed: {exc}", file=sys.stderr)
        return 2

    if args.tenant_id not in _api._tenants:
        print(f"squash cloud-status: tenant not found: {args.tenant_id}", file=sys.stderr)
        return 1

    status = _api._db_read_tenant_conformance(args.tenant_id)

    conformant: bool = status.get("conformant", False)
    score: float = float(status.get("compliance_score", 0.0))
    risk: str = status.get("enforcement_risk_level", "UNKNOWN")
    days: int = int(status.get("days_until_enforcement", 0))
    reasons: list = status.get("reasons", [])

    status_label = "CONFORMANT" if conformant else "NON-CONFORMANT"
    icon = "✓" if conformant else "✗"

    if not quiet:
        print(
            f"{icon} {args.tenant_id} | {status_label} | score: {score:.1f} | "
            f"{risk} | {days} days until enforcement"
        )
        for reason in reasons:
            print(f"  • {reason}")

    if getattr(args, "output_json", False):
        import json as _json
        print(_json.dumps(status, indent=2))

    return 0 if conformant else 2


def _cmd_cloud_report(args: argparse.Namespace, quiet: bool) -> int:
    """Print platform-wide EU AI Act conformance report. Exit 0=all conformant, 2=any non-conformant."""
    try:
        from squash import api as _api
    except ImportError as exc:
        print(f"squash is not installed: {exc}", file=sys.stderr)
        return 2

    if not _api._tenants:
        if not quiet:
            print("no tenants registered")
        return 0

    report = _api._db_read_conformance_report()
    total: int = report.get("total_tenants", 0)
    conformant_count: int = report.get("conformant_tenants", 0)
    non_conformant_count: int = report.get("non_conformant_tenants", 0)
    risk: str = report.get("enforcement_risk_level", "UNKNOWN")
    days: int = int(report.get("days_until_enforcement", 0))

    if not quiet:
        print(
            f"Platform Report | {total} tenant(s) | {conformant_count} conformant | "
            f"{non_conformant_count} non-conformant | {risk} | {days} days until enforcement"
        )
        print(f"{'Tenant':<30} {'Score':>7} {'Status':<16} {'Risk':<10} {'Days':>5}")
        print("-" * 72)
        for tid in _api._tenants:
            row = _api._db_read_tenant_conformance(tid)
            row_score: float = float(row.get("compliance_score", 0.0))
            row_status = "CONFORMANT" if row.get("conformant") else "NON-CONFORMANT"
            row_risk: str = row.get("enforcement_risk_level", "UNKNOWN")
            row_days: int = int(row.get("days_until_enforcement", 0))
            print(f"{tid:<30} {row_score:>7.1f} {row_status:<16} {row_risk:<10} {row_days:>5}")

    if getattr(args, "output_json", False):
        import json as _json
        print(_json.dumps(report, indent=2))

    return 0 if non_conformant_count == 0 else 2


def _cmd_cloud_export(args: argparse.Namespace, quiet: bool) -> int:
    """Export a complete compliance audit bundle for a tenant. Always exits 0 on success."""
    try:
        from squash import api as _api
    except ImportError as exc:
        print(f"squash is not installed: {exc}", file=sys.stderr)
        return 2

    if args.tenant_id not in _api._tenants:
        print(f"squash cloud-export: tenant not found: {args.tenant_id}", file=sys.stderr)
        return 1

    import json as _json

    bundle = _api._db_build_tenant_export(args.tenant_id)

    output_path = getattr(args, "output_path", None)
    if output_path and output_path != "-":
        path = Path(output_path)
        path.write_text(_json.dumps(bundle, indent=2), encoding="utf-8")
        if not quiet:
            print(f"✓ export written to {path}")
    else:
        print(_json.dumps(bundle, indent=2))

    return 0


# ── Wave 79 — Cloud CLI command implementations ──────────────────────────────

def _cmd_cloud_attest(args: argparse.Namespace, quiet: bool) -> int:
    """Attest a model for a tenant and register the result in the cloud inventory.

    Exit codes: 0=attested+passed+registered, 1=bad-args/tenant-not-found, 2=attest-failed.
    """
    try:
        from squash import api as _api
        from squash.attest import AttestConfig, AttestPipeline
    except ImportError as exc:
        print(f"squash is not installed: {exc}", file=sys.stderr)
        return 2

    if args.tenant_id not in _api._tenants:
        print(
            f"squash cloud-attest: tenant not found: {args.tenant_id}",
            file=sys.stderr,
        )
        return 1

    model_path = Path(args.model_path)
    if not model_path.exists():
        print(
            f"squash cloud-attest: model path does not exist: {model_path}",
            file=sys.stderr,
        )
        return 1

    output_dir = Path(args.output_path) if getattr(args, "output_path", None) else None

    config = AttestConfig(
        model_path=model_path,
        output_dir=output_dir,
        model_id=model_path.stem,
        policies=[args.policy],
        fail_on_violation=False,
        sign=False,
    )

    try:
        result = AttestPipeline.run(config)
    except FileNotFoundError as exc:
        print(f"squash cloud-attest: {exc}", file=sys.stderr)
        return 1
    except Exception as exc:  # noqa: BLE001
        print(f"squash cloud-attest: attestation error: {exc}", file=sys.stderr)
        return 2

    # Serialize policy results to a plain dict for storage
    policy_dict: dict = {
        name: {
            "passed": pr.passed if hasattr(pr, "passed") else bool(pr),
            "error_count": getattr(pr, "error_count", 0),
            "warning_count": getattr(pr, "warning_count", 0),
        }
        for name, pr in result.policy_results.items()
    }

    bom_path_str = str(result.cyclonedx_path) if result.cyclonedx_path else ""

    record: dict = {
        "model_id": result.model_id,
        "model_path": str(model_path),
        "bom_path": bom_path_str,
        "attestation_passed": result.passed,
        "policy_results": policy_dict,
        "vex_cves": [],
        "timestamp": "",
        "record_id": "",
    }
    import uuid as _uuid
    import datetime as _dt
    record["timestamp"] = _dt.datetime.now(_dt.timezone.utc).isoformat().replace("+00:00", "Z")
    record["record_id"] = str(_uuid.uuid4())

    _api._db_write_inventory(args.tenant_id, record)

    status_label = "PASS" if result.passed else "FAIL"
    icon = "\u2713" if result.passed else "\u2717"

    if not quiet:
        print(
            f"{icon} cloud-attest | {args.tenant_id} | {result.model_id} | {status_label}"
            f" | policy: {args.policy} | registered"
        )

    if getattr(args, "output_json", False):
        import json as _json
        print(_json.dumps(record, indent=2))

    return 0 if result.passed else 2


def _cmd_cloud_vex(args: argparse.Namespace, quiet: bool) -> int:
    """List VEX/CVE alerts for a tenant. Exit 0=success, 1=tenant-not-found, 2=server-error."""
    try:
        from squash import api as _api
    except ImportError as exc:
        print(f"squash is not installed: {exc}", file=sys.stderr)
        return 2

    if args.tenant_id not in _api._tenants:
        print(
            f"squash cloud-vex: tenant not found: {args.tenant_id}",
            file=sys.stderr,
        )
        return 1

    try:
        alerts: list = _api._db_read_vex_alerts(args.tenant_id)
    except Exception as exc:  # noqa: BLE001
        print(f"squash cloud-vex: error reading alerts: {exc}", file=sys.stderr)
        return 2

    # Apply filters
    vex_status = getattr(args, "vex_status", None)
    severity = getattr(args, "severity", None)
    limit: int = max(1, min(getattr(args, "limit", 50), 500))

    if vex_status:
        alerts = [a for a in alerts if a.get("status") == vex_status]
    if severity:
        alerts = [a for a in alerts if a.get("severity") == severity]
    alerts = alerts[-limit:]

    if getattr(args, "output_json", False):
        import json as _json
        print(_json.dumps({"tenant_id": args.tenant_id, "count": len(alerts), "alerts": alerts}, indent=2))
        return 0

    if not quiet:
        if not alerts:
            print(f"No VEX alerts for tenant: {args.tenant_id}")
        else:
            print(f"VEX Alerts | {args.tenant_id} | {len(alerts)} alert(s)")
            print(f"{'CVE':<20} {'Severity':<10} {'Status':<14} {'Model':<30} {'Date'}")
            print("-" * 90)
            for alert in alerts:
                cve = alert.get("cve_id", "-")
                sev = alert.get("severity", "-")
                stat = alert.get("status", "-")
                model = alert.get("model_id", "-")
                ts = alert.get("timestamp", "-")[:10] if alert.get("timestamp") else "-"
                print(f"{cve:<20} {sev:<10} {stat:<14} {model:<30} {ts}")

    return 0


def _cmd_cloud_risk(args: argparse.Namespace, quiet: bool) -> int:
    """Show EU AI Act risk profile for a tenant or platform.

    Exit codes:
        0 = tenant found and overall tier is MINIMAL or LIMITED (conformant-ish)
        1 = tenant not found (or missing positional arg without --overview)
        2 = overall tier is HIGH or UNACCEPTABLE (non-conformant)
    """
    try:
        from squash import api as _api
    except ImportError as exc:
        print(f"squash is not installed: {exc}", file=sys.stderr)
        return 2

    overview: bool = getattr(args, "overview", False)
    tenant_id: str | None = getattr(args, "tenant_id", None)
    output_json: bool = getattr(args, "output_json", False)

    if overview:
        # ── Platform overview ──────────────────────────────────────────────
        summary: dict[str, int] = {
            "UNACCEPTABLE": 0,
            "HIGH": 0,
            "LIMITED": 0,
            "MINIMAL": 0,
        }
        tier_order = ["UNACCEPTABLE", "HIGH", "LIMITED", "MINIMAL"]
        tenant_rows: list[dict] = []

        for tid in list(_api._tenants.keys()):
            inventory = _api._db_read_inventory(tid)
            vex = _api._db_read_vex_alerts(tid)
            open_vex = len(vex)
            if inventory:
                tiers = [_api._compute_model_risk_tier(rec, open_vex) for rec in inventory]
                overall_tier = min(
                    tiers,
                    key=lambda t: tier_order.index(t) if t in tier_order else len(tier_order),
                )
            else:
                overall_tier = "MINIMAL"
            summary[overall_tier] += 1
            tenant_rows.append(
                {
                    "tenant_id": tid,
                    "overall_risk_tier": overall_tier,
                    "model_count": len(inventory),
                }
            )

        if output_json:
            import json as _json
            print(
                _json.dumps(
                    {
                        "total_tenants": len(tenant_rows),
                        "risk_summary": summary,
                        "tenants": tenant_rows,
                    },
                    indent=2,
                )
            )
            return 0

        if not quiet:
            print(f"Risk Overview | {len(tenant_rows)} tenant(s)")
            print(
                f"  UNACCEPTABLE: {summary['UNACCEPTABLE']}  "
                f"HIGH: {summary['HIGH']}  "
                f"LIMITED: {summary['LIMITED']}  "
                f"MINIMAL: {summary['MINIMAL']}"
            )
            for row in tenant_rows:
                tier_icon = "\u2715" if row["overall_risk_tier"] in ("UNACCEPTABLE", "HIGH") else "\u2713"
                print(
                    f"  {tier_icon} {row['tenant_id']:<30} "
                    f"{row['overall_risk_tier']:<15} {row['model_count']} model(s)"
                )

        worst = "MINIMAL"
        if summary["UNACCEPTABLE"] > 0 or summary["HIGH"] > 0:
            worst = "HIGH"
        return 2 if worst in ("UNACCEPTABLE", "HIGH") else 0

    # ── Per-tenant profile ─────────────────────────────────────────────────
    if not tenant_id:
        print(
            "squash cloud-risk: specify a tenant_id or use --overview",
            file=sys.stderr,
        )
        return 1

    if tenant_id not in _api._tenants:
        print(
            f"squash cloud-risk: tenant not found: {tenant_id}",
            file=sys.stderr,
        )
        return 1

    inventory = _api._db_read_inventory(tenant_id)
    vex = _api._db_read_vex_alerts(tenant_id)
    open_vex = len(vex)

    tier_order = ["UNACCEPTABLE", "HIGH", "LIMITED", "MINIMAL"]
    model_profiles: list[dict] = []
    for rec in inventory:
        tier = _api._compute_model_risk_tier(rec, open_vex)
        policy_results = rec.get("policy_results", {})
        total = len(policy_results)
        failed = sum(1 for pr in policy_results.values() if not pr.get("passed", True))
        failure_rate = round(failed / total, 4) if total > 0 else 0.0
        model_profiles.append(
            {
                "model_id": rec.get("model_id", ""),
                "risk_tier": tier,
                "attestation_passed": bool(rec.get("attestation_passed", False)),
                "open_vex_alerts": open_vex,
                "policy_failure_rate": failure_rate,
            }
        )

    if model_profiles:
        overall_tier = min(
            (p["risk_tier"] for p in model_profiles),
            key=lambda t: tier_order.index(t) if t in tier_order else len(tier_order),
        )
    else:
        overall_tier = "MINIMAL"

    if output_json:
        import json as _json
        print(
            _json.dumps(
                {
                    "tenant_id": tenant_id,
                    "overall_risk_tier": overall_tier,
                    "model_count": len(model_profiles),
                    "models": model_profiles,
                },
                indent=2,
            )
        )
        return 0 if overall_tier in ("MINIMAL", "LIMITED") else 2

    if not quiet:
        icon = "\u2713" if overall_tier in ("MINIMAL", "LIMITED") else "\u2715"
        print(
            f"{icon} cloud-risk | {tenant_id} | {overall_tier} | {len(model_profiles)} model(s)"
        )
        if model_profiles:
            print(f"  {'Model':<35} {'Risk Tier':<15} {'Attested':<10} {'VEX Alerts'}")
            print("  " + "-" * 72)
            for mp in model_profiles:
                attest_mark = "PASS" if mp["attestation_passed"] else "FAIL"
                print(
                    f"  {mp['model_id']:<35} {mp['risk_tier']:<15} "
                    f"{attest_mark:<10} {mp['open_vex_alerts']}"
                )

    return 0 if overall_tier in ("MINIMAL", "LIMITED") else 2


def _cmd_cloud_risk(args: argparse.Namespace, quiet: bool) -> int:
    """Show EU AI Act risk profile for a tenant or platform.

    Exit codes:
        0 = tenant found and overall tier is MINIMAL or LIMITED (conformant-ish)
        1 = tenant not found (or missing positional arg without --overview)
        2 = overall tier is HIGH or UNACCEPTABLE (non-conformant)
    """
    try:
        from squash import api as _api
    except ImportError as exc:
        print(f"squash is not installed: {exc}", file=sys.stderr)
        return 2

    overview: bool = getattr(args, "overview", False)
    tenant_id: str | None = getattr(args, "tenant_id", None)
    output_json: bool = getattr(args, "output_json", False)

    if overview:
        # ── Platform overview ──────────────────────────────────────────────
        summary: dict[str, int] = {
            "UNACCEPTABLE": 0,
            "HIGH": 0,
            "LIMITED": 0,
            "MINIMAL": 0,
        }
        tier_order = ["UNACCEPTABLE", "HIGH", "LIMITED", "MINIMAL"]
        tenant_rows: list[dict] = []

        for tid in list(_api._tenants.keys()):
            inventory = _api._db_read_inventory(tid)
            vex = _api._db_read_vex_alerts(tid)
            open_vex = len(vex)
            if inventory:
                tiers = [_api._compute_model_risk_tier(rec, open_vex) for rec in inventory]
                overall_tier = min(
                    tiers,
                    key=lambda t: tier_order.index(t) if t in tier_order else len(tier_order),
                )
            else:
                overall_tier = "MINIMAL"
            summary[overall_tier] += 1
            tenant_rows.append(
                {
                    "tenant_id": tid,
                    "overall_risk_tier": overall_tier,
                    "model_count": len(inventory),
                }
            )

        if output_json:
            import json as _json
            print(
                _json.dumps(
                    {
                        "total_tenants": len(tenant_rows),
                        "risk_summary": summary,
                        "tenants": tenant_rows,
                    },
                    indent=2,
                )
            )
            return 0

        if not quiet:
            print(f"Risk Overview | {len(tenant_rows)} tenant(s)")
            print(
                f"  UNACCEPTABLE: {summary['UNACCEPTABLE']}  "
                f"HIGH: {summary['HIGH']}  "
                f"LIMITED: {summary['LIMITED']}  "
                f"MINIMAL: {summary['MINIMAL']}"
            )
            for row in tenant_rows:
                tier_icon = "\u2715" if row["overall_risk_tier"] in ("UNACCEPTABLE", "HIGH") else "\u2713"
                print(
                    f"  {tier_icon} {row['tenant_id']:<30} "
                    f"{row['overall_risk_tier']:<15} {row['model_count']} model(s)"
                )

        worst = "MINIMAL"
        if summary["UNACCEPTABLE"] > 0 or summary["HIGH"] > 0:
            worst = "HIGH"
        return 2 if worst in ("UNACCEPTABLE", "HIGH") else 0

    # ── Per-tenant profile ─────────────────────────────────────────────────
    if not tenant_id:
        print(
            "squash cloud-risk: specify a tenant_id or use --overview",
            file=sys.stderr,
        )
        return 1

    if tenant_id not in _api._tenants:
        print(
            f"squash cloud-risk: tenant not found: {tenant_id}",
            file=sys.stderr,
        )
        return 1

    inventory = _api._db_read_inventory(tenant_id)
    vex = _api._db_read_vex_alerts(tenant_id)
    open_vex = len(vex)

    tier_order = ["UNACCEPTABLE", "HIGH", "LIMITED", "MINIMAL"]
    model_profiles: list[dict] = []
    for rec in inventory:
        tier = _api._compute_model_risk_tier(rec, open_vex)
        policy_results = rec.get("policy_results", {})
        total = len(policy_results)
        failed = sum(1 for pr in policy_results.values() if not pr.get("passed", True))
        failure_rate = round(failed / total, 4) if total > 0 else 0.0
        model_profiles.append(
            {
                "model_id": rec.get("model_id", ""),
                "risk_tier": tier,
                "attestation_passed": bool(rec.get("attestation_passed", False)),
                "open_vex_alerts": open_vex,
                "policy_failure_rate": failure_rate,
            }
        )

    if model_profiles:
        overall_tier = min(
            (p["risk_tier"] for p in model_profiles),
            key=lambda t: tier_order.index(t) if t in tier_order else len(tier_order),
        )
    else:
        overall_tier = "MINIMAL"

    if output_json:
        import json as _json
        print(
            _json.dumps(
                {
                    "tenant_id": tenant_id,
                    "overall_risk_tier": overall_tier,
                    "model_count": len(model_profiles),
                    "models": model_profiles,
                },
                indent=2,
            )
        )
        return 0 if overall_tier in ("MINIMAL", "LIMITED") else 2

    if not quiet:
        icon = "\u2713" if overall_tier in ("MINIMAL", "LIMITED") else "\u2715"
        print(
            f"{icon} cloud-risk | {tenant_id} | {overall_tier} | {len(model_profiles)} model(s)"
        )
        if model_profiles:
            print(f"  {'Model':<35} {'Risk Tier':<15} {'Attested':<10} {'VEX Alerts'}")
            print("  " + "-" * 72)
            for mp in model_profiles:
                attest_mark = "PASS" if mp["attestation_passed"] else "FAIL"
                print(
                    f"  {mp['model_id']:<35} {mp['risk_tier']:<15} "
                    f"{attest_mark:<10} {mp['open_vex_alerts']}"
                )

    return 0 if overall_tier in ("MINIMAL", "LIMITED") else 2


def _cmd_cloud_remediate(args: argparse.Namespace, quiet: bool) -> int:
    """Generate a prioritised EU AI Act remediation plan for a tenant.

    Exit codes:
        0 = plan generated with zero critical steps (tenant is on track)
        1 = tenant not found
        2 = plan contains one or more priority-1 (critical) steps
    """
    try:
        from squash import api as _api
        from squash.risk import generate_remediation_plan
    except ImportError as exc:
        print(f"squash is not installed: {exc}", file=sys.stderr)
        return 2

    tenant_id: str = args.tenant_id
    output_json: bool = getattr(args, "output_json", False)

    if tenant_id not in _api._tenants:
        print(
            f"squash cloud-remediate: tenant not found: {tenant_id}",
            file=sys.stderr,
        )
        return 1

    inventory = _api._db_read_inventory(tenant_id)
    vex = _api._db_read_vex_alerts(tenant_id)
    open_vex = len(vex)

    tier_order = ["UNACCEPTABLE", "HIGH", "LIMITED", "MINIMAL"]
    if inventory:
        tiers = [_api._compute_model_risk_tier(rec, open_vex) for rec in inventory]
        overall_tier = min(
            tiers,
            key=lambda t: tier_order.index(t) if t in tier_order else len(tier_order),
        )
    else:
        overall_tier = "MINIMAL"

    # Aggregate worst-case policy results + attestation state
    all_policy_results: dict = {}
    all_attested = True
    for rec in inventory:
        if not rec.get("attestation_passed", True):
            all_attested = False
        for pname, presult in rec.get("policy_results", {}).items():
            if pname not in all_policy_results or not presult.get("passed", True):
                all_policy_results[pname] = presult

    steps = generate_remediation_plan(
        risk_tier=overall_tier,
        policy_results=all_policy_results,
        open_vex=open_vex,
        attestation_passed=all_attested,
    )
    critical_count = sum(1 for s in steps if s.priority == 1)

    if output_json:
        import json as _json
        print(
            _json.dumps(
                {
                    "tenant_id": tenant_id,
                    "risk_tier": overall_tier,
                    "total_steps": len(steps),
                    "critical_count": critical_count,
                    "steps": [
                        {
                            "id": s.id,
                            "priority": s.priority,
                            "action": s.action,
                            "description": s.description,
                            "evidence_required": s.evidence_required,
                            "estimated_effort": s.estimated_effort,
                        }
                        for s in steps
                    ],
                },
                indent=2,
            )
        )
        return 2 if critical_count > 0 else 0

    if not quiet:
        icon = "\u2713" if critical_count == 0 else "\u2715"
        print(
            f"{icon} cloud-remediate | {tenant_id} | {overall_tier} | "
            f"{len(steps)} step(s), {critical_count} critical"
        )
        for s in steps:
            pri_label = {1: "CRITICAL", 2: "HIGH", 3: "MEDIUM"}.get(s.priority, str(s.priority))
            print(f"  [{pri_label}] {s.action} ({s.estimated_effort})")
            print(f"         {s.description[:100]}")
            print(f"         Evidence: {s.evidence_required}")

    return 2 if critical_count > 0 else 0


# ---------------------------------------------------------------------------
# W135 / W136 — annex-iv generate + validate
# ---------------------------------------------------------------------------

def _cmd_annex_iv(args: argparse.Namespace, quiet: bool) -> int:  # noqa: C901
    """Dispatch annex-iv subcommands (generate / validate)."""
    subcmd = args.annex_iv_command

    if subcmd == "generate":
        return _cmd_annex_iv_generate(args, quiet)
    elif subcmd == "validate":
        return _cmd_annex_iv_validate(args, quiet)
    else:
        print("squash annex-iv: unknown subcommand", file=sys.stderr)
        return 1


def _cmd_annex_iv_generate(args: argparse.Namespace, quiet: bool) -> int:  # noqa: C901
    """W135 — generate Annex IV documentation from a training run directory."""
    from pathlib import Path as _Path

    try:
        from squash.artifact_extractor import ArtifactExtractor
        from squash.annex_iv_generator import AnnexIVGenerator, AnnexIVValidator
    except ImportError as exc:
        print(f"squash modules not available: {exc}", file=sys.stderr)
        return 2

    root = _Path(args.root).expanduser().resolve()
    if not root.exists():
        print(f"error: --root directory not found: {root}", file=sys.stderr)
        return 1

    output_dir = _Path(args.output_dir).expanduser().resolve() if args.output_dir else root
    output_dir.mkdir(parents=True, exist_ok=True)

    if not quiet:
        print(f"squash annex-iv generate | scanning {root}")

    # ── Phase 1: extract artifacts from run directory ─────────────────────────
    result = ArtifactExtractor.from_run_dir(root)

    if result.warnings and not quiet:
        for w in result.warnings:
            print(f"  [warn] {w}")

    # ── Phase 2: optional MLflow augmentation ─────────────────────────────────
    if args.mlflow_run:
        if not quiet:
            print(f"  [mlflow] fetching run {args.mlflow_run} from {args.mlflow_uri}")
        try:
            full = ArtifactExtractor.from_mlflow_run_full(
                args.mlflow_run, tracking_uri=args.mlflow_uri
            )
            if result.metrics is None:
                result.metrics = full.metrics
            if result.config is None:
                result.config = full.config
        except Exception as exc:
            print(f"  [warn] mlflow augmentation failed: {exc}", file=sys.stderr)

    # ── Phase 3: optional W&B augmentation ───────────────────────────────────
    if args.wandb_run:
        if not quiet:
            print(f"  [wandb] fetching run {args.wandb_run}")
        try:
            parts = args.wandb_run.split("/")
            run_id = parts[-1]
            project = parts[-2] if len(parts) >= 2 else None
            entity = parts[-3] if len(parts) >= 3 else None
            full = ArtifactExtractor.from_wandb_run_full(
                run_id, project=project, entity=entity
            )
            if result.metrics is None:
                result.metrics = full.metrics
            if result.config is None:
                result.config = full.config
        except Exception as exc:
            print(f"  [warn] wandb augmentation failed: {exc}", file=sys.stderr)

    # ── Phase 4: optional HuggingFace dataset provenance ─────────────────────
    if args.hf_datasets:
        if not quiet:
            print(f"  [hf] fetching provenance for: {', '.join(args.hf_datasets)}")
        try:
            datasets = ArtifactExtractor.from_huggingface_dataset_list(
                args.hf_datasets, token=args.hf_token
            )
            result.datasets.extend(datasets)
        except Exception as exc:
            print(f"  [warn] huggingface augmentation failed: {exc}", file=sys.stderr)

    # ── Phase 5: generate Annex IV document ──────────────────────────────────
    if not quiet:
        print("  [generate] building Annex IV document …")

    doc = AnnexIVGenerator().generate(
        result,
        system_name=args.system_name,
        version=args.version,
        intended_purpose=args.intended_purpose,
        risk_level=args.risk_level,
        general_description=args.general_description,
        hardware_requirements=args.hardware_requirements,
        deployment_context=args.deployment_context,
        risk_management=args.risk_management,
        oversight_description=args.oversight_description,
        model_type=args.model_type,
        lifecycle_plan=args.lifecycle_plan,
        monitoring_plan=args.monitoring_plan,
    )

    # ── Phase 6: save to disk ─────────────────────────────────────────────────
    written = doc.save(output_dir, formats=list(args.formats), stem=args.stem)

    # ── B2 (Sprint 15 W208) — branded PDF ────────────────────────────────────
    if getattr(args, "annex_iv_branded", False):
        try:
            from squash.pdf_report import BrandedPDFConfig, PDFReportBuilder
            from pathlib import Path as _Path
            logo_path = _Path(args.annex_iv_logo) if getattr(args, "annex_iv_logo", None) else None
            branded_cfg = BrandedPDFConfig(
                org_name=getattr(args, "annex_iv_org", "") or "",
                author=getattr(args, "annex_iv_author", "") or "",
                logo_path=logo_path,
                accent_color=getattr(args, "annex_iv_accent", "#22c55e") or "#22c55e",
            )
            branded_stem = (args.stem or "annex_iv") + "_branded"
            branded_written = PDFReportBuilder(branded_cfg).save(
                doc, output_dir, stem=branded_stem,
            )
            written.update({f"branded_{k}": v for k, v in branded_written.items()})
        except ImportError as exc:
            print(
                f"squash: branded PDF requires WeasyPrint — install with: "
                f"pip install weasyprint ({exc})",
                file=sys.stderr,
            )

    if not quiet:
        score_icon = "✅" if doc.overall_score >= 80 else ("⚠️" if doc.overall_score >= 40 else "❌")
        print(f"\n{score_icon}  Annex IV score: {doc.overall_score}/100 "
              f"({len(doc.complete_sections)}/12 sections complete)")
        for fmt, path in written.items():
            print(f"  [{fmt}] {path}")

    # ── Phase 7: validate ─────────────────────────────────────────────────────
    if args.no_validate:
        return 0

    report = AnnexIVValidator().validate(doc)

    if not quiet:
        print(f"\n{'Hard fails' if report.hard_fails else 'No hard fails'} | "
              f"{len(report.warnings)} warning(s) | "
              f"{len(report.info)} info")
        for f in report.hard_fails:
            print(f"  [FAIL] {f.section}: {f.message}")
        for w in report.warnings:
            print(f"  [WARN] {w.section}: {w.message}")

    if report.hard_fails:
        return 2
    if args.fail_on_warning and report.warnings:
        return 1
    return 0


def _cmd_annex_iv_validate(args: argparse.Namespace, quiet: bool) -> int:
    """W136 — validate an existing Annex IV JSON document."""
    from pathlib import Path as _Path
    import json as _json

    try:
        from squash.annex_iv_generator import AnnexIVDocument, AnnexIVValidator, AnnexIVSection
    except ImportError as exc:
        print(f"squash modules not available: {exc}", file=sys.stderr)
        return 2

    doc_path = _Path(args.document).expanduser().resolve()
    if not doc_path.exists():
        print(f"error: file not found: {doc_path}", file=sys.stderr)
        return 1

    try:
        raw = _json.loads(doc_path.read_text(encoding="utf-8"))
    except Exception as exc:
        print(f"error: could not parse JSON: {exc}", file=sys.stderr)
        return 1

    try:
        # Reconstruct AnnexIVDocument from saved JSON
        sections = []
        for s in raw.get("sections", []):
            sections.append(AnnexIVSection(
                key=s["key"],
                title=s["title"],
                article=s.get("article", ""),
                content=s.get("content", ""),
                completeness=s.get("completeness", 0),
                gaps=s.get("gaps", []),
            ))
        doc = AnnexIVDocument(
            system_name=raw.get("system_name", ""),
            version=raw.get("version", ""),
            generated_at=raw.get("generated_at", ""),
            sections=sections,
            overall_score=raw.get("overall_score", 0),
            metadata=raw.get("metadata", {}),
        )
    except Exception as exc:
        print(f"error: could not reconstruct AnnexIVDocument: {exc}", file=sys.stderr)
        return 1

    report = AnnexIVValidator().validate(doc)

    if not quiet:
        score_icon = "✅" if doc.overall_score >= 80 else ("⚠️" if doc.overall_score >= 40 else "❌")
        print(f"{score_icon}  {doc.system_name} v{doc.version} — score {doc.overall_score}/100")
        print(f"   Hard fails: {len(report.hard_fails)}  Warnings: {len(report.warnings)}  Info: {len(report.info)}")
        for f in report.hard_fails:
            print(f"  [FAIL] {f.section}: {f.message}")
        for w in report.warnings:
            print(f"  [WARN] {w.section}: {w.message}")
        for i in report.info:
            print(f"  [INFO] {i.section}: {i.message}")

    if report.hard_fails:
        return 2
    if args.fail_on_warning and report.warnings:
        return 1
    return 0


# ─────────────────────────────────────────────────────────────────────────────
# W160 — squash demo  (Konjo Edition)
# ─────────────────────────────────────────────────────────────────────────────

# ── ASCII banner ──────────────────────────────────────────────────────────────
_SQUASH_ART = [
    " ____   ___  _   _   _    ____  _   _ ",
    "/ ___| / _ \\| | | | / \\  / ___|| | | |",
    "\\___ \\| | | | | | |/ _ \\ \\___ \\| |_| |",
    " ___) | |_| | |_| / ___ \\ ___) |  _  |",
    "|____/ \\__\\_\\\\___/_/   \\_\\____/|_| |_|",
]
_ART_COLORS = ["#b794ff", "#a07cf8", "#7e91f0", "#5dd9ff", "#6df0c2"]

# ── Small Ollama models preferred for demo speed ──────────────────────────────
_DEMO_PREFERRED_MODELS = ["smollm", "qwen2.5", "qwen3", "tinyllama", "phi3"]

# ── Fallback synthetic model config ───────────────────────────────────────────
_DEMO_MODEL_CONFIG = """{
  "model_type": "bert",
  "hidden_size": 768,
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  "vocab_size": 30522,
  "architectures": ["BertForSequenceClassification"],
  "task_type": "text-classification"
}"""

_DEMO_TRAIN_CONFIG = """{
  "optimizer": {"type": "AdamW", "lr": 2e-5, "weight_decay": 0.01},
  "scheduler": "linear_warmup",
  "num_epochs": 3,
  "batch_size": 32,
  "max_seq_length": 128,
  "framework": "pytorch",
  "dataset": "imdb",
  "seed": 42
}"""

_DEMO_TRAIN_PY = '''"""Sample training script for squash demo."""
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from transformers import BertForSequenceClassification

model = BertForSequenceClassification.from_pretrained("bert-base-uncased")
optimizer = torch.optim.AdamW(model.parameters(), lr=2e-5, weight_decay=0.01)
criterion = nn.CrossEntropyLoss()

for epoch in range(3):
    for batch in DataLoader([]):
        outputs = model(**batch)
        loss = criterion(outputs.logits, batch["labels"])
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

torch.save(model.state_dict(), "model.pt")
'''


def _locate_demo_script(name: str) -> Path | None:
    candidates = [
        Path(__file__).resolve().parent.parent / "demo" / name,
        Path.cwd() / "demo" / name,
    ]
    for c in candidates:
        if c.exists():
            return c
    return None


def _run_demo_walkthrough() -> int:
    script = _locate_demo_script("demo.py")
    if script is None:
        print(
            "demo/demo.py not found. Install from source:\n"
            "  git clone https://github.com/konjoai/squash && cd squash && python demo/demo.py",
            file=sys.stderr,
        )
        return 2
    import subprocess as _sp
    return _sp.call([sys.executable, str(script)])


def _run_demo_server(port: int) -> int:
    script = _locate_demo_script("server.py")
    if script is None:
        print(
            "demo/server.py not found. Install from source:\n"
            "  git clone https://github.com/konjoai/squash && cd squash && python demo/server.py",
            file=sys.stderr,
        )
        return 2
    import subprocess as _sp
    return _sp.call([sys.executable, str(script), "--port", str(port)])


def _demo_score(findings: list) -> int:  # type: ignore[type-arg]
    if not findings:
        return 100
    score = 100
    for f in findings:
        if not getattr(f, "passed", True):
            score -= 15 if getattr(f, "severity", "") == "error" else 7
    return max(0, score)


def _find_ollama_models(count: int = 2) -> list[tuple[str, str, Path]]:
    """Return up to *count* (name, tag, manifest_path) tuples from the Ollama store.

    Prefers small models from _DEMO_PREFERRED_MODELS, then any others.
    """
    import random as _random
    base = Path.home() / ".ollama" / "models" / "manifests" / "registry.ollama.ai" / "library"
    if not base.exists():
        return []

    candidates: list[tuple[str, str, Path]] = []
    for model_dir in sorted(base.iterdir()):
        if not model_dir.is_dir():
            continue
        # Pick smallest/first tag
        tags = sorted(model_dir.iterdir(), key=lambda p: p.stat().st_size if p.is_file() else 999_999_999)
        for tag_file in tags:
            if tag_file.is_file():
                candidates.append((model_dir.name, tag_file.name, tag_file))
                break

    # Sort: preferred models first, then by manifest size (proxy for model size)
    def _sort_key(t: tuple[str, str, Path]) -> tuple[int, int]:
        name = t[0]
        pref = next((i for i, p in enumerate(_DEMO_PREFERRED_MODELS) if p in name), 99)
        size = t[2].stat().st_size
        return (pref, size)

    candidates.sort(key=_sort_key)
    return candidates[:count]


def _resolve_ollama_blob(manifest_path: Path) -> tuple[Path | None, int]:
    """Return (blob_path, size_bytes) for the GGUF model layer in *manifest_path*."""
    import json as _json
    blobs = Path.home() / ".ollama" / "models" / "blobs"
    try:
        m = _json.loads(manifest_path.read_text())
    except Exception:
        return None, 0
    for layer in m.get("layers", []):
        if layer.get("mediaType") == "application/vnd.ollama.image.model":
            digest = layer["digest"]
            blob_name = digest.replace(":", "-")
            bp = blobs / blob_name
            if bp.exists():
                return bp, layer.get("size", bp.stat().st_size)
    return None, 0


def _setup_ollama_model_dir(
    model_name: str, tag: str, manifest_path: Path, tmp_base: Path, well_documented: bool = False
) -> Path | None:
    """Symlink the GGUF blob into a temp dir that squash can attest."""
    import json as _json
    blob_path, blob_size = _resolve_ollama_blob(manifest_path)
    if blob_path is None:
        return None

    model_dir = tmp_base / f"{model_name}-{tag}"
    model_dir.mkdir(parents=True, exist_ok=True)

    # Symlink GGUF — avoids copying hundreds of MB
    gguf_link = model_dir / f"{model_name}.gguf"
    if not gguf_link.exists():
        gguf_link.symlink_to(blob_path)

    # Config — richer for "well_documented" model to produce better score
    cfg: dict = {
        "model_type": "gguf",
        "model_name": f"{model_name}:{tag}",
        "architecture": "transformer",
        "size_bytes": blob_size,
        "format": "gguf",
        "source": "ollama",
    }
    if well_documented:
        cfg.update({
            "quantization": "Q4_K_M",
            "license": "apache-2.0",
            "author": "HuggingFace",
            "training_data": ["common-crawl", "wikipedia"],
            "intended_use": "text-generation",
            "known_limitations": "May produce biased or inaccurate outputs.",
            "model_card": True,
        })
    (model_dir / "config.json").write_text(_json.dumps(cfg, indent=2))
    return model_dir


def _build_synthetic_model_dir(tmp_base: Path) -> Path:
    """Fallback: create a synthetic BERT-shaped model dir."""
    import struct as _struct
    model_dir = tmp_base / "bert-base-uncased"
    model_dir.mkdir(parents=True, exist_ok=True)
    header = b'{"weight":{"dtype":"F32","shape":[768,768],"data_offsets":[0,2359296]}}'
    hp = header + b" " * ((8 - len(header) % 8) % 8)
    (model_dir / "model.safetensors").write_bytes(_struct.pack("<Q", len(hp)) + hp + b"\x00" * 64)
    (model_dir / "config.json").write_text(_DEMO_MODEL_CONFIG)
    (model_dir / "training_config.json").write_text(_DEMO_TRAIN_CONFIG)
    (model_dir / "train.py").write_text(_DEMO_TRAIN_PY)
    return model_dir


def _cmd_demo(args: argparse.Namespace, quiet: bool) -> int:  # noqa: C901
    """W160 — Konjo Edition: animated, Ollama-native, side-by-side, sales-ready."""
    import random as _random
    import subprocess as _sp
    import tempfile
    import time
    import webbrowser

    # ── Mode dispatch ─────────────────────────────────────────────────────────
    if getattr(args, "explore", False) or getattr(args, "walkthrough", False):
        return _run_demo_walkthrough()
    if getattr(args, "server", False):
        return _run_demo_server(getattr(args, "port", 8002))

    no_open = getattr(args, "no_open", False) or quiet
    use_color = not (getattr(args, "no_color", False) or quiet)
    policy = getattr(args, "policy", None) or "eu-ai-act"

    # ── Rich setup ────────────────────────────────────────────────────────────
    _R = False
    _con = None
    if use_color:
        try:
            from rich.console import Console as _Console
            from rich.panel import Panel as _Panel
            from rich.progress import (
                Progress as _Progress,
                SpinnerColumn as _Spin,
                TextColumn as _TC,
                BarColumn as _Bar,
                TimeElapsedColumn as _TEC,
            )
            from rich.table import Table as _Table
            from rich.text import Text as _Text
            from rich.rule import Rule as _Rule
            from rich.columns import Columns as _Cols
            from rich import box as _box
            from rich.live import Live as _Live
            _R = True
            _con = _Console()
        except ImportError:
            _R = False

    def _p(msg: str = "") -> None:
        if _R:
            _con.print(msg)  # type: ignore[union-attr]
        else:
            print(msg)

    def _rule(title: str = "", color: str = "#232838") -> None:
        if _R:
            _con.print(_Rule(title, style=color))  # type: ignore[union-attr]
        else:
            w = 68
            if title:
                pad = (w - len(title) - 2) // 2
                print("─" * pad + f" {title} " + "─" * (w - pad - len(title) - 2))
            else:
                print("─" * w)

    def _step(icon: str, msg: str, detail: str = "", color: str = "#6df0c2") -> None:
        if _R:
            _con.print(f"  [{color}]{icon}[/{color}]  [white]{msg}[/white]" +  # type: ignore[union-attr]
                       (f"  [dim]{detail}[/dim]" if detail else ""))
        else:
            print(f"  {icon}  {msg}" + (f"  {detail}" if detail else ""))

    # ── ACT 0 — Animated banner ───────────────────────────────────────────────
    _p()
    if _R:
        from rich.text import Text as _Text2
        art = _Text2()
        for line, col in zip(_SQUASH_ART, _ART_COLORS):
            art.append(line + "\n", style=f"bold {col}")
        _con.print(art, justify="center")  # type: ignore[union-attr]
        time.sleep(0.25)
        _con.print(  # type: ignore[union-attr]
            _Panel.fit(  # type: ignore[union-attr]
                "[dim]Prove your AI is trustworthy.  [bold #b794ff]Konjo Edition.[/bold #b794ff][/dim]\n"
                "[dim]Scanning real models · EU AI Act · Cryptographic attestation[/dim]",
                border_style="#b794ff",
                padding=(0, 6),
            )
        )
    else:
        for line in _SQUASH_ART:
            print(line)
        print()
        print("  Prove your AI is trustworthy — Konjo Edition")
        print("  Scanning real models · EU AI Act · Cryptographic attestation")
    _p()
    time.sleep(0.9)

    # ── ACT I — Discover models ───────────────────────────────────────────────
    _rule("  SCANNING  ", "#b794ff")
    _p()

    ollama_models = _find_ollama_models(2)
    tmp_base = Path(tempfile.mkdtemp(prefix="squash_demo_"))
    model_dirs: list[tuple[str, Path]] = []

    if len(ollama_models) >= 2:
        for i, (mname, tag, mpath) in enumerate(ollama_models):
            well_doc = (i == 0)  # first model gets richer config for score contrast
            mdir = _setup_ollama_model_dir(mname, tag, mpath, tmp_base, well_documented=well_doc)
            if mdir:
                display = f"{mname}:{tag}"
                model_dirs.append((display, mdir))
                blob_path, blob_size = _resolve_ollama_blob(mpath)
                sz = f"{blob_size // 1024 // 1024} MB" if blob_size > 0 else "?"
                _step("✓", f"Found  [bold white]{display}[/bold white]", sz if _R else f"  {sz}")
                time.sleep(0.3)
    else:
        # Fallback to synthetic models
        for i, label in enumerate(["bert-base-uncased", "bert-large-uncased"]):
            mdir = _build_synthetic_model_dir(tmp_base)
            model_dirs.append((label, mdir))
            _step("✓", f"Using synthetic  [bold white]{label}[/bold white]", "sample model")
            time.sleep(0.3)

    if len(model_dirs) < 2:
        mdir = _build_synthetic_model_dir(tmp_base)
        model_dirs.append(("bert-base-uncased (fallback)", mdir))

    _p()
    _rule()

    # ── ACT II — Scan both models ─────────────────────────────────────────────
    try:
        from squash.attest import AttestConfig, AttestPipeline
    except ImportError as exc:
        print(f"squash modules not available: {exc}", file=sys.stderr)
        return 2

    # Persistent output dirs
    from datetime import datetime as _dt
    ts = _dt.now().strftime("%Y-%m-%d-%H%M%S")
    if args.output_dir:
        out_base = Path(args.output_dir).expanduser().resolve()
    else:
        desktop = Path.home() / "Desktop"
        out_base = (desktop / "squash-demo" / ts) if desktop.exists() else (
            Path.home() / ".squash" / "demos" / ts
        )
    out_base.mkdir(parents=True, exist_ok=True)

    # Use two frameworks for natural score contrast — stronger sales narrative
    _DEMO_POLICIES = [policy, "nist-ai-rmf"] if policy == "eu-ai-act" else [policy, "eu-ai-act"]

    results: list[tuple[str, object, Path, float, str]] = []  # (display_name, result, out_dir, ms, policy_used)

    for idx, (display_name, model_dir) in enumerate(model_dirs):
        model_policy = _DEMO_POLICIES[idx % len(_DEMO_POLICIES)]
        scan_steps = [
            "Hashing input manifest (SHA-256, Step 0)…",
            "Building CycloneDX ML-BOM…",
            "Building SPDX 2.3 AI profile…",
            "Running GGUF security scan…",
            f"Evaluating {model_policy.upper().replace('-', ' ')} policy…",
            "Writing signed attestation certificate…",
        ]

        _p()
        if _R:
            _con.print(  # type: ignore[union-attr]
                f"  [bold #5dd9ff]▶  Scanning[/bold #5dd9ff]  [bold white]{display_name}[/bold white]"
                f"  [dim]({model_policy})[/dim]"
            )
        else:
            print(f"  ▶  Scanning  {display_name}  ({model_policy})")

        out_dir = out_base / display_name.replace(":", "-").replace("/", "-")
        out_dir.mkdir(parents=True, exist_ok=True)

        config = AttestConfig(
            model_path=model_dir,
            output_dir=out_dir,
            model_id=display_name,
            policies=[model_policy],
            sign=False,
            fail_on_violation=False,
            emit_input_manifest=True,
        )

        import threading as _threading
        result_box: list = []
        exc_box: list = []

        def _run_attest() -> None:  # noqa: E306
            try:
                result_box.append(AttestPipeline.run(config))
            except Exception as e:
                exc_box.append(e)

        t0 = time.perf_counter()
        thr = _threading.Thread(target=_run_attest, daemon=True)
        thr.start()

        if _R:
            with _Progress(  # type: ignore[union-attr]
                _Spin(style="#b794ff"),  # type: ignore[union-attr]
                _TC("[dim]{task.description}[/dim]"),  # type: ignore[union-attr]
                _TEC(),  # type: ignore[union-attr]
                transient=True,
                console=_con,
            ) as prog:
                task = prog.add_task("", total=len(scan_steps))
                step_idx = 0
                while thr.is_alive() or step_idx < len(scan_steps):
                    if step_idx < len(scan_steps):
                        prog.update(task, description=f"  {scan_steps[step_idx]}")
                    thr.join(timeout=0.4)
                    if step_idx < len(scan_steps):
                        prog.advance(task)
                        step_idx += 1
                    if not thr.is_alive():
                        # Flush remaining steps quickly
                        while step_idx < len(scan_steps):
                            prog.update(task, description=f"  {scan_steps[step_idx]}")
                            prog.advance(task)
                            step_idx += 1
                            time.sleep(0.12)
                        break
        else:
            for s in scan_steps:
                print(f"    ⠋ {s}")
            thr.join()

        elapsed_ms = (time.perf_counter() - t0) * 1000

        result = result_box[0] if result_box else None
        if result is None:
            print(f"  [warn] scan failed for {display_name}", file=sys.stderr)
            continue

        # Tick completed steps
        completed = [
            ("✓", "Input manifest hashed", "SHA-256 · Step 0"),
            ("✓", "CycloneDX ML-BOM built", "v1.7"),
            ("✓", "SPDX AI profile built", "v2.3"),
            ("✓", "Security scan clean", "No exploits"),
            ("✓", f"{policy.upper().replace('-', ' ')} evaluated", ""),
            ("✓", "Attestation certificate", "squash-attest.json"),
        ]
        for icon, msg, det in completed:
            _step(icon, msg, det)
            time.sleep(0.1)

        results.append((display_name, result, out_dir, elapsed_ms, model_policy))
        time.sleep(0.4)

    if not results:
        print("No models could be scanned.", file=sys.stderr)
        return 1

    # ── ACT III — Computing scores (dramatic pause) ───────────────────────────
    _p()
    if _R:
        with _Progress(  # type: ignore[union-attr]
            _Spin("aesthetic", style="#b794ff"),  # type: ignore[union-attr]
            _TC("[dim]  Computing compliance scores…[/dim]"),  # type: ignore[union-attr]
            transient=True,
            console=_con,
        ) as p2:
            t2 = p2.add_task("")
            time.sleep(1.4)
    else:
        print("  Computing compliance scores…")
        time.sleep(1.4)

    # ── ACT IV — Side-by-side verdict ────────────────────────────────────────
    _p()
    _rule("  COMPLIANCE VERDICT  ", "#6df0c2")
    _p()
    time.sleep(0.5)

    all_model_findings: list[tuple[str, list, int, float, str]] = []
    for display_name, result, out_dir, elapsed_ms, mpol in results:
        findings: list = []
        for pr in getattr(result, "policy_results", {}).values():
            findings.extend(getattr(pr, "findings", []))
        score = _demo_score(findings)
        all_model_findings.append((display_name, findings, score, elapsed_ms, mpol))

    score_color = lambda s: "#6df0c2" if s >= 80 else ("#f7b955" if s >= 60 else "#ff6b8a")  # noqa: E731
    score_label = lambda s: "EXCELLENT" if s >= 90 else ("GOOD" if s >= 80 else ("NEEDS WORK" if s >= 60 else "HIGH RISK"))  # noqa: E731

    if _R and len(all_model_findings) >= 2:
        # Two-column comparison table
        t = _Table(  # type: ignore[union-attr]
            show_header=True,
            box=_box.ROUNDED,  # type: ignore[union-attr]
            border_style="#2f3548",
            header_style="bold #b794ff",
            expand=True,
        )
        t.add_column("", style="dim", width=22, no_wrap=True)
        for display_name, _, score, _, mpol in all_model_findings:
            sc = score_color(score)
            t.add_column(
                f"[bold white]{display_name}[/bold white]\n[dim]{mpol}[/dim]",
                justify="center",
                no_wrap=True,
            )

        # Score row
        score_cells = []
        for _, _, score, _, _ in all_model_findings:
            sc = score_color(score)
            sl = score_label(score)
            bar = "█" * int(score / 100 * 12) + "░" * (12 - int(score / 100 * 12))
            score_cells.append(f"[bold {sc}]{score}/100[/bold {sc}]\n[{sc}]{bar}[/{sc}]\n[dim]{sl}[/dim]")
        t.add_row("[bold]Compliance Score[/bold]", *score_cells)
        time.sleep(0.3)

        # Stats rows
        for row_label, extractor in [
            ("Passed", lambda f: str(sum(1 for x in f if getattr(x, "passed", True)))),
            ("Violations", lambda f: str(sum(1 for x in f if not getattr(x, "passed", True) and getattr(x, "severity", "") == "error"))),
            ("Warnings", lambda f: str(sum(1 for x in f if not getattr(x, "passed", True) and getattr(x, "severity", "") == "warning"))),
        ]:
            cells = [extractor(f) for _, f, _, _, _ in all_model_findings]
            t.add_row(f"[dim]{row_label}[/dim]", *cells)
            time.sleep(0.2)

        # Top failing check per model
        for fn_label, fn in [("Top issue", lambda f: next((getattr(x, "rule_id", "?") + " — " + getattr(x, "rationale", "")[:45] + "…" for x in f if not getattr(x, "passed", True)), "None ✓"))]:
            cells = [fn(f) for _, f, _, _, _ in all_model_findings]
            t.add_row(f"[dim]{fn_label}[/dim]", *[f"[dim]{c}[/dim]" for c in cells])

        _con.print(t)  # type: ignore[union-attr]
    else:
        # Plain text fallback
        for display_name, findings, score, elapsed_ms, mpol in all_model_findings:
            errs = sum(1 for f in findings if not getattr(f, "passed", True) and getattr(f, "severity", "") == "error")
            warns = sum(1 for f in findings if not getattr(f, "passed", True) and getattr(f, "severity", "") == "warning")
            passed = sum(1 for f in findings if getattr(f, "passed", True))
            print(f"  {display_name}: {score}/100 — {passed} passed, {errs} violations, {warns} warnings")

    _p()
    time.sleep(1.2)

    # ── ACT V — Generate HTML report ─────────────────────────────────────────
    _rule("  OUTPUT  ", "#5dd9ff")
    _p()

    # Collect artifacts for each model
    model_data_for_report: list[dict] = []
    for display_name, result, out_dir, elapsed_ms, _mpol in results:
        findings = []
        for pr in getattr(result, "policy_results", {}).values():
            findings.extend(getattr(pr, "findings", []))
        score = _demo_score(findings)
        artifacts = sorted(
            [(f.name, f.stat().st_size, f.read_text(errors="replace") if f.suffix == ".json" and f.stat().st_size < 64 * 1024 else None)
             for f in out_dir.rglob("*") if f.is_file()],
            key=lambda x: x[0],
        )
        model_data_for_report.append({
            "name": display_name,
            "policy": mpol,
            "score": score,
            "findings": findings,
            "artifacts": [(n, s, c) for n, s, c in artifacts],
            "elapsed_ms": elapsed_ms,
        })
        _step("✓", f"Artifacts  [bold white]{display_name}[/bold white]", f"{len(artifacts)} files → {out_dir}")

    try:
        from squash.demo_report import generate_comparison as _gen_cmp
        import squash as _sq
        version = getattr(_sq, "__version__", "3.0.1")
        report_path = _gen_cmp(
            models=model_data_for_report,
            policy=policy,
            output_dir=out_base,
            squash_version=version,
        )
        _step("✓", "HTML report generated", report_path.name)
    except Exception as _e:
        report_path = None
        _step("⚠", "Report generation skipped", str(_e)[:60], color="#f7b955")

    _p()

    # ── ACT VI — Launch interactive server in background ─────────────────────
    server_port = getattr(args, "port", 8002)
    _server_proc = None
    script = _locate_demo_script("server.py")
    if script and not no_open:
        try:
            _server_proc = _sp.Popen(
                [sys.executable, str(script), "--port", str(server_port)],
                stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
            )
            time.sleep(0.6)  # Let server spin up
            _step("✓", f"Interactive demo server", f"localhost:{server_port}", color="#b794ff")
        except Exception:
            pass

    # ── ACT VII — Transition animation → open browser ────────────────────────
    if not no_open and report_path and report_path.exists():
        _p()
        if _R:
            with _Progress(  # type: ignore[union-attr]
                _Bar(complete_style="#b794ff", finished_style="#6df0c2"),  # type: ignore[union-attr]
                _TC("[dim]  Launching compliance report…[/dim]"),  # type: ignore[union-attr]
                transient=True,
                console=_con,
            ) as prog:
                t3 = prog.add_task("", total=100)
                for i in range(0, 101, 5):
                    prog.update(t3, completed=i)
                    time.sleep(0.04)
        else:
            for i in range(3, 0, -1):
                print(f"  Launching report in {i}…")
                time.sleep(0.8)

        webbrowser.open(f"file://{report_path.resolve()}")
        _step("↗", "Compliance report", str(report_path.name), color="#5dd9ff")

    # ── Footer ────────────────────────────────────────────────────────────────
    _p()
    if _R:
        _con.print(  # type: ignore[union-attr]
            _Panel.fit(  # type: ignore[union-attr]
                "[dim]Real models · Real scan · Real cryptographic attestation.[/dim]\n"
                "[dim]This is what runs in your CI pipeline — [bold white]automatically.[/bold white][/dim]\n\n"
                "[bold #5dd9ff]pip install squash-ai[/bold #5dd9ff]"
                "[dim]  ·  [/dim]"
                "[bold white]squash attest ./your-model --policy eu-ai-act[/bold white]",
                border_style="#b794ff",
                padding=(1, 4),
            )
        )
    else:
        _rule()
        print("  Real models · Real scan · Real cryptographic attestation.")
        print("  pip install squash-ai && squash attest ./your-model --policy eu-ai-act")
        _rule()
    _p()

    # Cleanup temp model staging dirs (NOT output)
    import shutil as _shutil
    try:
        _shutil.rmtree(tmp_base, ignore_errors=True)
    except Exception:
        pass

    return 0


# ─────────────────────────────────────────────────────────────────────────────
# W162 — squash init
# ─────────────────────────────────────────────────────────────────────────────

_SQUASH_YML_TEMPLATE = """\
# .squash.yml — Squash AI compliance configuration
# Generated by: squash init
# Docs: https://github.com/konjoai/squash

project:
  name: "{project_name}"
  version: "1.0.0"
  risk_level: "limited"       # minimal | limited | high | unacceptable

attestation:
  model_path: "./models"      # path to model directory or file
  output_dir: "./attestation" # where artifacts are written

  # Compliance policy frameworks to evaluate
  policies:{policies_block}

  # Enable Sigstore keyless signing (requires internet)
  sign: false

  # Fail the CI job on policy violation
  fail_on_violation: true

  # Generate EU AI Act Annex IV technical documentation
  annex_iv: false

# MLframework detection: {framework}
framework:
  detected: "{framework}"

# CI/CD integration: add this to your pipeline
# GitHub Actions:  uses: konjoai/squash@v1
# GitLab CI:       include: integrations/gitlab-ci/squash.gitlab-ci.yml
# Jenkins:         squashAttest modelPath: "./models"
"""

_FRAMEWORK_INDICATORS = {
    "pytorch": ["torch", "pytorch", "*.pt", "*.pth", "*.bin"],
    "tensorflow": ["tensorflow", "keras", "saved_model", "*.pb", "*.h5"],
    "huggingface": ["transformers", "config.json", "tokenizer_config.json", "*.safetensors"],
    "mlflow": ["mlruns", "MLproject", "conda.yaml"],
    "wandb": ["wandb", ".wandb"],
    "jax": ["jax", "flax", "orbax"],
    "mlx": ["mlx"],
}


def _detect_framework(directory: "Path") -> str:
    """Detect ML framework from directory contents."""
    from pathlib import Path as _Path
    d = _Path(directory)

    # Check requirements files
    for req_file in ["requirements.txt", "pyproject.toml", "setup.py", "setup.cfg"]:
        req_path = d / req_file
        if req_path.exists():
            try:
                content = req_path.read_text(encoding="utf-8", errors="ignore").lower()
                for framework, indicators in _FRAMEWORK_INDICATORS.items():
                    if any(ind.lower() in content for ind in indicators if not ind.startswith("*")):
                        return framework
            except OSError:
                pass

    # Check for model files and directories
    for framework, indicators in _FRAMEWORK_INDICATORS.items():
        for pattern in indicators:
            if pattern.startswith("*"):
                if list(d.rglob(pattern)):
                    return framework
            elif (d / pattern).exists():
                return framework

    # Check Python imports in .py files
    for py_file in list(d.rglob("*.py"))[:20]:  # limit scan
        try:
            content = py_file.read_text(encoding="utf-8", errors="ignore")
            for framework in ["torch", "tensorflow", "jax", "mlx", "transformers"]:
                if f"import {framework}" in content or f"from {framework}" in content:
                    fw_map = {"torch": "pytorch", "tensorflow": "tensorflow",
                              "jax": "jax", "mlx": "mlx", "transformers": "huggingface"}
                    return fw_map.get(framework, framework)
        except OSError:
            pass

    return "unknown"


def _cmd_init(args: argparse.Namespace, quiet: bool) -> int:
    """W162 — scaffold .squash.yml and run a dry-run attestation."""
    from pathlib import Path as _Path

    project_dir = _Path(getattr(args, "dir", ".")).expanduser().resolve()
    if not project_dir.exists():
        print(f"error: directory not found: {project_dir}", file=sys.stderr)
        return 1

    squash_yml = project_dir / ".squash.yml"
    if squash_yml.exists() and not quiet:
        print(f"[squash init] .squash.yml already exists at {squash_yml}")
        print("  Delete it and re-run to regenerate.\n")
        return 0

    # Detect framework
    framework = getattr(args, "framework", "") or _detect_framework(project_dir)

    # Policies
    policies = getattr(args, "policy", None) or ["eu-ai-act"]
    policies_block = "\n" + "".join(f"    - {p}\n" for p in policies)

    project_name = project_dir.name

    yml_content = _SQUASH_YML_TEMPLATE.format(
        project_name=project_name,
        framework=framework,
        policies_block=policies_block.rstrip("\n"),
    )
    squash_yml.write_text(yml_content, encoding="utf-8")

    if not quiet:
        fw_display = f" [{framework}]" if framework != "unknown" else ""
        print(f"\n[squash init]{fw_display}")
        print(f"  ✅ Created {squash_yml}")
        print(f"  Framework detected: {framework}")
        print(f"  Policies: {', '.join(policies)}")

    # Dry run
    dry_run = getattr(args, "dry_run", True)
    if dry_run:
        if not quiet:
            print("\n  Running dry-run attestation to validate configuration…\n")
        try:
            from squash.attest import AttestConfig, AttestPipeline
            import tempfile

            config = AttestConfig(
                model_path=project_dir,
                output_dir=_Path(tempfile.mkdtemp(prefix="squash_init_")),
                policies=policies,
                sign=False,
                fail_on_violation=False,
            )
            result = AttestPipeline.run(config)
            if not quiet:
                icon = "✅" if result.passed else "⚠️"
                print(f"  {icon} Dry-run complete — passed: {result.passed}")
                print("\n  Next steps:")
                print("    1. Edit .squash.yml to match your model path")
                print("    2. Run: squash attest .")
                print("    3. Add to CI: uses: konjoai/squash@v1")
                print()
        except Exception as exc:  # noqa: BLE001
            if not quiet:
                print(f"  ⚠️  Dry-run skipped ({exc})")
                print("  Edit .squash.yml and run: squash attest .")

    return 0


# ─────────────────────────────────────────────────────────────────────────────
# W167 — squash watch
# ─────────────────────────────────────────────────────────────────────────────

_WATCH_EXTENSIONS = frozenset({
    ".safetensors", ".bin", ".pt", ".pth", ".pb", ".h5", ".onnx",
    ".pkl", ".joblib", ".json", ".yaml", ".yml",
})


def _snapshot_dir(directory: "Path") -> dict[str, float]:
    """Return a {relative_path: mtime} snapshot of watched files."""
    from pathlib import Path as _Path
    snap = {}
    for f in _Path(directory).rglob("*"):
        if f.is_file() and f.suffix in _WATCH_EXTENSIONS:
            try:
                snap[str(f.relative_to(directory))] = f.stat().st_mtime
            except (OSError, ValueError):
                pass
    return snap


def _cmd_watch(args: argparse.Namespace, quiet: bool) -> int:
    """W167 — watch a model directory and re-attest on file changes."""
    import time
    from pathlib import Path as _Path

    try:
        from squash.attest import AttestConfig, AttestPipeline
    except ImportError as exc:
        print(f"squash modules not available: {exc}", file=sys.stderr)
        return 2

    watch_path = _Path(getattr(args, "path", ".")).expanduser().resolve()
    if not watch_path.exists():
        print(f"error: path not found: {watch_path}", file=sys.stderr)
        return 1

    policies = getattr(args, "policy", ["eu-ai-act"])
    interval = max(1, getattr(args, "interval", 5))
    on_fail = getattr(args, "on_fail", "log")
    out_dir = _Path(args.output_dir).expanduser().resolve() if args.output_dir else watch_path / "attestation"
    out_dir.mkdir(parents=True, exist_ok=True)

    if not quiet:
        print(f"\n[squash watch] Watching {watch_path}")
        print(f"  Policies: {', '.join(policies)}")
        print(f"  Interval: {interval}s  |  On-fail: {on_fail}")
        print(f"  Press Ctrl+C to stop.\n")

    last_snap = _snapshot_dir(watch_path)
    run_count = 0

    def _run_attestation() -> bool:
        nonlocal run_count
        run_count += 1
        if not quiet:
            print(f"[squash watch] Run #{run_count} — {time.strftime('%H:%M:%S')}")
        config = AttestConfig(
            model_path=watch_path,
            output_dir=out_dir,
            policies=policies,
            sign=False,
            fail_on_violation=False,
        )
        try:
            result = AttestPipeline.run(config)
            icon = "✅" if result.passed else "❌"
            if not quiet:
                print(f"  {icon} {'PASSED' if result.passed else 'FAILED'}")
            return result.passed
        except Exception as exc:  # noqa: BLE001
            if not quiet:
                print(f"  ⚠️  Attestation error: {exc}")
            return False

    # Initial run
    passed = _run_attestation()
    if not passed and on_fail == "exit":
        return 1

    try:
        while True:
            time.sleep(interval)
            snap = _snapshot_dir(watch_path)
            if snap != last_snap:
                changed = set(snap) - set(last_snap) | {k for k in snap if snap[k] != last_snap.get(k)}
                if not quiet and changed:
                    print(f"[squash watch] Changed: {', '.join(sorted(changed)[:5])}")
                last_snap = snap
                passed = _run_attestation()
                if not passed:
                    if on_fail == "exit":
                        return 1
                    if on_fail == "notify":
                        try:
                            from squash.notifications import notify, ATTESTATION_FAILED
                            notify(ATTESTATION_FAILED, model_id=watch_path.name)
                        except Exception:  # noqa: BLE001
                            pass
    except KeyboardInterrupt:
        if not quiet:
            print(f"\n[squash watch] Stopped after {run_count} run(s).")

    return 0


# ─────────────────────────────────────────────────────────────────────────────
# W168 — squash install-hook
# ─────────────────────────────────────────────────────────────────────────────

_PRE_PUSH_HOOK = """\
#!/bin/sh
# squash pre-push hook — installed by: squash install-hook
set -e
echo "[squash] Running attestation before push…"
squash attest . {policy_flags} --fail-on-violation
echo "[squash] Attestation passed."
"""

_PRE_COMMIT_HOOK = """\
#!/bin/sh
# squash pre-commit hook — installed by: squash install-hook
set -e
echo "[squash] Running attestation before commit…"
squash attest . {policy_flags} --fail-on-violation
echo "[squash] Attestation passed."
"""


def _cmd_install_hook(args: argparse.Namespace, quiet: bool) -> int:
    """W168 — install squash as a git pre-push / pre-commit hook."""
    from pathlib import Path as _Path
    import stat

    repo_dir = _Path(getattr(args, "dir", ".")).expanduser().resolve()
    git_dir = repo_dir / ".git"
    if not git_dir.exists():
        print(f"error: not a git repository: {repo_dir}", file=sys.stderr)
        return 1

    hook_type = getattr(args, "hook_type", "pre-push")
    policies = getattr(args, "policy", ["eu-ai-act"])
    policy_flags = " ".join(f"--policy {p}" for p in policies) if policies else ""

    hook_path = git_dir / "hooks" / hook_type
    (git_dir / "hooks").mkdir(exist_ok=True)

    template = _PRE_PUSH_HOOK if hook_type == "pre-push" else _PRE_COMMIT_HOOK
    hook_content = template.format(policy_flags=policy_flags)

    if hook_path.exists():
        existing = hook_path.read_text(encoding="utf-8")
        if "squash" in existing:
            if not quiet:
                print(f"[squash install-hook] Hook already installed at {hook_path}")
            return 0
        backup = hook_path.with_suffix(".bak")
        hook_path.rename(backup)
        if not quiet:
            print(f"[squash install-hook] Backed up existing hook to {backup}")
        hook_content = existing.rstrip("\n") + "\n\n" + hook_content

    hook_path.write_text(hook_content, encoding="utf-8")
    hook_path.chmod(hook_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)

    if not quiet:
        print(f"\n[squash install-hook]")
        print(f"  ✅ Installed {hook_type} hook at {hook_path}")
        print(f"  Policies: {', '.join(policies)}")
        print(f"  Remove with: rm {hook_path}\n")

    return 0


def _cmd_iso42001(args: argparse.Namespace, quiet: bool) -> int:
    """W170 — ISO 42001 readiness assessment."""
    from squash.iso42001 import Iso42001Assessor

    model_path = Path(args.model_path)
    if not model_path.exists():
        print(f"[squash iso42001] ERROR: path not found: {model_path}", file=sys.stderr)
        return 1

    report = Iso42001Assessor.assess(model_path)

    output_path = args.output
    if output_path is None:
        output_path = model_path / "iso42001_report.json" if model_path.is_dir() else Path("iso42001_report.json")
    else:
        output_path = Path(output_path)

    report.save(output_path)

    if args.format == "json":
        print(json.dumps(report.to_dict(), indent=2))
    else:
        if not quiet:
            print(report.summary())
            print(f"\n[squash iso42001] Report written to {output_path}")
            print(f"  Readiness: {report.readiness_level.value}  Score: {report.overall_score:.1f}%")

    if args.fail_below is not None and report.overall_score < args.fail_below:
        print(f"[squash iso42001] FAIL: score {report.overall_score:.1f}% < threshold {args.fail_below}%",
              file=sys.stderr)
        return 2
    return 0


def _cmd_trust_package(args: argparse.Namespace, quiet: bool) -> int:
    """W171 — Trust Package export."""
    from squash.trust_package import TrustPackageBuilder

    model_path = Path(args.model_path)
    model_id = args.model_id or model_path.name
    output_path = Path(args.output) if args.output else Path(f"{model_id}-trust-package.zip")

    pkg = TrustPackageBuilder.build(
        model_path=model_path,
        output_path=output_path,
        model_id=model_id,
        sign=args.sign,
        verification_url=args.verification_url,
    )

    if not quiet:
        print(pkg.summary())
        print(f"\n[squash trust-package] Package written to {output_path}")
        print(f"  Artifacts: {len(pkg.artifacts_included)}")
        print(f"  Verify with: squash verify-trust-package {output_path}")
    return 0


def _cmd_verify_trust_package(args: argparse.Namespace, quiet: bool) -> int:
    """W171 — Trust Package verification."""
    from squash.trust_package import TrustPackageVerifier

    result = TrustPackageVerifier.verify(Path(args.package_path))

    if args.output_json:
        print(json.dumps({
            "passed": result.passed,
            "package_path": result.package_path,
            "integrity_errors": result.integrity_errors,
            "missing_artifacts": result.missing_artifacts,
            "compliance_summary": result.compliance_summary,
        }, indent=2))
    elif not quiet:
        print(result.summary())

    if args.fail_on_error and not result.passed:
        return 2
    return 0 if result.passed else 1


def _cmd_agent_audit(args: argparse.Namespace, quiet: bool) -> int:
    """W172 — OWASP Agentic AI Top 10 agent audit."""
    from squash.agent_audit import AgentAuditor, RiskLevel

    manifest_path = Path(args.manifest_path)
    if not manifest_path.exists():
        print(f"[squash agent-audit] ERROR: manifest not found: {manifest_path}", file=sys.stderr)
        return 1

    report = AgentAuditor.audit(manifest_path)

    if args.output:
        report.save(Path(args.output))

    if args.format == "json":
        print(json.dumps(report.to_dict(), indent=2))
    else:
        if not quiet:
            print(report.summary())
            if args.output:
                print(f"\n[squash agent-audit] Report written to {args.output}")
            print(f"\n[squash agent-audit] Overall Risk: {report.overall_risk.value}  Score: {report.risk_score}/100")

    if args.fail_on_critical and report.critical_count > 0:
        return 2
    if args.fail_on_high and (report.critical_count > 0 or report.high_count > 0):
        return 2
    return 0


def _cmd_incident(args: argparse.Namespace, quiet: bool) -> int:
    """W173 — Incident response package generation."""
    from squash.incident import IncidentResponder

    model_path = Path(args.model_path)
    pkg = IncidentResponder.respond(
        model_path=model_path,
        description=args.description,
        timestamp=args.timestamp,
        severity=args.severity,
        category=args.category,
        affected_persons=args.affected_persons,
        model_id=args.model_id,
    )

    output_dir = Path(args.output_dir) if args.output_dir else Path(f"incident-{pkg.incident_id}")
    pkg.save(output_dir)

    if not quiet:
        print(pkg.summary())
        print(f"\n[squash incident] Incident package written to {output_dir}/")

    return 0


def _cmd_annual_review(args: argparse.Namespace, quiet: bool) -> int:
    """W182 — Annual review generator."""
    from squash.annual_review import AnnualReviewGenerator
    model_paths = [Path(args.model_path)] if getattr(args, "model_path", None) else None
    models_dir = Path(args.models_dir) if getattr(args, "models_dir", None) else None
    review = AnnualReviewGenerator.generate(
        year=getattr(args, "year", None),
        models_dir=models_dir,
        model_paths=model_paths,
    )
    if getattr(args, "output_json", False):
        print(json.dumps(review.to_dict(), indent=2))
        return 0
    if not quiet:
        print(review.executive_summary())
    output_dir = Path(args.output_dir) if getattr(args, "output_dir", None) else Path(f"annual-review-{review.year}")
    written = review.save(output_dir)
    if not quiet:
        print(f"\n[squash annual-review] Written to {output_dir}/")
        for f in written:
            print(f"  {f}")
    return 0


def _cmd_publish(args: argparse.Namespace, quiet: bool) -> int:
    """W183 — Publish attestation to registry."""
    from squash.attestation_registry import AttestationRegistry
    model_path = Path(args.model_path)
    db = Path(args.db) if getattr(args, "db", None) else None
    attest_path = None
    for candidate in [
        model_path / "squash_attestation.json",
        model_path / "squash-attest.json",
    ]:
        if candidate.exists():
            attest_path = candidate
            break
    with AttestationRegistry(db) as reg:
        entry = reg.publish(
            model_id=getattr(args, "model_id", None) or model_path.name,
            attestation_path=attest_path,
            org=args.org,
            is_public=not getattr(args, "private", False),
        )
    if not quiet:
        print(f"[squash publish] Published: {entry.uri}")
        print(f"  Verify: {entry.verify_url}")
        print(f"  Entry ID: {entry.entry_id}")
    return 0


def _cmd_lookup(args: argparse.Namespace, quiet: bool) -> int:
    """W183 — Lookup attestation registry."""
    from squash.attestation_registry import AttestationRegistry
    db = Path(args.db) if getattr(args, "db", None) else None
    with AttestationRegistry(db) as reg:
        entries = reg.lookup(
            model_id=getattr(args, "model_id", None),
            org=getattr(args, "org", None),
            entry_id=getattr(args, "entry_id", None),
        )
    if getattr(args, "output_json", False):
        print(json.dumps([e.to_dict() for e in entries], indent=2))
    else:
        if not entries:
            print("[squash lookup] No entries found.")
        for e in entries:
            print(f"  {e.uri}  score={e.compliance_score}  published={e.published_at[:10]}")
    return 0


def _cmd_verify_entry(args: argparse.Namespace, quiet: bool) -> int:
    """W183 — Verify registry entry."""
    from squash.attestation_registry import AttestationRegistry
    db = Path(args.db) if getattr(args, "db", None) else None
    with AttestationRegistry(db) as reg:
        result = reg.verify(args.entry_id)
    status = "VALID" if result.valid else "INVALID"
    if not quiet:
        print(f"[squash verify-entry] {status}  {args.entry_id}")
        if result.error:
            print(f"  Error: {result.error}")
    return 0 if result.valid else 2


def _cmd_dashboard(args: argparse.Namespace, quiet: bool) -> int:
    """W184 — CISO dashboard."""
    from squash.dashboard import Dashboard
    model_paths = [Path(args.model_path)] if getattr(args, "model_path", None) else None
    models_dir = Path(args.models_dir) if getattr(args, "models_dir", None) else None
    d = Dashboard.build(models_dir=models_dir, model_paths=model_paths)
    if getattr(args, "output_json", False):
        print(json.dumps(d.to_dict(), indent=2))
    else:
        print(d.render_text(color=not getattr(args, "no_color", False)))
    return 0


def _cmd_regulatory(args: argparse.Namespace, quiet: bool) -> int:  # noqa: C901
    """W185 — Regulatory intelligence feed."""
    from squash.regulatory_feed import RegulatoryFeed
    feed = RegulatoryFeed()
    cmd = getattr(args, "regulatory_command", None)

    if cmd == "status" or cmd is None:
        s = feed.status()
        if getattr(args, "output_json", False):
            print(json.dumps({
                "total": s.total_regulations, "active": s.active_enforcement,
                "pending": s.pending_enforcement, "nearest_deadline": s.nearest_deadline,
                "days": s.nearest_deadline_days, "squash_coverage": s.squash_coverage,
            }, indent=2))
        else:
            print(s.compliance_impact_summary())
        return 0

    elif cmd == "list":
        jurisdiction = getattr(args, "jurisdiction", None)
        industry = getattr(args, "industry", None)
        if jurisdiction:
            regs = feed.regulations_by_jurisdiction(jurisdiction)
        elif industry:
            regs = feed.regulations_affecting_industry(industry)
        else:
            regs = feed.all_regulations()
        if getattr(args, "output_json", False):
            print(json.dumps(feed.export(), indent=2))
        else:
            for r in regs:
                print(r.summary())
        return 0

    elif cmd == "updates":
        since = getattr(args, "since", None)
        updates = feed.check_updates(since=since)
        if getattr(args, "output_json", False):
            print(json.dumps([{
                "reg_id": u.reg_id, "change_date": u.change_date,
                "impact": u.impact_level, "summary": u.change_summary,
            } for u in updates], indent=2))
        else:
            for u in updates:
                print(u.summary())
                print()
        return 0

    elif cmd == "deadlines":
        days = getattr(args, "days", 365)
        deadlines = feed.upcoming_deadlines(days=days)
        if getattr(args, "output_json", False):
            print(json.dumps([{
                "regulation": r.short_name, "deadline": r.enforcement_date,
                "days_remaining": d,
            } for r, d in deadlines], indent=2))
        else:
            if not deadlines:
                print(f"[squash regulatory] No enforcement deadlines in next {days} days.")
            for r, d in deadlines:
                print(f"  {d:4d} days — {r.short_name} ({r.enforcement_date})")
        return 0

    else:
        print(f"[squash regulatory] Unknown subcommand: {cmd}")
        return 1


def _cmd_due_diligence(args: argparse.Namespace, quiet: bool) -> int:
    """W186 — M&A due diligence package."""
    from squash.due_diligence import DueDiligenceGenerator
    model_paths = [Path(args.model_path)] if getattr(args, "model_path", None) else None
    models_dir = Path(args.models_dir) if getattr(args, "models_dir", None) else None
    pkg = DueDiligenceGenerator.generate(
        company_name=args.company,
        deal_type=args.deal_type,
        models_dir=models_dir,
        model_paths=model_paths,
    )
    if getattr(args, "output_json", False):
        print(json.dumps(pkg.to_dict(), indent=2))
        return 0
    if not quiet:
        print(pkg.executive_risk_summary())
    output_dir = Path(args.output_dir) if getattr(args, "output_dir", None) else Path(f"dd-{pkg.package_id}")
    written = pkg.save(output_dir)
    if not quiet:
        print(f"\n[squash due-diligence] Package written to {output_dir}/")
        for f in written:
            print(f"  {f}")
    return 0


def _cmd_vendor(args: argparse.Namespace, quiet: bool) -> int:  # noqa: C901
    """W178 — AI Vendor Risk Register."""
    from squash.vendor_registry import VendorRegistry
    from pathlib import Path as _Path

    db = _Path(args.db) if getattr(args, "db", None) else None

    with VendorRegistry(db) as reg:
        cmd = getattr(args, "vendor_command", None)

        if cmd == "add":
            vid = reg.add_vendor(
                name=args.name, website=args.website, risk_tier=args.risk_tier,
                use_case=args.use_case, data_access=args.data_access, notes=args.notes,
            )
            if not quiet:
                print(f"[squash vendor] Added vendor '{args.name}' ID={vid}")
            return 0

        elif cmd == "list":
            vendors = reg.list_vendors(tier=getattr(args, "tier", None))
            if getattr(args, "output_json", False):
                print(json.dumps([v.to_dict() for v in vendors], indent=2))
            else:
                if not vendors:
                    print("[squash vendor] No vendors registered.")
                for v in vendors:
                    print(f"  [{v.risk_tier.value.upper():8s}] {v.name:30s} {v.vendor_id}  "
                          f"status={v.assessment_status.value}")
            return 0

        elif cmd == "questionnaire":
            q = reg.generate_questionnaire(args.vendor_id)
            output = getattr(args, "output", None)
            if output:
                from pathlib import Path as _P
                _P(output).write_text(
                    json.dumps(q.to_dict(), indent=2) if output.endswith(".json")
                    else q.to_text()
                )
                if not quiet:
                    print(f"[squash vendor] Questionnaire written to {output}")
            else:
                print(q.to_text())
            return 0

        elif cmd == "import-trust-package":
            result = reg.import_trust_package(args.vendor_id, Path(args.package_path))
            if not quiet:
                status = "PASS" if result["passed"] else "FAIL"
                print(f"[squash vendor] Trust Package import: {status}  "
                      f"score={result.get('score', 'N/A')}")
            return 0 if result["passed"] else 1

        elif cmd == "summary":
            s = reg.risk_summary()
            if getattr(args, "output_json", False):
                print(json.dumps(s, indent=2))
            else:
                print(f"[squash vendor] Registry: {s['total_vendors']} vendors")
                for tier, count in s["by_risk_tier"].items():
                    if count:
                        print(f"  {tier.upper()}: {count}")
                if s["high_or_critical_unreviewed"] > 0:
                    print(f"  ⚠  {s['high_or_critical_unreviewed']} HIGH/CRITICAL vendors not reviewed")
            return 0

        else:
            print("[squash vendor] Specify a subcommand: add | list | questionnaire | import-trust-package | summary")
            return 1


def _cmd_registry(args: argparse.Namespace, quiet: bool) -> int:  # noqa: C901
    """W179 — AI Asset Registry."""
    from squash.asset_registry import AssetRegistry

    db = Path(args.db) if getattr(args, "db", None) else None

    with AssetRegistry(db) as reg:
        cmd = getattr(args, "registry_command", None)

        if cmd == "add":
            aid = reg.register(
                model_id=args.model_id, model_path=args.model_path,
                environment=args.environment, owner=args.owner, team=args.team,
                risk_tier=args.risk_tier, notes=args.notes,
                is_shadow_ai=getattr(args, "shadow", False),
            )
            if not quiet:
                print(f"[squash registry] Registered '{args.model_id}' ID={aid}")
            return 0

        elif cmd == "sync":
            aid = reg.sync_from_attestation(Path(args.model_path))
            if not quiet:
                if aid:
                    print(f"[squash registry] Synced from {args.model_path} → ID={aid}")
                else:
                    print(f"[squash registry] No attestation found in {args.model_path}")
            return 0

        elif cmd == "list":
            assets = reg.list_assets(
                environment=getattr(args, "environment", None),
                risk_tier=getattr(args, "risk_tier", None),
                shadow_only=getattr(args, "shadow_only", False),
            )
            if getattr(args, "output_json", False):
                print(json.dumps([a.to_dict() for a in assets], indent=2))
            else:
                if not assets:
                    print("[squash registry] No assets registered.")
                for a in assets:
                    score = f"{a.compliance_score:.0f}%" if a.compliance_score else "N/A"
                    print(f"  [{a.environment.value:12s}] {a.model_id:30s} "
                          f"score={score:5s} viol={a.open_violations} cve={a.open_cves}")
            return 0

        elif cmd == "summary":
            s = reg.summary()
            if getattr(args, "output_json", False):
                print(json.dumps({
                    "total": s.total_assets,
                    "by_environment": s.by_environment,
                    "by_risk_tier": s.by_risk_tier,
                    "compliant": s.compliant, "non_compliant": s.non_compliant,
                    "unattested": s.unattested, "stale": s.stale,
                    "shadow_ai": s.shadow_ai_count, "violations": s.total_violations,
                    "cves": s.total_cves, "drift": s.drift_count,
                }, indent=2))
            else:
                print(s.to_text())
            return 0

        elif cmd == "export":
            output_str = reg.export(format=getattr(args, "format", "json"))
            output_path = getattr(args, "output", None)
            if output_path:
                Path(output_path).write_text(output_str)
                if not quiet:
                    print(f"[squash registry] Exported to {output_path}")
            else:
                print(output_str)
            return 0

        else:
            print("[squash registry] Specify a subcommand: add | sync | list | summary | export")
            return 1


def _cmd_data_lineage(args: argparse.Namespace, quiet: bool) -> int:
    """W180 — Training Data Lineage Certificate."""
    from squash.data_lineage import DataLineageTracer, PIIRiskLevel

    model_path = Path(args.model_path)
    datasets = [d.strip() for d in args.datasets.split(",")] if args.datasets else None
    config_path = Path(args.config_path) if args.config_path else None

    cert = DataLineageTracer.trace(
        model_path=model_path,
        config_path=config_path,
        model_id=args.model_id,
        datasets=datasets,
    )

    output_path = Path(args.output) if args.output else model_path / "data_lineage_certificate.json"
    cert.save(output_path)

    if args.format == "json":
        print(json.dumps(cert.to_dict(), indent=2))
    elif not quiet:
        print(cert.summary())
        print(f"\n[squash data-lineage] Certificate written to {output_path}")

    if args.fail_on_pii and cert.overall_risk.value in ("high", "critical"):
        print(f"[squash data-lineage] FAIL: PII risk is {cert.overall_risk.value}", file=sys.stderr)
        return 2
    if args.fail_on_license and cert.license_issues:
        print(f"[squash data-lineage] FAIL: {len(cert.license_issues)} license issue(s)", file=sys.stderr)
        return 2
    return 0


def _cmd_bias_audit(args: argparse.Namespace, quiet: bool) -> int:
    """W181 — Bias Audit."""
    from squash.bias_audit import BiasAuditor, FairnessVerdict

    predictions_path = Path(args.predictions_path)
    if not predictions_path.exists():
        print(f"[squash bias-audit] ERROR: predictions file not found: {predictions_path}", file=sys.stderr)
        return 1

    protected = [a.strip() for a in args.protected.split(",")]

    report = BiasAuditor.audit_from_csv(
        predictions_path=predictions_path,
        protected_attributes=protected,
        label_col=args.label_col,
        pred_col=args.pred_col,
        model_id=args.model_id,
        standard=args.standard,
    )

    if args.output:
        report.save(Path(args.output))

    if args.format == "json":
        print(json.dumps(report.to_dict(), indent=2))
    elif not quiet:
        print(report.summary())
        if args.output and not quiet:
            print(f"\n[squash bias-audit] Report written to {args.output}")

    if args.fail_on_fail and report.overall_verdict == FairnessVerdict.FAIL:
        return 2
    if args.fail_on_warn and report.overall_verdict in (FairnessVerdict.FAIL, FairnessVerdict.WARN):
        return 2
    return 0


def _cmd_diff(args: argparse.Namespace, quiet: bool) -> int:
    """Compare two squash attestation JSON files."""
    from squash.sbom_diff import diff_attestations

    before_path = Path(args.before)
    after_path = Path(getattr(args, "after"))

    try:
        delta = diff_attestations(before_path, after_path)
    except (FileNotFoundError, ValueError) as exc:
        print(f"[squash diff] Error: {exc}", file=sys.stderr)
        return 1

    fmt = getattr(args, "format", "table")
    output_path = getattr(args, "output", None)

    if fmt == "json":
        text = json.dumps(delta.to_dict(), indent=2)
    elif fmt == "html":
        text = delta.to_html()
    elif fmt == "summary":
        text = delta.summary_line()
    else:
        text = delta.to_table()

    if output_path:
        Path(output_path).write_text(text)
        if not quiet:
            print(f"[squash diff] Written to {output_path}")
    else:
        print(text)

    if getattr(args, "fail_on_regression", False) and delta.is_regression:
        if not quiet:
            print("[squash diff] Regression detected — exiting 2", file=sys.stderr)
        return 2

    return 0


def _cmd_webhook(args: argparse.Namespace, quiet: bool) -> int:
    """Manage outbound webhook endpoints."""
    from squash.webhook_delivery import WebhookDelivery, WebhookEvent

    db_path = os.environ.get("SQUASH_WEBHOOK_DB", "squash_webhooks.db")
    wh = WebhookDelivery(db_path=db_path)
    cmd = getattr(args, "webhook_command", None)

    if cmd == "add":
        events_str = getattr(args, "events", None)
        if events_str:
            event_list = [WebhookEvent.from_str(e.strip()) for e in events_str.split(",")]
        else:
            event_list = WebhookEvent.all()
        secret = getattr(args, "secret", None)
        ep = wh.register(url=args.url, events=event_list, secret=secret)
        if not quiet:
            print(f"[squash webhook] Registered endpoint {ep.id}")
            print(f"  URL:    {ep.url}")
            print(f"  Events: {', '.join(e.value for e in ep.events)}")
            print(f"  Secret: {ep.secret}")
        return 0

    elif cmd == "list":
        show_all = getattr(args, "show_all", False)
        endpoints = wh.list_endpoints(active_only=not show_all)
        if not endpoints:
            print("[squash webhook] No endpoints registered.")
            return 0
        for ep in endpoints:
            status = "active" if ep.active else "inactive"
            print(f"  {ep.id}  [{status}]  {ep.url}")
            print(f"    events: {', '.join(e.value for e in ep.events)}")
            print(f"    deliveries: {ep.delivery_count}  last_status: {ep.last_status_code}")
        return 0

    elif cmd == "test":
        result = wh.test_endpoint(args.url)
        if result.success:
            print(f"[squash webhook] Test delivery succeeded ({result.status_code}) in {result.duration_ms:.0f}ms")
            return 0
        else:
            print(f"[squash webhook] Test delivery failed: {result.error or result.status_code}", file=sys.stderr)
            return 1

    elif cmd == "remove":
        removed = wh.remove(args.id)
        if removed:
            print(f"[squash webhook] Endpoint {args.id} deactivated.")
        else:
            print(f"[squash webhook] Endpoint {args.id} not found.", file=sys.stderr)
            return 1
        return 0

    else:
        print("squash webhook: specify a subcommand — add | list | test | remove")
        return 1


def _cmd_telemetry(args: argparse.Namespace, quiet: bool) -> int:
    """Configure and test OpenTelemetry integration."""
    from squash.telemetry import SquashTelemetry

    cmd = getattr(args, "telemetry_command", None)

    if cmd == "status":
        tel = SquashTelemetry.from_env()
        status = tel.status()
        print("[squash telemetry] Status")
        print(f"  Enabled:            {status.enabled}")
        print(f"  OTel available:     {status.otel_available}")
        print(f"  Exporter available: {status.exporter_available}")
        print(f"  Endpoint:           {status.endpoint or '(not configured)'}")
        print(f"  Service name:       {status.service_name}")
        print(f"  Spans emitted:      {status.spans_emitted}")
        if status.last_error:
            print(f"  Last error:         {status.last_error}")
        if not status.otel_available:
            print("\n  Install OTel: pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc")
        return 0

    elif cmd == "test":
        endpoint = getattr(args, "endpoint", None)
        http_endpoint = getattr(args, "http_endpoint", None)
        tel = SquashTelemetry(
            endpoint=endpoint or os.environ.get("SQUASH_OTEL_ENDPOINT"),
            http_endpoint=http_endpoint or os.environ.get("SQUASH_OTEL_HTTP_ENDPOINT"),
        )
        result = tel.test_connection()
        if result.emitted:
            print(f"[squash telemetry] Test span emitted — trace_id={result.trace_id}")
            return 0
        elif not result.otel_available:
            print("[squash telemetry] opentelemetry-api not installed. Install with:")
            print("  pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc")
            return 1
        else:
            print(f"[squash telemetry] Test span not emitted: {result.error or 'no endpoint configured'}")
            return 1

    elif cmd == "configure":
        endpoint = getattr(args, "endpoint", None)
        service = getattr(args, "service", "squash")
        print("[squash telemetry] Set these environment variables to enable telemetry:")
        if endpoint:
            print(f"  export SQUASH_OTEL_ENDPOINT={endpoint}")
        else:
            print("  export SQUASH_OTEL_ENDPOINT=http://localhost:4317")
        print(f"  export SQUASH_OTEL_SERVICE_NAME={service}")
        print("  export SQUASH_OTEL_ENABLED=true")
        return 0

    else:
        print("squash telemetry: specify a subcommand — status | test | configure")
        return 1


def _cmd_gitops(args: argparse.Namespace, quiet: bool) -> int:
    """ArgoCD / Flux GitOps enforcement gate."""
    from squash.integrations.gitops import (
        check_manifest_compliance,
        generate_webhook_manifest,
        annotate_deployment_command,
    )

    cmd = getattr(args, "gitops_command", None)

    if cmd == "check":
        manifest_path = Path(args.manifest_path)
        result = check_manifest_compliance(
            manifest_path=manifest_path,
            min_score=getattr(args, "min_score", 80.0),
            require_attestation=getattr(args, "require_attestation", True),
        )
        if getattr(args, "output_json", False):
            print(json.dumps(result.to_dict(), indent=2))
        else:
            icon = "✓" if result.passed else "✗"
            print(f"[squash gitops] {icon} {result.resource_kind}/{result.resource_name}")
            print(f"  Passed:     {result.passed}")
            print(f"  Reason:     {result.reason}")
            if result.attestation_id:
                print(f"  Attestation: {result.attestation_id}")
            if result.compliance_score is not None:
                print(f"  Score:      {result.compliance_score:.1f}")
        return 0 if result.passed else 2

    elif cmd == "webhook-manifest":
        yaml_str = generate_webhook_manifest(
            webhook_url=args.url,
            namespace=getattr(args, "namespace", "squash-system"),
            failure_policy=getattr(args, "failure_policy", "Fail"),
        )
        output = getattr(args, "output", None)
        if output:
            Path(output).write_text(yaml_str)
            print(f"[squash gitops] Written to {output}")
        else:
            print(yaml_str)
        return 0

    elif cmd == "annotate":
        cmd_str = annotate_deployment_command(
            deployment_name=args.deployment,
            attestation_id=args.attestation_id,
            compliance_score=args.compliance_score,
            policy=getattr(args, "policy", "eu-ai-act"),
            passed=getattr(args, "passed", True),
        )
        print(cmd_str)
        return 0

    else:
        print("squash gitops: specify a subcommand — check | webhook-manifest | annotate")
        return 1


def _cmd_industry_benchmark(args: argparse.Namespace, quiet: bool) -> int:
    """W249-W250 / D5 — Industry compliance benchmarking."""
    from squash.benchmark import (
        BenchmarkEngine,
        SECTORS,
        benchmark,
        build_profile_from_registry,
        build_profile_from_scores,
        get_baseline,
        load_result,
    )

    sub = getattr(args, "ib_command", None)

    def _make_profile(args):
        frameworks = [f.strip() for f in args.frameworks.split(",") if f.strip()] if args.frameworks else []
        if getattr(args, "registry_path", None):
            return build_profile_from_registry(
                Path(args.registry_path),
                model_id_filter=getattr(args, "model_filter", ""),
                period_days=args.period_days,
            )
        elif getattr(args, "scores", None):
            scores = [float(s.strip()) for s in args.scores.split(",") if s.strip()]
            return build_profile_from_scores(scores, frameworks, args.period_days)
        else:
            # No data — build a zero-score placeholder so CLI still runs
            return build_profile_from_scores([], frameworks, args.period_days)

    if sub == "report":
        profile = _make_profile(args)
        result = BenchmarkEngine().run(profile, args.sector_id)
        fmt = args.ib_format
        if fmt == "json":
            text = result.to_json()
        elif fmt == "md":
            text = result.to_markdown()
        else:
            text = _ib_text_summary(result)

        if args.out:
            Path(args.out).write_text(text, encoding="utf-8")
            if not quiet:
                print(f"✓ benchmark report written to {args.out}")
                print(result.summary())
        else:
            print(text)
        return 0

    if sub == "compare":
        sector_ids = [s.strip() for s in args.sectors.split(",") if s.strip()]
        profile = _make_profile(args)
        fmt = args.ib_format
        results = []
        for sid in sector_ids:
            try:
                r = BenchmarkEngine().run(profile, sid)
                results.append(r)
            except ValueError as exc:
                print(f"warning: {exc}", file=sys.stderr)

        if fmt == "json":
            print(json.dumps([r.to_dict() for r in results], indent=2))
        elif fmt == "md":
            for r in results:
                print(r.to_markdown())
                print()
        else:
            print(f"Score: {profile.score_mean:.1f}/100 | {profile.attestation_count} attestations")
            print()
            for r in results:
                pct = f"p{r.score_percentile:.0f}" if r.score_percentile is not None else "n/a"
                print(f"  {r.sector_name[:42]:42s} {r.tier:14s} {pct}")
        return 0

    if sub == "list-sectors":
        if args.output_json:
            print(json.dumps([
                {"sector_id": sid, "name": name,
                 "sample_size": get_baseline(sid).sample_size}
                for sid, name in SECTORS.items()
            ], indent=2))
        else:
            print("Available benchmark sectors:")
            for sid, name in SECTORS.items():
                b = get_baseline(sid)
                print(f"  {sid:24s}  n={b.sample_size:4d}  p50={b.score_p50:.0f}  {name}")
        return 0

    print("squash industry-benchmark: specify a subcommand — report | compare | list-sectors")
    return 1


def _ib_text_summary(result) -> str:
    pct = f"{result.score_percentile:.0f}th percentile" if result.score_percentile is not None else "percentile unavailable (< 5 attestations)"
    lines = [
        result.summary(),
        "",
        f"  Sector p50: {result.baseline.score_p50:.1f}  p75: {result.baseline.score_p75:.1f}  p90: {result.baseline.score_p90:.1f}",
        f"  Your score: {result.profile.score_mean:.1f}  → {pct}",
        f"  Drift rate: {result.profile.drift_rate_pct:.1f}% (sector avg: {result.baseline.drift_rate_pct:.1f}%)",
    ]
    if result.score_to_p75 > 0:
        lines.append(f"  To reach p75: +{result.score_to_p75:.1f} pts | To reach p90: +{result.score_to_p90:.1f} pts")
    gaps = [g for g in result.framework_gaps if not g.user_has_it]
    if gaps:
        lines.append(f"  Framework gaps: {', '.join(g.framework for g in gaps)}")
    return "\n".join(lines)


def _cmd_attest_identity(args: argparse.Namespace, quiet: bool) -> int:
    """W226-W228 / D2 — AI identity attestation."""
    from squash.identity_governor import (
        IdentityGovernor,
        IdentityPrincipal,
        LeastPrivilegePolicy,
        PrincipalType,
        Provider,
        ViolationSeverity,
        load_attestation,
        scaffold_policy,
        verify_attestation,
    )

    sub = getattr(args, "ai_command", None)

    if sub == "attest":
        # Load principal
        principal: IdentityPrincipal | None = None
        if args.principal_file:
            d = json.loads(Path(args.principal_file).read_text())
            principal = _principal_from_dict(d)
        elif args.provider == "aws-iam" and args.principal_name:
            from squash.integrations.aws_iam import AWSIAMAdapter
            principal = AWSIAMAdapter(region=args.aws_region).get_role(args.principal_name)
        elif args.provider == "azure-ad" and args.principal_name:
            token = args.api_token or os.environ.get("AZURE_ACCESS_TOKEN", "")
            from squash.integrations.azure_ad import AzureADAdapter
            principal = AzureADAdapter(access_token=token, tenant_id=args.tenant_id).get_principal(args.principal_name)
        elif args.provider == "okta" and args.principal_name:
            token = args.api_token or os.environ.get("OKTA_API_TOKEN", "")
            from squash.integrations.okta import OktaAdapter
            principal = OktaAdapter(domain=args.domain, api_token=token).get_app(args.principal_name)
        else:
            print("error: specify --principal-file or --provider + --principal", file=sys.stderr)
            return 1

        policy = None
        if args.policy_file:
            policy = LeastPrivilegePolicy.from_dict(json.loads(Path(args.policy_file).read_text()))

        priv_key = Path(args.priv_key) if args.priv_key else None
        cert = IdentityGovernor(priv_key_path=priv_key).attest(principal, policy)

        if args.ai_format == "md":
            text = cert.to_markdown()
        elif args.ai_format == "text":
            text = cert.summary()
        else:
            text = cert.to_json()

        if args.out:
            Path(args.out).write_text(text, encoding="utf-8")
            if not quiet:
                print(f"✓ identity attestation written to {args.out}")
                print(cert.summary())
        else:
            print(text)

        if args.fail_on_violation and any(
            v.severity == ViolationSeverity.CRITICAL for v in cert.violations
        ):
            return 2
        return 0

    if sub == "verify":
        try:
            cert = load_attestation(Path(args.cert_path))
        except Exception as exc:
            print(f"error: {exc}", file=sys.stderr)
            return 1
        ok, msg = verify_attestation(cert)
        if args.output_json:
            print(json.dumps({"ok": ok, "message": msg, "cert_id": cert.cert_id}, indent=2))
        else:
            print(f"{'✓' if ok else '✗'} {cert.cert_id}: {msg}")
        return 0 if ok else 2

    if sub == "list-principals":
        token = args.api_token
        principals = []
        if args.provider == "aws-iam":
            from squash.integrations.aws_iam import AWSIAMAdapter
            principals = AWSIAMAdapter(region=args.aws_region, tag_filter=args.filter_tag).list_principals()
        elif args.provider == "azure-ad":
            token = token or os.environ.get("AZURE_ACCESS_TOKEN", "")
            from squash.integrations.azure_ad import AzureADAdapter
            principals = AzureADAdapter(access_token=token, tenant_id=args.tenant_id).list_principals(args.filter_tag)
        elif args.provider == "okta":
            token = token or os.environ.get("OKTA_API_TOKEN", "")
            from squash.integrations.okta import OktaAdapter
            principals = OktaAdapter(domain=args.domain, api_token=token).list_principals(args.filter_tag)

        if args.output_json:
            print(json.dumps([p.to_dict() for p in principals], indent=2))
        else:
            print(f"{len(principals)} principal(s) [{args.provider}]")
            for p in principals:
                mfa = "MFA✓" if p.mfa_enabled else "MFA✗"
                age = f"{p.last_rotation_days}d" if p.last_rotation_days is not None else "?d"
                print(f"  {p.name}  scopes={len(p.scopes)}  {mfa}  rotation={age}")
        return 0

    if sub == "policy-init":
        policy_dict = scaffold_policy(args.principal_name)
        text = json.dumps(policy_dict, indent=2)
        if args.out:
            Path(args.out).write_text(text, encoding="utf-8")
            if not quiet:
                print(f"✓ policy scaffold written to {args.out}")
        else:
            print(text)
        return 0

    print("squash attest-identity: specify a subcommand — attest | verify | list-principals | policy-init")
    return 1


def _principal_from_dict(d: dict) -> "IdentityPrincipal":
    """Deserialise a principal dict (from --principal-file)."""
    from squash.identity_governor import IdentityPrincipal, PrincipalType, Provider
    return IdentityPrincipal(
        principal_id=d.get("principal_id", ""),
        name=d.get("name", ""),
        principal_type=PrincipalType(d.get("principal_type", "unknown")),
        provider=Provider(d.get("provider", "generic")),
        scopes=d.get("scopes", []),
        mfa_enabled=bool(d.get("mfa_enabled", False)),
        token_type=d.get("token_type", "unknown"),
        last_rotation_days=d.get("last_rotation_days"),
        created_at=d.get("created_at"),
        last_used_at=d.get("last_used_at"),
        tags=d.get("tags", {}),
        raw_metadata=d.get("raw_metadata", {}),
    )


def _cmd_hallucination_monitor(args: argparse.Namespace, quiet: bool) -> int:
    """W267-W269 / C10 — Runtime hallucination monitor."""
    from squash.hallucination_monitor import (
        BreachEngine,
        InferenceRequest,
        RequestSampler,
        RollingWindow,
        build_monitor_report,
        notify_breach,
        run_monitor,
        score_batch,
        score_live_response,
    )

    sub = getattr(args, "hm_command", None)

    if sub == "run":
        state_dir = Path(args.state_dir) if args.state_dir else None
        breaches: list = []

        def _on_breach(ev):
            breaches.append(ev)
            notify_breach(ev)
            if not quiet:
                print(f"\n🚨 {ev.summary()}", flush=True)

        run_monitor(
            endpoint=args.endpoint,
            model_id=args.model_id,
            sample_rate=args.sample_rate,
            threshold=args.threshold,
            window_minutes=args.window_minutes,
            poll_interval=args.poll_interval,
            state_dir=state_dir,
            on_breach=_on_breach,
            once=args.once,
        )

        window = RollingWindow(state_dir=state_dir)
        report = build_monitor_report(
            window, threshold=args.threshold,
            window_minutes=args.window_minutes, model_id=args.model_id,
        )
        if args.hm_format == "json" or args.once:
            print(json.dumps(report, indent=2))
        elif not quiet:
            rate = report["hallucination_rate"]
            n    = report["sample_count"]
            status = report["status"]
            print(f"{'✓' if status == 'OK' else '✗'} [{status}] "
                  f"rate={rate:.1%} n={n} threshold={args.threshold:.1%}")
        return 2 if breaches else 0

    if sub == "score":
        score, hallucinated, breakdown = score_live_response(
            response=args.response,
            context=args.context,
            ground_truth=args.ground_truth,
        )
        if args.output_json:
            print(json.dumps({
                "score": round(score, 4),
                "hallucinated": hallucinated,
                "breakdown": breakdown,
            }, indent=2))
        else:
            icon = "❌ HALLUCINATED" if hallucinated else "✅ faithful"
            print(f"{icon} (score={score:.3f})")
        return 2 if hallucinated else 0

    if sub == "status":
        state_dir = Path(args.state_dir) if args.state_dir else None
        window = RollingWindow(state_dir=state_dir)
        report = build_monitor_report(
            window, threshold=args.threshold,
            window_minutes=args.window_minutes,
        )
        if args.output_json:
            print(json.dumps(report, indent=2))
        else:
            rate   = report["hallucination_rate"]
            n      = report["sample_count"]
            status = report["status"]
            ci_lo  = report["ci_low"]
            ci_hi  = report["ci_high"]
            icon   = "✓" if status == "OK" else ("⚠" if status == "WARN" else "✗")
            print(f"{icon} [{status}] rate={rate:.1%} CI=[{ci_lo:.1%},{ci_hi:.1%}] "
                  f"n={n} threshold={args.threshold:.1%}")
        return 2 if report["status"] == "BREACH" else 0

    if sub == "batch":
        data = json.loads(Path(args.requests_file).read_text())
        reqs = [
            InferenceRequest(
                prompt=d.get("prompt", ""),
                response=d.get("response", ""),
                context=d.get("context", ""),
                ground_truth=d.get("ground_truth", ""),
                model_id=d.get("model_id", args.model_id),
            )
            for d in data
        ]
        result = score_batch(reqs, threshold=args.threshold, model_id=args.model_id)
        if args.output_json:
            print(json.dumps(result, indent=2))
        else:
            rate   = result["hallucination_rate"]
            passes = result["passes_threshold"]
            icon   = "✓" if passes else "✗"
            print(f"{icon} batch: rate={rate:.1%} n={result['sample_count']} "
                  f"threshold={args.threshold:.1%} {'PASS' if passes else 'FAIL'}")
            if result["breach"]:
                print(f"  BREACH: {result['breach']['summary'] if 'summary' in result['breach'] else ''}")
        if args.fail_on_breach and not result["passes_threshold"]:
            return 2
        return 0

    print("squash hallucination-monitor: specify a subcommand — run | score | status | batch")
    return 1


def _cmd_hallucination_attest(args: argparse.Namespace, quiet: bool) -> int:
    """W251-W252 / C7 — Hallucination rate attestation."""
    from squash.hallucination_attest import (
        HallucinationAttester,
        get_probes,
        load_attestation,
        load_custom_probes,
        verify_certificate,
    )

    sub = getattr(args, "ha_command", None)

    if sub == "attest":
        probes = None
        if args.probes_file:
            probes = load_custom_probes(Path(args.probes_file))
        elif args.probe_limit:
            probes = get_probes(args.domain, limit=args.probe_limit)

        try:
            cert = HallucinationAttester().attest(
                model_endpoint=args.model_endpoint,
                domain=args.domain,
                model_id=args.model_id,
                max_rate=args.max_rate,
                probes=probes,
                priv_key_path=Path(args.priv_key) if args.priv_key else None,
            )
        except ValueError as exc:
            print(f"error: {exc}", file=sys.stderr)
            return 1

        if args.ha_format == "md":
            text = cert.to_markdown()
        elif args.ha_format == "text":
            text = cert.summary()
        else:
            text = cert.to_json()

        if args.out:
            Path(args.out).write_text(text, encoding="utf-8")
            if not quiet:
                print(f"✓ hallucination certificate written to {args.out}")
                print(cert.summary())
        else:
            print(text)

        if args.fail_on_exceed and not cert.passes_threshold:
            if not quiet and args.ha_format != "text":
                print(
                    f"error: hallucination rate {cert.hallucination_rate:.2%} exceeds "
                    f"threshold {cert.threshold:.2%}",
                    file=sys.stderr,
                )
            return 2
        return 0

    if sub == "verify":
        try:
            cert = load_attestation(Path(args.cert_path))
        except Exception as exc:
            print(f"error: {exc}", file=sys.stderr)
            return 1
        ok, msg = verify_certificate(cert)
        if args.output_json:
            print(json.dumps({"ok": ok, "message": msg, "cert_id": cert.cert_id}, indent=2))
        else:
            print(f"{'✓' if ok else '✗'} {cert.cert_id}: {msg}")
        return 0 if ok else 2

    if sub == "show":
        try:
            cert = load_attestation(Path(args.cert_path))
        except Exception as exc:
            print(f"error: {exc}", file=sys.stderr)
            return 1
        print(cert.to_markdown())
        return 0

    if sub == "list-probes":
        probes = get_probes(args.domain)
        if args.output_json:
            print(json.dumps([{
                "probe_id": p.probe_id, "domain": p.domain,
                "question": p.question, "difficulty": p.difficulty,
            } for p in probes], indent=2))
        else:
            print(f"Domain: {args.domain} — {len(probes)} probes")
            for p in probes[:5]:
                print(f"  [{p.difficulty:6s}] {p.probe_id}: {p.question[:70]}")
            if len(probes) > 5:
                print(f"  ... and {len(probes) - 5} more")
        return 0

    print("squash hallucination-attest: specify a subcommand — attest | verify | show | list-probes")
    return 1


def _cmd_detect_washing(args: argparse.Namespace, quiet: bool) -> int:
    """W223-W225 / C2 — AI washing detection."""
    from squash.washing_detector import (
        AttestationEvidence,
        OverallVerdict,
        WashingDetector,
        load_evidence,
        load_report,
    )

    sub = getattr(args, "aw_command", None)

    if sub == "scan":
        # Resolve document paths (expand directories)
        doc_paths: list[Path] = []
        for raw in args.doc_paths:
            p = Path(raw)
            if p.is_dir():
                for ext in ("*.md", "*.txt", "*.html", "*.pdf"):
                    doc_paths.extend(sorted(p.rglob(ext)))
            elif p.exists():
                doc_paths.append(p)
            else:
                print(f"warning: {p} not found — skipped", file=sys.stderr)

        if not doc_paths:
            print("error: no readable documents found", file=sys.stderr)
            return 1

        evidence = load_evidence(
            master_record_path=Path(args.master_record) if args.master_record else None,
            bias_audit_path=Path(args.bias_audit) if args.bias_audit else None,
            data_lineage_path=Path(args.data_lineage) if args.data_lineage else None,
            model_id=args.model_id,
        )

        report = WashingDetector().scan(doc_paths, evidence=evidence, model_id=args.model_id)
        _output_washing_report(report, args.aw_format, args.out, quiet)

        fail_threshold = OverallVerdict(args.fail_on)
        if report.verdict >= fail_threshold:
            if not quiet and args.aw_format == "text":
                print(
                    f"error: verdict {report.verdict.value.upper()} "
                    f">= --fail-on {fail_threshold.value.upper()}",
                    file=sys.stderr,
                )
            return 2
        return 0

    if sub == "report":
        try:
            report = load_report(Path(args.report_path))
        except Exception as exc:
            print(f"error: {exc}", file=sys.stderr)
            return 1
        _output_washing_report(report, args.aw_format, None, quiet)
        return 0

    print("squash detect-washing: specify a subcommand — scan | report")
    return 1


def _output_washing_report(report: Any, fmt: str, out_path: str | None, quiet: bool) -> None:
    if fmt == "json":
        text = report.to_json()
    elif fmt == "md":
        text = report.to_markdown()
    else:
        lines = [report.summary(), ""]
        for f in report.findings:
            lines.append(f"  [{f.severity.value.upper()}] {f.rule_id}: {f.title}")
            lines.append(f"      claim: \"{f.claim.raw_text[:80]}\"")
            lines.append(f"      {f.legal_risk[:120]}")
        text = "\n".join(lines)
    if out_path:
        Path(out_path).write_text(text, encoding="utf-8")
        if not quiet:
            print(f"✓ AI washing report written to {out_path}")
    else:
        print(text)


def _cmd_license_check(args: argparse.Namespace, quiet: bool) -> int:
    """W196 / B10 — Licence conflict detection."""
    from squash.license_conflict import (
        LicenseConflictScanner,
        OverallRisk,
        UseCase,
        load_report,
        resolve_spdx,
    )

    sub = getattr(args, "lc_command", None)

    if sub == "scan":
        p = Path(args.project_path)
        if not p.exists():
            print(f"error: {p} not found", file=sys.stderr)
            return 1
        use_case = UseCase(args.use_case)
        report = LicenseConflictScanner().scan(p, use_case=use_case)
        _output_lc_report(report, args.lc_format, args.out, quiet)
        fail_level = OverallRisk(args.fail_on)
        if report.overall_risk >= fail_level:
            if not quiet and args.lc_format == "text":
                print(
                    f"error: overall risk {report.overall_risk.value.upper()} "
                    f">= --fail-on {fail_level.value.upper()}",
                    file=sys.stderr,
                )
            return 2
        return 0

    if sub == "explain":
        info = resolve_spdx(args.spdx_id)
        print(f"SPDX ID:       {info.spdx_id}")
        print(f"Name:          {info.name}")
        print(f"Kind:          {info.kind.value}")
        print(f"OSI approved:  {info.osi_approved}")
        print(f"Patent grant:  {info.patent_grant}")
        print(f"Commercial OK: {info.commercial_ok}")
        print(f"Copyleft/SA:   {info.source_required or info.share_alike}")
        print(f"SaaS trigger:  {info.saas_triggers}")
        if info.legal_basis:
            print(f"Legal basis:   {info.legal_basis}")
        return 0

    if sub == "report":
        try:
            report = load_report(Path(args.report_path))
        except Exception as exc:
            print(f"error: {exc}", file=sys.stderr)
            return 1
        _output_lc_report(report, args.lc_format, None, quiet)
        return 0

    print("squash license-check: specify a subcommand — scan | explain | report")
    return 1


def _output_lc_report(report: Any, fmt: str, out_path: str | None, quiet: bool) -> None:
    if fmt == "json":
        text = report.to_json()
    elif fmt == "md":
        text = report.to_markdown()
    else:
        lines = [report.summary(), ""]
        for f in report.findings:
            lines.append(f"  [{f.severity.value.upper()}] {f.rule_id}: {f.title}")
            lines.append(f"      {f.component_a.name} ({f.component_a.spdx_id})")
            lines.append(f"      → {f.remediation[:100]}")
        if report.obligations:
            lines += ["", "Obligations:"]
            for o in report.obligations[:5]:
                lines.append(f"  • {o[:120]}")
        text = "\n".join(lines)
    if out_path:
        Path(out_path).write_text(text, encoding="utf-8")
        if not quiet:
            print(f"✓ licence conflict report written to {out_path}")
    else:
        print(text)


def _cmd_data_poison(args: argparse.Namespace, quiet: bool) -> int:
    """W195 / B9 — Training data poisoning detection."""
    from squash.data_poison import (
        DataPoisonScanner,
        RiskLevel,
        load_report,
    )

    sub = getattr(args, "dp_command", None)

    if sub == "scan":
        dataset_path = Path(args.dataset_path)
        if not dataset_path.exists():
            print(f"error: {dataset_path} not found", file=sys.stderr)
            return 1

        provenance_data = None
        if args.provenance_path:
            try:
                provenance_data = json.loads(Path(args.provenance_path).read_text())
            except Exception as exc:
                print(f"warning: could not read provenance file: {exc}", file=sys.stderr)

        report = DataPoisonScanner().scan(dataset_path, provenance_data=provenance_data)
        _output_poison_report(report, args.scan_format, args.out, quiet)

        fail_threshold = RiskLevel(args.fail_on)
        if report.risk_level >= fail_threshold:
            if not quiet and args.scan_format == "text":
                print(
                    f"error: risk level {report.risk_level.value.upper()} "
                    f">= --fail-on {fail_threshold.value.upper()}",
                    file=sys.stderr,
                )
            return 2
        return 0

    if sub == "report":
        try:
            report = load_report(Path(args.report_path))
        except Exception as exc:
            print(f"error: {exc}", file=sys.stderr)
            return 1
        _output_poison_report(report, args.report_format, None, quiet)
        return 0

    print("squash data-poison: specify a subcommand — scan | report")
    return 1


def _output_poison_report(report: Any, fmt: str, out_path: str | None, quiet: bool) -> None:
    from squash.data_poison import DataPoisonReport
    if fmt == "json":
        text = report.to_json()
    elif fmt == "md":
        text = report.to_markdown()
    else:
        # Human-readable text summary
        lines = [report.summary(), ""]
        for c in report.checks:
            icon = "✓" if c.passed else "✗"
            lines.append(f"  {icon} {c.name} [{c.severity.value}] score={c.score:.3f}")
            for e in c.evidence[:2]:
                lines.append(f"      {e}")
        if report.remediations:
            lines += ["", "Remediations:"]
            for r in report.remediations[:3]:
                lines.append(f"  • {r[:120]}")
        text = "\n".join(lines)

    if out_path:
        Path(out_path).write_text(text, encoding="utf-8")
        if not quiet:
            print(f"✓ report written to {out_path}")
    else:
        print(text)


def _cmd_drift_cert(args: argparse.Namespace, quiet: bool) -> int:
    """W194 / B7 — Drift SLA Certificate issuer and verifier."""
    from squash.drift_certificate import (
        DriftCertificateIssuer,
        DriftSLASpec,
        ScoreLedger,
        default_ledger_path,
        load_certificate,
    )

    sub = getattr(args, "dc_command", None)

    if sub == "ingest":
        path = Path(args.master_record_path)
        if not path.exists():
            print(f"error: {path} not found", file=sys.stderr)
            return 1
        ledger_path = Path(args.ledger_path) if args.ledger_path else default_ledger_path()
        ledger = ScoreLedger(ledger_path=ledger_path)
        snap = ledger.ingest(path)
        if not quiet:
            print(f"✓ ingested {snap.attestation_id or snap.model_id} @ {snap.timestamp[:10]} (score={snap.score})")
        return 0

    if sub == "issue":
        ledger_path = Path(args.ledger_path) if args.ledger_path else default_ledger_path()
        ledger = ScoreLedger(ledger_path=ledger_path)
        spec = DriftSLASpec(
            model_id=args.model_id,
            framework=args.framework,
            min_score=args.min_score,
            window_days=args.window_days,
            max_violation_rate=args.max_violation_rate,
            min_snapshots=args.min_snapshots,
            org=args.org,
        )
        priv_key = Path(args.priv_key) if args.priv_key else None
        issuer = DriftCertificateIssuer(priv_key_path=priv_key)
        cert = issuer.issue(spec, ledger)

        fmt = args.issue_format
        if fmt == "md":
            text = cert.to_markdown()
        elif fmt == "html":
            text = cert.to_html()
        else:
            text = cert.to_json()

        if args.out:
            Path(args.out).write_text(text, encoding="utf-8")
            if not quiet:
                icon = "✅" if cert.result.passes_sla else "❌"
                print(f"{icon} certificate {cert.cert_id} written to {args.out} (rate={cert.result.compliance_rate:.1%})")
        else:
            print(text)
        return 0 if cert.result.passes_sla else 2

    if sub == "verify":
        path = Path(args.cert_path)
        if not path.exists():
            print(f"error: {path} not found", file=sys.stderr)
            return 1
        cert = load_certificate(path)
        ok, msg = DriftCertificateIssuer.verify(cert)
        if args.output_json:
            print(json.dumps({"ok": ok, "message": msg, "cert_id": cert.cert_id}, indent=2))
        else:
            icon = "✓" if ok else "✗"
            print(f"{icon} {cert.cert_id}: {msg}")
        return 0 if ok else 2

    if sub == "show":
        path = Path(args.cert_path)
        if not path.exists():
            print(f"error: {path} not found", file=sys.stderr)
            return 1
        cert = load_certificate(path)
        print(cert.to_markdown())
        return 0

    if sub == "export":
        path = Path(args.cert_path)
        if not path.exists():
            print(f"error: {path} not found", file=sys.stderr)
            return 1
        cert = load_certificate(path)
        fmt = args.export_format
        if fmt == "md":
            text = cert.to_markdown()
        elif fmt == "html":
            text = cert.to_html()
        elif fmt == "pdf":
            try:
                from weasyprint import HTML as WeasyprintHTML  # type: ignore
            except ImportError:
                print("error: weasyprint required for PDF export: pip install weasyprint", file=sys.stderr)
                return 2
            out_path = Path(args.out) if args.out else Path(f"{cert.cert_id}.pdf")
            WeasyprintHTML(string=cert.to_html()).write_pdf(str(out_path))
            if not quiet:
                print(f"✓ PDF written to {out_path}")
            return 0
        else:
            text = cert.to_json()

        out_path = Path(args.out) if args.out else Path(f"{cert.cert_id}.{fmt}")
        out_path.write_text(text, encoding="utf-8")
        if not quiet:
            print(f"✓ {fmt.upper()} written to {out_path}")
        return 0

    print("squash drift-cert: specify a subcommand — ingest | issue | verify | show | export")
    return 1


def _cmd_anchor(args: argparse.Namespace, quiet: bool) -> int:
    """W193 / B6 — audit-trail blockchain anchoring."""
    from squash.anchor import (
        AnchorLedger,
        EthereumAnchor,
        LocalAnchor,
        OpenTimestampsAnchor,
    )

    ledger_dir = Path(args.ledger_dir) if getattr(args, "ledger_dir", None) else None
    ledger = AnchorLedger(root_dir=ledger_dir)
    sub = getattr(args, "anchor_command", None)

    if sub == "add":
        path = Path(args.master_record_path)
        if not path.exists():
            print(f"error: master record not found: {path}", file=sys.stderr)
            return 1
        staged = ledger.stage(path)
        if not quiet:
            print(f"✓ staged {staged.attestation_id} ({staged.record_hash[:12]}…) — {len(ledger.staged())} pending")
        return 0

    if sub == "commit":
        backend_name = args.backend
        if backend_name == "local":
            if not args.priv_key:
                print("error: --priv-key required for local backend", file=sys.stderr)
                return 1
            backend = LocalAnchor(priv_key_path=Path(args.priv_key), pub_key_path=Path(args.pub_key) if args.pub_key else None)
        elif backend_name == "opentimestamps":
            backend = OpenTimestampsAnchor()
        elif backend_name == "ethereum":
            rpc_url = args.rpc_url or os.environ.get("SQUASH_ETH_RPC_URL")
            eth_key = args.eth_key or os.environ.get("SQUASH_ETH_KEY")
            if not rpc_url or not eth_key:
                print("error: ethereum backend requires --rpc-url and --eth-key (or $SQUASH_ETH_RPC_URL, $SQUASH_ETH_KEY)", file=sys.stderr)
                return 1
            backend = EthereumAnchor(rpc_url=rpc_url, private_key=eth_key)
        else:
            print(f"error: unknown backend {backend_name!r}", file=sys.stderr)
            return 1

        try:
            entry = ledger.commit(backend)
        except RuntimeError as exc:
            print(f"error: {exc}", file=sys.stderr)
            return 1
        except FileNotFoundError as exc:
            print(f"error: {exc}", file=sys.stderr)
            return 2

        if args.output_json:
            print(json.dumps(entry.to_dict(), indent=2, sort_keys=True))
            return 0
        if not quiet:
            a = entry.anchor
            print(f"✓ anchored {a.leaf_count} attestation(s) — backend={a.backend} root={a.root[:16]}… anchor_id={a.anchor_id}")
            print(f"  ledger: {ledger.ledger_path}")
            for s in entry.attestations:
                print(f"   • {s.attestation_id}  ({s.record_hash[:12]}…)")
        return 0

    if sub == "verify":
        ok, msg = ledger.verify(args.attestation_id)
        if args.output_json:
            print(json.dumps({"ok": ok, "message": msg, "attestation_id": args.attestation_id}, indent=2))
            return 0 if ok else 2
        icon = "✓" if ok else "✗"
        print(f"{icon} {args.attestation_id}: {msg}")
        return 0 if ok else 2

    if sub == "proof":
        try:
            doc = ledger.export_proof(args.attestation_id)
        except KeyError as exc:
            print(f"error: {exc}", file=sys.stderr)
            return 1
        text = json.dumps(doc, indent=2, sort_keys=True)
        if args.out:
            Path(args.out).write_text(text)
            if not quiet:
                print(f"✓ proof written to {args.out}")
        return 0

    if sub == "list":
        entries = ledger.entries()
        if args.output_json:
            print(json.dumps([e.to_dict() for e in entries], indent=2, sort_keys=True))
            return 0
        if not entries:
            print("(no anchors committed)")
            return 0
        for e in entries:
            a = e.anchor
            print(f"{a.iso_timestamp}  {a.anchor_id}  backend={a.backend:14s}  leaves={a.leaf_count:>4}  root={a.root[:16]}…")
        return 0

    if sub == "status":
        staged = ledger.staged()
        entries = ledger.entries()
        last = entries[-1] if entries else None
        if args.output_json:
            print(json.dumps({
                "staged": [s.to_dict() for s in staged],
                "last_anchor": last.anchor.to_dict() if last else None,
                "ledger_dir": str(ledger.root_dir),
            }, indent=2, sort_keys=True))
            return 0
        print(f"ledger: {ledger.root_dir}")
        print(f"staged: {len(staged)} attestation(s) pending")
        for s in staged:
            print(f"   • {s.attestation_id}  ({s.record_hash[:12]}…)")
        if last:
            a = last.anchor
            print(f"last anchor: {a.iso_timestamp}  {a.anchor_id}  backend={a.backend}  leaves={a.leaf_count}")
        else:
            print("last anchor: (none)")
        return 0

    print("squash anchor: specify a subcommand — add | commit | verify | proof | list | status")
    return 1


def _cmd_gateway_config(args, quiet):
    """B5 — Emit Kong / AWS API Gateway runtime gate config or source."""
    from squash.integrations.gateway import (
        emit_kong_config, emit_kong_plugin_dir,
        emit_aws_apigw_sam, emit_aws_authorizer_dir,
    )
    target = getattr(args, "gateway_target", None)
    output = getattr(args, "output", None)

    def _write_or_print(text):
        if output:
            Path(output).write_text(text)
            if not quiet:
                print(f"[squash gateway-config] Written to {output}")
        else:
            print(text)
        return 0

    def _write_dir(files):
        if not output:
            print("squash gateway-config: --output DIR is required when emitting a source tree", file=sys.stderr)
            return 2
        out_dir = Path(output)
        out_dir.mkdir(parents=True, exist_ok=True)
        for name, body in files.items():
            (out_dir / name).write_text(body)
        if not quiet:
            print(f"[squash gateway-config] Wrote {len(files)} files into {out_dir}")
            for name in files:
                print(f"  - {out_dir / name}")
        return 0

    if target == "kong":
        if getattr(args, "emit_plugin", False):
            return _write_dir(emit_kong_plugin_dir())
        text = emit_kong_config(
            min_score=args.min_score, header_name=args.header_name,
            squash_api_url=args.squash_api_url, service_name=args.service_name,
            upstream_url=args.upstream_url,
            route_paths=getattr(args, "route_paths", None),
            max_age_days=args.max_age_days,
            required_frameworks=getattr(args, "required_frameworks", None),
        )
        return _write_or_print(text)

    if target == "aws-apigw":
        if getattr(args, "emit_authorizer_dir", False):
            return _write_dir(emit_aws_authorizer_dir())
        if getattr(args, "emit_handler", False):
            return _write_or_print(emit_aws_authorizer_dir()["handler.py"])
        text = emit_aws_apigw_sam(
            min_score=args.min_score, function_name=args.function_name,
            squash_api_url=args.squash_api_url, header_name=args.header_name,
            max_age_days=args.max_age_days,
            required_frameworks=getattr(args, "required_frameworks", None),
            runtime=args.runtime,
        )
        return _write_or_print(text)

    print("squash gateway-config: specify a target — kong | aws-apigw", file=sys.stderr)
    return 1



def _cmd_scan_adapter(args: argparse.Namespace, quiet: bool) -> int:
    """B8 — LoRA / Adapter poisoning detection."""
    from squash.adapter_scanner import scan_adapter

    lora_path = Path(args.lora_path)
    cert_output = Path(args.cert_output) if getattr(args, "cert_output", None) else None
    require_sf = getattr(args, "require_safetensors", False)
    sign = getattr(args, "sign_cert", False)
    output_json = getattr(args, "output_json", False)

    report = scan_adapter(
        adapter_path=lora_path,
        require_safetensors=require_sf,
        sign=sign,
        output_path=cert_output,
    )

    if output_json:
        print(json.dumps(report.to_dict(), indent=2))
        return 0 if report.safe else (2 if not report.safe and any(
            f.severity == "critical" for f in report.findings) else 1)

    icon = "✓" if report.safe else "✗"
    print(f"[squash scan-adapter] {icon} {lora_path.name}")
    print(f"  Format:      {report.file_format}")
    print(f"  Risk level:  {report.risk_level}")
    print(f"  Tensors:     {report.n_tensors}  Parameters: {report.total_parameters:,}")
    print(f"  Findings:    {len(report.findings)} "
          f"({report.critical_count} critical, {report.high_count} high)")
    if report.concentration_score > 0:
        print(f"  Concentration: {report.concentration_score:.1%}")
    for f in report.findings:
        badge = {"critical": "🔴", "high": "🟠", "medium": "🟡",
                 "low": "🔵", "info": "ℹ"}.get(f.severity, "•")
        print(f"  {badge} [{f.code}] {f.title}")
    if report.certificate_path and not quiet:
        print(f"  Certificate: {report.certificate_path}")

    if any(f.severity == "critical" for f in report.findings):
        return 2
    if not report.safe:
        return 1
    return 0



def _cmd_board_report(args: argparse.Namespace, quiet: bool) -> int:
    """W174 — Board report generation."""
    from squash.board_report import BoardReportGenerator

    model_paths = None
    models_dir = Path(args.models_dir) if args.models_dir else None
    if args.model_path:
        model_paths = [Path(args.model_path)]

    report = BoardReportGenerator.generate(
        models_dir=models_dir,
        model_paths=model_paths,
        quarter=args.quarter,
    )

    if args.output_json:
        print(json.dumps(report.to_dict(), indent=2))
        return 0

    if not quiet:
        print(report.executive_summary())

    output_dir = Path(args.output_dir) if args.output_dir else Path(f"board-report-{report.quarter}")
    written = report.save(output_dir)

    if not quiet:
        print(f"\n[squash board-report] Report written to {output_dir}/")
        for f in written:
            print(f"  {f}")
    return 0


def _cmd_scan_agentic(args: argparse.Namespace, quiet: bool) -> int:
    """OWASP Agentic Top 10 2026 — ``squash scan-agentic``.

    Exit codes:
      0  All checks passed (no critical/high findings)
      1  Configuration / argument error
      2  One or more critical/high findings detected
    """
    import json as _json

    from squash.agentic import AgenticScanner

    config_path = Path(args.agentic_config)
    if not config_path.exists():
        print(f"error: config file not found: {config_path}", file=sys.stderr)
        return 1

    try:
        raw = config_path.read_text(encoding="utf-8")
        if config_path.suffix.lower() in {".yaml", ".yml"}:
            try:
                import yaml  # type: ignore[import-untyped]
                config: dict = yaml.safe_load(raw) or {}
            except ImportError:
                print(
                    "error: PyYAML is required for YAML configs — pip install pyyaml",
                    file=sys.stderr,
                )
                return 1
        else:
            config = _json.loads(raw)
    except (OSError, ValueError) as exc:
        print(f"error reading config: {exc}", file=sys.stderr)
        return 1

    result = AgenticScanner().scan(config)

    if not quiet:
        sev_badge = {"critical": "CRIT", "high": "HIGH", "medium": "MED ", "low": "LOW "}
        print(f"[squash scan-agentic] OWASP Agentic Top 10 2026")
        print(f"  Config:  {config_path}")
        print(f"  Score:   {result.score}/100")
        print(f"  Status:  {'PASS' if result.passed else 'FAIL'}")
        if result.findings:
            print(f"  Findings ({len(result.findings)}):")
            for f in result.findings:
                badge = sev_badge.get(f.severity, "    ")
                print(f"    [{badge}] {f.risk_id} — {f.title}")
                for ev in f.evidence:
                    print(f"           evidence: {ev}")
                print(f"           fix: {f.remediation}")
        else:
            print("  No findings — all agentic checks passed.")

    json_path = getattr(args, "json_result", None)
    if json_path:
        out = {
            "framework": result.framework,
            "passed": result.passed,
            "score": result.score,
            "summary": result.summary,
            "findings": [
                {
                    "risk_id": f.risk_id,
                    "title": f.title,
                    "severity": f.severity,
                    "description": f.description,
                    "evidence": f.evidence,
                    "remediation": f.remediation,
                    "owasp_ref": f.owasp_ref,
                }
                for f in result.findings
            ],
        }
        try:
            Path(json_path).write_text(_json.dumps(out, indent=2), encoding="utf-8")
            if not quiet:
                print(f"  JSON result: {json_path}")
        except OSError as exc:
            print(f"error writing JSON result: {exc}", file=sys.stderr)
            return 2

    return 0 if result.passed else 2


def main() -> None:
    parser = _build_parser()
    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(0)

    quiet: bool = getattr(args, "quiet", False)

    if not quiet:
        logging.basicConfig(
            level=logging.WARNING,
            format="%(levelname)s %(name)s: %(message)s",
        )

    if args.command == "policies":
        sys.exit(_cmd_policies(args, quiet))
    elif args.command == "scan":
        sys.exit(_cmd_scan(args, quiet))
    elif args.command == "sbom-diff":
        sys.exit(_cmd_sbom_diff(args, quiet))
    elif args.command == "diff":
        sys.exit(_cmd_diff(args, quiet))
    elif args.command == "verify":
        sys.exit(_cmd_verify(args, quiet))
    elif args.command == "self-verify":
        # Phase G.3 — chain walker
        from squash.self_verify import main as _self_verify_main

        argv = ["--attestation-dir", args.attestation_dir]
        if getattr(args, "offline", False):
            argv.append("--offline")
        if getattr(args, "json", False):
            argv.append("--json")
        sys.exit(_self_verify_main(argv))
    elif args.command == "keygen":
        sys.exit(_cmd_keygen(args, quiet))
    elif args.command == "verify-local":
        sys.exit(_cmd_verify_local(args, quiet))
    elif args.command == "pack-offline":
        sys.exit(_cmd_pack_offline(args, quiet))
    elif args.command == "report":
        sys.exit(_cmd_report(args, quiet))
    elif args.command == "vex":
        sys.exit(_cmd_vex(args, quiet))
    elif args.command == "attest-composed":
        sys.exit(_cmd_attest_composed(args, quiet))
    elif args.command == "push":
        sys.exit(_cmd_push(args, quiet))
    elif args.command == "attest":
        sys.exit(_cmd_attest(args, quiet))
    elif args.command == "ntia-check":
        sys.exit(_cmd_ntia_check(args, quiet))
    elif args.command == "slsa-attest":
        sys.exit(_cmd_slsa_attest(args, quiet))
    elif args.command == "merge":
        sys.exit(_cmd_merge(args, quiet))
    elif args.command == "risk-assess":
        sys.exit(_cmd_risk_assess(args, quiet))
    elif args.command == "monitor":
        sys.exit(_cmd_monitor(args, quiet))
    elif args.command == "ci-run":
        sys.exit(_cmd_ci_run(args, quiet))
    elif args.command == "k8s-webhook":
        sys.exit(_cmd_k8s_webhook(args, quiet))
    elif args.command == "shadow-ai":
        sys.exit(_cmd_shadow_ai(args, quiet))
    elif args.command == "vex-publish":
        sys.exit(_cmd_vex_publish(args, quiet))
    elif args.command == "attest-mlflow":
        sys.exit(_cmd_attest_mlflow(args, quiet))
    elif args.command == "attest-wandb":
        sys.exit(_cmd_attest_wandb(args, quiet))
    elif args.command == "attest-huggingface":
        sys.exit(_cmd_attest_huggingface(args, quiet))
    elif args.command == "attest-langchain":
        sys.exit(_cmd_attest_langchain(args, quiet))
    elif args.command == "attest-mcp":
        sys.exit(_cmd_attest_mcp(args, quiet))
    elif args.command == "audit":
        sys.exit(_cmd_audit(args, quiet))
    elif args.command == "scan-rag":
        sys.exit(_cmd_scan_rag(args, quiet))
    elif args.command == "lineage":
        sys.exit(_cmd_lineage(args, quiet))
    elif args.command == "drift-check":
        sys.exit(_cmd_drift_check(args, quiet))
    elif args.command == "remediate":
        sys.exit(_cmd_remediate(args, quiet))
    elif args.command == "evaluate":
        sys.exit(_cmd_evaluate(args, quiet))
    elif args.command == "edge-scan":
        sys.exit(_cmd_edge_scan(args, quiet))
    elif args.command == "chat":
        sys.exit(_cmd_chat(args, quiet))
    elif args.command == "model-card":
        sys.exit(_cmd_model_card(args, quiet))
    elif args.command == "cloud-status":
        sys.exit(_cmd_cloud_status(args, quiet))
    elif args.command == "cloud-report":
        sys.exit(_cmd_cloud_report(args, quiet))
    elif args.command == "cloud-export":
        sys.exit(_cmd_cloud_export(args, quiet))
    elif args.command == "cloud-attest":
        sys.exit(_cmd_cloud_attest(args, quiet))
    elif args.command == "cloud-vex":
        sys.exit(_cmd_cloud_vex(args, quiet))
    elif args.command == "cloud-risk":
        sys.exit(_cmd_cloud_risk(args, quiet))
    elif args.command == "cloud-remediate":
        sys.exit(_cmd_cloud_remediate(args, quiet))
    elif args.command == "annex-iv":
        sys.exit(_cmd_annex_iv(args, quiet))
    elif args.command == "demo":
        sys.exit(_cmd_demo(args, quiet))
    elif args.command == "init":
        sys.exit(_cmd_init(args, quiet))
    elif args.command == "watch":
        sys.exit(_cmd_watch(args, quiet))
    elif args.command == "install-hook":
        sys.exit(_cmd_install_hook(args, quiet))
    elif args.command == "annual-review":
        sys.exit(_cmd_annual_review(args, quiet))
    elif args.command == "publish":
        sys.exit(_cmd_publish(args, quiet))
    elif args.command == "lookup":
        sys.exit(_cmd_lookup(args, quiet))
    elif args.command == "verify-entry":
        sys.exit(_cmd_verify_entry(args, quiet))
    elif args.command == "dashboard":
        sys.exit(_cmd_dashboard(args, quiet))
    elif args.command == "regulatory":
        sys.exit(_cmd_regulatory(args, quiet))
    elif args.command == "due-diligence":
        sys.exit(_cmd_due_diligence(args, quiet))
    elif args.command == "vendor":
        sys.exit(_cmd_vendor(args, quiet))
    elif args.command == "registry":
        sys.exit(_cmd_registry(args, quiet))
    elif args.command == "data-lineage":
        sys.exit(_cmd_data_lineage(args, quiet))
    elif args.command == "bias-audit":
        sys.exit(_cmd_bias_audit(args, quiet))
    elif args.command == "iso42001":
        sys.exit(_cmd_iso42001(args, quiet))
    elif args.command == "trust-package":
        sys.exit(_cmd_trust_package(args, quiet))
    elif args.command == "verify-trust-package":
        sys.exit(_cmd_verify_trust_package(args, quiet))
    elif args.command == "agent-audit":
        sys.exit(_cmd_agent_audit(args, quiet))
    elif args.command == "incident":
        sys.exit(_cmd_incident(args, quiet))
    elif args.command == "board-report":
        sys.exit(_cmd_board_report(args, quiet))
    elif args.command == "telemetry":
        sys.exit(_cmd_telemetry(args, quiet))
    elif args.command == "gitops":
        sys.exit(_cmd_gitops(args, quiet))
    elif args.command == "industry-benchmark":
        sys.exit(_cmd_industry_benchmark(args, quiet))
    elif args.command == "attest-identity":
        sys.exit(_cmd_attest_identity(args, quiet))
    elif args.command == "hallucination-monitor":
        sys.exit(_cmd_hallucination_monitor(args, quiet))
    elif args.command == "hallucination-attest":
        sys.exit(_cmd_hallucination_attest(args, quiet))
    elif args.command == "detect-washing":
        sys.exit(_cmd_detect_washing(args, quiet))
    elif args.command == "license-check":
        sys.exit(_cmd_license_check(args, quiet))
    elif args.command == "data-poison":
        sys.exit(_cmd_data_poison(args, quiet))
    elif args.command == "drift-cert":
        sys.exit(_cmd_drift_cert(args, quiet))
    elif args.command == "anchor":
        sys.exit(_cmd_anchor(args, quiet))
    elif args.command == "gateway-config":
        sys.exit(_cmd_gateway_config(args, quiet))
    elif args.command == "scan-adapter":
        sys.exit(_cmd_scan_adapter(args, quiet))
    elif args.command == "chain-attest":
        sys.exit(_cmd_chain_attest(args, quiet))
    elif args.command == "registry-gate":
        sys.exit(_cmd_registry_gate(args, quiet))
    elif args.command == "soc2":
        sys.exit(_cmd_soc2(args, quiet))
    elif args.command == "score":
        sys.exit(_cmd_score(args, quiet))
    elif args.command == "attest-carbon":
        sys.exit(_cmd_attest_carbon(args, quiet))
    elif args.command == "deprecation-watch":
        sys.exit(_cmd_deprecation_watch(args, quiet))
    elif args.command == "request-approval":
        sys.exit(_cmd_request_approval(args, quiet))
    elif args.command == "approve":
        sys.exit(_cmd_approve(args, quiet))
    elif args.command == "approval-status":
        sys.exit(_cmd_approval_status(args, quiet))
    elif args.command == "approval-list":
        sys.exit(_cmd_approval_list(args, quiet))
    elif args.command == "approval-export":
        sys.exit(_cmd_approval_export(args, quiet))
    elif args.command == "digest":
        sys.exit(_cmd_digest(args, quiet))
    elif args.command == "genealogy":
        sys.exit(_cmd_genealogy(args, quiet))
    elif args.command == "copyright-check":
        sys.exit(_cmd_copyright_check(args, quiet))
    elif args.command == "insurance-package":
        sys.exit(_cmd_insurance_package(args, quiet))
    elif args.command == "simulate-audit":
        sys.exit(_cmd_simulate_audit(args, quiet))
    elif args.command == "watch-regulatory":
        sys.exit(_cmd_watch_regulatory(args, quiet))
    elif args.command == "freeze":
        sys.exit(_cmd_freeze(args, quiet))
    elif args.command == "github-app":
        sys.exit(_cmd_github_app(args, quiet))
    elif args.command == "compliance-matrix":
        sys.exit(_cmd_compliance_matrix(args, quiet))
    elif args.command == "scan-agentic":
        sys.exit(_cmd_scan_agentic(args, quiet))
    else:
        parser.print_help()
        sys.exit(1)


if __name__ == "__main__":
    main()
