Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
"""``hermes verify`` — detect a project's run recipe and smoke-test it.
|
||||
|
||||
Scoped port of superagent-ai/grok-cli's verify subsystem entrypoint.
|
||||
Statically detects the project kind (or loads the saved manifest at
|
||||
``.hermes/environment.json``), then runs bootstrap/build/test phases and an
|
||||
optional background start + readiness poll, printing an evidence summary.
|
||||
|
||||
Completed runs are recorded into the coding verification evidence ledger
|
||||
(:mod:`agent.verification_evidence`), so a passing ``hermes verify`` satisfies
|
||||
the verify-on-stop guard the same way a passing canonical test command does.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_verify_command(args) -> int:
|
||||
from agent.verify import (
|
||||
load_or_detect,
|
||||
manifest_path,
|
||||
run_verify,
|
||||
save_manifest,
|
||||
)
|
||||
|
||||
root = Path(getattr(args, "path", None) or ".").resolve()
|
||||
if not root.is_dir():
|
||||
print(f"error: not a directory: {root}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
recipe, source = load_or_detect(root)
|
||||
if recipe is None:
|
||||
message = (
|
||||
f"No recognizable project found at {root}.\n"
|
||||
f"Create {manifest_path(root)} to define a recipe manually."
|
||||
)
|
||||
if args.json:
|
||||
print(json.dumps({"ok": False, "error": "no-recipe", "root": str(root)}))
|
||||
else:
|
||||
print(message, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if source == "detected":
|
||||
_merge_project_facts_commands(root, recipe)
|
||||
|
||||
if args.port:
|
||||
recipe.port = args.port
|
||||
|
||||
if args.save:
|
||||
path = save_manifest(root, recipe)
|
||||
if not args.json:
|
||||
print(f"Saved manifest: {path}")
|
||||
|
||||
if args.detect_only:
|
||||
payload = {"source": source, "recipe": recipe.to_dict()}
|
||||
print(json.dumps(payload, indent=None if args.json else 2))
|
||||
return 0
|
||||
|
||||
phases = None
|
||||
if args.phase:
|
||||
phases = tuple(args.phase)
|
||||
|
||||
result = run_verify(
|
||||
root,
|
||||
recipe,
|
||||
phases=phases,
|
||||
phase_timeout=args.timeout,
|
||||
ready_timeout=args.ready_timeout,
|
||||
skip_start=args.skip_start,
|
||||
port_override=args.port,
|
||||
)
|
||||
|
||||
_record_evidence(root, recipe, result, partial=bool(phases or args.skip_start))
|
||||
|
||||
if args.json:
|
||||
payload = result.to_dict()
|
||||
payload["source"] = source
|
||||
print(json.dumps(payload))
|
||||
return 0 if result.ok else 1
|
||||
|
||||
_print_human_report(recipe, source, result)
|
||||
return 0 if result.ok else 1
|
||||
|
||||
|
||||
def _merge_project_facts_commands(root: Path, recipe) -> None:
|
||||
"""Fold ``detect_project_facts`` verify commands into a detected recipe.
|
||||
|
||||
Layer ownership: ``agent.coding_context`` owns the cheap prompt-time facts
|
||||
(test/lint/build commands surfaced in the workspace snapshot and the
|
||||
verify-on-stop nudge); ``agent.verify.recipes`` owns the deep runtime
|
||||
recipe (framework, start command, port, readiness). When the two disagree
|
||||
the runtime recipe must not *lose* commands the prompt layer already
|
||||
promised the model — e.g. ``scripts/run_tests.sh`` or a ``pytest`` config
|
||||
the recipe detector doesn't know about — so any project-facts verify
|
||||
command not already covered is appended to the recipe's test list.
|
||||
|
||||
Never applied to a saved manifest (the user-edited manifest is the source
|
||||
of truth) and never raises: this is a best-effort union.
|
||||
"""
|
||||
try:
|
||||
from agent.coding_context import detect_project_facts
|
||||
|
||||
facts_commands = list(detect_project_facts(root).verify_commands)
|
||||
except Exception:
|
||||
return
|
||||
existing = {c.strip() for c in (*recipe.bootstrap, *recipe.build, *recipe.test) if c}
|
||||
for command in facts_commands:
|
||||
command = command.strip()
|
||||
if command and command not in existing:
|
||||
recipe.test.append(command)
|
||||
existing.add(command)
|
||||
|
||||
|
||||
def _record_evidence(root: Path, recipe, result, *, partial: bool) -> None:
|
||||
"""Record the completed run into the verification evidence ledger.
|
||||
|
||||
Best-effort and fail-silent: a ledger problem must never change the CLI's
|
||||
exit code or output. ``partial`` (an explicit ``--phase`` subset or
|
||||
``--skip-start``) downgrades the scope to ``targeted`` so a partial pass
|
||||
is never presented as a full workspace green.
|
||||
"""
|
||||
try:
|
||||
from agent.verification_evidence import record_verify_run
|
||||
|
||||
tails: list[str] = []
|
||||
for p in result.phases:
|
||||
if p.output_tail:
|
||||
tails.append(f"[{p.phase}] {p.command}\n{p.output_tail}")
|
||||
if result.readiness is not None:
|
||||
r = result.readiness
|
||||
readiness_line = (
|
||||
f"[start] {recipe.start} -> "
|
||||
+ (f"ready (HTTP {r.status_code})" if r.ready else f"not ready ({r.error or 'timeout'})")
|
||||
)
|
||||
tails.append(readiness_line)
|
||||
record_verify_run(
|
||||
root=root,
|
||||
session_id=os.environ.get("HERMES_SESSION_ID"),
|
||||
ok=result.ok,
|
||||
command="hermes verify",
|
||||
scope="targeted" if partial else "full",
|
||||
output="\n".join(tails),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _print_human_report(recipe, source, result) -> None:
|
||||
print(f"Recipe: {recipe.name} ({recipe.kind}) — source: {source}")
|
||||
print()
|
||||
if result.phases:
|
||||
width = max(len(p.phase) for p in result.phases)
|
||||
for p in result.phases:
|
||||
status = "PASS" if p.ok else ("TIMEOUT" if p.timed_out else "FAIL")
|
||||
print(f" {p.phase.ljust(width)} {status:<7} {p.duration:6.1f}s {p.command}")
|
||||
else:
|
||||
print(" (no phases executed)")
|
||||
|
||||
if result.readiness is not None:
|
||||
r = result.readiness
|
||||
status = f"ready (HTTP {r.status_code})" if r.ready else f"not ready ({r.error or 'timeout'})"
|
||||
print(f" {'start'.ljust(max(5, max((len(p.phase) for p in result.phases), default=5)))} "
|
||||
f"{'PASS' if r.ready else 'FAIL':<7} {r.duration:6.1f}s {recipe.start}")
|
||||
print()
|
||||
print(f"Readiness: {r.url} -> {status}")
|
||||
|
||||
failed = [p for p in result.phases if not p.ok]
|
||||
print()
|
||||
print(f"Result: {'OK' if result.ok else 'FAILED'}")
|
||||
for p in failed:
|
||||
print(f"\n--- output tail: {p.phase} ({p.command}) ---")
|
||||
print(p.output_tail.rstrip())
|
||||
if result.readiness is not None and not result.readiness.ready and result.readiness.output_tail:
|
||||
print("\n--- output tail: start ---")
|
||||
print(result.readiness.output_tail.rstrip())
|
||||
Reference in New Issue
Block a user