#!/usr/bin/env bash # Drive and narrate the Hermes code-proposal demo. # # This is the proposal loop: a build fails on a real defect, Ariadne asks # Hermes for a minimal patch, validates it as data, pushes a branch and opens a # pull request. Nothing merges. The point of this half is the stop, not the fix. # # hermes_code_demo.sh monitor # follow the Test Automation Diagram live # hermes_code_demo.sh reset # restore the demo repository to pre-run state # hermes_code_demo.sh preflight # confirm the lab is ready to demo # hermes_code_demo.sh run # seed the defect and narrate the loop # hermes_code_demo.sh status # current incident/alert state, no changes # # The autonomous-repair demo is a separate script: hermes_triage_demo.sh. # # FIRST RUN: copy hermes_demo.env.example to hermes_demo.env in this directory # and fill it in. That file is git-ignored precisely so it can hold real # tokens; this script sources it automatically. You also need bstein/ # hermes-code-demo cloned locally (default ~/Development/hermes-code-demo, # override with CODE_REPO_DIR). # # The only thing this mutates is the demo repository: it pushes a seeded defect # to master and reverts it on reset. set -euo pipefail # shellcheck source=scripts/ops/hermes_demo_lib.sh . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hermes_demo_lib.sh" # DEMO_REPOS is the whole blast radius and is deliberately explicit: a real # service's issues are genuine triage records, and clearing them to tidy a demo # would destroy the evidence the system exists to produce. DEMO_REPOS="${DEMO_REPOS:-hermes-code-demo}" cmd_reset() { say "Reset — restoring the code demo to its pre-run state" if [ -z "${GITEA_TOKEN:-}" ]; then note "GITEA_TOKEN unset; skipping repository cleanup" else for repo in $DEMO_REPOS; do note "clearing bstein/$repo (demo repository)" local items items="$(gitea_get "/api/v1/repos/bstein/$repo/issues?state=all&limit=100" | python3 -c 'import json,sys for i in json.load(sys.stdin): print(i["number"], "pr" if i.get("pull_request") else "issue")' 2>/dev/null || true)" if [ -z "$items" ]; then note " no issues or pull requests" else while read -r num kind; do [ -z "$num" ] && continue curl -s --max-time 25 -o /dev/null -X DELETE -H "Authorization: token $GITEA_TOKEN" \ "$GITEA_URL/api/v1/repos/bstein/$repo/issues/$num" note " deleted $kind #$num" done <<< "$items" fi local branches branches="$(gitea_get "/api/v1/repos/bstein/$repo/branches" | python3 -c 'import json,sys,urllib.parse for b in json.load(sys.stdin): if b["name"].startswith("hermes-repair/"): print(urllib.parse.quote(b["name"], safe=""))' 2>/dev/null || true)" if [ -z "$branches" ]; then note " no repair branches" else for ref in $branches; do curl -s --max-time 25 -o /dev/null -X DELETE -H "Authorization: token $GITEA_TOKEN" \ "$GITEA_URL/api/v1/repos/bstein/$repo/branches/$ref" note " deleted branch $(printf '%b' "${ref//%/\\x}")" done fi done fi # The demo seeds its defect by pushing to master, and the fix only lands if # someone merges the pull request - which, by design, nobody does during a # demo. So master stays broken, and the next run aborts on "defect already # present". Reset has to undo the seed rather than just report it, or the # second demo of the day fails before it starts. if [ -d "$CODE_REPO_DIR/.git" ]; then note "restoring the demo repository working state" ( cd "$CODE_REPO_DIR" && git checkout -q master && git fetch -q origin && git reset -q --hard origin/master ) || note " could not sync master" if grep -q 'percent / 100' "$CODE_REPO_DIR/src/discount.py" 2>/dev/null; then note " src/discount.py is correct; demo is armable" else note " src/discount.py carries the seeded defect; reverting it on master" ( cd "$CODE_REPO_DIR" && python3 - <<'PY' import pathlib, re, sys path = pathlib.Path("src/discount.py") source = path.read_text() # Matches the seeded `percent / 10` without also matching a correct # `percent / 100`, so re-running reset on a healthy file changes nothing. fixed = re.sub(r"percent / 10(?!\d)", "percent / 100", source) if fixed == source: sys.exit("unrecognised defect; fix src/discount.py by hand") path.write_text(fixed) PY git commit -qam "revert: restore the discount divisor" && git push -q origin master && note " reverted and pushed; demo is armable" ) || note " revert failed — fix src/discount.py by hand" fi else note "demo repository not cloned at $CODE_REPO_DIR; skipping" fi say "Ready" note "real service repositories were not touched" note "run 'preflight' next, then 'run'" } cmd_preflight() { require_jenkins say "Preflight — code demo" if [ -d "$CODE_REPO_DIR/.git" ]; then if grep -q 'percent / 100' "$CODE_REPO_DIR/src/discount.py" 2>/dev/null; then note "demo repository: src/discount.py is correct; armable" else note "demo repository: src/discount.py carries a defect — run 'reset' first" fi else note "demo repository: NOT CLONED at $CODE_REPO_DIR" fi local open_prs open_prs="$(gitea_get "/api/v1/repos/bstein/hermes-code-demo/pulls?state=open" 2>/dev/null | python3 -c 'import json,sys; print(len(json.load(sys.stdin)))' 2>/dev/null || echo '?')" note "open hermes-code-demo PRs: $open_prs (must be 0 — the duplicate guard refuses while one is open)" note "code proposals enabled: $(kubectl -n maintenance exec deploy/ariadne -c ariadne -- printenv ARIADNE_HERMES_CODE_ENABLED 2>/dev/null)" note "fix categories: $(kubectl -n maintenance exec deploy/ariadne -c ariadne -- printenv ARIADNE_HERMES_FIX_CATEGORIES 2>/dev/null)" shared_preflight } cmd_status() { shared_status say "Open proposals" note "https://scm.bstein.dev/bstein/hermes-code-demo/pulls" } cmd_run() { require_jenkins [ -d "$CODE_REPO_DIR/.git" ] || { echo "clone bstein/hermes-code-demo to $CODE_REPO_DIR first" >&2; exit 1; } local start_num next_num start_num="$(last_build_number "$CODE_JOB")" next_num=$((start_num + 1)) say "Seeding a one-line defect in src/discount.py" ( cd "$CODE_REPO_DIR" && git checkout -q master && git pull -q && python3 - <<'PY' import pathlib p = pathlib.Path("src/discount.py") s = p.read_text() old, new = "percent / 100", "percent / 10" if old not in s: raise SystemExit("defect already present or file changed; run reset first") p.write_text(s.replace(old, new)) PY git commit -qam "refactor: simplify discount percentage math" && git push -q origin master ) note "pushed: a plausible-looking change that breaks three regression tests" say "Running the test gate -> build #$next_num" note "HTTP $(jenkins_post "/job/$CODE_JOB/build")" note "result: $(wait_for_build "$CODE_JOB" "$next_num")" say "Ariadne collects evidence and asks Hermes for a minimal patch" note "Hermes returns an anchored patch as data; Ariadne validates path, size," note "changed lines, and that the anchor is unique, then pushes hermes-repair/$next_num" for _ in $(seq 1 40); do sleep 15 ariadne_ticks 300 1 | grep -q "code_fix_proposed" && break done ariadne_ticks 400 3 say "Pull request awaiting human review (nothing merges automatically)" note "https://scm.bstein.dev/bstein/hermes-code-demo/pulls" } case "${1:-}" in run|code) cmd_run ;; status) cmd_status ;; preflight) cmd_preflight ;; reset) cmd_reset ;; monitor) run_monitor "$CODE_JOB" ;; *) sed -n '2,23p' "$0" | sed 's/^# \{0,1\}//' ; exit 1 ;; esac