Files
hermes-agent/scripts/ci/required_results.py
Teknium ae3598c7cd feat(ci): reviewers see every change to the system prompt and tool schemas
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).
2026-10-11 01:21:18 -07:00

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())