2828# Posting requires --post-comment; without it the report/JSON only prints (useful
2929# for a local dry view). Nothing is posted when nothing was triaged. Two tuning-
3030# signal surfaces, both supported: the posted comment's 👍/👎 reaction (read by the
31- # metricsai weekly harvest) on CI/--post-comment runs, and --submit's terminal
32- # prompt (written straight to the Testing Events sheet) on local runs.
31+ # metricsai weekly harvest) on CI/--post-comment runs, and a terminal "helpful?"
32+ # prompt written straight to the Testing Events sheet on local runs. That prompt
33+ # fires BY DEFAULT on any interactive run when METRICSAI_WEBHOOK_URL/_KEY are set
34+ # — no --submit needed; --submit just forces it explicitly. A failed sheet write
35+ # is surfaced loudly but is non-fatal (the run still exits 0).
3336#
3437# Usage:
3538# test-classifier-dispatcher.sh # no args → --unpushed (local committed+staged, report-only)
3639# test-classifier-dispatcher.sh origin/main # bare ref → --against origin/main
3740# test-classifier-dispatcher.sh --pr 1234 --post-comment # post the PR comment
38- # test-classifier-dispatcher.sh --pr 1234 --submit # post + prompt "helpful?" + append a Testing Events row
41+ # test-classifier-dispatcher.sh --pr 1234 --submit # force the "helpful?" prompt + row (auto when webhook env is set)
3942# test-classifier-dispatcher.sh --against origin/main # explicit base ref
4043# test-classifier-dispatcher.sh --unpushed # local: committed+staged, NO PR (report-only)
4144# test-classifier-dispatcher.sh --json-only # emit only the JSON block
@@ -91,10 +94,12 @@ SKILL_PATH_CANONICAL="${SKILLS_ROOT}/test-classifier/SKILL.md"
9194#
9295# --pr <number> Explicit PR number (overrides auto-discovery)
9396# --post-comment Post ONE PR comment via gh api (omit to print only)
94- # --submit Implies --post-comment; then (interactive only) prompts
95- # "Was this helpful?" and appends one Testing Events row
96- # to the Sheet with the verdict + 👍/👎. Non-TTY/CI: posts
97- # and skips the prompt + row.
97+ # --submit Explicitly force the "Was this helpful?" prompt + the
98+ # Testing Events row, and (for back-compat) implies
99+ # --post-comment. NOTE: the prompt + row already happen
100+ # BY DEFAULT on any interactive run when the webhook env
101+ # vars are set — so --submit is only needed to also post
102+ # the comment, or to be explicit. Non-TTY/CI: skipped.
98103# --gate Exit 1 if the result is CLASSIFIED (CI-blocking mode)
99104# --json-only Print only the JSON block (machine consumption)
100105# --no-run-suite Read-only INFERRED pass: do NOT run the repo's suite
@@ -503,10 +508,12 @@ if (( WANT_HELP == 1 )); then
503508test-classifier options (in addition to the shared options above):
504509 --pr <number> Explicit PR number for posting (overrides auto-discovery).
505510 --post-comment Post ONE PR comment with the verdicts + a 👍/👎 reaction
506- ask (omitted when --submit captures the signal locally).
507- Omit for a local report-only run (prints, posts nothing).
508- --submit Implies --post-comment; then (interactive only) prompts
509- "Was this helpful?" and appends one Testing Events row.
511+ ask (omitted when the local "helpful?" prompt captures the
512+ signal). Omit for a report-only run (prints, posts nothing).
513+ --submit Force the "Was this helpful?" prompt + Testing Events row,
514+ and (back-compat) imply --post-comment. NOT usually needed:
515+ the prompt + row already fire by default on an interactive
516+ run when the webhook env vars are set (see below).
510517 --gate Exit 1 when the result is CLASSIFIED (CI-blocking mode).
511518 --json-only Print only the machine-readable JSON block.
512519 --no-run-suite Read-only INFERRED pass: predict from the diff instead of
@@ -522,7 +529,9 @@ Environment:
522529 OBSERVED is the default; set AI_RUN_SUITE=0 (or pass
523530 --no-run-suite) for a read-only INFERRED pass.
524531 METRICSAI_WEBHOOK_URL / METRICSAI_WEBHOOK_KEY
525- Required only by --submit to append the Testing Events row.
532+ When BOTH are set, an interactive run prompts "helpful?"
533+ and writes a Testing Events row automatically (no --submit
534+ needed). A failed write is surfaced but non-fatal.
526535HELP_ADDENDUM
527536 ai_review::print_help
528537 exit 0
@@ -821,14 +830,19 @@ post_comment_to_github() {
821830 echo " [test-classifier] Comment posted (issue comment)."
822831}
823832
824- # ── --submit: prompt "Was this helpful?" and post one row via the webhook ── ─
833+ # ── Prompt "Was this helpful?" and post one Testing Events row via the webhook ─
825834# Streamlines the manual local loop: instead of (post comment → react 👍/👎 on
826835# GitHub → a separate weekly harvest), capture the developer's signal right after
827836# the run and POST it to the metricsai Google Apps Script webhook — the SAME
828837# transport metricsai uses (flat JSON body, fields aligned by header name, plus
829838# reserved `_tab` and `_key`). No service account, no gcloud, no token expiry —
830839# just two static env vars the developer sets once.
831840#
841+ # This runs by DEFAULT on any interactive run when the webhook env vars are set
842+ # (see the auto_submit logic in test_classifier::run) — the developer no longer
843+ # needs to pass --submit. The explicit --submit flag remains as an opt-in and is
844+ # a no-op when auto-submit already applies.
845+ #
832846# Fields (aligned by header name on the Apps Script side):
833847# repo, pr, comment_id, comment_created_at, verdict, category, confidence,
834848# thumbs_up, thumbs_down, reason (+ _tab="Testing Events", _key=<api key>)
@@ -838,10 +852,11 @@ post_comment_to_github() {
838852# METRICSAI_WEBHOOK_KEY the "AI Metrics" API key (body _key) (required)
839853# METRICSAI_WEBHOOK_TAB destination tab; defaults to Testing Events
840854#
841- # Gated: only runs interactively (TTY, not CI) — non-interactive runs post the
842- # comment and skip the prompt + POST (no human signal to record, no hang).
843- # Missing URL/key → records the answer to the terminal, warns, still posts the
844- # comment.
855+ # Gated: only runs interactively (TTY, not CI) — non-interactive runs skip the
856+ # prompt + POST (no human signal to record, no hang). Missing URL/key → captures
857+ # the answer in the terminal and warns, but writes no row. A failed POST is
858+ # surfaced loudly (HTTP code + response body) but is NON-FATAL: the run exits 0
859+ # because the classification itself succeeded.
845860#
846861# Args: PR number, the extracted classifier JSON.
847862submit_metrics_row () {
@@ -857,11 +872,11 @@ submit_metrics_row() {
857872 # must gate on the SAME thing: can we open /dev/tty? In CI / a real non-TTY run
858873 # there is no controlling terminal, so this still skips correctly.
859874 if [[ " ${CI:- } " == " true" ]] || ! { : <> /dev/tty; } 2> /dev/null; then
860- ai_review::info " --submit : non-interactive run (no controlling terminal) — comment posted; skipping the helpfulness prompt + metrics row." >&2
875+ ai_review::info " metrics : non-interactive run (no controlling terminal) — skipping the helpfulness prompt + Testing Events row." >&2
861876 return 0
862877 fi
863878 if ! command -v python3 & > /dev/null; then
864- ai_review::warn " --submit : python3 not found; cannot build the metrics row — skipping it." >&2
879+ ai_review::warn " metrics : python3 not found; cannot build the Testing Events row — skipping it." >&2
865880 return 0
866881 fi
867882
@@ -886,7 +901,7 @@ submit_metrics_row() {
886901 ;;
887902 * )
888903 # Empty Enter or anything unrecognized = skip (don't guess a verdict).
889- ai_review::info " --submit : skipped (no y/n answer) — comment still posted ." >&2
904+ ai_review::info " metrics : skipped (no y/n answer) — nothing recorded to the sheet ." >&2
890905 return 0
891906 ;;
892907 esac
@@ -895,7 +910,7 @@ submit_metrics_row() {
895910 local webhook_key=" ${METRICSAI_WEBHOOK_KEY:- } "
896911 local webhook_tab=" ${METRICSAI_WEBHOOK_TAB:- Testing Events} "
897912 if [[ -z " ${webhook_url} " || -z " ${webhook_key} " ]]; then
898- ai_review::warn " --submit : METRICSAI_WEBHOOK_URL / METRICSAI_WEBHOOK_KEY not set — recorded your answer but didn't post a row." >&2
913+ ai_review::warn " metrics : METRICSAI_WEBHOOK_URL / METRICSAI_WEBHOOK_KEY not set — captured your answer but didn't post a row." >&2
899914 ai_review::log " Export both (the metricsai webhook URL + the AI Metrics API key) to enable the sink." >&2
900915 return 0
901916 fi
@@ -942,14 +957,28 @@ print(json.dumps(dict(zip(keys, sys.argv[1:1+len(keys)]))))
942957 # it can 405 on the final Drive hop even though the row landed, so we must NOT
943958 # use `-f -L` (that reports a false failure). Capture the first-hop status and
944959 # treat 200/302 as success; don't follow the redirect.
945- local http_code
946- http_code=" $( curl -sS -o /dev/null -w ' %{http_code}' -X POST " ${webhook_url} " \
960+ # Capture BOTH the response body and the first-hop status so a real failure
961+ # (e.g. the Apps Script denying access to the sheet, an auth error, a 4xx/5xx)
962+ # is surfaced verbatim rather than reduced to a bare code. Body and status are
963+ # split on a trailing sentinel line we ask curl to append via -w.
964+ local resp http_code
965+ resp=" $( curl -sS -X POST " ${webhook_url} " \
947966 -H " Content-Type: application/json" \
948- -d " ${body} " 2> /dev/null || echo " 000" ) "
967+ -d " ${body} " -w $' \n __HTTP_CODE__:%{http_code}' 2>&1 || true) "
968+ http_code=" $( printf ' %s' " ${resp} " | sed -n ' s/.*__HTTP_CODE__:\([0-9]*\).*/\1/p' | tail -1) "
969+ [[ -n " ${http_code} " ]] || http_code=" 000"
970+ local resp_body
971+ resp_body=" $( printf ' %s' " ${resp} " | sed ' s/__HTTP_CODE__:[0-9]*$//' | sed ' /^$/d' ) "
949972 if [[ " ${http_code} " == " 200" || " ${http_code} " == " 302" ]]; then
950- ai_review::ok " --submit: posted a row to the metrics webhook (tab=${webhook_tab} , verdict=${verdict:- ?} , $( [ " ${thumbs_up} " = 1 ] && echo 👍 || echo 👎) )."
973+ ai_review::ok " Recorded a Testing Events row (tab=${webhook_tab} , verdict=${verdict:- ?} , $( [ " ${thumbs_up} " = 1 ] && echo 👍 || echo 👎) )."
951974 else
952- ai_review::warn " --submit: webhook POST failed (HTTP ${http_code} ; your answer was not recorded). Check METRICSAI_WEBHOOK_URL / METRICSAI_WEBHOOK_KEY." >&2
975+ # Loud, but non-fatal: the classification itself succeeded, so we still
976+ # exit 0. Surface the HTTP code AND the response body so the user can see
977+ # WHY it failed (e.g. no access to the sheet) — don't swallow it.
978+ ai_review::warn " Testing Events row was NOT recorded — webhook POST failed (HTTP ${http_code} )." >&2
979+ ai_review::warn " Your y/n answer was captured locally but did not reach the sheet." >&2
980+ [[ -n " ${resp_body} " ]] && ai_review::warn " Webhook response: ${resp_body} " >&2
981+ ai_review::warn " Check METRICSAI_WEBHOOK_URL / METRICSAI_WEBHOOK_KEY and that you have access to the target sheet." >&2
953982 fi
954983}
955984
@@ -1047,11 +1076,30 @@ test_classifier::run() {
10471076 ;;
10481077 esac
10491078
1079+ # ── Decide whether to capture the helpfulness signal (the Testing Events row).
1080+ # Historically this required the explicit --submit flag. Now it is the DEFAULT
1081+ # on any interactive run when the metricsai webhook is configured: if both
1082+ # METRICSAI_WEBHOOK_URL and METRICSAI_WEBHOOK_KEY are set AND we have a
1083+ # controlling terminal AND tests were actually classified, we prompt y/n and
1084+ # write the row — no --submit needed. --submit stays as an explicit opt-in
1085+ # (and is a harmless no-op when auto-submit already applies). CI / non-TTY runs
1086+ # never prompt (submit_metrics_row itself guards on /dev/tty). A failed write
1087+ # is surfaced loudly but never fails the run — the classification still stands.
1088+ local auto_submit=0
1089+ if [[ -n " ${METRICSAI_WEBHOOK_URL:- } " && -n " ${METRICSAI_WEBHOOK_KEY:- } " ]] \
1090+ && [[ " ${CI:- } " != " true" ]] && { : <> /dev/tty; } 2> /dev/null \
1091+ && [[ " ${result} " == " CLASSIFIED" ]]; then
1092+ auto_submit=1
1093+ fi
1094+ local do_submit=0
1095+ (( SUBMIT == 1 || auto_submit == 1 )) && do_submit=1
1096+
10501097 # ── Post ONE PR comment with the verdicts. ────────────────────────────────
10511098 # --post-comment is what CI passes to actually post; omit it for a local dry
10521099 # view (the JSON/report still prints to stdout). Nothing is posted when nothing
10531100 # was triaged (NO_ACTION). The comment carries the 👍/👎 reaction ask on a
1054- # CI/--post-comment run; a local --submit run omits it (signal via the prompt).
1101+ # CI/--post-comment run; when we capture the signal via the terminal prompt
1102+ # (--submit or auto-submit) the ask is omitted (the prompt is the signal).
10551103 if (( POST_COMMENT == 1 )) ; then
10561104 if [[ " ${result} " == " NO_ACTION" ]]; then
10571105 ai_review::info " Result is NO_ACTION — nothing to triage, so no PR comment is posted."
@@ -1069,20 +1117,26 @@ test_classifier::run() {
10691117 ai_review::log " <!-- AI_CLASSIFIER_JSON_END -->"
10701118 exit 1
10711119 fi
1072- # Include the 👍/👎 reaction ask UNLESS this is a local --submit run (which
1073- # already captures the signal via its terminal prompt ). CI / plain
1074- # --post-comment → ask (1); --submit → no ask (0). Both surfaces stay alive.
1120+ # Include the 👍/👎 reaction ask UNLESS we're capturing the signal via the
1121+ # terminal prompt (--submit or auto-submit ). CI / plain --post-comment →
1122+ # ask (1); prompt-capturing run → no ask (0). Both surfaces stay alive.
10751123 local want_reaction_ask=1
1076- (( SUBMIT == 1 )) && want_reaction_ask=0
1124+ (( do_submit == 1 )) && want_reaction_ask=0
10771125 local comment_body
10781126 comment_body=" $( render_pr_comment_body " ${json_block} " " ${want_reaction_ask} " ) "
10791127 post_comment_to_github " ${AI_REVIEW_PR_NUMBER} " " ${comment_body} "
1128+ fi
1129+ fi
10801130
1081- # --submit: prompt for the helpfulness signal and append a Testing Events
1082- # row (interactive only; no-ops cleanly in CI / non-TTY).
1083- if (( SUBMIT == 1 )) ; then
1084- submit_metrics_row " ${AI_REVIEW_PR_NUMBER} " " ${json_block} "
1085- fi
1131+ # ── Capture the helpfulness signal + write the Testing Events row. ─────────
1132+ # Runs on --submit OR auto-submit, INDEPENDENT of whether a comment was posted:
1133+ # a local report-only run with no PR still prompts and writes a row (with empty
1134+ # comment fields). Guarded internally on TTY + CLASSIFIED; no-ops in CI.
1135+ if (( do_submit == 1 )) && [[ " ${result} " == " CLASSIFIED" ]]; then
1136+ local json_block_submit
1137+ json_block_submit=" $( extract_classifier_json " ${classifier_output} " ) "
1138+ if [[ -n " ${json_block_submit} " ]]; then
1139+ submit_metrics_row " ${AI_REVIEW_PR_NUMBER:- } " " ${json_block_submit} "
10861140 fi
10871141 fi
10881142
0 commit comments