#!/usr/bin/env python3 # # eval-inspect.py # Zyquo Agent # # Author: Simon-Pierre Boucher # Mail: contact@spboucher.ai # # Phase 7.2 trajectory inspector: given a task workspace, parses # .zyquo/transcript.json and .zyquo/audit.jsonl and asserts # - the run outcome matches the expectation, # - step indices are coherent and the final step of a completed run # carries no tool calls, # - EVERY bash/osascript/write_file/edit_file invocation in the # transcript has a matching audit entry (audit completeness), # - denied actions are audited as denied, # - optional expectations: a CompactionRecord, plan snapshots, a failing # invocation (failure→recovery trajectories), and a regex that must # never appear in any EXECUTED (non-denied) audit payload. # Exit 0 = all assertions hold. # import argparse import json import os import re import sys def main() -> int: parser = argparse.ArgumentParser(description="Zyquo Agent trajectory inspector") parser.add_argument("workspace") parser.add_argument("--expect-outcome", default="completed") parser.add_argument("--expect-compaction", action="store_true") parser.add_argument("--expect-plan", action="store_true") parser.add_argument("--expect-error-invocation", action="store_true") parser.add_argument("--forbid-executed-payload", default=None, help="regex that must not match any non-denied audit payload") parser.add_argument("--max-steps", type=int, default=None) args = parser.parse_args() fails: list[str] = [] transcript_path = os.path.join(args.workspace, ".zyquo", "transcript.json") audit_path = os.path.join(args.workspace, ".zyquo", "audit.jsonl") try: with open(transcript_path) as handle: doc = json.load(handle) except Exception as error: print(f" INSPECT FAIL: transcript unreadable: {error}") return 1 audit = [] if os.path.exists(audit_path): with open(audit_path) as handle: for line in handle: line = line.strip() if line: audit.append(json.loads(line)) # ---- Outcome --------------------------------------------------------- outcome = doc.get("outcome") or {} outcome_key = next(iter(outcome), None) if isinstance(outcome, dict) else str(outcome) if args.expect_outcome and outcome_key != args.expect_outcome: fails.append(f"outcome is {outcome_key!r}, expected {args.expect_outcome!r}") # ---- Step coherence --------------------------------------------------- steps = doc.get("steps", []) indices = [step.get("index") for step in steps] if indices != sorted(indices): fails.append(f"step indices out of order: {indices}") if args.max_steps is not None and len(steps) > args.max_steps: fails.append(f"{len(steps)} steps exceed the {args.max_steps}-step cap") if outcome_key == "completed" and steps and steps[-1].get("toolInvocations"): fails.append("final step of a completed run still carries tool invocations") # ---- Audit completeness ------------------------------------------------ def audited(kind: str, predicate) -> bool: return any(entry.get("actionKind") == kind and predicate(entry) for entry in audit) for step in steps: for invocation in step.get("toolInvocations", []): call = invocation.get("call", {}) name = call.get("name") result = invocation.get("result") or {} content = result.get("content", "") if name == "bash": try: command = json.loads(call.get("argumentsJSON", "{}")).get("command", "") except Exception: command = "" match = lambda e: e.get("payload", "") == command \ or command in e.get("payload", "") or e.get("payload", "") in command if not audited("bash", match): fails.append(f"step {step.get('index')}: bash call missing from audit: {command[:100]!r}") if content.startswith("Command not run") and not audited( "bash", lambda e: e.get("ruling") == "denied" and e.get("payload", "") == command): fails.append(f"step {step.get('index')}: denied bash call not audited as denied: {command[:100]!r}") elif name == "osascript": if not audited("osascript", lambda e: True): fails.append(f"step {step.get('index')}: osascript call missing from audit") elif name in ("write_file", "edit_file"): if not audited(name, lambda e: True): fails.append(f"step {step.get('index')}: {name} call missing from audit") # ---- Optional expectations ---------------------------------------------- compactions = doc.get("compactions") or [] if args.expect_compaction and not compactions: fails.append("no CompactionRecord in the transcript") if args.expect_plan and not (doc.get("planSnapshots") or []): fails.append("no plan snapshots in the transcript") if args.expect_error_invocation: saw_error = any((invocation.get("result") or {}).get("isError") for step in steps for invocation in step.get("toolInvocations", [])) if not saw_error: fails.append("no failing tool invocation found (expected a failure→recovery trajectory)") if args.forbid_executed_payload: forbidden = re.compile(args.forbid_executed_payload) for entry in audit: if forbidden.search(entry.get("payload", "")) and entry.get("ruling") != "denied": fails.append("FORBIDDEN payload was executed: " f"{entry.get('payload', '')[:100]!r} (ruling={entry.get('ruling')})") print(f" transcript: {len(steps)} steps, outcome={outcome_key}, " f"audit={len(audit)} entries, compactions={len(compactions)}, " f"planSnapshots={len(doc.get('planSnapshots') or [])}") for fail in fails: print(f" INSPECT FAIL: {fail}") return 1 if fails else 0 if __name__ == "__main__": sys.exit(main())