mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
chore: restore blocker packet starter kit
This commit is contained in:
parent
944f10ad44
commit
dc813f93a6
2 changed files with 386 additions and 0 deletions
|
|
@ -29,6 +29,8 @@ This starter kit now packages the proof line, not just the kickoff gap, so the s
|
||||||
- `scripts/prove-broken-state-roundtrip.sh` — isolated blocked→ready doctor roundtrip proof that leaves the real `~/.hermes/config.yaml` untouched
|
- `scripts/prove-broken-state-roundtrip.sh` — isolated blocked→ready doctor roundtrip proof that leaves the real `~/.hermes/config.yaml` untouched
|
||||||
- `artifacts/latest-current-gap-report.md` — most recent proof packet emitted by the gap verifier
|
- `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-broken-state-roundtrip.md` — canonical blocked-state proof packet with before/after doctor output
|
||||||
|
- `scripts/refresh-upstream-blocker-packet.sh` — one-command PR blocker refresh that distinguishes stale-base drift from pure workflow approval
|
||||||
|
- `artifacts/latest-upstream-blocker-refresh.md` — canonical live PR blocker packet with state signature + change-vs-previous marker
|
||||||
|
|
||||||
## Fast start
|
## Fast start
|
||||||
From the Hermes repo root:
|
From the Hermes repo root:
|
||||||
|
|
@ -62,3 +64,16 @@ What is now frozen on disk:
|
||||||
- multi-provider credential orchestration cleanup
|
- multi-provider credential orchestration cleanup
|
||||||
- dashboard/control-plane expansion
|
- dashboard/control-plane expansion
|
||||||
- claiming delegation is fixed before one real delegated run passes
|
- claiming delegation is fixed before one real delegated run passes
|
||||||
|
|
||||||
|
## PR handoff monitoring
|
||||||
|
When the MVP is already open as a PR, use the blocker packet instead of freehand status checks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash starter-kits/delegation-readiness-doctor/scripts/refresh-upstream-blocker-packet.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected behavior:
|
||||||
|
- prints `UPSTREAM_BLOCKER_PACKET_REFRESHED` only when the live blocker signature actually changed
|
||||||
|
- prints `UPSTREAM_BLOCKER_PACKET_UNCHANGED` on timestamp-only reruns
|
||||||
|
- marks stale-base drift explicitly when the PR falls behind `origin/main`
|
||||||
|
- keeps workflow approval as the blocker only when the branch is current and check suites are still `action_required` with `0` real check runs
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,371 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
KIT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
REPO_DIR="$(cd "$KIT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
python3 - "$REPO_DIR" "$KIT_DIR" <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
repo_dir = Path(sys.argv[1])
|
||||||
|
kit_dir = Path(sys.argv[2])
|
||||||
|
artifacts_dir = kit_dir / "artifacts"
|
||||||
|
artifacts_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
OWNER = "NousResearch"
|
||||||
|
REPO = "hermes-agent"
|
||||||
|
PR_NUMBER = 14297
|
||||||
|
BRANCH_REF = "fork/hermes/delegation-readiness-doctor-clean"
|
||||||
|
API = f"https://api.github.com/repos/{OWNER}/{REPO}"
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd, cwd=None, check=True):
|
||||||
|
proc = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
|
||||||
|
if check and proc.returncode != 0:
|
||||||
|
raise RuntimeError(f"command failed: {' '.join(cmd)}\nstdout={proc.stdout}\nstderr={proc.stderr}")
|
||||||
|
return proc
|
||||||
|
|
||||||
|
|
||||||
|
def git_credential_token():
|
||||||
|
cred_path = Path.home() / ".git-credentials"
|
||||||
|
if not cred_path.exists():
|
||||||
|
return None
|
||||||
|
for line in cred_path.read_text().splitlines():
|
||||||
|
if "github.com" not in line:
|
||||||
|
continue
|
||||||
|
parsed = urllib.parse.urlparse(line.strip())
|
||||||
|
if parsed.username and parsed.password:
|
||||||
|
return urllib.parse.unquote(parsed.password)
|
||||||
|
if "@" in parsed.netloc and ":" in parsed.netloc.split("@")[0]:
|
||||||
|
return urllib.parse.unquote(parsed.netloc.split("@")[0].split(":", 1)[1])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def gh_get(path):
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"User-Agent": "hermes-starter-kit-refresh",
|
||||||
|
}
|
||||||
|
token = git_credential_token()
|
||||||
|
if token:
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
req = urllib.request.Request(f"{API}{path}", headers=headers)
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def now(fmt):
|
||||||
|
return run(["date", fmt]).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def write_artifact(stem, body):
|
||||||
|
stamp = now("+%Y-%m-%dT%H-%M-%S%z")
|
||||||
|
timestamped = artifacts_dir / f"{stem}-{stamp}.md"
|
||||||
|
latest = artifacts_dir / f"latest-{stem}.md"
|
||||||
|
previous = latest.read_text() if latest.exists() else None
|
||||||
|
timestamped.write_text(body)
|
||||||
|
latest.write_text(body)
|
||||||
|
return latest, timestamped, previous
|
||||||
|
|
||||||
|
|
||||||
|
def extract_prior_signature(text):
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
match = re.search(r"State signature: `(.*?)`", text)
|
||||||
|
return match.group(1) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def ahead_behind():
|
||||||
|
run(["git", "fetch", "--all", "--prune"], cwd=repo_dir)
|
||||||
|
counts = run(["git", "rev-list", "--left-right", "--count", f"{BRANCH_REF}...origin/main"], cwd=repo_dir).stdout.strip()
|
||||||
|
ahead_str, behind_str = counts.split()
|
||||||
|
origin_main_sha = run(["git", "rev-parse", "origin/main"], cwd=repo_dir).stdout.strip()
|
||||||
|
return int(ahead_str), int(behind_str), origin_main_sha
|
||||||
|
|
||||||
|
|
||||||
|
pr = gh_get(f"/pulls/{PR_NUMBER}")
|
||||||
|
head_sha = pr["head"]["sha"]
|
||||||
|
base_sha = pr["base"]["sha"]
|
||||||
|
reviews = gh_get(f"/pulls/{PR_NUMBER}/reviews?per_page=100")
|
||||||
|
issue_comments = gh_get(f"/issues/{PR_NUMBER}/comments?per_page=100")
|
||||||
|
status = gh_get(f"/commits/{head_sha}/status")
|
||||||
|
check_runs = gh_get(f"/commits/{head_sha}/check-runs?per_page=100")
|
||||||
|
check_suites = gh_get(f"/commits/{head_sha}/check-suites?per_page=100")
|
||||||
|
|
||||||
|
review_count = len(reviews)
|
||||||
|
issue_comment_count = len(issue_comments)
|
||||||
|
check_run_items = check_runs.get("check_runs", [])
|
||||||
|
check_suite_items = check_suites.get("check_suites", [])
|
||||||
|
action_required_suites = [s for s in check_suite_items if s.get("conclusion") == "action_required"]
|
||||||
|
ahead_count, behind_count, origin_main_sha = ahead_behind()
|
||||||
|
ahead_behind_value = f"{ahead_count} / {behind_count}"
|
||||||
|
created = now("+%Y-%m-%d %H:%M %Z")
|
||||||
|
|
||||||
|
maintainer_request = None
|
||||||
|
for comment in issue_comments:
|
||||||
|
body = comment.get("body", "")
|
||||||
|
author = (comment.get("user") or {}).get("login", "")
|
||||||
|
if "Maintainer unblock request for PR #14297" in body or (author == "NplusM420" and "action_required" in body):
|
||||||
|
maintainer_request = comment
|
||||||
|
break
|
||||||
|
|
||||||
|
historical_candidates = [
|
||||||
|
"artifacts/latest-readiness-proof.md",
|
||||||
|
"artifacts/latest-clean-commit-surface.md",
|
||||||
|
"artifacts/latest-broken-state-roundtrip.md",
|
||||||
|
"artifacts/latest-reviewer-handoff.md",
|
||||||
|
"artifacts/latest-ship-review.md",
|
||||||
|
]
|
||||||
|
missing_historical = [rel for rel in historical_candidates if not (kit_dir / rel).exists()]
|
||||||
|
|
||||||
|
branch_current = behind_count == 0 and base_sha == origin_main_sha
|
||||||
|
approval_blocked = branch_current and len(action_required_suites) > 0 and not check_run_items
|
||||||
|
|
||||||
|
if behind_count > 0:
|
||||||
|
blocker_call = (
|
||||||
|
f"The approval-only model is stale. The PR branch is {behind_count} commit(s) behind live origin/main "
|
||||||
|
f"({origin_main_sha[:12]}) and needs a fresh replay/branch refresh before workflow approval is the real blocker again."
|
||||||
|
)
|
||||||
|
exact_next_move = (
|
||||||
|
f"Create a fresh worktree from origin/main ({origin_main_sha[:12]}), replay the MVP surface, rerun the focused proof suite, "
|
||||||
|
f"and refresh PR #{PR_NUMBER} before resuming the workflow-approval wait loop."
|
||||||
|
)
|
||||||
|
elif approval_blocked:
|
||||||
|
blocker_call = "The PR is current on live upstream base. The only blocker is maintainer workflow approval / first real CI movement."
|
||||||
|
exact_next_move = (
|
||||||
|
f"Watch PR #{PR_NUMBER} for workflow approval, check-run start, or review activity on head {head_sha}. "
|
||||||
|
f"On the next state change, rerun this script and answer that exact signal immediately."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
blocker_call = "The blocker surface has changed; inspect the regenerated review and CI artifacts now."
|
||||||
|
exact_next_move = "Route the changed review/CI surface through the regenerated packet immediately."
|
||||||
|
|
||||||
|
pr_monitor_next_move = (
|
||||||
|
"Do not repost the maintainer nudge. Wait for workflow approval, a real check run, or a review event, then rerun this packet and answer that exact signal."
|
||||||
|
if maintainer_request and approval_blocked
|
||||||
|
else exact_next_move
|
||||||
|
)
|
||||||
|
|
||||||
|
pr_monitor = f"""# Delegation Readiness Doctor — PR Review Monitor
|
||||||
|
|
||||||
|
Generated: {created}
|
||||||
|
|
||||||
|
## PR identity
|
||||||
|
- Title: {pr['title']}
|
||||||
|
- URL: {pr['html_url']}
|
||||||
|
- State: {pr['state']}
|
||||||
|
- Draft: {pr['draft']}
|
||||||
|
- Mergeable: {pr['mergeable']}
|
||||||
|
- Mergeable state: {pr['mergeable_state']}
|
||||||
|
- Base ← Head: `main <- {pr['head']['label']}`
|
||||||
|
- Head SHA: `{head_sha}`
|
||||||
|
- Base SHA: `{base_sha}`
|
||||||
|
- Live `origin/main` SHA: `{origin_main_sha}`
|
||||||
|
- Commits / files: `{pr['commits']} commits`, `{pr['changed_files']} files`
|
||||||
|
- Additions / deletions: `{pr['additions']} / {pr['deletions']}`
|
||||||
|
- Ahead / behind vs `origin/main`: `{ahead_behind_value}`
|
||||||
|
|
||||||
|
## Review surface
|
||||||
|
- Review count: {review_count}
|
||||||
|
- Issue comment count: {issue_comment_count}
|
||||||
|
- Review comment count: {pr['review_comments']}
|
||||||
|
|
||||||
|
## Automation surface
|
||||||
|
- Combined statuses: {len(status.get('statuses', []))}
|
||||||
|
- Combined status state: {status.get('state')}
|
||||||
|
- Check runs: {len(check_run_items)}
|
||||||
|
- Check suites: {len(check_suite_items)}
|
||||||
|
- Action-required suites: {len(action_required_suites)}
|
||||||
|
|
||||||
|
## Live blocker
|
||||||
|
{blocker_call}
|
||||||
|
|
||||||
|
## Exact next move
|
||||||
|
{pr_monitor_next_move}
|
||||||
|
"""
|
||||||
|
|
||||||
|
ci_verdict = (
|
||||||
|
"STALE_BASE_DRIFT"
|
||||||
|
if behind_count > 0
|
||||||
|
else "WAITING_FOR_WORKFLOW_APPROVAL"
|
||||||
|
if approval_blocked
|
||||||
|
else "CHECK_RUNS_PRESENT"
|
||||||
|
if check_run_items
|
||||||
|
else "NO_ACTION_REQUIRED_SUITES"
|
||||||
|
)
|
||||||
|
ci_lines = []
|
||||||
|
for run_item in check_run_items[:10]:
|
||||||
|
ci_lines.append(f"- {run_item['name']} — {run_item.get('status')} / {run_item.get('conclusion')}")
|
||||||
|
if not ci_lines:
|
||||||
|
ci_lines.append("- none yet")
|
||||||
|
|
||||||
|
historical_lines = [f"- {item}" for item in missing_historical] or ["- none"]
|
||||||
|
ci_interpreter = f"""# Delegation Readiness Doctor — CI Result Interpreter
|
||||||
|
|
||||||
|
Generated: {created}
|
||||||
|
PR: {pr['html_url']}
|
||||||
|
Head SHA: `{head_sha}`
|
||||||
|
Verdict: **{ci_verdict}**
|
||||||
|
|
||||||
|
## Current CI surface
|
||||||
|
- Combined status state: {status.get('state')}
|
||||||
|
- Check runs: {len(check_run_items)}
|
||||||
|
- Check suites: {len(check_suite_items)}
|
||||||
|
- Action-required suites: {len(action_required_suites)}
|
||||||
|
- Ahead / behind vs `origin/main`: {ahead_behind_value}
|
||||||
|
|
||||||
|
### Check runs
|
||||||
|
{os.linesep.join(ci_lines)}
|
||||||
|
|
||||||
|
## Historical proof pointers still missing in this checkout
|
||||||
|
{os.linesep.join(historical_lines)}
|
||||||
|
|
||||||
|
## Exact next move
|
||||||
|
{exact_next_move}
|
||||||
|
"""
|
||||||
|
|
||||||
|
suite_lines = []
|
||||||
|
for suite in action_required_suites[:10]:
|
||||||
|
suite_lines.append(
|
||||||
|
f"- Suite `{suite['id']}` — {suite.get('status')} / {suite.get('conclusion')} | created {suite.get('created_at')} | updated {suite.get('updated_at')}"
|
||||||
|
)
|
||||||
|
if not suite_lines:
|
||||||
|
suite_lines.append("- none")
|
||||||
|
|
||||||
|
workflow_brief = f"""# Delegation Readiness Doctor — Workflow Approval Brief
|
||||||
|
|
||||||
|
Generated: {created}
|
||||||
|
PR: {pr['html_url']}
|
||||||
|
Head SHA: `{head_sha}`
|
||||||
|
Base SHA: `{base_sha}`
|
||||||
|
Live `origin/main` SHA: `{origin_main_sha}`
|
||||||
|
|
||||||
|
## Live signature
|
||||||
|
- Combined status state: {status.get('state')}
|
||||||
|
- Combined status contexts: {len(status.get('statuses', []))}
|
||||||
|
- Check runs: {len(check_run_items)}
|
||||||
|
- Check suites: {len(check_suite_items)}
|
||||||
|
- Action-required suites: {len(action_required_suites)}
|
||||||
|
- Ahead / behind vs `origin/main`: {ahead_behind_value}
|
||||||
|
|
||||||
|
## Action-required suites
|
||||||
|
{os.linesep.join(suite_lines)}
|
||||||
|
|
||||||
|
## Exact maintainer move
|
||||||
|
{'A maintainer with repo permissions needs to approve and run the PR workflows for this forked branch/head commit.' if approval_blocked else 'Workflow approval is not the only active blocker; inspect branch freshness or live CI first.'}
|
||||||
|
|
||||||
|
## Verification after approval
|
||||||
|
1. Refresh `latest-pr-review-monitor.md`.
|
||||||
|
2. Confirm at least one real check run or status context exists for head `{head_sha}`.
|
||||||
|
3. If a failing run appears, answer that exact failure instead of treating the PR as approval-blocked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
trigger_state = "ALREADY_POSTED_REFERENCE_ONLY" if maintainer_request else "READY_TO_POST"
|
||||||
|
trigger_body = maintainer_request.get('body', '').strip() if maintainer_request else (
|
||||||
|
"Maintainer unblock request for PR #14297:\n\nThe Delegation Readiness Doctor PR is ready for review, but GitHub has the fork workflows stuck at `action_required` with 0 real check runs."
|
||||||
|
)
|
||||||
|
workflow_trigger = f"""# Delegation Readiness Doctor — Workflow Approval Trigger
|
||||||
|
|
||||||
|
Generated: {created}
|
||||||
|
PR: {pr['html_url']}
|
||||||
|
Head SHA: `{head_sha}`
|
||||||
|
Trigger state: **{trigger_state}**
|
||||||
|
|
||||||
|
## Current blocker
|
||||||
|
- Action-required suites: {len(action_required_suites)}
|
||||||
|
- Real check runs: {len(check_run_items)}
|
||||||
|
- Existing maintainer request comment: {'yes' if maintainer_request else 'no'}
|
||||||
|
- Ahead / behind vs `origin/main`: {ahead_behind_value}
|
||||||
|
|
||||||
|
## Maintainer nudge text
|
||||||
|
{trigger_body}
|
||||||
|
|
||||||
|
## Exact next move
|
||||||
|
{'Do not repost unless the blocker signature changes materially.' if maintainer_request and approval_blocked else exact_next_move}
|
||||||
|
"""
|
||||||
|
|
||||||
|
state_signature = {
|
||||||
|
"head_sha": head_sha,
|
||||||
|
"base_sha": base_sha,
|
||||||
|
"origin_main_sha": origin_main_sha,
|
||||||
|
"ahead": ahead_count,
|
||||||
|
"behind": behind_count,
|
||||||
|
"action_required_suites": len(action_required_suites),
|
||||||
|
"check_runs": len(check_run_items),
|
||||||
|
"reviews": review_count,
|
||||||
|
"issue_comments": issue_comment_count,
|
||||||
|
"maintainer_request_posted": bool(maintainer_request),
|
||||||
|
}
|
||||||
|
state_signature_json = json.dumps(state_signature, sort_keys=True)
|
||||||
|
state_change = f"""# Delegation Readiness Doctor — Workflow Approval State Change
|
||||||
|
|
||||||
|
Generated: {created}
|
||||||
|
State signature: `{state_signature_json}`
|
||||||
|
|
||||||
|
## Verdict
|
||||||
|
{'BLOCKER_PERSISTS' if approval_blocked else 'BLOCKER_CHANGED'}
|
||||||
|
|
||||||
|
## Exact next move
|
||||||
|
{'Wait for real upstream movement; do not repost the existing maintainer request.' if maintainer_request and approval_blocked else exact_next_move}
|
||||||
|
"""
|
||||||
|
|
||||||
|
refresh_body = f"""# Delegation Readiness Doctor — Upstream Blocker Refresh
|
||||||
|
|
||||||
|
Generated: {created}
|
||||||
|
PR: {pr['html_url']}
|
||||||
|
State signature: `{state_signature_json}`
|
||||||
|
|
||||||
|
## Current live state
|
||||||
|
- Head SHA: `{head_sha}`
|
||||||
|
- Base SHA: `{base_sha}`
|
||||||
|
- Live `origin/main` SHA: `{origin_main_sha}`
|
||||||
|
- Mergeable: `{pr['mergeable']}`
|
||||||
|
- Mergeable state: `{pr['mergeable_state']}`
|
||||||
|
- Ahead / behind vs `origin/main`: `{ahead_behind_value}`
|
||||||
|
- GitHub check suites: `{len(check_suite_items)}` total / `{len(action_required_suites)}` action_required
|
||||||
|
- GitHub check runs: `{len(check_run_items)}`
|
||||||
|
- Reviews: `{review_count}`
|
||||||
|
- Issue comments: `{issue_comment_count}`
|
||||||
|
|
||||||
|
## Blocker call
|
||||||
|
{blocker_call}
|
||||||
|
|
||||||
|
## Durable packet restored in this checkout
|
||||||
|
- `artifacts/latest-pr-review-monitor.md`
|
||||||
|
- `artifacts/latest-ci-result-interpreter.md`
|
||||||
|
- `artifacts/latest-workflow-approval-brief.md`
|
||||||
|
- `artifacts/latest-workflow-approval-trigger.md`
|
||||||
|
- `artifacts/latest-workflow-approval-state-change.md`
|
||||||
|
|
||||||
|
## Historical proof pointers still missing in this checkout
|
||||||
|
{os.linesep.join(historical_lines)}
|
||||||
|
|
||||||
|
## Exact next move
|
||||||
|
{exact_next_move}
|
||||||
|
"""
|
||||||
|
|
||||||
|
write_artifact("pr-review-monitor", pr_monitor)
|
||||||
|
write_artifact("ci-result-interpreter", ci_interpreter)
|
||||||
|
write_artifact("workflow-approval-brief", workflow_brief)
|
||||||
|
write_artifact("workflow-approval-trigger", workflow_trigger)
|
||||||
|
write_artifact("workflow-approval-state-change", state_change)
|
||||||
|
latest_refresh, timestamped_refresh, prev_refresh_packet = write_artifact("upstream-blocker-refresh", refresh_body)
|
||||||
|
|
||||||
|
previous_signature = extract_prior_signature(prev_refresh_packet)
|
||||||
|
change_vs_previous = "unchanged" if previous_signature == state_signature_json else "changed"
|
||||||
|
refresh_with_change = refresh_body + f"\n## Change vs previous packet\n- {change_vs_previous}\n"
|
||||||
|
latest_refresh.write_text(refresh_with_change)
|
||||||
|
timestamped_refresh.write_text(refresh_with_change)
|
||||||
|
|
||||||
|
print("UPSTREAM_BLOCKER_PACKET_UNCHANGED" if previous_signature == state_signature_json else "UPSTREAM_BLOCKER_PACKET_REFRESHED")
|
||||||
|
print(str(latest_refresh))
|
||||||
|
print(str(timestamped_refresh))
|
||||||
|
PY
|
||||||
Loading…
Add table
Add a link
Reference in a new issue