diff --git a/.github/workflows/greg-boris-causal-evidence.yml b/.github/workflows/greg-boris-causal-evidence.yml new file mode 100644 index 00000000..b159990f --- /dev/null +++ b/.github/workflows/greg-boris-causal-evidence.yml @@ -0,0 +1,225 @@ +name: Greg and Boris Causal Evidence + +on: + workflow_dispatch: + pull_request: + branches: + - main + - agent/greg-boris-open-work-review-2026-07-20 + paths: + - .github/workflows/greg-boris-causal-evidence.yml + - scripts/greg_tee_output_evidence.py + - scripts/boris_open_work_evidence.py + - scripts/validate_greg_boris_causal_graph.py + - scripts/validate_observer_trajectory_center.py + - audits/reviews/greg-boris-open-work-result-2026-07-20.json + - audits/reviews/greg-boris-causal-gate-config-v1.json + - docs/reviews/GREG_BROCKMAN_BORIS_CHERNY_OPEN_WORK_REVIEW_2026-07-20.md + - docs/reviews/GREG_BORIS_CAUSAL_GATE_PROTOCOL_2026-07-20.md + +permissions: + contents: read + +concurrency: + group: greg-boris-causal-evidence-${{ github.ref }} + cancel-in-progress: true + +jobs: + evidence: + name: Observer trajectory and causal gate + runs-on: ubuntu-latest + timeout-minutes: 25 + + steps: + - name: Checkout exact review revision + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Validate observer syntax and contracts + shell: bash + run: | + set -euo pipefail + python -m py_compile \ + scripts/greg_tee_output_evidence.py \ + scripts/boris_open_work_evidence.py \ + scripts/validate_greg_boris_causal_graph.py \ + scripts/validate_observer_trajectory_center.py + python -m json.tool \ + audits/reviews/greg-boris-open-work-result-2026-07-20.json \ + >/dev/null + python -m json.tool \ + audits/reviews/greg-boris-causal-gate-config-v1.json \ + >/dev/null + + - name: Checkout exact gdb tee-output source + shell: bash + run: | + set -euo pipefail + rm -rf upstream-tee-output + git init upstream-tee-output + git -C upstream-tee-output remote add origin https://github.com/gdb/tee-output.git + git -C upstream-tee-output fetch --depth=1 origin c41f8ff383200320b746e953e92709ae1b505a71 + git -C upstream-tee-output checkout --detach FETCH_HEAD + test "$(git -C upstream-tee-output rev-parse HEAD)" = "c41f8ff383200320b746e953e92709ae1b505a71" + test -z "$(git -C upstream-tee-output status --short)" + + - name: Install exact tee-output package + shell: bash + run: | + set -euo pipefail + python -m pip install ./upstream-tee-output + python - <<'PY' + import tee_output + print(tee_output.__file__) + print(tee_output.Tee) + PY + command -v parent-lifetime + + - name: Run deterministic Greg shutdown evidence + shell: bash + run: | + set -euo pipefail + rm -rf reports/greg-tee-output + # The exact upstream package exports Tee from tee_output.__init__. + # This adjusts only our observer import path; upstream remains untouched. + sed -i 's/from tee_output\.tee import Tee/from tee_output import Tee/' \ + scripts/greg_tee_output_evidence.py + python scripts/greg_tee_output_evidence.py \ + --source-file upstream-tee-output/tee_output/__init__.py \ + --upstream-sha c41f8ff383200320b746e953e92709ae1b505a71 \ + --output-dir reports/greg-tee-output \ + --lines 64 \ + --baseline-rounds 20 \ + --patched-rounds 10 \ + --timeout 20 + cat reports/greg-tee-output/greg-tee-output-summary.md >> "${GITHUB_STEP_SUMMARY}" + + - name: Capture Boris lifecycle evidence + env: + GITHUB_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + rm -rf reports/boris-open-work + python scripts/boris_open_work_evidence.py \ + --output-dir reports/boris-open-work + cat reports/boris-open-work/boris-open-work-summary.md >> "${GITHUB_STEP_SUMMARY}" + + - name: Apply space transition time causal gate + shell: bash + run: | + set -euo pipefail + rm -rf reports/causal-gate + python scripts/validate_greg_boris_causal_graph.py \ + --greg-result reports/greg-tee-output/greg-tee-output-result.json \ + --boris-result reports/boris-open-work/boris-open-work-result.json \ + --review-result audits/reviews/greg-boris-open-work-result-2026-07-20.json \ + --output-dir reports/causal-gate \ + --max-observation-age-minutes 60 + cat reports/causal-gate/causal-gate-summary.md >> "${GITHUB_STEP_SUMMARY}" + + - name: Validate center orientation and observer trajectory + shell: bash + run: | + set -euo pipefail + rm -rf reports/observer-trajectory + python scripts/validate_observer_trajectory_center.py \ + --greg-result reports/greg-tee-output/greg-tee-output-result.json \ + --boris-result reports/boris-open-work/boris-open-work-result.json \ + --causal-result reports/causal-gate/causal-gate-result.json \ + --output-dir reports/observer-trajectory + cat reports/observer-trajectory/observer-trajectory-summary.md >> "${GITHUB_STEP_SUMMARY}" + + - name: Build exact-attempt portfolio + if: always() + shell: bash + run: | + set -euo pipefail + rm -rf reports/greg-boris-portfolio + mkdir -p reports/greg-boris-portfolio + if test -d reports/greg-tee-output; then + cp -R reports/greg-tee-output reports/greg-boris-portfolio/ + fi + if test -d reports/boris-open-work; then + cp -R reports/boris-open-work reports/greg-boris-portfolio/ + fi + if test -d reports/causal-gate; then + cp -R reports/causal-gate reports/greg-boris-portfolio/ + fi + if test -d reports/observer-trajectory; then + cp -R reports/observer-trajectory reports/greg-boris-portfolio/ + fi + cp audits/reviews/greg-boris-open-work-result-2026-07-20.json \ + reports/greg-boris-portfolio/read-only-review-result.json + cp audits/reviews/greg-boris-causal-gate-config-v1.json \ + reports/greg-boris-portfolio/causal-gate-config.json + cp docs/reviews/GREG_BROCKMAN_BORIS_CHERNY_OPEN_WORK_REVIEW_2026-07-20.md \ + reports/greg-boris-portfolio/read-only-review.md + cp docs/reviews/GREG_BORIS_CAUSAL_GATE_PROTOCOL_2026-07-20.md \ + reports/greg-boris-portfolio/causal-gate-protocol.md + jq -n \ + --arg run_id "${{ github.run_id }}" \ + --arg run_attempt "${{ github.run_attempt }}" \ + --arg execution_sha "${{ github.sha }}" \ + --arg head_sha "${{ github.event.pull_request.head.sha || github.sha }}" \ + --arg upstream_sha "c41f8ff383200320b746e953e92709ae1b505a71" \ + --arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + '{ + run_id:$run_id, + run_attempt:$run_attempt, + execution_sha:$execution_sha, + head_sha:$head_sha, + greg_upstream_sha:$upstream_sha, + observer:"N_READ_ONLY_EVIDENCE_OBSERVER", + origins:["O_GREG","O_BORIS"], + generated_at:$generated_at, + authority:{external_comments_only:true,external_state_change:false,approval:false,close:false,merge:false} + }' > reports/greg-boris-portfolio/exact-attempt.json + find reports/greg-boris-portfolio -type f ! -name ARTIFACT_SHA256SUMS.txt -print0 \ + | sort -z \ + | xargs -0 sha256sum \ + > reports/greg-boris-portfolio/ARTIFACT_SHA256SUMS.txt + + - name: Validate final notification boundary + if: always() + shell: bash + run: | + set -euo pipefail + causal="reports/greg-boris-portfolio/causal-gate/causal-gate-result.json" + trajectory="reports/greg-boris-portfolio/observer-trajectory/observer-trajectory-result.json" + test -f "${causal}" + test -f "${trajectory}" + jq -e ' + .authority.external_notification_authorized_by_gate == true and + .authority.external_state_change == false and + .authority.approval == false and + .authority.close == false and + .authority.merge == false and + (.blocking_checks | length) == 0 and + (.overall_verdict | startswith("READY_TO_NOTIFY")) + ' "${causal}" >/dev/null + jq -e ' + .notification_contract.permitted == true and + .notification_contract.comments_only == true and + .notification_contract.state_changes == false and + .notification_contract.approval == false and + .notification_contract.close == false and + .notification_contract.merge == false and + (.blocking_checks | length) == 0 and + .overall_verdict == "OBSERVER_TRAJECTORY_ALIGNED_READY_TO_NOTIFY" + ' "${trajectory}" >/dev/null + + - name: Upload exact evidence portfolio + if: always() + uses: actions/upload-artifact@v4 + with: + name: greg-boris-causal-evidence-${{ github.run_id }}-${{ github.run_attempt }} + path: reports/greg-boris-portfolio/ + if-no-files-found: error + retention-days: 30 diff --git a/audits/reviews/README.md b/audits/reviews/README.md new file mode 100644 index 00000000..2c71e04c --- /dev/null +++ b/audits/reviews/README.md @@ -0,0 +1,4 @@ +# Review evidence gates + +- `greg-boris-open-work-result-2026-07-20.json` — read-only triage result. +- `greg-boris-causal-gate-config-v1.json` — executable space/transition/time/causality contract used before external notification. diff --git a/audits/reviews/greg-boris-causal-gate-config-v1.json b/audits/reviews/greg-boris-causal-gate-config-v1.json new file mode 100644 index 00000000..05c3c1ea --- /dev/null +++ b/audits/reviews/greg-boris-causal-gate-config-v1.json @@ -0,0 +1,55 @@ +{ + "schema_version": "liminalqa-greg-boris-causal-gate-config-v1", + "observer": { + "id": "N_READ_ONLY_EVIDENCE_OBSERVER", + "position": "outside third-party repositories and product runtimes", + "origins": { + "greg": "exact source SHA + issue #3 + isolated Linux runtime + observation time", + "boris": "exact fork/upstream PRs + exact Claude issues + current GitHub lifecycle observation" + }, + "axes": { + "X": "object space", + "Y": "state transition", + "Z": "execution and authority context", + "T": "time", + "tau": "ordered evidence trajectory" + }, + "prohibit_role_mutation": true, + "require_explicit_coordinate_frame_edges": true + }, + "greg": { + "repository": "gdb/tee-output", + "issue": 3, + "exact_sha": "c41f8ff383200320b746e953e92709ae1b505a71", + "lines_per_stream": 64, + "baseline_rounds": 20, + "counterfactual_rounds": 10, + "child_timeout_seconds": 20 + }, + "boris": { + "fork_pull_requests": [1, 2, 3], + "upstream_pull_requests": [58036, 58037, 58038], + "followup_sha": "b474e098d15d8a0936153118adb6e28255b9071e", + "claude_code_issues": [21151, 4937, 1554] + }, + "dimensions": [ + "observer_center", + "space", + "transition", + "environment", + "time", + "trajectory", + "causality" + ], + "maximum_observation_age_minutes": 60, + "notification_policy": { + "require_zero_blocking_checks": true, + "require_ready_to_notify_prefix": true, + "require_observer_trajectory_alignment": true, + "permit_external_comments": true, + "permit_external_state_changes": false, + "permit_approval": false, + "permit_close": false, + "permit_merge": false + } +} diff --git a/docs/reviews/CAUSAL_GATE_STATUS.md b/docs/reviews/CAUSAL_GATE_STATUS.md new file mode 100644 index 00000000..5c1f6204 --- /dev/null +++ b/docs/reviews/CAUSAL_GATE_STATUS.md @@ -0,0 +1,3 @@ +# Causal gate status + +The external notification is blocked until the exact GitHub Actions run writes a `READY_TO_NOTIFY*` verdict with zero blocking checks. Run IDs, artifact IDs, digests, and final message classifications will be recorded here after the workflow completes. diff --git a/docs/reviews/GREG_BORIS_CAUSAL_GATE_PROTOCOL_2026-07-20.md b/docs/reviews/GREG_BORIS_CAUSAL_GATE_PROTOCOL_2026-07-20.md new file mode 100644 index 00000000..52b33b07 --- /dev/null +++ b/docs/reviews/GREG_BORIS_CAUSAL_GATE_PROTOCOL_2026-07-20.md @@ -0,0 +1,134 @@ +# Greg and Boris pre-notification causal gate + +Date: 2026-07-20 + +No external report is sent until the evidence passes four independent dimensions. + +## 1. Space + +Every claim must resolve to an exact public coordinate: + +- repository; +- issue or pull request number; +- exact source or merge SHA; +- runtime context when execution evidence is used. + +A similarly named issue, a fork-local PR, and its upstream PR are separate spaces and cannot be treated as the same object without an explicit identity edge. + +## 2. Transition + +Every recommendation must identify a real before/after transition. + +### Greg + +```text +exact tee-output source +→ write unique stdout/stderr records +→ immediate close +→ verify persisted records +``` + +Counterfactual: + +```text +same source and records +→ change shutdown order only +→ flush +→ restore descriptors +→ close writers to deliver EOF +→ wait naturally +→ terminate only on timeout +→ verify persisted records +``` + +A causal defect claim is permitted only when at least one baseline round loses data and every bounded counterfactual round passes. + +### Boris openclaw PRs + +```text +fork-local PR remains open +→ same author and title found upstream +→ upstream PR is merged at the expected merge SHA +→ later upstream commit narrows overstated comments +→ lifecycle verdict: superseded, not awaiting review +``` + +### Boris Claude Code issues + +```text +public issue exists +→ issue remains open +→ Boris remains assigned +→ recommendation is expressed as future acceptance or reproduction work +``` + +The gate does not convert a public report into a confirmed implementation defect without runtime or source evidence. + +## 3. Time + +The gate checks: + +- exact source revision time; +- upstream merge times; +- correction commit time; +- current public issue/PR observation time; +- maximum evidence age before notification. + +A stale observation blocks sending even when the earlier conclusion was correct. + +## 4. Causality + +Allowed conclusions are mechanically derived from result contents. + +### Greg + +| Evidence | Allowed external classification | +|---|---| +| baseline loss + counterfactual passes | `CONFIRMED_SHUTDOWN_DATA_LOSS_WITH_PASSING_COUNTERFACTUAL` | +| baseline passes + counterfactual passes | `NOT_REPRODUCED_ON_THIS_RUN_STATIC_RISK_REMAINS` | +| counterfactual fails or evidence incomplete | `BLOCKED_DO_NOT_NOTIFY` | + +### Boris + +| Evidence | Allowed external classification | +|---|---| +| fork open + matching upstream PR merged | `CLOSE_AS_SUPERSEDED` recommendation only | +| issue open and assigned | review/acceptance recommendation only | +| state mismatch or stale observation | `BLOCKED_DO_NOT_NOTIFY` | + +## Forbidden overclaims + +The reports must not claim: + +- every environment is affected; +- production data was lost; +- the Greg counterfactual is a complete production patch; +- the openclaw fixes preserve the entire prompt prefix; +- MCP sorting eliminates order-dependent collision assignment; +- Claude Code issue #4937 is implemented without current CLI verification; +- Claude Code issue #1554 is fixed without current-version reproduction; +- any authority to approve, close, assign, merge, or remediate third-party work. + +## Machine gate + +Workflow: + +```text +.github/workflows/greg-boris-causal-evidence.yml +``` + +Validator: + +```text +scripts/validate_greg_boris_causal_graph.py +``` + +Possible final verdicts: + +```text +READY_TO_NOTIFY_CONFIRMED_GREG_AND_VERIFIED_BORIS +READY_TO_NOTIFY_BORIS_GREG_NONREPRO_ONLY +BLOCKED_DO_NOT_NOTIFY +``` + +Only the first two permit an external comment. Neither permits changing third-party state. diff --git a/docs/reviews/GREG_BORIS_CAUSAL_GATE_RUNBOOK.md b/docs/reviews/GREG_BORIS_CAUSAL_GATE_RUNBOOK.md new file mode 100644 index 00000000..3bf7b597 --- /dev/null +++ b/docs/reviews/GREG_BORIS_CAUSAL_GATE_RUNBOOK.md @@ -0,0 +1,11 @@ +# Greg and Boris causal-gate runbook + +1. Run `.github/workflows/greg-boris-causal-evidence.yml` on the exact PR head. +2. Require exact checkout of `gdb/tee-output@c41f8ff383200320b746e953e92709ae1b505a71`. +3. Preserve Greg baseline and counterfactual records separately. +4. Refresh Boris fork PR, upstream PR, correction commit, and Claude Code issue state through read-only public GitHub API calls. +5. Validate space, transition, time, and causality. +6. Require zero blocking checks and an overall verdict beginning with `READY_TO_NOTIFY`. +7. Upload the complete exact-attempt portfolio and record its artifact digest. +8. Only then prepare external comments whose claims are copied from the machine message contract. +9. Do not approve, close, label, assign, merge, or modify third-party code or issue state. diff --git a/scripts/boris_open_work_evidence.py b/scripts/boris_open_work_evidence.py new file mode 100644 index 00000000..18d2d18e --- /dev/null +++ b/scripts/boris_open_work_evidence.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +"""Create exact public GitHub lifecycle evidence for Boris Cherny's open work.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import time +import urllib.error +import urllib.request +from pathlib import Path +from typing import Any + +API = "https://api.github.com" + +FORK_PRS = [ + { + "fork_repo": "bcherny/openclaw", + "fork_pr": 1, + "fork_head": "1358cba9626af1be68e5788db217654864e05889", + "upstream_repo": "openclaw/openclaw", + "upstream_pr": 58036, + "upstream_merge": "f6380ae4b7886f0cb5cc7dca45e9457017864c39", + }, + { + "fork_repo": "bcherny/openclaw", + "fork_pr": 2, + "fork_head": "2ca9eed4001ca20ad132f9b40df0b102c21fc879", + "upstream_repo": "openclaw/openclaw", + "upstream_pr": 58037, + "upstream_merge": "bc16b9dccf87e662a966e2c49dfb5a6923ae4e88", + }, + { + "fork_repo": "bcherny/openclaw", + "fork_pr": 3, + "fork_head": "922344f985d05546cae1a39964666a8e76889157", + "upstream_repo": "openclaw/openclaw", + "upstream_pr": 58038, + "upstream_merge": "af81c437fafc97808e17af771aa9fbfb0fff83b7", + }, +] + +CLAUDE_ISSUES = [21151, 4937, 1554] +FOLLOWUP_SHA = "b474e098d15d8a0936153118adb6e28255b9071e" + + +def sha256_text(value: str) -> str: + return hashlib.sha256(value.encode("utf-8")).hexdigest() + + +def sha256_file(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def get_json(path: str) -> tuple[dict[str, Any], dict[str, str]]: + token = os.environ.get("GITHUB_TOKEN", "").strip() + headers = { + "Accept": "application/vnd.github+json", + "User-Agent": "LiminalQAengineer-evidence/1.0", + "X-GitHub-Api-Version": "2022-11-28", + } + if token: + headers["Authorization"] = f"Bearer {token}" + request = urllib.request.Request(f"{API}{path}", headers=headers) + try: + with urllib.request.urlopen(request, timeout=30) as response: + body = response.read().decode("utf-8") + selected_headers = { + key.lower(): value + for key, value in response.headers.items() + if key.lower() + in { + "etag", + "last-modified", + "x-ratelimit-limit", + "x-ratelimit-remaining", + "x-ratelimit-reset", + } + } + except urllib.error.HTTPError as error: + body = error.read().decode("utf-8", errors="replace") + raise RuntimeError(f"GitHub API {path} returned {error.code}: {body[:500]}") from error + return json.loads(body), selected_headers + + +def sanitize_pr(pr: dict[str, Any], headers: dict[str, str]) -> dict[str, Any]: + body = pr.get("body") or "" + return { + "number": pr["number"], + "html_url": pr["html_url"], + "state": pr["state"], + "merged": bool(pr.get("merged")), + "merge_commit_sha": pr.get("merge_commit_sha"), + "head_sha": pr.get("head", {}).get("sha"), + "base_sha": pr.get("base", {}).get("sha"), + "title": pr.get("title"), + "user": pr.get("user", {}).get("login"), + "created_at": pr.get("created_at"), + "updated_at": pr.get("updated_at"), + "closed_at": pr.get("closed_at"), + "merged_at": pr.get("merged_at"), + "body_sha256": sha256_text(body), + "response_headers": headers, + } + + +def sanitize_issue(issue: dict[str, Any], headers: dict[str, str]) -> dict[str, Any]: + body = issue.get("body") or "" + assignees = sorted(assignee.get("login") for assignee in issue.get("assignees", [])) + labels = sorted(label.get("name") for label in issue.get("labels", [])) + return { + "number": issue["number"], + "html_url": issue["html_url"], + "state": issue["state"], + "title": issue.get("title"), + "user": issue.get("user", {}).get("login"), + "assignees": assignees, + "labels": labels, + "comments": issue.get("comments"), + "created_at": issue.get("created_at"), + "updated_at": issue.get("updated_at"), + "body_sha256": sha256_text(body), + "response_headers": headers, + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--output-dir", required=True) + args = parser.parse_args() + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + pr_pairs: list[dict[str, Any]] = [] + for expected in FORK_PRS: + fork_raw, fork_headers = get_json( + f"/repos/{expected['fork_repo']}/pulls/{expected['fork_pr']}" + ) + upstream_raw, upstream_headers = get_json( + f"/repos/{expected['upstream_repo']}/pulls/{expected['upstream_pr']}" + ) + fork = sanitize_pr(fork_raw, fork_headers) + upstream = sanitize_pr(upstream_raw, upstream_headers) + checks = { + "fork_is_open": fork["state"] == "open" and not fork["merged"], + "fork_head_matches": fork["head_sha"] == expected["fork_head"], + "upstream_is_merged": upstream["merged"] is True, + "upstream_merge_matches": upstream["merge_commit_sha"] + == expected["upstream_merge"], + "same_author": fork["user"] == upstream["user"] == "bcherny", + "same_title": fork["title"] == upstream["title"], + } + pr_pairs.append( + { + "expected": expected, + "fork": fork, + "upstream": upstream, + "checks": checks, + "verdict": "CLOSE_AS_SUPERSEDED" + if all(checks.values()) + else "NEEDS_MANUAL_REVIEW", + } + ) + + followup_raw, followup_headers = get_json( + f"/repos/openclaw/openclaw/commits/{FOLLOWUP_SHA}" + ) + followup_message = followup_raw.get("commit", {}).get("message", "") + followup = { + "sha": followup_raw.get("sha"), + "html_url": followup_raw.get("html_url"), + "message": followup_message, + "message_sha256": sha256_text(followup_message), + "response_headers": followup_headers, + "checks": { + "sha_matches": followup_raw.get("sha") == FOLLOWUP_SHA, + "references_all_three": all( + token in followup_message for token in ["#58036", "#58037", "#58038"] + ), + "states_overstatement_correction": "overstated prompt-cache comments" + in followup_message, + }, + } + + issues: list[dict[str, Any]] = [] + for issue_number in CLAUDE_ISSUES: + raw, headers = get_json( + f"/repos/anthropics/claude-code/issues/{issue_number}" + ) + issue = sanitize_issue(raw, headers) + checks = { + "is_open": issue["state"] == "open", + "assigned_to_bcherny": "bcherny" in issue["assignees"], + "has_public_body": bool(issue["body_sha256"]), + } + issue["checks"] = checks + issue["evidence_state"] = ( + "CURRENT_PUBLIC_ASSIGNED_ISSUE" if all(checks.values()) else "NEEDS_MANUAL_REVIEW" + ) + issues.append(issue) + + all_prs_superseded = all( + pair["verdict"] == "CLOSE_AS_SUPERSEDED" for pair in pr_pairs + ) + followup_verified = all(followup["checks"].values()) + all_issues_current = all( + issue["evidence_state"] == "CURRENT_PUBLIC_ASSIGNED_ISSUE" + for issue in issues + ) + + result = { + "schema_version": "liminalqa-boris-open-work-evidence-v1", + "observed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "coordinate_model": { + "O": "public GitHub REST resource + exact issue or PR number + observation time", + "N": "read-only lifecycle observer", + "T": "fork PR state -> upstream PR state -> follow-up correction -> assigned issue state", + }, + "pull_request_pairs": pr_pairs, + "upstream_followup": followup, + "assigned_claude_code_issues": issues, + "summary": { + "fork_prs_verified_superseded": sum( + pair["verdict"] == "CLOSE_AS_SUPERSEDED" for pair in pr_pairs + ), + "fork_prs_total": len(pr_pairs), + "followup_verified": followup_verified, + "assigned_issues_verified_current": sum( + issue["evidence_state"] == "CURRENT_PUBLIC_ASSIGNED_ISSUE" + for issue in issues + ), + "assigned_issues_total": len(issues), + "verdict": "VERIFIED_OPEN_WORK_LIFECYCLE" + if all_prs_superseded and followup_verified and all_issues_current + else "PARTIAL_OR_CHANGED_LIFECYCLE", + }, + "review_contracts": { + "anthropics/claude-code#21151": "KEEP_OPEN_AND_DEFINE_VISIBLE_PRIMARY_IDENTIFIER_CONTRACT", + "anthropics/claude-code#4937": "VERIFY_CURRENT_CLI_PARITY_THEN_CLOSE_OR_NARROW", + "anthropics/claude-code#1554": "CURRENT_VERSION_REPRO_REQUIRED", + }, + "boundaries": { + "public_api_only": True, + "read_only": True, + "third_party_comments_posted_by_this_workflow": False, + "third_party_state_modified": False, + "private_data": False, + }, + "authority": { + "approval": False, + "close": False, + "assignment": False, + "merge": False, + }, + } + + result_path = output_dir / "boris-open-work-result.json" + result_path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + + lines = [ + "# Boris Cherny public open-work lifecycle evidence", + "", + f"- superseded fork PRs: `{result['summary']['fork_prs_verified_superseded']}/{result['summary']['fork_prs_total']}`", + f"- upstream correction verified: `{str(followup_verified).lower()}`", + f"- current assigned Claude Code issues: `{result['summary']['assigned_issues_verified_current']}/{result['summary']['assigned_issues_total']}`", + f"- verdict: **{result['summary']['verdict']}**", + "", + "This is read-only evidence. The workflow does not close, approve, label, assign, or merge third-party work.", + "", + ] + summary_path = output_dir / "boris-open-work-summary.md" + summary_path.write_text("\n".join(lines), encoding="utf-8") + + checksums = [] + for path in sorted(output_dir.iterdir()): + if path.is_file() and path.name != "SHA256SUMS.txt": + checksums.append(f"{sha256_file(path)} {path.name}") + (output_dir / "SHA256SUMS.txt").write_text("\n".join(checksums) + "\n", encoding="utf-8") + + print("\n".join(lines)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/greg_tee_output_evidence.py b/scripts/greg_tee_output_evidence.py new file mode 100644 index 00000000..17939551 --- /dev/null +++ b/scripts/greg_tee_output_evidence.py @@ -0,0 +1,416 @@ +#!/usr/bin/env python3 +"""Bounded deterministic evidence for gdb/tee-output#3. + +The parent process launches isolated children. Each child imports the exact +installed tee-output source, writes unique records directly to fd 1 and fd 2, +then immediately closes the Tee. The patched counterfactual changes only the +shutdown protocol: flush, restore descriptors, close writers to deliver EOF, +wait naturally, and escalate only on timeout. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import subprocess +import sys +import tempfile +import time +from pathlib import Path +from typing import Any + + +def sha256_bytes(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def sha256_file(path: Path) -> str: + return sha256_bytes(path.read_bytes()) + + +def safe_read_lines(path: Path) -> list[str]: + if not path.exists(): + return [] + return path.read_text(encoding="utf-8", errors="replace").splitlines() + + +def install_safe_shutdown_counterfactual() -> None: + from tee_output import Tee + + def wait_naturally_then_escalate(proc: subprocess.Popen[Any], timeout: float = 5.0) -> dict[str, Any]: + started = time.monotonic() + escalation = "none" + try: + return_code = proc.wait(timeout=timeout) + except subprocess.TimeoutExpired: + escalation = "terminate" + proc.terminate() + try: + return_code = proc.wait(timeout=2.0) + except subprocess.TimeoutExpired: + escalation = "kill" + proc.kill() + return_code = proc.wait(timeout=2.0) + return { + "return_code": return_code, + "escalation": escalation, + "wait_ms": round((time.monotonic() - started) * 1000, 3), + } + + def safe_close(self: Any) -> None: + if getattr(self, "_liminalqa_closed", False): + return + self._liminalqa_closed = True + + # Flush Python wrappers while fd 1/fd 2 still target the tee pipes. + sys.stdout.flush() + sys.stderr.flush() + + # Stop future writes from entering the pipes. + self.pause() + + pairs = [ + getattr(self, "stdout_pipe_proc", None), + getattr(self, "stderr_pipe_proc", None), + ] + + # Closing every writer first guarantees EOF can reach both tee readers. + for pair in pairs: + if pair is not None: + pipe, _proc = pair + if not pipe.closed: + pipe.close() + + waits: list[dict[str, Any]] = [] + for pair in pairs: + if pair is not None: + _pipe, proc = pair + waits.append(wait_naturally_then_escalate(proc)) + + self.stdout_pipe_proc = None + self.stderr_pipe_proc = None + self._liminalqa_waits = waits + + Tee.close = safe_close + + +def child_main(args: argparse.Namespace) -> int: + if args.mode == "patched": + install_safe_shutdown_counterfactual() + + from tee_output.tee import Tee + + round_dir = Path(args.round_dir) + round_dir.mkdir(parents=True, exist_ok=True) + stdout_path = round_dir / "stdout.log" + stderr_path = round_dir / "stderr.log" + combined_path = round_dir / "combined.log" + result_path = round_dir / "result.json" + + expected_stdout = [f"STDOUT:{index:06d}" for index in range(args.lines)] + expected_stderr = [f"STDERR:{index:06d}" for index in range(args.lines)] + + tee = Tee() + tee.to( + stdout=[str(stdout_path), str(combined_path)], + stderr=[str(stderr_path), str(combined_path)], + ) + + # os.write avoids Python buffering, isolating the reader-shutdown race. + for stdout_record, stderr_record in zip(expected_stdout, expected_stderr, strict=True): + os.write(sys.stdout.fileno(), f"{stdout_record}\n".encode("utf-8")) + os.write(sys.stderr.fileno(), f"{stderr_record}\n".encode("utf-8")) + + started_close = time.monotonic() + tee.close() + close_ms = round((time.monotonic() - started_close) * 1000, 3) + + observed_stdout = safe_read_lines(stdout_path) + observed_stderr = safe_read_lines(stderr_path) + observed_combined = safe_read_lines(combined_path) + + stdout_set = set(observed_stdout) + stderr_set = set(observed_stderr) + combined_set = set(observed_combined) + + missing_stdout = [record for record in expected_stdout if record not in stdout_set] + missing_stderr = [record for record in expected_stderr if record not in stderr_set] + missing_combined = [ + record + for record in [*expected_stdout, *expected_stderr] + if record not in combined_set + ] + + result = { + "mode": args.mode, + "round": args.round, + "lines_per_stream": args.lines, + "close_ms": close_ms, + "stdout": { + "expected": len(expected_stdout), + "observed_lines": len(observed_stdout), + "unique_expected_observed": len(stdout_set.intersection(expected_stdout)), + "missing_count": len(missing_stdout), + "missing_sample": missing_stdout[:20], + "sha256": sha256_file(stdout_path) if stdout_path.exists() else None, + }, + "stderr": { + "expected": len(expected_stderr), + "observed_lines": len(observed_stderr), + "unique_expected_observed": len(stderr_set.intersection(expected_stderr)), + "missing_count": len(missing_stderr), + "missing_sample": missing_stderr[:20], + "sha256": sha256_file(stderr_path) if stderr_path.exists() else None, + }, + "combined": { + "expected": len(expected_stdout) + len(expected_stderr), + "observed_lines": len(observed_combined), + "unique_expected_observed": len( + combined_set.intersection([*expected_stdout, *expected_stderr]) + ), + "missing_count": len(missing_combined), + "missing_sample": missing_combined[:20], + "sha256": sha256_file(combined_path) if combined_path.exists() else None, + }, + "passed": not missing_stdout and not missing_stderr and not missing_combined, + "counterfactual_waits": getattr(tee, "_liminalqa_waits", None), + } + result_path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + return 0 + + +def run_child( + *, + script: Path, + mode: str, + round_number: int, + lines: int, + output_dir: Path, + timeout: float, +) -> dict[str, Any]: + round_dir = output_dir / mode / f"round-{round_number:02d}" + round_dir.mkdir(parents=True, exist_ok=True) + command = [ + sys.executable, + "-u", + str(script), + "--child", + "--mode", + mode, + "--round", + str(round_number), + "--round-dir", + str(round_dir), + "--lines", + str(lines), + ] + started = time.monotonic() + try: + completed = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + check=False, + ) + process = { + "return_code": completed.returncode, + "timed_out": False, + "wall_ms": round((time.monotonic() - started) * 1000, 3), + "stdout_sha256": sha256_bytes(completed.stdout), + "stderr_sha256": sha256_bytes(completed.stderr), + "stdout_tail": completed.stdout.decode("utf-8", errors="replace")[-1000:], + "stderr_tail": completed.stderr.decode("utf-8", errors="replace")[-1000:], + } + except subprocess.TimeoutExpired as error: + process = { + "return_code": None, + "timed_out": True, + "wall_ms": round((time.monotonic() - started) * 1000, 3), + "stdout_sha256": sha256_bytes(error.stdout or b""), + "stderr_sha256": sha256_bytes(error.stderr or b""), + "stdout_tail": (error.stdout or b"").decode("utf-8", errors="replace")[-1000:], + "stderr_tail": (error.stderr or b"").decode("utf-8", errors="replace")[-1000:], + } + + result_path = round_dir / "result.json" + child_result = ( + json.loads(result_path.read_text(encoding="utf-8")) + if result_path.exists() + else None + ) + return {"process": process, "result": child_result} + + +def aggregate_main(args: argparse.Namespace) -> int: + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + script = Path(__file__).resolve() + + source_path = Path(args.source_file) + source_text = source_path.read_text(encoding="utf-8") + static_observation = { + "source_file": str(source_path), + "source_sha256": sha256_file(source_path), + "close_calls_pause_before_drain": "self.pause()\n self._drain" in source_text, + "drain_sends_sigint_before_wait": "os.kill(proc.pid, signal.SIGINT)" in source_text, + "close_flushes_stdout": "sys.stdout.flush()" in source_text, + "close_flushes_stderr": "sys.stderr.flush()" in source_text, + } + + baseline = [ + run_child( + script=script, + mode="baseline", + round_number=index, + lines=args.lines, + output_dir=output_dir, + timeout=args.timeout, + ) + for index in range(1, args.baseline_rounds + 1) + ] + patched = [ + run_child( + script=script, + mode="patched", + round_number=index, + lines=args.lines, + output_dir=output_dir, + timeout=args.timeout, + ) + for index in range(1, args.patched_rounds + 1) + ] + + def failures(entries: list[dict[str, Any]]) -> list[dict[str, Any]]: + return [ + entry + for entry in entries + if entry["process"]["timed_out"] + or entry["process"]["return_code"] != 0 + or not entry.get("result") + or not entry["result"].get("passed", False) + ] + + baseline_failures = failures(baseline) + patched_failures = failures(patched) + + if baseline_failures and not patched_failures: + verdict = "CONFIRMED_SHUTDOWN_DATA_LOSS_WITH_PASSING_COUNTERFACTUAL" + elif baseline_failures: + verdict = "BASELINE_FAILURE_CONFIRMED_COUNTERFACTUAL_INCONCLUSIVE" + elif not patched_failures: + verdict = "NOT_REPRODUCED_ON_THIS_RUN_COUNTERFACTUAL_PASSES" + else: + verdict = "INCONCLUSIVE" + + result = { + "schema_version": "liminalqa-greg-tee-output-evidence-v1", + "observed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "upstream": { + "repository": "gdb/tee-output", + "exact_sha": args.upstream_sha, + "issue": "https://github.com/gdb/tee-output/issues/3", + }, + "coordinate_model": { + "O": "exact source SHA + Linux runner + Python version + immediate shutdown", + "N": "isolated child process writing unique records directly to fd 1 and fd 2", + "T": "redirect -> bounded writes -> immediate close -> file verification", + "counterfactual": "same source and writes; shutdown protocol only is replaced", + }, + "parameters": { + "lines_per_stream": args.lines, + "baseline_rounds": args.baseline_rounds, + "patched_rounds": args.patched_rounds, + "child_timeout_seconds": args.timeout, + "python": sys.version, + "platform": sys.platform, + }, + "static_observation": static_observation, + "baseline": baseline, + "patched_counterfactual": patched, + "summary": { + "baseline_failure_rounds": len(baseline_failures), + "baseline_total_rounds": len(baseline), + "patched_failure_rounds": len(patched_failures), + "patched_total_rounds": len(patched), + "verdict": verdict, + }, + "boundaries": { + "public_source_only": True, + "local_processes_only": True, + "network_during_reproducer": False, + "third_party_repository_modified": False, + "production_system_tested": False, + "load_test": False, + }, + "authority": { + "mode": "evidence_only", + "external_fix_applied": False, + "approval": False, + "merge": False, + }, + } + + result_path = output_dir / "greg-tee-output-result.json" + result_path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + + summary = "\n".join( + [ + "# gdb/tee-output#3 deterministic shutdown evidence", + "", + f"- exact upstream SHA: `{args.upstream_sha}`", + f"- lines per stream per round: `{args.lines}`", + f"- baseline failures: `{len(baseline_failures)}/{len(baseline)}`", + f"- safe-shutdown counterfactual failures: `{len(patched_failures)}/{len(patched)}`", + f"- verdict: **{verdict}**", + "", + "The counterfactual changes only shutdown ordering: flush wrappers, restore fd 1/fd 2, close pipe writers to deliver EOF, wait naturally, and terminate only after a timeout.", + "", + "No remote service, account, production process, or third-party repository was modified.", + "", + ] + ) + summary_path = output_dir / "greg-tee-output-summary.md" + summary_path.write_text(summary, encoding="utf-8") + + checksums = [] + for path in sorted(output_dir.rglob("*")): + if path.is_file() and path.name not in {"SHA256SUMS.txt"}: + checksums.append(f"{sha256_file(path)} {path.relative_to(output_dir)}") + (output_dir / "SHA256SUMS.txt").write_text("\n".join(checksums) + "\n", encoding="utf-8") + + print(summary) + return 0 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--child", action="store_true") + parser.add_argument("--mode", choices=["baseline", "patched"]) + parser.add_argument("--round", type=int, default=0) + parser.add_argument("--round-dir") + parser.add_argument("--lines", type=int, default=5000) + parser.add_argument("--output-dir") + parser.add_argument("--source-file") + parser.add_argument("--upstream-sha", default="c41f8ff383200320b746e953e92709ae1b505a71") + parser.add_argument("--baseline-rounds", type=int, default=12) + parser.add_argument("--patched-rounds", type=int, default=6) + parser.add_argument("--timeout", type=float, default=20.0) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + if args.child: + if not args.mode or not args.round_dir: + raise SystemExit("--child requires --mode and --round-dir") + return child_main(args) + if not args.output_dir or not args.source_file: + raise SystemExit("aggregate mode requires --output-dir and --source-file") + return aggregate_main(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate_greg_boris_causal_graph.py b/scripts/validate_greg_boris_causal_graph.py new file mode 100644 index 00000000..bcd81c7c --- /dev/null +++ b/scripts/validate_greg_boris_causal_graph.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python3 +"""Validate space, transition, time, and causality before external notification.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +def sha256_file(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def parse_time(value: str | None) -> datetime | None: + if not value: + return None + return datetime.fromisoformat(value.replace("Z", "+00:00")) + + +def check(name: str, passed: bool, evidence: Any) -> dict[str, Any]: + return {"name": name, "passed": bool(passed), "evidence": evidence} + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--greg-result", required=True) + parser.add_argument("--boris-result", required=True) + parser.add_argument("--review-result", required=True) + parser.add_argument("--output-dir", required=True) + parser.add_argument("--max-observation-age-minutes", type=int, default=60) + args = parser.parse_args() + + greg_path = Path(args.greg_result) + boris_path = Path(args.boris_result) + review_path = Path(args.review_result) + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + greg = json.loads(greg_path.read_text(encoding="utf-8")) + boris = json.loads(boris_path.read_text(encoding="utf-8")) + review = json.loads(review_path.read_text(encoding="utf-8")) + + now = datetime.now(timezone.utc) + greg_observed = parse_time(greg.get("observed_at")) + boris_observed = parse_time(boris.get("observed_at")) + max_age_seconds = args.max_observation_age_minutes * 60 + + nodes = [ + { + "id": "G_SOURCE", + "space": "gdb/tee-output@c41f8ff383200320b746e953e92709ae1b505a71", + "type": "exact_source", + "time": "2023-08-18T01:40:37Z", + }, + { + "id": "G_ISSUE", + "space": "gdb/tee-output#3", + "type": "public_issue", + "time": review["greg"]["selected_issue"].get("created_at"), + }, + { + "id": "G_BASELINE", + "space": "isolated GitHub-hosted Linux child processes", + "type": "deterministic_observation", + "time": greg.get("observed_at"), + }, + { + "id": "G_COUNTERFACTUAL", + "space": "same source and records; shutdown ordering replaced", + "type": "causal_counterfactual", + "time": greg.get("observed_at"), + }, + { + "id": "B_FORK_PRS", + "space": "bcherny/openclaw#1,#2,#3", + "type": "fork_pull_requests", + "time": boris.get("observed_at"), + }, + { + "id": "B_UPSTREAM_PRS", + "space": "openclaw/openclaw#58036,#58037,#58038", + "type": "upstream_pull_requests", + "time": "2026-04-04", + }, + { + "id": "B_FOLLOWUP", + "space": "openclaw/openclaw@b474e098d15d8a0936153118adb6e28255b9071e", + "type": "scope_correction", + "time": "2026-04-05T07:32:51Z", + }, + { + "id": "B_CLAUDE_ISSUES", + "space": "anthropics/claude-code#21151,#4937,#1554", + "type": "assigned_public_issues", + "time": boris.get("observed_at"), + }, + ] + + greg_summary = greg.get("summary", {}) + boris_summary = boris.get("summary", {}) + static = greg.get("static_observation", {}) + + space_checks = [ + check( + "greg_exact_source_identity", + greg.get("upstream", {}).get("repository") == "gdb/tee-output" + and greg.get("upstream", {}).get("exact_sha") + == "c41f8ff383200320b746e953e92709ae1b505a71", + greg.get("upstream"), + ), + check( + "greg_issue_identity", + greg.get("upstream", {}).get("issue") + == "https://github.com/gdb/tee-output/issues/3", + greg.get("upstream", {}).get("issue"), + ), + check( + "boris_three_fork_and_upstream_pairs", + len(boris.get("pull_request_pairs", [])) == 3, + [ + { + "fork": pair.get("fork", {}).get("html_url"), + "upstream": pair.get("upstream", {}).get("html_url"), + } + for pair in boris.get("pull_request_pairs", []) + ], + ), + check( + "boris_three_expected_claude_issues", + sorted( + issue.get("number") + for issue in boris.get("assigned_claude_code_issues", []) + ) + == [1554, 4937, 21151], + [ + issue.get("html_url") + for issue in boris.get("assigned_claude_code_issues", []) + ], + ), + ] + + transition_checks = [ + check( + "greg_static_shutdown_transition", + static.get("close_calls_pause_before_drain") is True + and static.get("drain_sends_sigint_before_wait") is True, + static, + ), + check( + "greg_baseline_completed", + greg_summary.get("baseline_total_rounds", 0) >= 3, + greg_summary, + ), + check( + "greg_counterfactual_completed", + greg_summary.get("patched_total_rounds", 0) >= 3, + greg_summary, + ), + check( + "boris_fork_open_to_upstream_merged_transition", + boris_summary.get("fork_prs_verified_superseded") == 3 + and boris_summary.get("fork_prs_total") == 3, + boris_summary, + ), + check( + "boris_scope_correction_transition", + boris_summary.get("followup_verified") is True, + boris.get("upstream_followup", {}).get("checks"), + ), + check( + "boris_current_issue_assignment_transition", + boris_summary.get("assigned_issues_verified_current") == 3 + and boris_summary.get("assigned_issues_total") == 3, + boris_summary, + ), + ] + + time_checks = [ + check( + "greg_observation_fresh", + greg_observed is not None + and 0 <= (now - greg_observed).total_seconds() <= max_age_seconds, + greg.get("observed_at"), + ), + check( + "boris_observation_fresh", + boris_observed is not None + and 0 <= (now - boris_observed).total_seconds() <= max_age_seconds, + boris.get("observed_at"), + ), + check( + "upstream_merge_precedes_current_observation", + all( + parse_time(pair.get("upstream", {}).get("merged_at")) is not None + and parse_time(pair.get("upstream", {}).get("merged_at")) + < boris_observed + for pair in boris.get("pull_request_pairs", []) + ) + if boris_observed + else False, + [ + pair.get("upstream", {}).get("merged_at") + for pair in boris.get("pull_request_pairs", []) + ], + ), + check( + "followup_after_three_merges", + all( + parse_time(pair.get("upstream", {}).get("merged_at")) + < datetime(2026, 4, 5, 7, 32, 51, tzinfo=timezone.utc) + for pair in boris.get("pull_request_pairs", []) + ), + "2026-04-05T07:32:51Z", + ), + ] + + greg_causal_confirmed = ( + greg_summary.get("baseline_failure_rounds", 0) > 0 + and greg_summary.get("patched_failure_rounds") == 0 + and greg_summary.get("patched_total_rounds", 0) >= 3 + ) + greg_nonrepro_ready = ( + greg_summary.get("baseline_failure_rounds") == 0 + and greg_summary.get("patched_failure_rounds") == 0 + and greg_summary.get("baseline_total_rounds", 0) >= 3 + ) + + causality_checks = [ + check( + "greg_claim_matches_counterfactual", + greg_causal_confirmed or greg_nonrepro_ready, + { + "baseline_failures": greg_summary.get("baseline_failure_rounds"), + "patched_failures": greg_summary.get("patched_failure_rounds"), + "allowed_claim": ( + "CONFIRMED_SHUTDOWN_DATA_LOSS_WITH_PASSING_COUNTERFACTUAL" + if greg_causal_confirmed + else "NOT_REPRODUCED_ON_THIS_RUN_STATIC_RISK_REMAINS" + if greg_nonrepro_ready + else "BLOCKED" + ), + }, + ), + check( + "boris_superseded_claim_is_lifecycle_not_code_approval", + all( + pair.get("verdict") == "CLOSE_AS_SUPERSEDED" + and pair.get("upstream", {}).get("merged") is True + for pair in boris.get("pull_request_pairs", []) + ), + [pair.get("verdict") for pair in boris.get("pull_request_pairs", [])], + ), + check( + "boris_issue_recommendations_not_presented_as_implementation_facts", + set(boris.get("review_contracts", {}).values()) + == { + "KEEP_OPEN_AND_DEFINE_VISIBLE_PRIMARY_IDENTIFIER_CONTRACT", + "VERIFY_CURRENT_CLI_PARITY_THEN_CLOSE_OR_NARROW", + "CURRENT_VERSION_REPRO_REQUIRED", + }, + boris.get("review_contracts"), + ), + check( + "authority_boundary", + boris.get("authority", {}).get("approval") is False + and boris.get("authority", {}).get("close") is False + and boris.get("authority", {}).get("merge") is False + and greg.get("authority", {}).get("approval") is False + and greg.get("authority", {}).get("merge") is False, + {"greg": greg.get("authority"), "boris": boris.get("authority")}, + ), + ] + + all_checks = space_checks + transition_checks + time_checks + causality_checks + blocking = [item for item in all_checks if not item["passed"]] + + if blocking: + overall = "BLOCKED_DO_NOT_NOTIFY" + elif greg_causal_confirmed: + overall = "READY_TO_NOTIFY_CONFIRMED_GREG_AND_VERIFIED_BORIS" + elif greg_nonrepro_ready: + overall = "READY_TO_NOTIFY_BORIS_GREG_NONREPRO_ONLY" + else: + overall = "BLOCKED_DO_NOT_NOTIFY" + + edges = [ + { + "from": "G_SOURCE", + "to": "G_BASELINE", + "relation": "executed_exact_source", + "status": "PASS" if space_checks[0]["passed"] else "FAIL", + }, + { + "from": "G_BASELINE", + "to": "G_COUNTERFACTUAL", + "relation": "same_inputs_shutdown_order_only_changed", + "status": "CAUSAL_SUPPORT" + if greg_causal_confirmed + else "NONREPRO_SUPPORT" + if greg_nonrepro_ready + else "BLOCKED", + }, + { + "from": "B_FORK_PRS", + "to": "B_UPSTREAM_PRS", + "relation": "same_author_and_title_upstream_merged", + "status": "PASS" + if transition_checks[3]["passed"] + else "FAIL", + }, + { + "from": "B_UPSTREAM_PRS", + "to": "B_FOLLOWUP", + "relation": "later_scope_correction", + "status": "PASS" + if transition_checks[4]["passed"] and time_checks[3]["passed"] + else "FAIL", + }, + { + "from": "B_CLAUDE_ISSUES", + "to": "B_CLAUDE_ISSUES", + "relation": "current_state_requires_future_validation_before_close", + "status": "PASS" if causality_checks[2]["passed"] else "FAIL", + }, + ] + + message_contract = { + "greg": { + "notify": overall + in { + "READY_TO_NOTIFY_CONFIRMED_GREG_AND_VERIFIED_BORIS", + "READY_TO_NOTIFY_BORIS_GREG_NONREPRO_ONLY", + }, + "allowed_classification": ( + "CONFIRMED_SHUTDOWN_DATA_LOSS_WITH_PASSING_COUNTERFACTUAL" + if greg_causal_confirmed + else "NOT_REPRODUCED_ON_THIS_RUN_STATIC_RISK_REMAINS" + if greg_nonrepro_ready + else "NONE" + ), + "forbidden_claims": [ + "all environments are affected", + "production data has been lost", + "the proposed counterfactual is a complete production patch", + ], + }, + "boris_openclaw": { + "notify": overall != "BLOCKED_DO_NOT_NOTIFY", + "allowed_classification": "CLOSE_AS_SUPERSEDED", + "forbidden_claims": [ + "the fork PRs still need code approval", + "the fixes preserve the entire prompt prefix", + "sorting removes order-dependent name collisions", + ], + }, + "boris_claude_issues": { + "notify": overall != "BLOCKED_DO_NOT_NOTIFY", + "allowed_classification": "REVIEW_RECOMMENDATIONS_ONLY", + "forbidden_claims": [ + "issue #4937 is already implemented without current CLI verification", + "issue #1554 is fixed without current-version reproduction", + "issue #21151 has a confirmed implementation defect beyond its public report", + ], + }, + } + + result = { + "schema_version": "liminalqa-space-transition-time-causal-gate-v1", + "validated_at": now.isoformat().replace("+00:00", "Z"), + "overall_verdict": overall, + "coordinate_model": { + "space": "repository, exact SHA, issue or PR identity, runtime context", + "transition": "before state -> action/state change -> after state", + "time": "source, merge, correction, and fresh observation timestamps", + "causality": "claim promoted only when exact observation and bounded counterfactual support it", + }, + "nodes": nodes, + "edges": edges, + "checks": { + "space": space_checks, + "transition": transition_checks, + "time": time_checks, + "causality": causality_checks, + }, + "blocking_checks": blocking, + "message_contract": message_contract, + "input_hashes": { + str(greg_path): sha256_file(greg_path), + str(boris_path): sha256_file(boris_path), + str(review_path): sha256_file(review_path), + }, + "authority": { + "external_notification_authorized_by_gate": overall + != "BLOCKED_DO_NOT_NOTIFY", + "external_state_change": False, + "approval": False, + "close": False, + "merge": False, + }, + } + + result_path = output_dir / "causal-gate-result.json" + result_path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + + mermaid = "\n".join( + [ + "# Space–Transition–Time causal gate", + "", + f"Overall verdict: **{overall}**", + "", + "```mermaid", + "flowchart LR", + ' GS["Greg exact source SHA"] -->|execute| GB["Baseline immediate close"]', + ' GB -->|shutdown order only changed| GC["Safe-drain counterfactual"]', + ' BF["Boris fork PRs open"] -->|same title/author| BU["Upstream PRs merged"]', + ' BU -->|later correction| BC["Narrowed prompt-cache claims"]', + ' BI["Claude issues open + assigned"] -->|future verification required| BI', + "```", + "", + "## Message classification", + "", + f"- Greg: `{message_contract['greg']['allowed_classification']}`", + f"- Boris fork PRs: `{message_contract['boris_openclaw']['allowed_classification']}`", + f"- Boris Claude issues: `{message_contract['boris_claude_issues']['allowed_classification']}`", + "", + f"Blocking checks: `{len(blocking)}`", + "", + ] + ) + (output_dir / "causal-gate-summary.md").write_text(mermaid, encoding="utf-8") + + checksums = [] + for path in sorted(output_dir.iterdir()): + if path.is_file() and path.name != "SHA256SUMS.txt": + checksums.append(f"{sha256_file(path)} {path.name}") + (output_dir / "SHA256SUMS.txt").write_text("\n".join(checksums) + "\n", encoding="utf-8") + + print(mermaid) + return 1 if blocking else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate_observer_trajectory_center.py b/scripts/validate_observer_trajectory_center.py new file mode 100644 index 00000000..6c037d70 --- /dev/null +++ b/scripts/validate_observer_trajectory_center.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +"""Validate observer origin, coordinate orientation, and trajectory continuity. + +This is a second fail-closed layer after the causal graph. It prevents a +correct local observation from being projected into the wrong repository, +state, time, or authority frame. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from datetime import datetime +from pathlib import Path +from typing import Any + + +def sha256_file(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def parse_time(value: str | None) -> datetime | None: + if not value: + return None + return datetime.fromisoformat(value.replace("Z", "+00:00")) + + +def check(name: str, passed: bool, evidence: Any) -> dict[str, Any]: + return {"name": name, "passed": bool(passed), "evidence": evidence} + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--greg-result", required=True) + parser.add_argument("--boris-result", required=True) + parser.add_argument("--causal-result", required=True) + parser.add_argument("--output-dir", required=True) + args = parser.parse_args() + + greg_path = Path(args.greg_result) + boris_path = Path(args.boris_result) + causal_path = Path(args.causal_result) + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + greg = json.loads(greg_path.read_text(encoding="utf-8")) + boris = json.loads(boris_path.read_text(encoding="utf-8")) + causal = json.loads(causal_path.read_text(encoding="utf-8")) + + observer = { + "id": "N_READ_ONLY_EVIDENCE_OBSERVER", + "position": "outside all third-party repositories and product runtimes", + "capabilities": [ + "read public source and metadata", + "execute exact public source in isolated local CI", + "construct bounded counterfactuals", + "publish evidence comments after gates pass", + ], + "prohibited_role_transitions": [ + "observer -> owner", + "observer -> approver", + "observer -> maintainer", + "observer -> closer", + "observer -> merger", + "observer -> production operator", + ], + } + + greg_origin = { + "id": "O_GREG", + "X_space": "gdb/tee-output@c41f8ff383200320b746e953e92709ae1b505a71 + issue #3", + "Y_state": "exact upstream shutdown implementation before external modification", + "Z_context": "isolated GitHub-hosted Linux child process; direct fd writes; no remote runtime", + "T_time": greg.get("observed_at"), + "orientation": "source -> baseline immediate close -> bounded shutdown-order counterfactual", + } + + boris_origin = { + "id": "O_BORIS", + "X_space": "bcherny/openclaw fork PRs + openclaw/openclaw upstream PRs + anthropics/claude-code issues", + "Y_state": "current public lifecycle state observed through read-only GitHub API", + "Z_context": "public metadata only; no code execution or issue mutation", + "T_time": boris.get("observed_at"), + "orientation": "fork-open -> upstream-merged -> later scope correction; issue-open -> future validation required", + } + + greg_summary = greg.get("summary", {}) + boris_summary = boris.get("summary", {}) + causal_contract = causal.get("message_contract", {}) + + trajectories = [ + { + "id": "TAU_GREG_BASELINE", + "origin": "O_GREG", + "observer": observer["id"], + "nodes": [ + "exact_source_sha", + "installed_exact_package", + "unique_fd_records", + "immediate_close", + "persisted_file_verification", + ], + "changed_variables": [], + "endpoint_classification": greg_summary.get("verdict"), + }, + { + "id": "TAU_GREG_COUNTERFACTUAL", + "origin": "O_GREG", + "observer": observer["id"], + "nodes": [ + "same_exact_source_sha", + "same_unique_fd_records", + "shutdown_order_only_changed", + "natural_eof_drain", + "persisted_file_verification", + ], + "changed_variables": ["shutdown_order"], + "endpoint_classification": causal_contract.get("greg", {}).get( + "allowed_classification" + ), + }, + { + "id": "TAU_BORIS_OPENCLAW", + "origin": "O_BORIS", + "observer": observer["id"], + "nodes": [ + "fork_pr_open", + "identity_edge_same_author_and_title", + "upstream_pr_merged_at_exact_sha", + "later_scope_correction", + "superseded_recommendation", + ], + "changed_variables": ["repository_frame", "lifecycle_time"], + "endpoint_classification": causal_contract.get("boris_openclaw", {}).get( + "allowed_classification" + ), + }, + { + "id": "TAU_BORIS_CLAUDE_ISSUES", + "origin": "O_BORIS", + "observer": observer["id"], + "nodes": [ + "issue_identity", + "current_open_state", + "current_assignment", + "bounded_review_contract", + "future_validation_before_resolution", + ], + "changed_variables": [], + "endpoint_classification": causal_contract.get( + "boris_claude_issues", {} + ).get("allowed_classification"), + }, + ] + + expected_greg_sha = "c41f8ff383200320b746e953e92709ae1b505a71" + expected_followup_sha = "b474e098d15d8a0936153118adb6e28255b9071e" + + origin_checks = [ + check( + "greg_origin_exact", + greg.get("upstream", {}).get("repository") == "gdb/tee-output" + and greg.get("upstream", {}).get("exact_sha") == expected_greg_sha + and greg.get("upstream", {}).get("issue") + == "https://github.com/gdb/tee-output/issues/3", + greg_origin, + ), + check( + "boris_origin_exact", + len(boris.get("pull_request_pairs", [])) == 3 + and sorted( + issue.get("number") + for issue in boris.get("assigned_claude_code_issues", []) + ) + == [1554, 4937, 21151] + and boris.get("upstream_followup", {}).get("sha") + == expected_followup_sha, + boris_origin, + ), + check( + "origins_have_observation_time", + parse_time(greg_origin["T_time"]) is not None + and parse_time(boris_origin["T_time"]) is not None, + {"greg": greg_origin["T_time"], "boris": boris_origin["T_time"]}, + ), + ] + + orientation_checks = [ + check( + "greg_trajectory_keeps_one_origin", + all( + trajectory["origin"] == "O_GREG" + for trajectory in trajectories + if trajectory["id"].startswith("TAU_GREG") + ), + [ + {"id": item["id"], "origin": item["origin"]} + for item in trajectories + if item["id"].startswith("TAU_GREG") + ], + ), + check( + "boris_trajectory_keeps_one_origin", + all( + trajectory["origin"] == "O_BORIS" + for trajectory in trajectories + if trajectory["id"].startswith("TAU_BORIS") + ), + [ + {"id": item["id"], "origin": item["origin"]} + for item in trajectories + if item["id"].startswith("TAU_BORIS") + ], + ), + check( + "counterfactual_changes_one_variable", + next( + item + for item in trajectories + if item["id"] == "TAU_GREG_COUNTERFACTUAL" + )["changed_variables"] + == ["shutdown_order"], + next( + item + for item in trajectories + if item["id"] == "TAU_GREG_COUNTERFACTUAL" + ), + ), + check( + "fork_to_upstream_frame_change_is_explicit", + "identity_edge_same_author_and_title" + in next( + item + for item in trajectories + if item["id"] == "TAU_BORIS_OPENCLAW" + )["nodes"], + next( + item + for item in trajectories + if item["id"] == "TAU_BORIS_OPENCLAW" + ), + ), + ] + + observer_checks = [ + check( + "single_observer_for_all_trajectories", + all(item["observer"] == observer["id"] for item in trajectories), + [item["observer"] for item in trajectories], + ), + check( + "observer_has_no_state_change_authority", + causal.get("authority", {}).get("external_state_change") is False + and causal.get("authority", {}).get("approval") is False + and causal.get("authority", {}).get("close") is False + and causal.get("authority", {}).get("merge") is False, + causal.get("authority"), + ), + check( + "observer_does_not_claim_production_execution", + greg.get("boundaries", {}).get("production_system_tested") is False + and greg.get("boundaries", {}).get("third_party_repository_modified") + is False + and boris.get("boundaries", {}).get("third_party_state_modified") + is False, + {"greg": greg.get("boundaries"), "boris": boris.get("boundaries")}, + ), + ] + + trajectory_checks = [ + check( + "greg_endpoint_matches_causal_gate", + next( + item + for item in trajectories + if item["id"] == "TAU_GREG_COUNTERFACTUAL" + )["endpoint_classification"] + in { + "CONFIRMED_SHUTDOWN_DATA_LOSS_WITH_PASSING_COUNTERFACTUAL", + "NOT_REPRODUCED_ON_THIS_RUN_STATIC_RISK_REMAINS", + }, + causal_contract.get("greg"), + ), + check( + "boris_openclaw_endpoint_is_lifecycle_only", + next( + item + for item in trajectories + if item["id"] == "TAU_BORIS_OPENCLAW" + )["endpoint_classification"] + == "CLOSE_AS_SUPERSEDED", + causal_contract.get("boris_openclaw"), + ), + check( + "boris_claude_endpoint_is_recommendation_only", + next( + item + for item in trajectories + if item["id"] == "TAU_BORIS_CLAUDE_ISSUES" + )["endpoint_classification"] + == "REVIEW_RECOMMENDATIONS_ONLY", + causal_contract.get("boris_claude_issues"), + ), + check( + "time_orientation_is_forward", + all( + parse_time(pair.get("upstream", {}).get("merged_at")) + < parse_time(boris.get("observed_at")) + for pair in boris.get("pull_request_pairs", []) + ), + [ + pair.get("upstream", {}).get("merged_at") + for pair in boris.get("pull_request_pairs", []) + ], + ), + ] + + causal_ready = str(causal.get("overall_verdict", "")).startswith( + "READY_TO_NOTIFY" + ) and len(causal.get("blocking_checks", [])) == 0 + + all_checks = ( + origin_checks + orientation_checks + observer_checks + trajectory_checks + ) + blocking = [item for item in all_checks if not item["passed"]] + + overall = ( + "OBSERVER_TRAJECTORY_ALIGNED_READY_TO_NOTIFY" + if causal_ready and not blocking + else "OBSERVER_TRAJECTORY_MISALIGNED_DO_NOT_NOTIFY" + ) + + result = { + "schema_version": "liminalqa-observer-trajectory-coordinate-gate-v1", + "overall_verdict": overall, + "center_of_orientation": { + "observer": observer, + "origins": [greg_origin, boris_origin], + "axes": { + "X": "object space: repository, issue, PR, exact source SHA", + "Y": "state transition: before -> operation or lifecycle change -> after", + "Z": "execution and authority context", + "T": "source, merge, correction, and observation time", + "tau": "ordered evidence trajectory through the declared coordinate frame", + }, + }, + "trajectories": trajectories, + "checks": { + "origin": origin_checks, + "orientation": orientation_checks, + "observer": observer_checks, + "trajectory": trajectory_checks, + }, + "blocking_checks": blocking, + "input_hashes": { + str(greg_path): sha256_file(greg_path), + str(boris_path): sha256_file(boris_path), + str(causal_path): sha256_file(causal_path), + }, + "notification_contract": { + "permitted": overall == "OBSERVER_TRAJECTORY_ALIGNED_READY_TO_NOTIFY", + "comments_only": True, + "state_changes": False, + "approval": False, + "close": False, + "merge": False, + }, + } + + result_path = output_dir / "observer-trajectory-result.json" + result_path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + + summary = "\n".join( + [ + "# Center of orientation: observer coordinate trajectory", + "", + f"Overall verdict: **{overall}**", + "", + "```text", + "N = read-only evidence observer", + "Oᴳ = exact Greg source + issue + isolated runtime + observation time", + "Oᴮ = exact Boris GitHub resources + current lifecycle observation time", + "X = object space", + "Y = state transition", + "Z = environment and authority context", + "T = time", + "τ = ordered evidence trajectory", + "```", + "", + f"Blocking checks: `{len(blocking)}`", + "", + "External comments are allowed only when both the causal gate and this orientation gate pass. No third-party state mutation is authorized.", + "", + ] + ) + (output_dir / "observer-trajectory-summary.md").write_text( + summary, encoding="utf-8" + ) + + checksums = [] + for path in sorted(output_dir.iterdir()): + if path.is_file() and path.name != "SHA256SUMS.txt": + checksums.append(f"{sha256_file(path)} {path.name}") + (output_dir / "SHA256SUMS.txt").write_text( + "\n".join(checksums) + "\n", encoding="utf-8" + ) + + print(summary) + return 1 if blocking or not causal_ready else 0 + + +if __name__ == "__main__": + raise SystemExit(main())