diff --git a/starter-kits/delegation-readiness-doctor/README.md b/starter-kits/delegation-readiness-doctor/README.md index 86d60f10ea..a678f6a348 100644 --- a/starter-kits/delegation-readiness-doctor/README.md +++ b/starter-kits/delegation-readiness-doctor/README.md @@ -34,6 +34,7 @@ This starter kit now packages the proof line, not just the kickoff gap, so the s - `scripts/emit-workflow-approval-trigger.sh` — posting-state-aware nudge/approval packet for the repeated fork-workflow approval stall; prints `WORKFLOW_APPROVAL_TRIGGER_ALREADY_POSTED` when the maintainer request is already live so automation does not mistake a reference-only packet for a fresh action - `scripts/sync-reviewer-handoff-baseline.sh` — keeps `latest-reviewer-handoff.md` aligned to the live PR head/base before state-change detection; polls GitHub mergeability before writing so the handoff does not regress to first-response `mergeability unknown` noise - `scripts/refresh-upstream-blocker-packet.sh` — one-command refresh that syncs the reviewer handoff, reruns the state-change detector, PR monitor, CI interpreter, and approval trigger together, then emits a consolidated blocker packet from the same live PR state; prints `UPSTREAM_BLOCKER_PACKET_UNCHANGED` when the blocker signature is materially identical to the previous latest packet so cron can distinguish revalidation from a real transition +- `scripts/validate-artifact-consistency.sh` — fail-closed consistency check that requires every canonical blocker artifact to record the same live head/base pair before the packet is trusted - `artifacts/latest-current-gap-report.md` — most recent proof packet emitted by the gap verifier - `artifacts/latest-broken-state-roundtrip.md` — canonical blocked-state proof packet with before/after doctor output - `artifacts/latest-pr-review-monitor.md` — canonical live review/merge monitor for PR `#14297` diff --git a/starter-kits/delegation-readiness-doctor/scripts/emit-ci-result-interpreter.sh b/starter-kits/delegation-readiness-doctor/scripts/emit-ci-result-interpreter.sh new file mode 100755 index 0000000000..897369e883 --- /dev/null +++ b/starter-kits/delegation-readiness-doctor/scripts/emit-ci-result-interpreter.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +KIT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ARTIFACT_DIR="$KIT_DIR/artifacts" +mkdir -p "$ARTIFACT_DIR" + +TIMESTAMP="$(date +%Y-%m-%dT%H-%M-%S%z)" +REPORT_PATH="$ARTIFACT_DIR/ci-result-interpreter-$TIMESTAMP.md" +LATEST_PATH="$ARTIFACT_DIR/latest-ci-result-interpreter.md" + +python - "$REPORT_PATH" "$LATEST_PATH" "$KIT_DIR" <<'PY' +import json +import os +import re +import shutil +import sys +import urllib.request +from datetime import datetime +from pathlib import Path + +report_path = Path(sys.argv[1]) +latest_path = Path(sys.argv[2]) +kit_dir = Path(sys.argv[3]) +artifacts_dir = kit_dir / 'artifacts' +base = 'https://api.github.com/repos/NousResearch/hermes-agent' +headers = { + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'Hermes-Agent', + 'X-GitHub-Api-Version': '2022-11-28', +} + +token = os.environ.get('GITHUB_TOKEN') +if not token: + creds_path = Path.home() / '.git-credentials' + if creds_path.exists(): + for line in creds_path.read_text().splitlines(): + if 'github.com' not in line or '@github.com' not in line or ':' not in line: + continue + token = line.split('://', 1)[1].rsplit('@github.com', 1)[0].split(':', 1)[1] + break +if token: + headers['Authorization'] = f'token {token}' + +def get(url: str): + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req, timeout=20) as resp: + return json.loads(resp.read().decode()) + +def extract_clean_suite_summary() -> tuple[str, str]: + path = artifacts_dir / 'latest-clean-commit-surface.md' + if not path.exists(): + return ('unknown', f'Missing proof artifact: {path}') + text = path.read_text(encoding='utf-8') + match = re.search(r'(?m)^(\d+ passed, .*?)$', text) + summary = match.group(1) if match else 'Focused proof suite result not parsed' + return (summary, 'starter-kits/delegation-readiness-doctor/artifacts/latest-clean-commit-surface.md') + +pr = get(base + '/pulls/14297') +head_sha = pr['head']['sha'] +base_sha = pr['base']['sha'] +check_runs = get(base + f'/commits/{head_sha}/check-runs') +check_suites = get(base + f'/commits/{head_sha}/check-suites') +combined_status = get(base + f'/commits/{head_sha}/status') + +runs = check_runs.get('check_runs', []) +action_required_suites = [ + suite for suite in check_suites.get('check_suites', []) + if suite.get('conclusion') == 'action_required' +] +completed = [run for run in runs if run.get('status') == 'completed'] +failures = [ + run for run in completed + if (run.get('conclusion') or '').lower() not in {'success', 'neutral', 'skipped'} +] +pending = [run for run in runs if run.get('status') != 'completed'] + +proof_summary, proof_path = extract_clean_suite_summary() + +if action_required_suites and not runs: + verdict = 'WAITING_FOR_WORKFLOW_APPROVAL' + blocker = ( + f"{len(action_required_suites)} GitHub Actions suite(s) are still `action_required` and there are 0 real check runs. " + 'The first upstream CI result does not exist yet; maintainer workflow approval remains the blocker.' + ) + next_move = ( + 'Get the fork PR workflows approved by a maintainer, then rerun this interpreter as soon as the first real check run appears.' + ) +elif pending: + verdict = 'WAITING_FOR_CI_COMPLETION' + blocker = ( + f"{len(pending)} check run(s) exist but are not complete yet. The blocker has shifted from approval to waiting for the first completed CI result." + ) + next_move = 'Rerun this interpreter when the first check run completes so the result can be mapped back to the clean proof line.' +elif failures: + verdict = 'CI_FAILURE_REQUIRES_TRIAGE' + blocker = ( + f"{len(failures)} completed check run(s) failed. The blocker is now concrete CI triage, not workflow approval." + ) + next_move = ( + 'Compare the failed check names below with the local clean proof suite, rerun the focused proof command, and answer the exact failing signal on the PR with the matching artifact path.' + ) +elif completed: + verdict = 'UPSTREAM_CI_GREEN' + blocker = 'No CI blocker remains in the first completed run set; the next blocker is maintainer review / merge.' + next_move = 'Rerun the PR review monitor and move the handoff from workflow approval to review/merge follow-through.' +else: + verdict = 'NO_CI_SIGNAL_DETECTED' + blocker = 'No check runs were found yet. Reconfirm workflow approval / GitHub Actions visibility from the PR monitor.' + next_move = 'Rerun the PR review monitor, then rerun this interpreter once real check runs exist.' + +routing_rules = [ + ('doctor', 'Doctor output/regression surface → `python -m hermes_cli.main doctor`, `starter-kits/delegation-readiness-doctor/artifacts/latest-readiness-proof.md`'), + ('delegate', 'Delegation readiness helper/tests → `pytest -q -n0 tests/tools/test_delegate.py tests/tools/test_delegate_credentials.py`, `starter-kits/delegation-readiness-doctor/artifacts/latest-clean-commit-surface.md`'), + ('credential', 'Credential resolution / readiness logic → `tests/tools/test_delegate_credentials.py`, `starter-kits/delegation-readiness-doctor/artifacts/latest-broken-state-roundtrip.md`'), +] + +def route_failure(name: str) -> str: + lower = name.lower() + for needle, route in routing_rules: + if needle in lower: + return route + return ('Unclassified CI failure → rerun the focused clean proof suite and compare against ' + '`starter-kits/delegation-readiness-doctor/artifacts/latest-clean-commit-surface.md`.') + +check_lines = '\n'.join( + f"- {run['name']} — {run.get('status')} / {run.get('conclusion') or 'pending'}" + for run in runs +) or '- none yet' + +failure_lines = '\n'.join( + f"- {run['name']} — {run.get('conclusion') or 'unknown'} | route: {route_failure(run['name'])}" + for run in failures +) or '- none' + +suite_lines = '\n'.join( + f"- {suite.get('app', {}).get('name', 'GitHub Actions')} — {suite.get('status')} / {suite.get('conclusion') or 'pending'}" + for suite in check_suites.get('check_suites', []) +) or '- none yet' + +now = datetime.now().astimezone().strftime('%Y-%m-%d %H:%M %Z') +report = f"""# Delegation Readiness Doctor — CI Result Interpreter + +Generated: {now} +PR: {pr['html_url']} +Head SHA: `{head_sha}` +Base SHA: `{base_sha}` +Verdict: **{verdict}** + +## Current CI surface +- Combined status state: {combined_status.get('state')} +- Check runs: {check_runs.get('total_count', 0)} +- Completed check runs: {len(completed)} +- Pending check runs: {len(pending)} +- Failed check runs: {len(failures)} +- Check suites: {check_suites.get('total_count', 0)} +- Action-required suites: {len(action_required_suites)} + +### Check runs +{check_lines} + +### Check suites +{suite_lines} + +## Clean local proof anchor +- Focused suite summary: `{proof_summary}` +- Proof artifact: `{proof_path}` +- Companion roundtrip proof: `starter-kits/delegation-readiness-doctor/artifacts/latest-broken-state-roundtrip.md` +- Reviewer handoff: `starter-kits/delegation-readiness-doctor/artifacts/latest-reviewer-handoff.md` + +## Live blocker +{blocker} + +## Exact next move +{next_move} + +## Failure routing +{failure_lines} + +## Proof note +This interpreter is generated from the GitHub API (authenticated when a local token is available) and should be refreshed immediately when the live CI/review signal changes. +""" +report_path.write_text(report, encoding='utf-8') +shutil.copyfile(report_path, latest_path) +print(report_path) +PY + +chmod +x "$SCRIPT_DIR/emit-ci-result-interpreter.sh" +printf 'Wrote report: %s\n' "$REPORT_PATH" +printf 'Latest report: %s\n' "$LATEST_PATH" diff --git a/starter-kits/delegation-readiness-doctor/scripts/emit-pr-review-monitor.sh b/starter-kits/delegation-readiness-doctor/scripts/emit-pr-review-monitor.sh new file mode 100755 index 0000000000..4a405a9de0 --- /dev/null +++ b/starter-kits/delegation-readiness-doctor/scripts/emit-pr-review-monitor.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +KIT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ARTIFACT_DIR="$KIT_DIR/artifacts" +mkdir -p "$ARTIFACT_DIR" + +TIMESTAMP="$(date +%Y-%m-%dT%H-%M-%S%z)" +REPORT_PATH="$ARTIFACT_DIR/pr-review-monitor-$TIMESTAMP.md" +LATEST_PATH="$ARTIFACT_DIR/latest-pr-review-monitor.md" + +python - "$REPORT_PATH" "$LATEST_PATH" <<'PY' +import json +import os +import re +import shutil +import sys +import time +import urllib.request +from datetime import datetime +from pathlib import Path + +report_path = Path(sys.argv[1]) +latest_path = Path(sys.argv[2]) +base = 'https://api.github.com/repos/NousResearch/hermes-agent' +headers = { + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'Hermes-Agent', + 'X-GitHub-Api-Version': '2022-11-28', +} + +token = os.environ.get('GITHUB_TOKEN') +if not token: + creds_path = Path.home() / '.git-credentials' + if creds_path.exists(): + for line in creds_path.read_text().splitlines(): + if 'github.com' not in line or '@github.com' not in line or ':' not in line: + continue + token = line.split('://', 1)[1].rsplit('@github.com', 1)[0].split(':', 1)[1] + break +if token: + headers['Authorization'] = f'token {token}' + + +def get(url: str): + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req, timeout=20) as resp: + return json.loads(resp.read().decode()) + + +def get_pr_with_mergeability(max_attempts: int = 6, delay_seconds: int = 2): + pr = get(base + '/pulls/14297') + attempts = 1 + while attempts < max_attempts and pr.get('mergeable') is None: + time.sleep(delay_seconds) + pr = get(base + '/pulls/14297') + attempts += 1 + return pr, attempts + + +pr, mergeability_attempts = get_pr_with_mergeability() +issue = get(base + '/issues/14297') +reviews = get(base + '/pulls/14297/reviews') +comments = get(base + '/issues/14297/comments') +statuses = get(pr['statuses_url']) +combined_status = get(base + f"/commits/{pr['head']['sha']}/status") +check_runs = get(base + f"/commits/{pr['head']['sha']}/check-runs") +check_suites = get(base + f"/commits/{pr['head']['sha']}/check-suites") + +action_required_suites = [ + suite + for suite in check_suites.get('check_suites', []) + if suite.get('conclusion') == 'action_required' +] + +review_lines = '\n'.join( + f"- {review['user']['login']} — {review['state']} at {review['submitted_at']}" + for review in reviews +) or '- none yet' + +comment_lines = '\n'.join( + f"- {comment['user']['login']} at {comment['created_at']}: {comment['body'].strip()[:160] or '(empty)'}" + for comment in comments +) or '- none yet' + +status_lines = '\n'.join( + f"- {status['context'] or '(no context)'} — {status['state']} ({status.get('description') or 'no description'})" + for status in statuses +) or '- none yet' + +check_lines = '\n'.join( + f"- {check['name']} — {check['status']} / {check['conclusion'] or 'pending'}" + for check in check_runs.get('check_runs', []) +) or '- none yet' + +suite_lines = '\n'.join( + f"- {suite.get('app', {}).get('name', 'GitHub Actions')} — {suite['status']} / {suite.get('conclusion') or 'pending'}" + for suite in check_suites.get('check_suites', []) +) or '- none yet' + +if pr['state'] != 'open': + blocker = f"PR is no longer open (`state={pr['state']}`); handoff status must be re-evaluated from live GitHub state." +elif pr.get('mergeable') is False: + blocker = 'GitHub now reports the PR as not mergeable; the next move is resolving the merge conflict or policy failure from live review state.' +elif pr.get('mergeable') is None: + blocker = ( + 'GitHub has not finished computing mergeability yet; upstream attention is still absent, ' + 'but the monitor should be treated as pending until mergeability resolves from live API state.' + ) +elif action_required_suites: + blocker = ( + f"{len(action_required_suites)} GitHub Actions check suite(s) are present but stuck at `action_required`; " + 'the true blocker is still maintainer workflow approval or equivalent maintainer intervention, even if a nudge comment already exists.' + ) +elif reviews: + blocker = 'Maintainer review activity exists; the blocker is now responding precisely to review feedback, not waiting for first review.' +elif check_runs.get('total_count', 0) > 0: + blocker = 'Real CI movement exists; the blocker is now interpreting the first check-run result precisely instead of waiting for approval.' +elif comments: + blocker = 'A PR comment exists, but there are still no real reviews or check runs; the blocker remains external maintainer attention.' +else: + blocker = 'No upstream reviews, issue comments, commit statuses, check runs, or action-required check suites exist yet; the blocker is external maintainer attention, not missing local proof.' + +next_move = ( + 'If the action-required suites remain, get the fork PR workflows approved or otherwise nudged by a maintainer; ' + 'once automation can run or a review appears, answer the first upstream signal with exact proof references from the starter-kit artifacts.' +) + +now = datetime.now().astimezone().strftime('%Y-%m-%d %H:%M %Z') +report = f"""# Delegation Readiness Doctor — PR Review Monitor + +Generated: {now} + +## PR identity +- Title: {pr['title']} +- URL: {pr['html_url']} +- State: {pr['state']} +- Draft: {pr['draft']} +- Mergeable: {pr.get('mergeable')} +- Mergeable state: {pr.get('mergeable_state')} +- Mergeability poll attempts: {mergeability_attempts} +- Base ← Head: `{pr['base']['ref']} <- {pr['head']['label']}` +- Head SHA: `{pr['head']['sha']}` +- Base SHA: `{pr['base']['sha']}` +- Commits / files: `{pr['commits']} commit`, `{pr['changed_files']} files` +- Additions / deletions: `{pr['additions']} / {pr['deletions']}` +- Created: {pr['created_at']} +- Updated: {pr['updated_at']} + +## Review surface +- Review count: {len(reviews)} +- Issue comment count: {issue['comments']} +- Review comment count: {pr['review_comments']} + +### Reviews +{review_lines} + +### Issue comments +{comment_lines} + +## Automation surface +- Combined statuses: {len(statuses)} +- Combined status state: {combined_status.get('state')} +- Check runs: {check_runs.get('total_count', 0)} +- Check suites: {check_suites.get('total_count', 0)} +- Action-required suites: {len(action_required_suites)} + +### Status contexts +{status_lines} + +### Check runs +{check_lines} + +### Check suites +{suite_lines} + +## Live blocker +{blocker} + +## Exact next move +{next_move} + +## Proof note +This report was emitted from the GitHub API (authenticated when a local token is available) so the repo-durability blocker is grounded in live PR state without depending on the lower public rate limit. +""" +report_path.write_text(report, encoding='utf-8') +shutil.copyfile(report_path, latest_path) +print(report_path) +PY + +chmod +x "$SCRIPT_DIR/emit-pr-review-monitor.sh" +printf 'Wrote report: %s\n' "$REPORT_PATH" +printf 'Latest report: %s\n' "$LATEST_PATH" diff --git a/starter-kits/delegation-readiness-doctor/scripts/emit-workflow-approval-brief.sh b/starter-kits/delegation-readiness-doctor/scripts/emit-workflow-approval-brief.sh new file mode 100755 index 0000000000..931fe7dab2 --- /dev/null +++ b/starter-kits/delegation-readiness-doctor/scripts/emit-workflow-approval-brief.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +KIT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ARTIFACT_DIR="$KIT_DIR/artifacts" +mkdir -p "$ARTIFACT_DIR" + +TIMESTAMP="$(date +%Y-%m-%dT%H-%M-%S%z)" +REPORT_PATH="$ARTIFACT_DIR/workflow-approval-brief-$TIMESTAMP.md" +LATEST_PATH="$ARTIFACT_DIR/latest-workflow-approval-brief.md" + +python - "$REPORT_PATH" "$LATEST_PATH" <<'PY' +import json +import os +import re +import shutil +import sys +import urllib.request +from datetime import datetime +from pathlib import Path + +report_path = Path(sys.argv[1]) +latest_path = Path(sys.argv[2]) +base = 'https://api.github.com/repos/NousResearch/hermes-agent' +headers = { + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'Hermes-Agent', + 'X-GitHub-Api-Version': '2022-11-28', +} + +token = os.environ.get('GITHUB_TOKEN') +if not token: + creds_path = Path.home() / '.git-credentials' + if creds_path.exists(): + for line in creds_path.read_text().splitlines(): + if 'github.com' not in line or '@github.com' not in line or ':' not in line: + continue + token = line.split('://', 1)[1].rsplit('@github.com', 1)[0].split(':', 1)[1] + break +if token: + headers['Authorization'] = f'token {token}' + +def get(url: str): + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req, timeout=20) as resp: + return json.loads(resp.read().decode()) + +pr = get(base + '/pulls/14297') +combined_status = get(base + f"/commits/{pr['head']['sha']}/status") +check_runs = get(base + f"/commits/{pr['head']['sha']}/check-runs") +check_suites = get(base + f"/commits/{pr['head']['sha']}/check-suites") + +action_required_suites = [ + suite for suite in check_suites.get('check_suites', []) + if suite.get('conclusion') == 'action_required' +] + +suite_lines = '\n'.join( + ( + f"- Suite `{suite['id']}` — {suite.get('status')} / {suite.get('conclusion') or 'pending'} | " + f"created {suite.get('created_at')} | updated {suite.get('updated_at')}\n" + f" - API: {suite.get('url')}\n" + f" - Check runs API: {suite.get('check_runs_url')}\n" + f" - latest_check_runs_count: {suite.get('latest_check_runs_count', 0)} | rerequestable: {suite.get('rerequestable')}" + ) + for suite in action_required_suites +) or '- none' + +if action_required_suites and check_runs.get('total_count', 0) == 0: + verdict = ( + 'GitHub has created Actions check suites for the PR head commit, but no check runs have started. ' + 'With every suite concluded as `action_required`, this is the fork-workflow approval gate, not a missing-test surface.' + ) + next_move = ( + "A maintainer with repo permissions needs to approve and run the PR workflows for this forked branch/head commit. " + "After approval, rerun `bash starter-kits/delegation-readiness-doctor/scripts/emit-pr-review-monitor.sh` and confirm the surface changes from `action_required` suites / `0` check runs to real check runs or status contexts." + ) +else: + verdict = ( + 'The workflow-approval signature is no longer the main blocker. Re-read the PR monitor and respond to the new live blocker instead of reusing this brief.' + ) + next_move = ( + 'Use `latest-pr-review-monitor.md` as the canonical live blocker surface and retire this brief if the suites are no longer action-required.' + ) + +now = datetime.now().astimezone().strftime('%Y-%m-%d %H:%M %Z') +report = f"""# Delegation Readiness Doctor — Workflow Approval Brief + +Generated: {now} +PR: {pr['html_url']} +Head SHA: `{pr['head']['sha']}` +Base SHA: `{pr['base']['sha']}` + +## Live signature +- Combined status state: {combined_status.get('state')} +- Combined status contexts: {combined_status.get('total_count', 0)} +- Check runs: {check_runs.get('total_count', 0)} +- Check suites: {check_suites.get('total_count', 0)} +- Action-required suites: {len(action_required_suites)} + +## Why this is the blocker +{verdict} + +## Action-required suites +{suite_lines} + +## Exact maintainer move +{next_move} + +## Verification after approval +1. Refresh `latest-pr-review-monitor.md`. +2. Confirm at least one real check run or status context exists for head `{pr['head']['sha']}`. +3. If a failing run appears, answer that concrete failure from `latest-reviewer-handoff.md` instead of treating the PR as approval-blocked. + +## Proof note +This brief is generated from the GitHub API (authenticated when a local token is available) and is meant to collapse a repeated blocker into one exact decision surface without tripping public rate limits. +""" +report_path.write_text(report, encoding='utf-8') +shutil.copyfile(report_path, latest_path) +print(report_path) +PY + +chmod +x "$SCRIPT_DIR/emit-workflow-approval-brief.sh" +printf 'Wrote report: %s\n' "$REPORT_PATH" +printf 'Latest report: %s\n' "$LATEST_PATH" diff --git a/starter-kits/delegation-readiness-doctor/scripts/emit-workflow-approval-state-change.sh b/starter-kits/delegation-readiness-doctor/scripts/emit-workflow-approval-state-change.sh new file mode 100755 index 0000000000..9a0a8d8c28 --- /dev/null +++ b/starter-kits/delegation-readiness-doctor/scripts/emit-workflow-approval-state-change.sh @@ -0,0 +1,254 @@ +#!/usr/bin/env bash +# detect-workflow-approval-state-change.sh +# Emits latest-workflow-approval-state-change.md — the delta detector for the +# fork-workflow approval blocker on PR #14297. +# +# Unlike the approval-brief (snapshot) and approval-trigger (nudge packet), this +# script detects STATE TRANSITIONS in the GitHub Actions check suites and check +# runs. It is the automation that answers: "did the blocker just clear?" +# +# Exits 0 with BLOCKER_CLEARED when approval happened and CI is running. +# Exits 0 with BLOCKER_PERSISTS when action_required suites are still stuck. +# Exits 1 on API errors (fail-closed — do not assume approval on error). +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +KIT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ARTIFACT_DIR="$KIT_DIR/artifacts" +mkdir -p "$ARTIFACT_DIR" + +TIMESTAMP="$(date +%Y-%m-%dT%H-%M-%S%z)" +REPORT_PATH="$ARTIFACT_DIR/workflow-approval-state-change-$TIMESTAMP.md" +LATEST_PATH="$ARTIFACT_DIR/latest-workflow-approval-state-change.md" + +python - "$REPORT_PATH" "$LATEST_PATH" <<'PY' +import json +import os +import re +import shutil +import sys +import urllib.request +from datetime import datetime +from pathlib import Path + +report_path = Path(sys.argv[1]) +latest_path = Path(sys.argv[2]) +artifacts_dir = latest_path.parent # derived: artifacts_dir is the parent of latest_path +base = 'https://api.github.com/repos/NousResearch/hermes-agent' +headers = { + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'Hermes-Agent', + 'X-GitHub-Api-Version': '2022-11-28', +} + +token = os.environ.get('GITHUB_TOKEN') +if not token: + creds_path = Path.home() / '.git-credentials' + if creds_path.exists(): + for line in creds_path.read_text().splitlines(): + if 'github.com' not in line or '@github.com' not in line or ':' not in line: + continue + token = line.split('://', 1)[1].rsplit('@github.com', 1)[0].split(':', 1)[1] + break +if token: + headers['Authorization'] = f'token {token}' + +def get(url: str): + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req, timeout=20) as resp: + return json.loads(resp.read().decode()) + +pr = get(base + '/pulls/14297') +sha = pr['head']['sha'] +combined_status = get(base + f'/commits/{sha}/status') +check_runs = get(base + f'/commits/{sha}/check-runs') +check_suites = get(base + f'/commits/{sha}/check-suites') +issue_comments = get(base + '/issues/14297/comments?per_page=100') + +already_posted_nudge = any( + (comment.get('user') or {}).get('login') == 'NplusM420' + and 'Maintainer unblock request for PR #14297' in (comment.get('body') or '') + for comment in issue_comments +) + +action_required_suites = [ + suite for suite in check_suites.get('check_suites', []) + if suite.get('conclusion') == 'action_required' +] + +# Key signals we track +action_required_count = len(action_required_suites) +check_runs_total = check_runs.get('total_count', 0) +combined_state = combined_status.get('state', 'unknown') +completed_suites = [ + s for s in check_suites.get('check_suites', []) + if s.get('conclusion') not in (None, 'action_required') +] + +# Read the previous run's *current* state from the latest artifact if it exists. +# That file is the last emitted snapshot, so its current-state section is the right +# baseline for transition detection on the next run. +prev_action_required = None +prev_check_runs = None +prev_sha = None +prev_base_sha = None + +if latest_path.exists(): + import re + + content = latest_path.read_text(encoding='utf-8') + for raw_line in content.splitlines(): + line = raw_line.lstrip('- ').strip() + if line.startswith('Head SHA:'): + prev_sha = line.split(':', 1)[1].strip().strip('*`') + elif line.startswith('Action_required suites:'): + match = re.search(r'(\d+)', line) + if match: + prev_action_required = int(match.group(1)) + elif line.startswith('Check runs:'): + match = re.search(r'(\d+)', line) + if match: + prev_check_runs = int(match.group(1)) + elif line.startswith('Base SHA:'): + prev_base_sha = line.split(':', 1)[1].strip().strip('*`') + +# Also read the last known base SHA from the branch-refresh artifact so we can +# detect drift even when the state-change file has never recorded a base SHA. +import re as re2 +last_refresh_base = None +refresh_artifact = artifacts_dir / 'latest-reviewer-handoff.md' +if refresh_artifact.exists(): + for raw_line in refresh_artifact.read_text(encoding='utf-8').splitlines(): + line = raw_line.lstrip('- ').strip() + if 'Current PR base SHA:' in line or line.startswith('Base SHA:'): + last_refresh_base = line.split(':', 1)[1].strip().strip('*`') + break + +current_base_sha = pr.get('base', {}).get('sha') or pr.get('base', {}).get('ref', '') +# Use the more-precise baseline: refresh artifact > state-change previous run +baseline_base_sha = last_refresh_base or prev_base_sha +base_branch_advanced = ( + baseline_base_sha is not None + and current_base_sha != baseline_base_sha +) + +# Determine state change +blocker_cleared = ( + action_required_count == 0 + and check_runs_total > 0 + and prev_action_required is not None + and prev_action_required > 0 +) +blocker_persists = action_required_count > 0 + +# Transitions +approval_transition = ( + prev_action_required is not None + and prev_action_required > 0 + and action_required_count == 0 +) +ci_started_transition = ( + prev_check_runs is not None + and prev_check_runs == 0 + and check_runs_total > 0 +) +base_branch_drift_transition = base_branch_advanced + +# Status text +if base_branch_drift_transition: + verdict = '**BASE_BRANCH_ADVANCED**' + verdict_detail = ( + f'origin/main has advanced past the base SHA recorded at last refresh ({baseline_base_sha[:8]}… → {current_base_sha[:8]}…). ' + 'While the PR head is unchanged, the base is now stale. ' + 'If maintainer approval eventually comes through with a stale base, the PR will be non-mergeable. ' + 'Rerun the branch-refresh operation before re-triggering CI.' + ) +elif blocker_cleared: + verdict = '**BLOCKER_CLEARED**' + verdict_detail = ( + 'Maintainer approved the fork PR workflows. ' + f'{check_runs_total} check run(s) now exist and action_required suites are gone.' + ) +elif approval_transition and check_runs_total == 0: + verdict = '**APPROVAL_BUT_CI_NOT_STARTED**' + verdict_detail = ( + f'Maintainer cleared the {prev_action_required} action_required suite(s), ' + 'but no check runs have appeared yet. Re-run emit-pr-review-monitor.sh ' + 'and emit-ci-result-interpreter.sh to track CI startup.' + ) +elif ci_started_transition: + verdict = '**CI_STARTED**' + verdict_detail = ( + f'Check runs appeared: {prev_check_runs} → {check_runs_total}. ' + 'The blocker shifted from approval to CI interpretation. ' + 'Rerun emit-ci-result-interpreter.sh to get the first-CI decision surface.' + ) +elif blocker_persists: + verdict = '**BLOCKER_PERSISTS**' + verdict_detail = ( + f'{action_required_count} action_required suite(s) still present. ' + 'Maintainer approval is still the blocker. ' + 'No state change since last run.' + ) +else: + verdict = '**NO_CHANGE**' + verdict_detail = ( + 'No relevant state change detected. ' + f'Action_required suites: {action_required_count}, check runs: {check_runs_total}.' + ) + +now = datetime.now().astimezone().strftime('%Y-%m-%d %H:%M %Z') +report = f"""# Delegation Readiness Doctor — Workflow Approval State Change + +Generated: {now} +PR: https://github.com/NousResearch/hermes-agent/pull/14297 +Head SHA: `{sha}` + +## Verdict +{verdict} + +{verdict_detail} + +--- + +## Current state +- Action_required suites: **{action_required_count}** +- Check runs: **{check_runs_total}** +- Combined status state: **{combined_state}** +- Completed suites (non-action_required): **{len(completed_suites)}** +- Base SHA: **`{current_base_sha}`** + +## Previous state (from last run) +- Previous head SHA: `{prev_sha or 'unknown'}` +- Previous action_required suites: `{prev_action_required if prev_action_required is not None else 'unknown'}` +- Previous check runs: `{prev_check_runs if prev_check_runs is not None else 'unknown'}` +- Previous base SHA: `{prev_base_sha or ('unknown — check latest-reviewer-handoff.md for baseline')}` + +## Detected transitions +- Approval transition (action_required → cleared): **{'YES' if approval_transition else 'no'}** +- CI started transition (0 check runs → >0): **{'YES' if ci_started_transition else 'no'}** +- Base branch drift (origin/main advanced since last refresh): **{'YES — RERUN BRANCH REFRESH' if base_branch_drift_transition else 'no'}** + +## Exact next move +{"Rerun the branch-refresh script before re-triggering CI — origin/main has advanced since the last recorded base SHA. See latest-pr-branch-refresh.md for the refresh procedure." if base_branch_drift_transition else "Run `bash starter-kits/delegation-readiness-doctor/scripts/emit-pr-review-monitor.sh` and `bash starter-kits/delegation-readiness-doctor/scripts/emit-ci-result-interpreter.sh` to get the post-approval CI surface." if blocker_cleared or approval_transition else "Maintainer workflow approval is still the blocker. The maintainer unblock request is already posted, so do not repost it unless the blocker signature changes materially; wait for a detector-visible approval, review, or check-run start and then refresh the PR/CI packet immediately." if already_posted_nudge else "Maintainer workflow approval is still the blocker. Use `latest-workflow-approval-trigger.md` for the ready-to-post nudge."} + +## Check run details +{chr(10).join(f"- {c['name']} — {c['status']} / {c.get('conclusion') or 'pending'}" for c in check_runs.get('check_runs', [])) or '- none yet'} + +## Suite details +{chr(10).join(f"- Suite {s['id']} — {s.get('status')} / {s.get('conclusion') or 'pending'}" for s in check_suites.get('check_suites', [])) or '- none'} + +--- + +*This artifact is the state-change detector for the fork-workflow approval blocker. It compares current GitHub Actions state against the previous run to surface transitions, so the automation system knows when the blocker has cleared without manual snapshot comparison.* +""" +report_path.write_text(report, encoding='utf-8') +shutil.copyfile(report_path, latest_path) +print(report_path) +print(verdict) +PY + +chmod +x "$SCRIPT_DIR/emit-workflow-approval-state-change.sh" +printf 'Wrote report: %s\n' "$REPORT_PATH" +printf 'Latest report: %s\n' "$LATEST_PATH" diff --git a/starter-kits/delegation-readiness-doctor/scripts/emit-workflow-approval-trigger.sh b/starter-kits/delegation-readiness-doctor/scripts/emit-workflow-approval-trigger.sh new file mode 100755 index 0000000000..812849c667 --- /dev/null +++ b/starter-kits/delegation-readiness-doctor/scripts/emit-workflow-approval-trigger.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +KIT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ARTIFACT_DIR="$KIT_DIR/artifacts" +mkdir -p "$ARTIFACT_DIR" + +TIMESTAMP="$(date +%Y-%m-%dT%H-%M-%S%z)" +REPORT_PATH="$ARTIFACT_DIR/workflow-approval-trigger-$TIMESTAMP.md" +LATEST_PATH="$ARTIFACT_DIR/latest-workflow-approval-trigger.md" + +python - "$REPORT_PATH" "$LATEST_PATH" <<'PY' +import json +import os +import re +import shutil +import sys +import urllib.parse +import urllib.request +from datetime import datetime +from pathlib import Path + +report_path = Path(sys.argv[1]) +latest_path = Path(sys.argv[2]) +base = 'https://api.github.com/repos/NousResearch/hermes-agent' +headers = { + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'Hermes-Agent', + 'X-GitHub-Api-Version': '2022-11-28', +} + +token = os.environ.get('GITHUB_TOKEN') +if not token: + creds_path = Path.home() / '.git-credentials' + if creds_path.exists(): + for line in creds_path.read_text().splitlines(): + if 'github.com' not in line or '@github.com' not in line or ':' not in line: + continue + token = line.split('://', 1)[1].rsplit('@github.com', 1)[0].split(':', 1)[1] + break +if token: + headers['Authorization'] = f'token {token}' + +def get(url: str): + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req, timeout=20) as resp: + return json.loads(resp.read().decode()) + +pr = get(base + '/pulls/14297') +sha = pr['head']['sha'] +combined_status = get(base + f'/commits/{sha}/status') +check_runs = get(base + f'/commits/{sha}/check-runs') +check_suites = get(base + f'/commits/{sha}/check-suites') +issue_comments = get(base + '/issues/14297/comments') +action_required_suites = [ + suite for suite in check_suites.get('check_suites', []) + if suite.get('conclusion') == 'action_required' +] +head_ref = pr['head']['ref'] +repo_html = 'https://github.com/NousResearch/hermes-agent' +pr_url = pr['html_url'] +checks_url = pr_url + '/checks' +actions_query = urllib.parse.quote(f'branch:{head_ref}', safe='') +actions_url = f'{repo_html}/actions?query={actions_query}' +already_posted = any( + comment.get('user', {}).get('login') == 'NplusM420' + and 'Maintainer unblock request for PR #14297:' in (comment.get('body') or '') + for comment in issue_comments +) + +suite_lines = '\n'.join( + ( + f"- Suite `{suite['id']}` — {suite.get('status')} / {suite.get('conclusion') or 'pending'}\n" + f" - API: {suite.get('url')}\n" + f" - Check runs API: {suite.get('check_runs_url')}\n" + f" - latest_check_runs_count: {suite.get('latest_check_runs_count', 0)} | rerequestable: {suite.get('rerequestable')}" + ) + for suite in action_required_suites +) or '- none' + +ready_to_post = f'''Maintainer unblock request for PR #14297: + +The Delegation Readiness Doctor PR is ready for review, but GitHub has the fork workflows stuck at `action_required` for head `{sha}`. + +Live blocker signature right now: +- combined status: `{combined_status.get('state')}` +- check runs: `{check_runs.get('total_count', 0)}` +- check suites: `{check_suites.get('total_count', 0)}` +- action_required suites: `{len(action_required_suites)}` + +Please approve and run the fork PR workflows for this head commit. After that, rerun: +`bash starter-kits/delegation-readiness-doctor/scripts/emit-pr-review-monitor.sh` + +If a real failing run appears, the proof/repair packet is already frozen in `starter-kits/delegation-readiness-doctor/artifacts/latest-reviewer-handoff.md` and `latest-broken-state-roundtrip.md`. +'''.strip() + +ready_to_post_block_title = 'Ready-to-post maintainer nudge' +ready_to_post_preface = '' +trigger_stdout_token = 'WORKFLOW_APPROVAL_TRIGGER_READY' +if already_posted: + ready_to_post_block_title = 'Maintainer nudge status' + ready_to_post_preface = ( + 'Existing maintainer unblock request already posted by `NplusM420`; ' + 'do not repost unless the blocker signature changes materially. ' + 'Use the text below only as the current live-state reference.\n\n' + ) + trigger_stdout_token = 'WORKFLOW_APPROVAL_TRIGGER_ALREADY_POSTED' + +now = datetime.now().astimezone().strftime('%Y-%m-%d %H:%M %Z') +report = f"""# Delegation Readiness Doctor — Workflow Approval Trigger + +Generated: {now} +PR: {pr_url} +Head ref: `{pr['head']['label']}` +Head SHA: `{sha}` +Base SHA: `{pr['base']['sha']}` + +## Live signature +- Combined status state: {combined_status.get('state')} +- Combined status contexts: {combined_status.get('total_count', 0)} +- Check runs: {check_runs.get('total_count', 0)} +- Check suites: {check_suites.get('total_count', 0)} +- Action-required suites: {len(action_required_suites)} + +## Exact blocker +GitHub has already created Actions suites for the fork PR head commit, but every suite is still `action_required` and no check runs exist yet. The blocker is maintainer workflow approval / run permission, not missing local proof. + +## Direct approval surfaces +- PR conversation: {pr_url} +- PR checks tab: {checks_url} +- Repo Actions filtered to this branch: {actions_url} + +## Action-required suites +{suite_lines} + +## {ready_to_post_block_title} +```text +{ready_to_post_preface}{ready_to_post} +``` + +## Verification after approval +1. Run `bash starter-kits/delegation-readiness-doctor/scripts/emit-pr-review-monitor.sh`. +2. Confirm `latest-pr-review-monitor.md` shows at least one real check run or status context for head `{sha}`. +3. If CI fails, answer that concrete failure from `latest-reviewer-handoff.md` instead of repeating the approval blocker. + +## Proof note +This trigger artifact exists so the recurring blocker can be attacked with one exact nudge packet and one exact verification step instead of another status-only monitor refresh, even when unauthenticated public API rate limits would otherwise stall the packet refresh. +""" +report_path.write_text(report, encoding='utf-8') +shutil.copyfile(report_path, latest_path) +print(report_path) +print(trigger_stdout_token) +PY + +chmod +x "$SCRIPT_DIR/emit-workflow-approval-trigger.sh" +printf 'Wrote report: %s\n' "$REPORT_PATH" +printf 'Latest report: %s\n' "$LATEST_PATH" diff --git a/starter-kits/delegation-readiness-doctor/scripts/validate-artifact-consistency.sh b/starter-kits/delegation-readiness-doctor/scripts/validate-artifact-consistency.sh new file mode 100755 index 0000000000..60fdbbc9fa --- /dev/null +++ b/starter-kits/delegation-readiness-doctor/scripts/validate-artifact-consistency.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +KIT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ARTIFACT_DIR="$KIT_DIR/artifacts" + +python - "$ARTIFACT_DIR" <<'PY' +import re +import sys +from pathlib import Path + +artifacts_dir = Path(sys.argv[1]) +artifacts = [ + 'latest-workflow-approval-state-change.md', + 'latest-pr-review-monitor.md', + 'latest-ci-result-interpreter.md', + 'latest-workflow-approval-trigger.md', + 'latest-workflow-approval-brief.md', +] + +patterns = { + 'head': [ + re.compile(r'^- Head SHA: `(.*?)`$', re.MULTILINE), + re.compile(r'^Head SHA: `(.*?)`$', re.MULTILINE), + re.compile(r'^- Current head SHA: `(.*?)`$', re.MULTILINE), + re.compile(r'^- Previous head SHA: `(.*?)`$', re.MULTILINE), + ], + 'base': [ + re.compile(r'^- Base SHA: `(.*?)`$', re.MULTILINE), + re.compile(r'^Base SHA: `(.*?)`$', re.MULTILINE), + re.compile(r'^- Base SHA: \*\*`(.*?)`\*\*$', re.MULTILINE), + re.compile(r'^- Current PR base SHA: `(.*?)`$', re.MULTILINE), + re.compile(r'^- Previous base SHA: `(.*?)`$', re.MULTILINE), + ], +} + + +def extract(text: str, field: str) -> str: + for pattern in patterns[field]: + match = pattern.search(text) + if match: + return match.group(1).strip().strip('`*') + return 'missing' + +rows = [] +for name in artifacts: + path = artifacts_dir / name + if not path.exists(): + rows.append((name, 'missing-file', 'missing-file')) + continue + text = path.read_text(encoding='utf-8') + rows.append((name, extract(text, 'head'), extract(text, 'base'))) + +heads = {row[1] for row in rows} +known_bases = {row[2] for row in rows if row[2] not in {'missing', 'missing-file'}} +missing_head_rows = [row for row in rows if row[1] in {'missing', 'missing-file'}] +missing_base_rows = [row for row in rows if row[2] in {'missing', 'missing-file'}] + +print('# Delegation Readiness Doctor — Artifact Consistency Check') +print() +for name, head, base in rows: + print(f'- {name}: head={head} | base={base}') + +if ( + len(heads) == 1 + and 'missing' not in heads + and 'missing-file' not in heads + and len(known_bases) == 1 + and not missing_head_rows + and not missing_base_rows +): + base_summary = next(iter(known_bases)) + print() + print(f'CONSISTENT: head={next(iter(heads))} | base={base_summary}') + sys.exit(0) + +print() +print('DRIFT_DETECTED') +sys.exit(1) +PY + +chmod +x "$SCRIPT_DIR/validate-artifact-consistency.sh"