From 8a1d71c4e9c15f0b173cbee47ae7529d4e2d765e Mon Sep 17 00:00:00 2001 From: Evelyn Chen Date: Thu, 27 Aug 2026 16:55:08 +0000 Subject: [PATCH] Enforce provenance on every agent change Authored-By: Codex (GPT-5) --- README.md | 16 ++++++++++------ action.yaml | 2 +- check.sh | 20 +++++++++++++++----- tests/run | 26 ++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e42471e..87f7997 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Title: [bug-x7k2m9] Fix terminal resize loss (chore PRs: no [bug-id] prefix - `` is `bug-` followed by lowercase alphanumeric characters - `` is lowercase alphanumeric with hyphens, starting with a letter or digit - When both the branch and the title carry a bug-id they **must match** +- Titles are plain-language imperatives without Conventional Commit syntax - Break-glass: PRs authored by `dfritz` are exempt from all checks ## Usage @@ -57,7 +58,7 @@ job — the step still fails, but the job cannot block the PR. ## Behavior -The check validates four things for a Bug-backed Agent PR: +The check validates four things for every non-break-glass Agent PR: 1. **Branch form** — must be `/bug-/` or `chore/`. Unknown roles, missing `bug-` segment, uppercase bug-ids, and empty @@ -65,14 +66,17 @@ The check validates four things for a Bug-backed Agent PR: 2. **Title form** — for a `role/bug` branch the title must start with `[bug-] `. For a `chore` branch the title must have no `[bug-id]` - prefix. If both carry a bug-id they must match. + prefix. If both carry a bug-id they must match. Neither form accepts + Conventional Commit syntax. -3. **Tracking and attribution** — `## Tracking` contains both the literal +3. **Tracking and attribution** — on Bug-backed work, `## Tracking` contains both the literal `Fixes bug-` automation token and the matching navigable - `https://agenthub.fritzlab.net/bug-` URL. A separate `## Attribution` - section contains the canonical `Authored-By` product/model watermark. + `https://agenthub.fritzlab.net/bug-` URL. Every PR has a separate + `## Attribution` section containing the canonical `Authored-By` + product/model watermark. -4. **Commit attribution** — every commit in `base-sha..head-sha` ends with +4. **Commit attribution** — every commit in `base-sha..head-sha`, including + commits on `chore/` branches, ends with the canonical `Authored-By` trailer, separated from the message body by a blank line. The naming job must check out full history before this action. diff --git a/action.yaml b/action.yaml index 0e5c2f8..00cfe95 100644 --- a/action.yaml +++ b/action.yaml @@ -8,7 +8,7 @@ description: | Standard: branch: /bug-/ e.g. dev/bug-x7k2m9/fix-terminal-resize chore: chore/ bug-less trivia only - title: [bug-] Description (chore PRs: no [bug-id] prefix) + title: [bug-] Description (chore PRs: no [bug-id] prefix; no Conventional Commit syntax) Role must be a roster handle: dev | ux | ops | security | perf | architect | support Break-glass: PRs authored by dfritz are exempt. diff --git a/check.sh b/check.sh index e8d39c1..ee756fe 100755 --- a/check.sh +++ b/check.sh @@ -17,7 +17,7 @@ VISIBLE_BODY=$(printf '%s\n' "${BODY}" | awk ' { line=$0 fence_pos=1 - while (fence_pos <= 4 && substr(line, fence_pos, 1) == " ") fence_pos++ + while (fence_pos <= 3 && substr(line, fence_pos, 1) == " ") fence_pos++ fence_char=substr(line, fence_pos, 1) fence_run=0 if (fence_char == "`" || fence_char == "~") { @@ -58,7 +58,8 @@ VISIBLE_BODY=$(printf '%s\n' "${BODY}" | awk ' close_pos=pos + ticks found_close=0 while (close_pos <= length(line)) { - if (substr(line, close_pos, ticks) == substr(line, pos, ticks) && + if (substr(line, close_pos - 1, 1) != "`" && + substr(line, close_pos, ticks) == substr(line, pos, ticks) && substr(line, close_pos + ticks, 1) != "`") { found_close=1 break @@ -116,6 +117,12 @@ if echo "${TITLE}" | grep -qE "^\[bug-[a-z0-9]+\] ."; then TITLE_BUG=$(echo "${TITLE}" | sed -E 's|^\[(bug-[a-z0-9]+)\].*|\1|') fi +PLAIN_TITLE=$(printf '%s\n' "${TITLE}" | sed -E 's/^\[bug-[a-z0-9]+\][[:space:]]+//') +if printf '%s\n' "${PLAIN_TITLE}" | grep -qE '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([^)]*\))?!?:[[:space:]]'; then + echo "FAIL[check-naming]: title must be a plain-language imperative without Conventional Commit syntax" + FAILED=1 +fi + if [ "${BRANCH_KIND}" = "role-bug" ]; then if [ -z "${TITLE_BUG}" ]; then echo "FAIL[check-naming]: title must lead with [${BRANCH_BUG}]" @@ -146,6 +153,12 @@ if [ "${BRANCH_KIND}" = "role-bug" ]; then fi fi +elif [ "${BRANCH_KIND}" = "chore" ] && [ -n "${TITLE_BUG}" ]; then + echo "FAIL[check-naming]: chore branch should not carry a [bug-id] title prefix" + FAILED=1 +fi + +if [ "${BRANCH_KIND}" != "invalid" ]; then attribution=$(printf '%s\n' "${VISIBLE_BODY}" | awk ' /^## Attribution[[:space:]]*$/ { in_section=1; next } /^## / && in_section { exit } @@ -192,9 +205,6 @@ if [ "${BRANCH_KIND}" = "role-bug" ]; then echo "FAIL[check-naming]: base-sha and head-sha must be lowercase hexadecimal commit SHAs" FAILED=1 fi -elif [ "${BRANCH_KIND}" = "chore" ] && [ -n "${TITLE_BUG}" ]; then - echo "FAIL[check-naming]: chore branch should not carry a [bug-id] title prefix" - FAILED=1 fi if [ "${FAILED}" -eq 0 ]; then diff --git a/tests/run b/tests/run index bfb7df1..2085280 100755 --- a/tests/run +++ b/tests/run @@ -73,6 +73,22 @@ check_contract_pass() { fi } +check_chore_contract() { + local desc="$1" body="$2" base="$3" head="$4" diagnostic="$5" + local out rc=0 + out=$(cd "${FIXTURES}" && HEAD_BRANCH="chore/contract" \ + PR_TITLE="Improve delivery contract" PR_AUTHOR="dev" \ + PR_BODY="$body" BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$? + if [ "$rc" -eq 1 ] && printf '%s\n' "$out" | grep -Fq "$diagnostic"; then + echo "PASS [$desc]" + PASS=$((PASS + 1)) + else + echo "FAIL [$desc]: expected exit=1 and diagnostic '$diagnostic', got exit=$rc" + echo " output: $out" + FAIL=$((FAIL + 1)) + fi +} + # break-glass check "dfritz exempt — invalid branch" 0 "totally/wrong-branch" "no prefix" "dfritz" check "dfritz exempt — mismatch" 0 "dev/bug-abc/thing" "[bug-xyz] Thing" "dfritz" @@ -98,6 +114,8 @@ check "bug-id mismatch" 1 "dev/bug-x7k2m9/fix-resize" "[bug- # fail: chore branch with [bug-id] title prefix check "chore with bug-id title" 1 "chore/bump-deps" "[bug-abc123] Bump deps" "dev" +check "role/bug Conventional title" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix(hub): fix resize" "dev" +check "chore Conventional title" 1 "chore/bump-deps" "chore(deps): bump deps" "dev" # fail: invalid branch forms check "invalid — no role prefix" 1 "feature/foo-bar" "Add feature" "dev" @@ -126,6 +144,12 @@ check_contract "hidden provenance does not satisfy the visible contract" \ check_contract "comment removal cannot synthesize section headings" \ $'## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) ' \ "$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section" +check_contract "four-space indented backticks do not expose comments" \ + $' ````\n' \ + "$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section" +check_contract "unequal backtick runs do not expose comments" \ + $'`' \ + "$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section" check_contract_pass "inline code containing comment opener stays visible" \ $'Use `