A one-line code change can rewrite what every session sends to the model -- a guidance constant moving tiers, a toolset gaining a tool, a dynamic rewriter editing a description -- without the diff showing it. The rendered prompt surface is now committed and reviewed like code. - scripts/ci/prompt_surface.py renders the system prompt (build_system_prompt_parts) and the agent's tools[] through the production code for nine surfaces (cli x claude/gpt/gemini/hermes, cli in a code workspace, desktop, telegram, discord, cron) into tests/fixtures/prompt_surface/. A fresh home in a child process; clock, host OS, network, tool availability, env probe and machine paths pinned, so a laptop and CI produce the same bytes. Each tool schema is stored once, with a per-surface copy only where a surface serves a different schema. - tests/ci/test_prompt_surface.py fails when the committed snapshot is stale, so a surface change must carry the regenerated snapshot in the same PR. - prompt-surface-diff.yml (advisory, PR-only, stdlib-only, no PR code runs) posts per-surface token deltas, prompt hunks and per-tool description/param changes into the unified review comment. - CODEOWNERS requests hermes-agent-core on the snapshot; a new prompt-surface-review ruleset requires two of their approvals. - Docs: prompt-assembly.md (how it works), contributing.md (what to do when the test fails).
121 lines
4.4 KiB
Python
121 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Strict CI aggregate gate (shared by ci.yaml and the stable orchestrator).
|
|
|
|
Behavior tests: tests/ci/test_required_results.py
|
|
|
|
Input: the JSON ``toJSON(needs)`` of the all-checks-pass job on stdin.
|
|
Failures always fail the gate. A skipped or missing update/E2E consumer also
|
|
fails whenever ``detect.outputs`` selects its lane. Other skips additionally
|
|
fail in release mode unless the job is in :data:`EXCLUDED_JOBS` (PR-only jobs
|
|
that cannot run on a tag event, plus the deferred Desktop E2E). The OSV scan
|
|
is advisory in its findings only — its execution is required.
|
|
|
|
echo "$NEEDS" | python3 scripts/ci/required_results.py [--release]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from typing import Any
|
|
|
|
# Jobs that can never run on a release (push/tag) event, plus the deferred
|
|
# Desktop E2E. These are the only skips a strict run tolerates.
|
|
PR_ONLY_JOBS = ("history-check", "lockfile-diff", "prompt-surface-diff", "supply-chain")
|
|
DEFERRED_JOBS = ("e2e-desktop",)
|
|
EXCLUDED_JOBS = frozenset((*PR_ONLY_JOBS, *DEFERRED_JOBS))
|
|
|
|
NEEDS_JSON_OUTPUT = "needs-json"
|
|
|
|
# Reusable-workflow parents of the update/E2E consumers. The actual detect
|
|
# outputs already arrive in toJSON(needs); a selected consumer cannot skip
|
|
# merely because this is a PR rather than a release.
|
|
_UPDATE_CONSUMER_LANES = {
|
|
"tests": ("python", "e2e", "e2e_upgrade"),
|
|
"tests-os": ("python", "desktop_updater", "e2e", "e2e_upgrade"),
|
|
"e2e-desktop-update": ("e2e_desktop_update",),
|
|
}
|
|
|
|
|
|
def evaluate_gate(
|
|
needs: dict[str, dict[str, Any]] | None,
|
|
release: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Verdict for a ``needs`` context; see the module docstring.
|
|
|
|
Returns ``ok``, plus sorted ``failed`` (every non-success, non-allowed
|
|
entry) and ``allowed_skips``.
|
|
"""
|
|
failed: list[str] = []
|
|
allowed_skips: list[str] = []
|
|
entries = needs or {}
|
|
lanes = (entries.get("detect") or {}).get("outputs") or {}
|
|
required = {job for job, keys in _UPDATE_CONSUMER_LANES.items()
|
|
if any(lanes.get(key) in ("true", True) for key in keys)}
|
|
if not entries:
|
|
failed.append("<no-needs>")
|
|
failed.extend(required - entries.keys())
|
|
for name, info in entries.items():
|
|
result = (info or {}).get("result")
|
|
if result == "success":
|
|
continue
|
|
if result == "skipped" and name not in required and (not release or name in EXCLUDED_JOBS):
|
|
allowed_skips.append(name)
|
|
continue
|
|
failed.append(name)
|
|
return {
|
|
"ok": not failed,
|
|
"failed": sorted(failed),
|
|
"allowed_skips": sorted(allowed_skips),
|
|
}
|
|
|
|
|
|
def compact_results(needs: dict[str, dict[str, Any]] | None) -> dict[str, str]:
|
|
"""{job_name: result} for every entry, for the PR comment assembler."""
|
|
return {name: (info or {}).get("result", "") for name, info in (needs or {}).items()}
|
|
|
|
|
|
def render_report(needs: dict[str, dict[str, Any]] | None, verdict: dict[str, Any]) -> list[str]:
|
|
"""Per-job lines plus ::error:: annotations for the failed set."""
|
|
lines: list[str] = []
|
|
for name in sorted(needs or {}):
|
|
result = (needs[name] or {}).get("result", "unknown")
|
|
icon = {"success": "✅", "skipped": "⏭️"}.get(result, "❌")
|
|
lines.append(f"{icon} {name}: {result}")
|
|
if verdict["failed"]:
|
|
failed = verdict["failed"]
|
|
lines.append(f"::error::{len(failed)} job(s) failed: {', '.join(failed)}")
|
|
return lines
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
parser.add_argument(
|
|
"--release",
|
|
action="store_true",
|
|
help="Strict mode: a skipped excluded job is the only tolerated skip.",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
needs = json.load(sys.stdin)
|
|
verdict = evaluate_gate(needs, release=args.release)
|
|
compact = compact_results(needs)
|
|
|
|
output_file = os.environ.get("GITHUB_OUTPUT")
|
|
if output_file:
|
|
with open(output_file, "a", encoding="utf-8") as fh:
|
|
fh.write(f"{NEEDS_JSON_OUTPUT}={json.dumps(compact)}\n")
|
|
|
|
print(f"{NEEDS_JSON_OUTPUT}={json.dumps(compact)}")
|
|
for line in render_report(needs, verdict):
|
|
print(line)
|
|
if verdict["ok"]:
|
|
print("All checks passed" if args.release else "All checks passed (or were skipped)")
|
|
return 0 if verdict["ok"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|