diff --git a/.github/scripts/pitot_e2e_report.py b/.github/scripts/pitot_e2e_report.py index 0fd008ff1..30d7380dc 100644 --- a/.github/scripts/pitot_e2e_report.py +++ b/.github/scripts/pitot_e2e_report.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Publish a trusted, sticky PR summary for Pitot's per-agent E2E workflows.""" +"""Publish a trusted, sticky PR summary for Pitot's unified E2E workflow.""" from __future__ import annotations @@ -9,20 +9,22 @@ import os from pathlib import Path import re +import sys import urllib.parse import urllib.request import zipfile MARKER = "" -WORKFLOWS = { - "claude": "Pitot E2E — Claude", - "cursor": "Pitot E2E — Cursor", - "codex": "Pitot E2E — Codex", - "gemini": "Pitot E2E — Gemini", - "opencode": "Pitot E2E — OpenCode", -} -PLATFORMS = ("ubuntu", "macos", "windows") +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "labs/15-pitot/scripts")) +from pitot_adapter_supervisor import ContractError, validate_manifest + + +MANIFEST = json.loads((ROOT / "labs/15-pitot/adapter-verification.json").read_text(encoding="utf-8")) +AGENTS = tuple(agent["id"] for agent in MANIFEST["agents"]) +AGENT_LABELS = {agent["id"]: agent["label"] for agent in MANIFEST["agents"]} +PLATFORMS = tuple(platform["id"] for platform in MANIFEST["platforms"]) MAX_ARTIFACT_BYTES = 1_000_000 @@ -93,10 +95,21 @@ def validate_result( return value -def aggregate(platform_results: dict[str, dict[str, object] | None]) -> str: - if any(platform not in platform_results or platform_results[platform] is None for platform in PLATFORMS): +def validate_inventory(value: object) -> dict[str, object]: + try: + validate_manifest(value) + except ContractError as error: + raise ValueError(f"invalid supervised inventory: {error}") from error + return value + + +def aggregate( + platform_results: dict[str, dict[str, object] | None], + platforms: tuple[str, ...] = PLATFORMS, +) -> str: + if any(platform not in platform_results or platform_results[platform] is None for platform in platforms): return "pending" - return "passing" if all(platform_results[p]["status"] == "pass" for p in PLATFORMS) else "failing" + return "passing" if all(platform_results[p]["status"] == "pass" for p in platforms) else "failing" def result_cell(result: dict[str, object] | None) -> str: @@ -125,6 +138,8 @@ def render_comment( results: dict[str, dict[str, dict[str, object] | None]], run_urls: dict[str, str], head_sha: str, + agent_records: tuple[dict[str, str], ...] | None = None, + platforms: tuple[str, ...] = PLATFORMS, ) -> str: lines = [ MARKER, @@ -136,16 +151,18 @@ def render_comment( "|---|---|---|---|---|---|", ] icons = {"passing": "✅ Passing", "failing": "❌ Failing", "pending": "⏳ Pending"} - for agent in WORKFLOWS: - platforms = results.get(agent, {}) - status = aggregate(platforms) + records = agent_records or tuple({"id": agent, "label": AGENT_LABELS[agent]} for agent in AGENTS) + for record in records: + agent = record["id"] + platform_results = results.get(agent, {}) + status = aggregate(platform_results, platforms) url = run_urls.get(agent) - label = agent.capitalize() if agent != "opencode" else "OpenCode" + label = record["label"] linked_label = f"[{label}]({url})" if url else label evidence = "All platforms are required" if status != "pending" else "Waiting for all platform artifacts" lines.append( - f"| {linked_label} | {result_cell(platforms.get('ubuntu'))} | " - f"{result_cell(platforms.get('macos'))} | {result_cell(platforms.get('windows'))} | " + f"| {linked_label} | {result_cell(platform_results.get('ubuntu'))} | " + f"{result_cell(platform_results.get('macos'))} | {result_cell(platform_results.get('windows'))} | " f"{icons[status]} | {evidence} |" ) lines.extend(["", f"Source commit: `{head_sha[:12]}`", ""]) @@ -213,6 +230,45 @@ def load_artifact( ) +def load_inventory(github: GitHub, artifact: dict[str, object]) -> dict[str, object]: + archive = github.download(str(artifact["archive_download_url"])) + with zipfile.ZipFile(io.BytesIO(archive)) as bundle: + names = bundle.namelist() + if names != ["adapter-verification.json"] or bundle.getinfo(names[0]).file_size > MAX_ARTIFACT_BYTES: + raise ValueError("inventory artifact must contain one bounded adapter-verification.json") + return validate_inventory(json.loads(bundle.read(names[0]))) + + +def collect_results( + github: GitHub, + by_name: dict[str, dict[str, object]], + inventory: dict[str, object], + head_sha: str, + run_url: str, +) -> dict[str, dict[str, dict[str, object]]]: + agent_ids = tuple(agent["id"] for agent in inventory["agents"]) + platforms = tuple(platform["id"] for platform in inventory["platforms"]) + results: dict[str, dict[str, dict[str, object]]] = {agent: {} for agent in agent_ids} + for agent in agent_ids: + for platform in platforms: + artifact = by_name.get(f"pitot-e2e-{agent}-{platform}") + if artifact is not None: + try: + results[agent][platform] = load_artifact( + github, + artifact, + agent=agent, + platform=platform, + expected_sha=head_sha, + expected_run_url=run_url, + ) + continue + except (ValueError, json.JSONDecodeError, zipfile.BadZipFile): + pass + results[agent][platform] = failed_result(agent, platform, head_sha, run_url) + return results + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--event", type=Path, required=True) @@ -236,41 +292,21 @@ def main() -> int: print("No pull request is associated with this workflow run; nothing to report.") return 0 head_sha = workflow_run["head_sha"] - query = urllib.parse.urlencode({"head_sha": head_sha, "event": "pull_request", "per_page": 100}) - runs = github.request("GET", f"/actions/runs?{query}")["workflow_runs"] + run_url = workflow_run["html_url"] - results: dict[str, dict[str, dict[str, object] | None]] = { - agent: {platform: None for platform in PLATFORMS} for agent in WORKFLOWS - } - run_urls: dict[str, str] = {} - for agent, workflow_name in WORKFLOWS.items(): - candidates = [run for run in runs if run.get("name") == workflow_name] - if not candidates: - continue - run = max(candidates, key=lambda value: value["id"]) - run_urls[agent] = run["html_url"] - if run["status"] != "completed": - continue - artifacts = github.request("GET", f"/actions/runs/{run['id']}/artifacts")["artifacts"] - by_name = {artifact["name"]: artifact for artifact in artifacts if not artifact.get("expired")} - for platform in PLATFORMS: - artifact = by_name.get(f"pitot-e2e-{agent}-{platform}") - if artifact is not None: - try: - results[agent][platform] = load_artifact( - github, - artifact, - agent=agent, - platform=platform, - expected_sha=run["head_sha"], - expected_run_url=run["html_url"], - ) - except (ValueError, json.JSONDecodeError, zipfile.BadZipFile): - results[agent][platform] = failed_result(agent, platform, head_sha, run["html_url"]) - elif run.get("conclusion") != "success": - results[agent][platform] = failed_result(agent, platform, head_sha, run["html_url"]) + artifacts = github.request("GET", f"/actions/runs/{workflow_run['id']}/artifacts")["artifacts"] + by_name = {artifact["name"]: artifact for artifact in artifacts if not artifact.get("expired")} + inventory_artifact = by_name.get("pitot-e2e-inventory") + if inventory_artifact is None: + raise ValueError("unified E2E run is missing its supervised adapter inventory") + inventory = load_inventory(github, inventory_artifact) + agent_records = tuple(inventory["agents"]) + agent_ids = tuple(agent["id"] for agent in agent_records) + platforms = tuple(platform["id"] for platform in inventory["platforms"]) + run_urls = {agent: run_url for agent in agent_ids} + results = collect_results(github, by_name, inventory, head_sha, run_url) - body = render_comment(results, run_urls, head_sha) + body = render_comment(results, run_urls, head_sha, agent_records, platforms) issue_number = pull_requests[0]["number"] comments = github.request("GET", f"/issues/{issue_number}/comments?per_page=100") existing = next( diff --git a/.github/workflows/pitot-e2e-agent.yml b/.github/workflows/pitot-e2e-agent.yml index 0ba379853..7f7c43336 100644 --- a/.github/workflows/pitot-e2e-agent.yml +++ b/.github/workflows/pitot-e2e-agent.yml @@ -7,6 +7,14 @@ on: description: Supported Pitot coding-agent host required: true type: string + platform: + description: Canonical Pitot platform identifier + required: true + type: string + runner: + description: GitHub-hosted runner selected by the supervisor + required: true + type: string permissions: contents: read @@ -16,18 +24,8 @@ env: jobs: e2e: - name: ${{ inputs.agent }} (${{ matrix.platform }}) - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-latest - platform: ubuntu - - runner: macos-latest - platform: macos - - runner: windows-latest - platform: windows - runs-on: ${{ matrix.runner }} + name: ${{ inputs.agent }} (${{ inputs.platform }}) + runs-on: ${{ inputs.runner }} steps: - uses: actions/checkout@v7 - uses: actions/setup-go@v7 @@ -75,7 +73,7 @@ jobs: run: >- python labs/15-pitot/tests/run_e2e_report.py --agent "${{ inputs.agent }}" - --platform "${{ matrix.platform }}" + --platform "${{ inputs.platform }}" --output "${{ runner.temp }}/pitot-e2e/result.json" -- "$PITOT_BASH" "labs/15-pitot/tests/e2e_${{ inputs.agent }}_cli_test.sh" < /dev/null @@ -83,7 +81,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: pitot-e2e-${{ inputs.agent }}-${{ matrix.platform }} + name: pitot-e2e-${{ inputs.agent }}-${{ inputs.platform }} path: ${{ runner.temp }}/pitot-e2e/result.json if-no-files-found: error retention-days: 14 diff --git a/.github/workflows/pitot-e2e-claude.yml b/.github/workflows/pitot-e2e-claude.yml deleted file mode 100644 index bb8f9989b..000000000 --- a/.github/workflows/pitot-e2e-claude.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Pitot E2E — Claude - -on: - pull_request: - paths: &pitot_paths - - "labs/15-pitot/**" - - "go.work" - - ".github/workflows/pitot-e2e-*.yml" - - ".github/scripts/pitot_e2e_report.py" - push: - branches: [main] - paths: *pitot_paths - workflow_dispatch: - -jobs: - verify: - uses: ./.github/workflows/pitot-e2e-agent.yml - with: - agent: claude diff --git a/.github/workflows/pitot-e2e-codex.yml b/.github/workflows/pitot-e2e-codex.yml deleted file mode 100644 index c38752f50..000000000 --- a/.github/workflows/pitot-e2e-codex.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Pitot E2E — Codex - -on: - pull_request: - paths: &pitot_paths - - "labs/15-pitot/**" - - "go.work" - - ".github/workflows/pitot-e2e-*.yml" - - ".github/scripts/pitot_e2e_report.py" - push: - branches: [main] - paths: *pitot_paths - workflow_dispatch: - -jobs: - verify: - uses: ./.github/workflows/pitot-e2e-agent.yml - with: - agent: codex diff --git a/.github/workflows/pitot-e2e-cursor.yml b/.github/workflows/pitot-e2e-cursor.yml deleted file mode 100644 index b5db90d1a..000000000 --- a/.github/workflows/pitot-e2e-cursor.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Pitot E2E — Cursor - -on: - pull_request: - paths: &pitot_paths - - "labs/15-pitot/**" - - "go.work" - - ".github/workflows/pitot-e2e-*.yml" - - ".github/scripts/pitot_e2e_report.py" - push: - branches: [main] - paths: *pitot_paths - workflow_dispatch: - -jobs: - verify: - uses: ./.github/workflows/pitot-e2e-agent.yml - with: - agent: cursor diff --git a/.github/workflows/pitot-e2e-gemini.yml b/.github/workflows/pitot-e2e-gemini.yml deleted file mode 100644 index 4afd38039..000000000 --- a/.github/workflows/pitot-e2e-gemini.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Pitot E2E — Gemini - -on: - pull_request: - paths: &pitot_paths - - "labs/15-pitot/**" - - "go.work" - - ".github/workflows/pitot-e2e-*.yml" - - ".github/scripts/pitot_e2e_report.py" - push: - branches: [main] - paths: *pitot_paths - workflow_dispatch: - -jobs: - verify: - uses: ./.github/workflows/pitot-e2e-agent.yml - with: - agent: gemini diff --git a/.github/workflows/pitot-e2e-opencode.yml b/.github/workflows/pitot-e2e-opencode.yml deleted file mode 100644 index cf9f7ea96..000000000 --- a/.github/workflows/pitot-e2e-opencode.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Pitot E2E — OpenCode - -on: - pull_request: - paths: &pitot_paths - - "labs/15-pitot/**" - - "go.work" - - ".github/workflows/pitot-e2e-*.yml" - - ".github/scripts/pitot_e2e_report.py" - push: - branches: [main] - paths: *pitot_paths - workflow_dispatch: - -jobs: - verify: - uses: ./.github/workflows/pitot-e2e-agent.yml - with: - agent: opencode diff --git a/.github/workflows/pitot-e2e-report.yml b/.github/workflows/pitot-e2e-report.yml index 371269581..df03f5958 100644 --- a/.github/workflows/pitot-e2e-report.yml +++ b/.github/workflows/pitot-e2e-report.yml @@ -1,13 +1,9 @@ +# Generated by pitot_adapter_supervisor.py. Do not edit directly. name: Report Pitot E2E results on: workflow_run: - workflows: - - Pitot E2E — Claude - - Pitot E2E — Cursor - - Pitot E2E — Codex - - Pitot E2E — Gemini - - Pitot E2E — OpenCode + workflows: [Pitot E2E] types: [completed] permissions: diff --git a/.github/workflows/pitot-e2e.yml b/.github/workflows/pitot-e2e.yml new file mode 100644 index 000000000..5e0f0b18f --- /dev/null +++ b/.github/workflows/pitot-e2e.yml @@ -0,0 +1,52 @@ +# Generated by pitot_adapter_supervisor.py. Do not edit directly. +name: Pitot E2E + +on: + pull_request: + paths: &pitot_paths + - "labs/15-pitot/**" + - "go.work" + - ".github/workflows/pitot-e2e*.yml" + - ".github/scripts/pitot_e2e_report.py" + push: + branches: [main] + paths: *pitot_paths + workflow_dispatch: + +permissions: + contents: read + +jobs: + inventory: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.supervisor.outputs.matrix }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version-file: labs/15-pitot/pitot/go.mod + cache-dependency-path: labs/15-pitot/pitot/go.mod + - id: supervisor + shell: bash + run: | + matrix="$(python3 labs/15-pitot/scripts/pitot_adapter_supervisor.py matrix)" + echo "matrix=$matrix" >> "$GITHUB_OUTPUT" + - name: Upload supervised adapter inventory + uses: actions/upload-artifact@v4 + with: + name: pitot-e2e-inventory + path: labs/15-pitot/adapter-verification.json + if-no-files-found: error + retention-days: 14 + + verify: + needs: inventory + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.inventory.outputs.matrix) }} + uses: ./.github/workflows/pitot-e2e-agent.yml + with: + agent: ${{ matrix.agent }} + platform: ${{ matrix.platform }} + runner: ${{ matrix.runner }} diff --git a/.github/workflows/pitot-lab.yml b/.github/workflows/pitot-lab.yml index f98329025..28a9811c7 100644 --- a/.github/workflows/pitot-lab.yml +++ b/.github/workflows/pitot-lab.yml @@ -6,12 +6,16 @@ on: - "labs/15-pitot/**" - "go.work" - ".github/workflows/pitot-lab.yml" + - ".github/workflows/pitot-e2e*.yml" + - ".github/scripts/pitot_e2e_report.py" push: branches: [main] paths: - "labs/15-pitot/**" - "go.work" - ".github/workflows/pitot-lab.yml" + - ".github/workflows/pitot-e2e*.yml" + - ".github/scripts/pitot_e2e_report.py" workflow_dispatch: permissions: @@ -105,5 +109,7 @@ jobs: run: bash labs/15-pitot/scripts/generate_types.sh - name: Verify generated Python SDK syntax run: python -m py_compile labs/15-pitot/pitot-distribution/sdk/python/pitot/types.py + - name: Supervise adapter verification boundary + run: python labs/15-pitot/scripts/pitot_adapter_supervisor.py check - name: Verify projection and public surface run: python -m unittest discover -s labs/15-pitot/tests -p 'test_*.py' diff --git a/labs/15-pitot/adapter-verification.json b/labs/15-pitot/adapter-verification.json new file mode 100644 index 000000000..5566cff76 --- /dev/null +++ b/labs/15-pitot/adapter-verification.json @@ -0,0 +1,16 @@ +{ + "schema_version": 1, + "platforms": [ + {"id": "ubuntu", "runner": "ubuntu-latest"}, + {"id": "macos", "runner": "macos-latest"}, + {"id": "windows", "runner": "windows-latest"} + ], + "agents": [ + {"id": "claude", "label": "Claude"}, + {"id": "cursor", "label": "Cursor"}, + {"id": "codex", "label": "Codex"}, + {"id": "gemini", "label": "Gemini"}, + {"id": "kimi", "label": "Kimi Code"}, + {"id": "opencode", "label": "OpenCode"} + ] +} diff --git a/labs/15-pitot/pitot-distribution/UPSTREAM.json b/labs/15-pitot/pitot-distribution/UPSTREAM.json index 368dc4ac5..53f8f6150 100644 --- a/labs/15-pitot/pitot-distribution/UPSTREAM.json +++ b/labs/15-pitot/pitot-distribution/UPSTREAM.json @@ -1,7 +1,7 @@ { "files": { "CONTRIBUTING.md": "0613b71aa497f8ca7d7296bf34ade87bfc7237a664d2e812d9b77b3b6befb0ad", - "README.md": "16ba79860542e5e9c6fc704e6f853a017b16db0a3a8f5ea50d1908d922489d1f", + "README.md": "99e2c764ec29dfcdb42ff09caa6583d0de9192dc34460a8a563d8ae5ea47dfa0", "adapters/adapters.go": "b516fdd0fdd805a08cb1467f78ee367d5f280d0084a55169d09c6f3d0b795fc0", "assets/pitot-boundary.png": "8a0ddb7d81831d94e14813f50ea4ca8670d77417f339ed2f91f0c653bf52f41d", "assets/pitot-boundary.svg": "0c3871d70c84748573f231842091deb38a6def2862403ca34e8cc4493b9c9ebf", diff --git a/labs/15-pitot/pitot-distribution/release-notes/2026-07-22-supervise-adapter-verification.md b/labs/15-pitot/pitot-distribution/release-notes/2026-07-22-supervise-adapter-verification.md new file mode 100644 index 000000000..403a5bf2e --- /dev/null +++ b/labs/15-pitot/pitot-distribution/release-notes/2026-07-22-supervise-adapter-verification.md @@ -0,0 +1,5 @@ +### Supervise the complete adapter verification boundary + +Pitot now derives its cross-platform E2E matrix, pull-request report, and public +verification summary from one canonical adapter manifest. A built-in adapter can +no longer pass source tests while remaining absent from CI or public status. diff --git a/labs/15-pitot/public-readme-preview/README.md b/labs/15-pitot/public-readme-preview/README.md index 6f815fc51..882be1f2a 100644 --- a/labs/15-pitot/public-readme-preview/README.md +++ b/labs/15-pitot/public-readme-preview/README.md @@ -6,15 +6,15 @@ The open sensor and control transport for coding-agent tooling.
+ -The badged hosts are verified upstream in Intelligence Flow on Ubuntu, macOS, and Windows.
+Every supervised adapter is required on Ubuntu, macOS, and Windows.
+ +Supervised adapters: Claude · Cursor · Codex · Gemini · Kimi Code · OpenCode
+One language-neutral boundary for Claude Code, Cursor, Codex, Gemini, Kimi Code, OpenCode, and compatible runtimes. diff --git a/labs/15-pitot/scripts/pitot_adapter_inventory.go b/labs/15-pitot/scripts/pitot_adapter_inventory.go new file mode 100644 index 000000000..9e43f6c6e --- /dev/null +++ b/labs/15-pitot/scripts/pitot_adapter_inventory.go @@ -0,0 +1,22 @@ +// Command pitot_adapter_inventory prints the shipped Pitot adapter IDs for the +// repository supervisor. It is internal verification machinery, not a public +// Pitot command. +package main + +import ( + "encoding/json" + "os" + + "github.com/operatorstack/pitot/adapters" +) + +func main() { + hosts := adapters.Supported() + values := make([]string, len(hosts)) + for index, host := range hosts { + values[index] = string(host) + } + if err := json.NewEncoder(os.Stdout).Encode(values); err != nil { + panic(err) + } +} diff --git a/labs/15-pitot/scripts/pitot_adapter_supervisor.py b/labs/15-pitot/scripts/pitot_adapter_supervisor.py new file mode 100644 index 000000000..d34d4b157 --- /dev/null +++ b/labs/15-pitot/scripts/pitot_adapter_supervisor.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python3 +"""Supervise Pitot's complete built-in adapter verification boundary.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import re +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[3] +MANIFEST = Path("labs/15-pitot/adapter-verification.json") +README = Path("labs/15-pitot/public-readme-preview/README.md") +UNIFIED_WORKFLOW = Path(".github/workflows/pitot-e2e.yml") +REPORT_WORKFLOW = Path(".github/workflows/pitot-e2e-report.yml") +REUSABLE_WORKFLOW = Path(".github/workflows/pitot-e2e-agent.yml") +INVENTORY_HELPER = Path("labs/15-pitot/scripts/pitot_adapter_inventory.go") +README_START = "" +README_END = "" +ID_PATTERN = re.compile(r"^[a-z][a-z0-9-]*$") +LABEL_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 .+_-]{0,39}$") + + +class ContractError(ValueError): + pass + + +def load_manifest(root: Path = ROOT) -> dict[str, object]: + try: + value = json.loads((root / MANIFEST).read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise ContractError(f"cannot load {MANIFEST}: {error}") from error + validate_manifest(value) + return value + + +def validate_manifest(value: object) -> None: + if not isinstance(value, dict) or set(value) != {"schema_version", "platforms", "agents"}: + raise ContractError("manifest must contain schema_version, platforms, and agents") + if value["schema_version"] != 1: + raise ContractError("unsupported manifest schema_version") + platforms = value["platforms"] + agents = value["agents"] + if not isinstance(platforms, list) or not isinstance(agents, list) or not agents: + raise ContractError("platforms and non-empty agents must be lists") + expected_platforms = [ + {"id": "ubuntu", "runner": "ubuntu-latest"}, + {"id": "macos", "runner": "macos-latest"}, + {"id": "windows", "runner": "windows-latest"}, + ] + if platforms != expected_platforms: + raise ContractError("platforms must be the required Ubuntu, macOS, and Windows matrix") + ids: list[str] = [] + labels: list[str] = [] + for agent in agents: + if not isinstance(agent, dict) or set(agent) != {"id", "label"}: + raise ContractError("each agent must contain exactly id and label") + agent_id, label = agent["id"], agent["label"] + if not isinstance(agent_id, str) or not ID_PATTERN.fullmatch(agent_id): + raise ContractError(f"invalid agent id: {agent_id!r}") + if not isinstance(label, str) or not LABEL_PATTERN.fullmatch(label): + raise ContractError(f"agent {agent_id} has an invalid label") + ids.append(agent_id) + labels.append(label) + if len(ids) != len(set(ids)): + raise ContractError("agent ids must be unique") + if len(labels) != len(set(labels)): + raise ContractError("agent labels must be unique") + + +def built_in_adapters(root: Path = ROOT) -> list[str]: + completed = subprocess.run( + ["go", "run", str(root / INVENTORY_HELPER)], + cwd=root, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + value = json.loads(completed.stdout) + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ContractError("Go adapter inventory returned an invalid result") + return value + + +def matrix(manifest: dict[str, object]) -> list[dict[str, str]]: + return [ + {"agent": agent["id"], "platform": platform["id"], "runner": platform["runner"]} + for agent in manifest["agents"] + for platform in manifest["platforms"] + ] + + +def readme_block(manifest: dict[str, object]) -> str: + labels = " · ".join(agent["label"] for agent in manifest["agents"]) + return "\n".join( + [ + README_START, + '
", + "", + 'Every supervised adapter is required on Ubuntu, macOS, and Windows.
', + "", + f'Supervised adapters: {labels}
', + README_END, + ] + ) + + +def replace_readme_block(text: str, manifest: dict[str, object]) -> str: + pattern = re.compile(re.escape(README_START) + r".*?" + re.escape(README_END), re.DOTALL) + if len(pattern.findall(text)) != 1: + raise ContractError("README must contain exactly one supervisor marker block") + return pattern.sub(readme_block(manifest), text) + + +def unified_workflow() -> str: + return """# Generated by pitot_adapter_supervisor.py. Do not edit directly. +name: Pitot E2E + +on: + pull_request: + paths: &pitot_paths + - "labs/15-pitot/**" + - "go.work" + - ".github/workflows/pitot-e2e*.yml" + - ".github/scripts/pitot_e2e_report.py" + push: + branches: [main] + paths: *pitot_paths + workflow_dispatch: + +permissions: + contents: read + +jobs: + inventory: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.supervisor.outputs.matrix }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version-file: labs/15-pitot/pitot/go.mod + cache-dependency-path: labs/15-pitot/pitot/go.mod + - id: supervisor + shell: bash + run: | + matrix="$(python3 labs/15-pitot/scripts/pitot_adapter_supervisor.py matrix)" + echo "matrix=$matrix" >> "$GITHUB_OUTPUT" + - name: Upload supervised adapter inventory + uses: actions/upload-artifact@v4 + with: + name: pitot-e2e-inventory + path: labs/15-pitot/adapter-verification.json + if-no-files-found: error + retention-days: 14 + + verify: + needs: inventory + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.inventory.outputs.matrix) }} + uses: ./.github/workflows/pitot-e2e-agent.yml + with: + agent: ${{ matrix.agent }} + platform: ${{ matrix.platform }} + runner: ${{ matrix.runner }} +""" + + +def report_workflow() -> str: + return """# Generated by pitot_adapter_supervisor.py. Do not edit directly. +name: Report Pitot E2E results + +on: + workflow_run: + workflows: [Pitot E2E] + types: [completed] + +permissions: + actions: read + contents: read + pull-requests: write + +concurrency: + group: pitot-e2e-report-${{ github.event.workflow_run.head_sha }} + cancel-in-progress: true + +jobs: + report: + if: github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Update sticky PR report + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: >- + python3 .github/scripts/pitot_e2e_report.py + --event "$GITHUB_EVENT_PATH" + --repository "$GITHUB_REPOSITORY" +""" + + +def contract_errors(root: Path, adapters: list[str] | None = None) -> list[str]: + try: + manifest = load_manifest(root) + except ContractError as error: + return [str(error)] + errors: list[str] = [] + inventory = adapters if adapters is not None else built_in_adapters(root) + declared = [agent["id"] for agent in manifest["agents"]] + missing = sorted(set(inventory) - set(declared)) + extra = sorted(set(declared) - set(inventory)) + if missing: + errors.append(f"built-in adapters missing from manifest: {', '.join(missing)}") + if extra: + errors.append(f"manifest agents are not built-in adapters: {', '.join(extra)}") + for agent_id in declared: + script = root / f"labs/15-pitot/tests/e2e_{agent_id}_cli_test.sh" + if not script.is_file(): + errors.append(f"adapter {agent_id} is missing {script.relative_to(root)}") + expected_files = { + UNIFIED_WORKFLOW: unified_workflow(), + REPORT_WORKFLOW: report_workflow(), + } + for relative, expected in expected_files.items(): + path = root / relative + actual = path.read_text(encoding="utf-8") if path.is_file() else "" + if actual != expected: + errors.append(f"generated surface drifted: {relative}") + reusable_path = root / REUSABLE_WORKFLOW + reusable = reusable_path.read_text(encoding="utf-8") if reusable_path.is_file() else "" + required_reusable_contract = ( + "platform:\n description: Canonical Pitot platform identifier", + "runner:\n description: GitHub-hosted runner selected by the supervisor", + "runs-on: ${{ inputs.runner }}", + '--agent "${{ inputs.agent }}"', + '--platform "${{ inputs.platform }}"', + "name: pitot-e2e-${{ inputs.agent }}-${{ inputs.platform }}", + ) + if any(fragment not in reusable for fragment in required_reusable_contract) or "matrix.platform" in reusable: + errors.append(f"reusable workflow escaped supervisor inputs: {REUSABLE_WORKFLOW}") + readme_path = root / README + if readme_path.is_file(): + actual = readme_path.read_text(encoding="utf-8") + try: + expected = replace_readme_block(actual, manifest) + if actual != expected: + errors.append(f"generated surface drifted: {README}") + except ContractError as error: + errors.append(str(error)) + else: + errors.append(f"missing {README}") + return errors + + +def render(root: Path = ROOT) -> None: + manifest = load_manifest(root) + readme_path = root / README + readme_path.write_text( + replace_readme_block(readme_path.read_text(encoding="utf-8"), manifest), + encoding="utf-8", + ) + (root / UNIFIED_WORKFLOW).write_text(unified_workflow(), encoding="utf-8") + (root / REPORT_WORKFLOW).write_text(report_workflow(), encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("operation", choices=("check", "matrix", "render")) + parser.add_argument("--repo", type=Path, default=ROOT) + args = parser.parse_args() + root = args.repo.resolve() + try: + if args.operation == "render": + render(root) + print("PASS: rendered Pitot adapter verification surfaces") + return 0 + errors = contract_errors(root) + if errors: + raise ContractError("; ".join(errors)) + if args.operation == "matrix": + manifest = load_manifest(root) + print(json.dumps({"include": matrix(manifest)}, separators=(",", ":"))) + else: + print(f"PASS: supervised {len(load_manifest(root)['agents'])} Pitot adapters") + return 0 + except (ContractError, subprocess.CalledProcessError) as error: + print(f"BLOCKED: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/labs/15-pitot/tests/run_e2e_report.py b/labs/15-pitot/tests/run_e2e_report.py index 157d7901b..80ca513f3 100644 --- a/labs/15-pitot/tests/run_e2e_report.py +++ b/labs/15-pitot/tests/run_e2e_report.py @@ -12,8 +12,10 @@ import sys -AGENTS = {"claude", "cursor", "codex", "gemini", "opencode"} -PLATFORMS = {"ubuntu", "macos", "windows"} +ROOT = Path(__file__).resolve().parents[3] +MANIFEST = json.loads((ROOT / "labs/15-pitot/adapter-verification.json").read_text(encoding="utf-8")) +AGENTS = {agent["id"] for agent in MANIFEST["agents"]} +PLATFORMS = {platform["id"] for platform in MANIFEST["platforms"]} RESULT_PATTERN = re.compile(r"^PITOT_E2E_RESULT mode=(real_cli|hook_subprocess)$", re.MULTILINE) diff --git a/labs/15-pitot/tests/test_adapter_supervisor.py b/labs/15-pitot/tests/test_adapter_supervisor.py new file mode 100644 index 000000000..5d4d6baef --- /dev/null +++ b/labs/15-pitot/tests/test_adapter_supervisor.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import copy +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[3] +SPEC = importlib.util.spec_from_file_location( + "pitot_adapter_supervisor", + ROOT / "labs/15-pitot/scripts/pitot_adapter_supervisor.py", +) +supervisor = importlib.util.module_from_spec(SPEC) +assert SPEC and SPEC.loader +SPEC.loader.exec_module(supervisor) + + +def _fixture(root: Path) -> tuple[Path, list[str]]: + manifest = copy.deepcopy(supervisor.load_manifest(ROOT)) + manifest_path = root / supervisor.MANIFEST + manifest_path.parent.mkdir(parents=True) + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + tests = root / "labs/15-pitot/tests" + tests.mkdir(parents=True) + ids = [agent["id"] for agent in manifest["agents"]] + for agent_id in ids: + (tests / f"e2e_{agent_id}_cli_test.sh").write_text("#!/usr/bin/env bash\n") + readme = root / supervisor.README + readme.parent.mkdir(parents=True) + readme.write_text( + f"# Pitot\n\n{supervisor.README_START}\nplaceholder\n{supervisor.README_END}\n", + encoding="utf-8", + ) + (root / ".github/workflows").mkdir(parents=True) + reusable = root / supervisor.REUSABLE_WORKFLOW + reusable.write_text( + "platform:\n description: Canonical Pitot platform identifier\n" + "runner:\n description: GitHub-hosted runner selected by the supervisor\n" + "runs-on: ${{ inputs.runner }}\n" + '--agent "${{ inputs.agent }}"\n' + '--platform "${{ inputs.platform }}"\n' + "name: pitot-e2e-${{ inputs.agent }}-${{ inputs.platform }}\n" + ) + supervisor.render(root) + return manifest_path, ids + + +class AdapterSupervisorTests(unittest.TestCase): + def test_real_repository_contract_is_complete(self): + self.assertEqual(supervisor.contract_errors(ROOT), []) + + def test_matrix_contains_every_agent_on_every_platform(self): + manifest = supervisor.load_manifest(ROOT) + matrix = supervisor.matrix(manifest) + self.assertEqual(len(matrix), 18) + self.assertEqual( + {(item["agent"], item["platform"]) for item in matrix if item["agent"] == "kimi"}, + {("kimi", "ubuntu"), ("kimi", "macos"), ("kimi", "windows")}, + ) + + def test_missing_and_extra_inventory_entries_fail(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + manifest_path, ids = _fixture(root) + value = json.loads(manifest_path.read_text()) + value["agents"] = value["agents"][1:] + manifest_path.write_text(json.dumps(value)) + errors = supervisor.contract_errors(root, ids) + self.assertTrue(any("missing from manifest" in error for error in errors)) + + value["agents"].append({"id": "phantom", "label": "Phantom"}) + manifest_path.write_text(json.dumps(value)) + errors = supervisor.contract_errors(root, ids) + self.assertTrue(any("not built-in adapters" in error for error in errors)) + + def test_duplicate_id_and_label_fail(self): + manifest = copy.deepcopy(supervisor.load_manifest(ROOT)) + manifest["agents"].append(copy.deepcopy(manifest["agents"][0])) + with self.assertRaisesRegex(supervisor.ContractError, "ids must be unique"): + supervisor.validate_manifest(manifest) + manifest = copy.deepcopy(supervisor.load_manifest(ROOT)) + manifest["agents"][1]["label"] = manifest["agents"][0]["label"] + with self.assertRaisesRegex(supervisor.ContractError, "labels must be unique"): + supervisor.validate_manifest(manifest) + + def test_missing_script_fails(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + _, ids = _fixture(root) + (root / "labs/15-pitot/tests/e2e_kimi_cli_test.sh").unlink() + self.assertTrue(any("adapter kimi is missing" in error for error in supervisor.contract_errors(root, ids))) + + def test_readme_and_workflow_drift_fail(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + _, ids = _fixture(root) + readme = root / supervisor.README + readme.write_text(readme.read_text().replace("Every supervised", "Some supervised")) + self.assertTrue(any("README.md" in error for error in supervisor.contract_errors(root, ids))) + + supervisor.render(root) + workflow = root / supervisor.UNIFIED_WORKFLOW + workflow.write_text(workflow.read_text() + "# drift\n") + self.assertTrue(any("pitot-e2e.yml" in error for error in supervisor.contract_errors(root, ids))) + + def test_reusable_workflow_cannot_select_its_own_platforms(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + _, ids = _fixture(root) + workflow = root / supervisor.REUSABLE_WORKFLOW + workflow.write_text(workflow.read_text() + "matrix.platform\n") + self.assertTrue(any("escaped supervisor" in error for error in supervisor.contract_errors(root, ids))) + + +if __name__ == "__main__": + unittest.main() diff --git a/labs/15-pitot/tests/test_e2e_reporting.py b/labs/15-pitot/tests/test_e2e_reporting.py index f64d204b0..2a2dad60b 100644 --- a/labs/15-pitot/tests/test_e2e_reporting.py +++ b/labs/15-pitot/tests/test_e2e_reporting.py @@ -1,6 +1,7 @@ from __future__ import annotations import importlib.util +import json from pathlib import Path import subprocess import sys @@ -22,6 +23,10 @@ def _load(name: str, path: Path): runner = _load("run_e2e_report", ROOT / "labs/15-pitot/tests/run_e2e_report.py") reporter = _load("pitot_e2e_report", ROOT / ".github/scripts/pitot_e2e_report.py") +supervisor = _load( + "pitot_adapter_supervisor", + ROOT / "labs/15-pitot/scripts/pitot_adapter_supervisor.py", +) def _result(agent="claude", platform="ubuntu", status="pass", mode="real_cli"): @@ -129,13 +134,48 @@ def test_artifact_redirect_rejects_http_downgrade(self): def test_workflow_matrix_covers_every_reported_platform(self): workflow = (ROOT / ".github/workflows/pitot-e2e-agent.yml").read_text() - runners = {"ubuntu": "ubuntu-latest", "macos": "macos-latest", "windows": "windows-latest"} + unified = (ROOT / ".github/workflows/pitot-e2e.yml").read_text() self.assertEqual(set(reporter.PLATFORMS), set(runner.PLATFORMS)) - for platform, image in runners.items(): - self.assertIn(f"runner: {image}\n platform: {platform}", workflow) + generated = supervisor.matrix(supervisor.load_manifest(ROOT)) + self.assertEqual(len(generated), len(reporter.AGENTS) * len(reporter.PLATFORMS)) + self.assertEqual( + {(item["agent"], item["platform"]) for item in generated}, + {(agent, platform) for agent in reporter.AGENTS for platform in reporter.PLATFORMS}, + ) + self.assertIn("runs-on: ${{ inputs.runner }}", workflow) + self.assertIn('--platform "${{ inputs.platform }}"', workflow) self.assertIn('bash_path="$(cygpath -m "$bash_path")"', workflow) self.assertIn('-- "$PITOT_BASH"', workflow) self.assertNotIn('-- bash "labs/15-pitot/tests/e2e_', workflow) + self.assertIn("fromJSON(needs.inventory.outputs.matrix)", unified) + self.assertIn("name: pitot-e2e-inventory", unified) + + def test_inventory_schema_rejects_identity_injection(self): + inventory = json.loads((ROOT / "labs/15-pitot/adapter-verification.json").read_text()) + self.assertIs(reporter.validate_inventory(inventory), inventory) + inventory["agents"][0]["label"] = "Claude | @everyone" + with self.assertRaisesRegex(ValueError, "label"): + reporter.validate_inventory(inventory) + + def test_missing_artifacts_fail_every_inventory_cell(self): + inventory = reporter.validate_inventory( + json.loads((ROOT / "labs/15-pitot/adapter-verification.json").read_text()) + ) + results = reporter.collect_results( + None, + {}, + inventory, + "a" * 40, + "https://github.com/operatorstack/intelligence-flow/actions/runs/1", + ) + self.assertEqual(len(results), 6) + self.assertTrue( + all( + result["status"] == "fail" + for platforms in results.values() + for result in platforms.values() + ) + ) def test_validates_exact_artifact_schema(self): value = _result() @@ -201,7 +241,7 @@ def test_aggregate_requires_all_platforms(self): def test_rendered_comment_is_sticky_and_shows_modes(self): results = { agent: {platform: None for platform in reporter.PLATFORMS} - for agent in reporter.WORKFLOWS + for agent in reporter.AGENTS } results["claude"] = { "ubuntu": _result(), @@ -215,6 +255,7 @@ def test_rendered_comment_is_sticky_and_shows_modes(self): self.assertIn("Windows", body) self.assertIn("All platforms are required", body) self.assertIn("OpenCode", body) + self.assertIn("Kimi Code", body) if __name__ == "__main__":