From 00b0d760c02edd670b968cc999e0e5f0409794c5 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:43:09 -0500 Subject: [PATCH 1/7] feat(standards): include GEMINI.md in agent configuration standard and audit --- .github/workflows/agent-shield-reusable.yml | 7 +++ GEMINI.md | 5 ++ scripts/compliance-audit.sh | 29 ++++++++- scripts/compliance-remediate.sh | 4 ++ standards/agent-standards.md | 13 +++- test/scripts/compliance-audit/gemini-md.bats | 62 +++++++++++++++++++ .../routing-messages.bats | 24 +++++++ 7 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 GEMINI.md create mode 100644 test/scripts/compliance-audit/gemini-md.bats diff --git a/.github/workflows/agent-shield-reusable.yml b/.github/workflows/agent-shield-reusable.yml index a8ed224cc..24210e4ff 100644 --- a/.github/workflows/agent-shield-reusable.yml +++ b/.github/workflows/agent-shield-reusable.yml @@ -32,6 +32,7 @@ on: type: string default: | CLAUDE.md + GEMINI.md AGENTS.md org-standards-ref: description: "String AGENTS.md must reference (regex, basic grep -E)" @@ -89,6 +90,12 @@ jobs: status=1 fi + if [ -f "GEMINI.md" ] && \ + ! grep -qi 'AGENTS.md' GEMINI.md; then + echo "::error file=GEMINI.md::Must reference AGENTS.md" + status=1 + fi + if [ -f "AGENTS.md" ] && \ ! grep -qiE "$ORG_REF" AGENTS.md; then echo "::error file=AGENTS.md::Must reference org standards ($ORG_REF)" diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 000000000..823b695c8 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,5 @@ +# GEMINI.md + +This repository follows the development and agentic architecture standards defined in [AGENTS.md](AGENTS.md) and [`combsmith/.github/AGENTS.md`](https://github.com/combsmith/.github/blob/main/AGENTS.md). + +Read [AGENTS.md](AGENTS.md) for full context, project conventions, testing guidelines, and architectural rules. diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index 544a9fe6d..b92eb9562 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -2433,6 +2433,32 @@ check_claude_md() { fi } +# --------------------------------------------------------------------------- +# Check: GEMINI.md exists and references AGENTS.md +# --------------------------------------------------------------------------- +check_gemini_md() { + local repo="$1" + + local content + content=$(gh_api "repos/$ORG/$repo/contents/GEMINI.md" --jq '.content' 2>/dev/null || echo "") + + if [ -z "$content" ]; then + add_finding "$repo" "standards" "missing-gemini-md" "error" \ + "Missing \`GEMINI.md\` — every repo must have a GEMINI.md that references AGENTS.md" \ + "AGENTS.md" + return + fi + + local decoded + decoded=$(echo "$content" | base64 -d 2>/dev/null || echo "") + + if ! echo "$decoded" | grep -qi 'AGENTS\.md'; then + add_finding "$repo" "standards" "gemini-md-missing-agents-ref" "error" \ + "\`GEMINI.md\` does not reference \`AGENTS.md\`" \ + "AGENTS.md" + fi +} + # --------------------------------------------------------------------------- # Check: AGENTS.md exists and references org .github/AGENTS.md # --------------------------------------------------------------------------- @@ -3018,7 +3044,7 @@ create_umbrella_issue() { "reusable-workflows|Reusable Workflows (disabled_manually)|gh workflow disable -R " "action-pinning|Action SHA Pinning|pin actions to SHA in each workflow file" "dependabot|Dependabot Configuration|per-repo .github/dependabot.yml" - "standards|Agent Standards (CLAUDE.md / AGENTS.md / copilot-setup-steps.yml)|per-repo doc and workflow additions" + "standards|Agent Standards (CLAUDE.md / GEMINI.md / AGENTS.md / copilot-setup-steps.yml)|per-repo doc and workflow additions" ) local body @@ -3613,6 +3639,7 @@ main() { check_verbatim_stubs "$repo" check_centralized_check_names "$repo" check_claude_md "$repo" + check_gemini_md "$repo" check_agents_md "$repo" check_copilot_setup_steps "$repo" check_copilot_instructions "$repo" diff --git a/scripts/compliance-remediate.sh b/scripts/compliance-remediate.sh index 1576c5653..b04b64836 100644 --- a/scripts/compliance-remediate.sh +++ b/scripts/compliance-remediate.sh @@ -874,6 +874,10 @@ remediate_finding() { report_skip "$repo" "$check" \ "CLAUDE.md creation/update requires content knowledge — trigger Claude agent on the finding issue" ;; + standards/missing-gemini-md | standards/gemini-md-missing-agents-ref) + report_skip "$repo" "$check" \ + "GEMINI.md creation/update requires content knowledge — trigger Claude agent on the finding issue" + ;; standards/missing-agents-md | standards/agents-md-missing-org-ref) report_skip "$repo" "$check" \ "AGENTS.md creation/update requires content knowledge — trigger Claude agent on the finding issue" diff --git a/standards/agent-standards.md b/standards/agent-standards.md index c9d465918..6635ad889 100644 --- a/standards/agent-standards.md +++ b/standards/agent-standards.md @@ -1,7 +1,7 @@ # Agent Configuration Standards Standards for repositories that use AI agent configurations (CLAUDE.md, -AGENTS.md, BMAD modules, Claude plugins, MCP server configs). +GEMINI.md, AGENTS.md, BMAD modules, Claude plugins, MCP server configs). --- @@ -12,6 +12,7 @@ Every repository MUST have: | File | Purpose | Compliance Check | |------|---------|-----------------| | `CLAUDE.md` | Project-level instructions for Claude Code | error if missing | +| `GEMINI.md` | Project-level instructions for Gemini CLI / Antigravity | error if missing | | `AGENTS.md` | Development standards for AI agents | error if missing | ## Compliance Exemptions — Files Agents Must Not Modify @@ -37,6 +38,13 @@ to all repos via the `@v1` tag bump. - MUST NOT contain overly permissive tool authorization (e.g., `dangerouslySkipPermissions`) - SHOULD define project-specific context (tech stack, conventions, key files) +### GEMINI.md Requirements + +- MUST reference `AGENTS.md` for development standards +- MUST NOT contain secrets, API keys, or credentials +- MUST NOT contain overly permissive tool authorization +- SHOULD reference the org-level standards (`combsmith/.github/AGENTS.md`) or project-specific context + ### AGENTS.md Requirements - MUST reference the org-level standards: `combsmith/.github/AGENTS.md` @@ -82,8 +90,9 @@ generic AgentShield scanner: | Rule | Severity | Description | |------|----------|-------------| -| `required-files` | error | CLAUDE.md and AGENTS.md must exist | +| `required-files` | error | CLAUDE.md, GEMINI.md, and AGENTS.md must exist | | `claude-reference` | error | CLAUDE.md must reference AGENTS.md | +| `gemini-reference` | error | GEMINI.md must reference AGENTS.md | | `org-reference` | error | AGENTS.md must reference `combsmith/.github/AGENTS.md` | | `valid-frontmatter` | error | All SKILL.md files must have YAML frontmatter with `name` and `description` | diff --git a/test/scripts/compliance-audit/gemini-md.bats b/test/scripts/compliance-audit/gemini-md.bats new file mode 100644 index 000000000..592d8536e --- /dev/null +++ b/test/scripts/compliance-audit/gemini-md.bats @@ -0,0 +1,62 @@ +#!/usr/bin/env bats +# Unit tests for check_gemini_md in scripts/compliance-audit.sh +# (standards/agent-standards.md). + +bats_require_minimum_version 1.5.0 + +REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" +SCRIPT="$REPO_ROOT/scripts/compliance-audit.sh" + +setup() { + TMPDIR_TEST="$(mktemp -d)" + export TMPDIR_TEST +} + +teardown() { + rm -rf "$TMPDIR_TEST" +} + +run_check_gemini_md() { + local repo="$1" gemini_body="$2" + bash -c ' + set -euo pipefail + export ORG="combsmith" + export OUTPUT_DIR="'"$TMPDIR_TEST"'" + source "'"$SCRIPT"'" + FINDINGS_FILE="$OUTPUT_DIR/findings.json" + echo "[]" > "$FINDINGS_FILE" + findings_count=0 + GEMINI_BODY="$2" + gh_api() { + if [ -z "$GEMINI_BODY" ]; then + return 1 + fi + printf "%s" "$GEMINI_BODY" | base64 | tr -d "\n" + printf "\n" + } + check_gemini_md "$1" + cat "$FINDINGS_FILE" + ' _ "$repo" "$gemini_body" +} + +@test "check_gemini_md: compliant GEMINI.md referencing AGENTS.md produces no finding" { + run run_check_gemini_md "incubator" "# GEMINI.md +See [AGENTS.md](AGENTS.md) and https://github.com/combsmith/.github/blob/main/AGENTS.md" + [ "$status" -eq 0 ] + [ "$output" = "[]" ] +} + +@test "check_gemini_md: missing GEMINI.md raises missing-gemini-md error" { + run run_check_gemini_md "incubator" "" + [ "$status" -eq 0 ] + [[ "$output" == *'"check": "missing-gemini-md"'* ]] + [[ "$output" == *'"severity": "error"'* ]] +} + +@test "check_gemini_md: GEMINI.md without AGENTS.md reference raises gemini-md-missing-agents-ref error" { + run run_check_gemini_md "incubator" "# GEMINI.md +Project instructions without referencing the required file." + [ "$status" -eq 0 ] + [[ "$output" == *'"check": "gemini-md-missing-agents-ref"'* ]] + [[ "$output" == *'"severity": "error"'* ]] +} diff --git a/test/scripts/compliance-remediate/routing-messages.bats b/test/scripts/compliance-remediate/routing-messages.bats index f8aa1de37..6a86a5cf2 100644 --- a/test/scripts/compliance-remediate/routing-messages.bats +++ b/test/scripts/compliance-remediate/routing-messages.bats @@ -160,3 +160,27 @@ teardown() { ! grep -q 'review manually' "$report_dir/skipped.md" } + +# --------------------------------------------------------------------------- +# standards/missing-gemini-md & standards/gemini-md-missing-agents-ref +# --------------------------------------------------------------------------- + +@test "missing-gemini-md and gemini-md-missing-agents-ref route to the content-agent path" { + for check_name in missing-gemini-md gemini-md-missing-agents-ref; do + findings="$(tt_write_finding "broodminder-export" "standards" "$check_name")" + report_dir="${TT_TMP}/report-${check_name}" + + GH_TOKEN=fake \ + FINDINGS_FILE="$findings" \ + REPORT_DIR="$report_dir" \ + DRY_RUN=false \ + run bash "$TT_SCRIPT" + + [ "$status" -eq 0 ] + + grep -q "$check_name" "$report_dir/skipped.md" + ! grep -q "$check_name" "$report_dir/remediation-report.md" + grep -q 'GEMINI.md creation/update requires content knowledge' "$report_dir/skipped.md" + ! grep -q 'review manually' "$report_dir/skipped.md" + done +} From e491f62023915dbcf44a79536e37506460c5b255 Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:46:20 +0000 Subject: [PATCH 2/7] fix(bot): hardening GEMINI.md reference checks and API error handling - Workflow: Use fixed-string matching (grep -qFi) for AGENTS.md check to prevent unintended pattern matches - Compliance audit: Distinguish HTTP 404 (missing file) from transient API failures (rate-limit, network errors) for CLAUDE.md and GEMINI.md checks; mark repo inconclusive for non-404 failures instead of adding findings Co-Authored-By: Claude Haiku 4.5 --- .github/workflows/agent-shield-reusable.yml | 2 +- scripts/compliance-audit.sh | 56 +++++++++++++++------ 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/.github/workflows/agent-shield-reusable.yml b/.github/workflows/agent-shield-reusable.yml index 24210e4ff..1d768549a 100644 --- a/.github/workflows/agent-shield-reusable.yml +++ b/.github/workflows/agent-shield-reusable.yml @@ -91,7 +91,7 @@ jobs: fi if [ -f "GEMINI.md" ] && \ - ! grep -qi 'AGENTS.md' GEMINI.md; then + ! grep -qFi 'AGENTS.md' GEMINI.md; then echo "::error file=GEMINI.md::Must reference AGENTS.md" status=1 fi diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index e3125f5fa..d73441cdf 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -2457,18 +2457,32 @@ check_centralized_check_names() { check_claude_md() { local repo="$1" - local content - content=$(gh_api "repos/$ORG/$repo/contents/CLAUDE.md" --jq '.content' 2>/dev/null || echo "") + local content rc raw i + for i in 1 2 3; do + raw=$(gh api "repos/$ORG/$repo/contents/CLAUDE.md" 2>&1) && rc=0 || rc=$? + [ "$rc" -eq 0 ] && break + response_is_http_404 "$raw" && break + [ "$i" -lt 3 ] && sleep $((i * 2)) + done - if [ -z "$content" ]; then - add_finding "$repo" "standards" "missing-claude-md" "error" \ - "Missing \`CLAUDE.md\` — every repo must have a CLAUDE.md that references AGENTS.md" \ - "AGENTS.md" + if [ "$rc" -ne 0 ]; then + if response_is_http_404 "$raw"; then + add_finding "$repo" "standards" "missing-claude-md" "error" \ + "Missing \`CLAUDE.md\` — every repo must have a CLAUDE.md that references AGENTS.md" \ + "AGENTS.md" + else + info "CLAUDE.md not readable for $repo (not a 404) — marking the repo inconclusive" + mark_repo_inconclusive "$repo" + fi return fi + content=$(printf '%s' "$raw" | jq -r '.content? // empty' 2>/dev/null || echo "") + [ -n "$content" ] || return + local decoded - decoded=$(echo "$content" | base64 -d 2>/dev/null || echo "") + decoded=$(printf '%s' "$content" | base64 -d 2>/dev/null) || return + [ -n "$decoded" ] || return if ! echo "$decoded" | grep -qi 'AGENTS\.md'; then add_finding "$repo" "standards" "claude-md-missing-agents-ref" "error" \ @@ -2483,18 +2497,32 @@ check_claude_md() { check_gemini_md() { local repo="$1" - local content - content=$(gh_api "repos/$ORG/$repo/contents/GEMINI.md" --jq '.content' 2>/dev/null || echo "") + local content rc raw i + for i in 1 2 3; do + raw=$(gh api "repos/$ORG/$repo/contents/GEMINI.md" 2>&1) && rc=0 || rc=$? + [ "$rc" -eq 0 ] && break + response_is_http_404 "$raw" && break + [ "$i" -lt 3 ] && sleep $((i * 2)) + done - if [ -z "$content" ]; then - add_finding "$repo" "standards" "missing-gemini-md" "error" \ - "Missing \`GEMINI.md\` — every repo must have a GEMINI.md that references AGENTS.md" \ - "AGENTS.md" + if [ "$rc" -ne 0 ]; then + if response_is_http_404 "$raw"; then + add_finding "$repo" "standards" "missing-gemini-md" "error" \ + "Missing \`GEMINI.md\` — every repo must have a GEMINI.md that references AGENTS.md" \ + "AGENTS.md" + else + info "GEMINI.md not readable for $repo (not a 404) — marking the repo inconclusive" + mark_repo_inconclusive "$repo" + fi return fi + content=$(printf '%s' "$raw" | jq -r '.content? // empty' 2>/dev/null || echo "") + [ -n "$content" ] || return + local decoded - decoded=$(echo "$content" | base64 -d 2>/dev/null || echo "") + decoded=$(printf '%s' "$content" | base64 -d 2>/dev/null) || return + [ -n "$decoded" ] || return if ! echo "$decoded" | grep -qi 'AGENTS\.md'; then add_finding "$repo" "standards" "gemini-md-missing-agents-ref" "error" \ From cb85abdcf7305a7e034b6fab5a4f13edfa6a9f50 Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:56:38 +0000 Subject: [PATCH 3/7] fix(reviews): extract AGENTS.md xref verdict, handle empty agent files, stub gh in GEMINI tests Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/agent-shield-reusable.yml | 31 +++++++++----- .github/workflows/compliance-audit-tests.yml | 3 ++ scripts/compliance-audit.sh | 40 ++++++++++++++----- scripts/lib/agent-md-xref.sh | 17 ++++++++ .../compliance-audit/agent-md-xref.bats | 35 ++++++++++++++++ test/scripts/compliance-audit/gemini-md.bats | 39 +++++++++++++----- 6 files changed, 135 insertions(+), 30 deletions(-) create mode 100644 scripts/lib/agent-md-xref.sh create mode 100644 test/scripts/compliance-audit/agent-md-xref.bats diff --git a/.github/workflows/agent-shield-reusable.yml b/.github/workflows/agent-shield-reusable.yml index 1d768549a..497f075c4 100644 --- a/.github/workflows/agent-shield-reusable.yml +++ b/.github/workflows/agent-shield-reusable.yml @@ -78,23 +78,32 @@ jobs: done <<< "$REQUIRED_FILES" exit $status + # The AGENTS.md reference verdict lives in scripts/lib/agent-md-xref.sh + # (bats-tested; shared with the fleet compliance audit). Fetch just that + # file from the org repo; this step is thin glue around it. + - name: Fetch cross-reference helper + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p "$RUNNER_TEMP/org-lib" + gh api -H 'Accept: application/vnd.github.raw+json' \ + repos/combsmith/.github/contents/scripts/lib/agent-md-xref.sh \ + > "$RUNNER_TEMP/org-lib/agent-md-xref.sh" + - name: Validate cross-references env: ORG_REF: ${{ inputs.org-standards-ref }} run: | + # shellcheck source=/dev/null + . "$RUNNER_TEMP/org-lib/agent-md-xref.sh" status=0 - if [ -f "CLAUDE.md" ] && \ - ! grep -qi 'AGENTS.md' CLAUDE.md; then - echo "::error file=CLAUDE.md::Must reference AGENTS.md" - status=1 - fi - - if [ -f "GEMINI.md" ] && \ - ! grep -qFi 'AGENTS.md' GEMINI.md; then - echo "::error file=GEMINI.md::Must reference AGENTS.md" - status=1 - fi + for f in CLAUDE.md GEMINI.md; do + if [ -f "$f" ] && ! agent_md_references_agents < "$f"; then + echo "::error file=$f::Must reference AGENTS.md" + status=1 + fi + done if [ -f "AGENTS.md" ] && \ ! grep -qiE "$ORG_REF" AGENTS.md; then diff --git a/.github/workflows/compliance-audit-tests.yml b/.github/workflows/compliance-audit-tests.yml index dd27a0089..7e37659d4 100644 --- a/.github/workflows/compliance-audit-tests.yml +++ b/.github/workflows/compliance-audit-tests.yml @@ -3,6 +3,7 @@ # # Triggered on any PR that touches: # - scripts/compliance-audit.sh +# - .github/workflows/agent-shield-reusable.yml (cross-reference helper wiring) # - test/scripts/compliance-audit/** # - .github/workflows/compliance-audit-and-improvement.yml # - test/workflows/compliance-audit/** @@ -21,6 +22,7 @@ on: pull_request: paths: - 'scripts/compliance-audit.sh' + - '.github/workflows/agent-shield-reusable.yml' - 'scripts/lib/**' - 'test/scripts/compliance-audit/**' - '.github/workflows/compliance-audit-and-improvement.yml' @@ -32,6 +34,7 @@ on: - main paths: - 'scripts/compliance-audit.sh' + - '.github/workflows/agent-shield-reusable.yml' - 'scripts/lib/**' - 'test/scripts/compliance-audit/**' - '.github/workflows/compliance-audit-and-improvement.yml' diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index d73441cdf..8b28dc2b5 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -281,6 +281,10 @@ RULESETS_SRC_DIR="${RULESETS_SRC_DIR:-$SCRIPT_DIR/../standards/rulesets}" # shellcheck source=lib/push-protection.sh . "$SCRIPT_DIR/lib/push-protection.sh" +# Shared agent-file cross-reference verdict (also used by agent-shield-reusable.yml). +# shellcheck source=lib/agent-md-xref.sh +. "$SCRIPT_DIR/lib/agent-md-xref.sh" + # Shared label source of truth — STANDARD_LABEL_SPECS (fixed set) and # persona_opt_out_label_configs (derived :hands-off family). Sourced after # warn() is defined so the library uses this script's logging helper (issue #1139). @@ -2477,14 +2481,22 @@ check_claude_md() { return fi + # An empty file is a successful API response with empty content: it is + # non-compliant (no AGENTS.md reference), not a reason to abort the audit. + # Undecodable content is inconclusive. Neither may fall through to a bare + # `return` carrying a failed test status, which would trip `set -e`. content=$(printf '%s' "$raw" | jq -r '.content? // empty' 2>/dev/null || echo "") - [ -n "$content" ] || return - local decoded - decoded=$(printf '%s' "$content" | base64 -d 2>/dev/null) || return - [ -n "$decoded" ] || return + local decoded="" + if [ -n "$content" ]; then + if ! decoded=$(printf '%s' "$content" | base64 -d 2>/dev/null); then + info "CLAUDE.md content not decodable for $repo — marking the repo inconclusive" + mark_repo_inconclusive "$repo" + return 0 + fi + fi - if ! echo "$decoded" | grep -qi 'AGENTS\.md'; then + if ! printf '%s' "$decoded" | agent_md_references_agents; then add_finding "$repo" "standards" "claude-md-missing-agents-ref" "error" \ "\`CLAUDE.md\` does not reference \`AGENTS.md\`" \ "AGENTS.md" @@ -2517,14 +2529,22 @@ check_gemini_md() { return fi + # An empty file is a successful API response with empty content: it is + # non-compliant (no AGENTS.md reference), not a reason to abort the audit. + # Undecodable content is inconclusive. Neither may fall through to a bare + # `return` carrying a failed test status, which would trip `set -e`. content=$(printf '%s' "$raw" | jq -r '.content? // empty' 2>/dev/null || echo "") - [ -n "$content" ] || return - local decoded - decoded=$(printf '%s' "$content" | base64 -d 2>/dev/null) || return - [ -n "$decoded" ] || return + local decoded="" + if [ -n "$content" ]; then + if ! decoded=$(printf '%s' "$content" | base64 -d 2>/dev/null); then + info "GEMINI.md content not decodable for $repo — marking the repo inconclusive" + mark_repo_inconclusive "$repo" + return 0 + fi + fi - if ! echo "$decoded" | grep -qi 'AGENTS\.md'; then + if ! printf '%s' "$decoded" | agent_md_references_agents; then add_finding "$repo" "standards" "gemini-md-missing-agents-ref" "error" \ "\`GEMINI.md\` does not reference \`AGENTS.md\`" \ "AGENTS.md" diff --git a/scripts/lib/agent-md-xref.sh b/scripts/lib/agent-md-xref.sh new file mode 100644 index 000000000..77566097f --- /dev/null +++ b/scripts/lib/agent-md-xref.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# agent-md-xref.sh — pure verdict for the agent-file cross-reference rule. +# +# Sourced by scripts/compliance-audit.sh (check_claude_md / check_gemini_md) +# and fetched by .github/workflows/agent-shield-reusable.yml ("Validate +# cross-references"), so the fleet audit and the per-repo CI gate share ONE +# decision. Bats coverage: test/scripts/compliance-audit/agent-md-xref.bats. +# +# agent_md_references_agents — reads a file body on stdin; exit 0 when it +# contains the literal filename AGENTS.md +# (case-insensitive, fixed-string match, so +# "AGENTS-md" or "AGENTSxmd" never satisfy it). +# Empty input is non-compliant (exit 1). + +agent_md_references_agents() { + grep -qFi 'AGENTS.md' +} diff --git a/test/scripts/compliance-audit/agent-md-xref.bats b/test/scripts/compliance-audit/agent-md-xref.bats new file mode 100644 index 000000000..d61a827c1 --- /dev/null +++ b/test/scripts/compliance-audit/agent-md-xref.bats @@ -0,0 +1,35 @@ +#!/usr/bin/env bats +# Unit tests for scripts/lib/agent-md-xref.sh — the shared AGENTS.md +# cross-reference verdict used by the fleet audit and agent-shield-reusable.yml. + +REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" +LIB="$REPO_ROOT/scripts/lib/agent-md-xref.sh" +WORKFLOW="$REPO_ROOT/.github/workflows/agent-shield-reusable.yml" + +setup() { source "$LIB"; } + +@test "xref: body naming AGENTS.md passes" { + run agent_md_references_agents <<< "See [AGENTS.md](AGENTS.md)." + [ "$status" -eq 0 ] +} + +@test "xref: match is case-insensitive" { + run agent_md_references_agents <<< "see agents.md" + [ "$status" -eq 0 ] +} + +@test "xref: punctuation cannot stand in for the dot (fixed-string match)" { + run agent_md_references_agents <<< "AGENTSxmd AGENTS-md" + [ "$status" -ne 0 ] +} + +@test "xref: empty body fails" { + run agent_md_references_agents < /dev/null + [ "$status" -ne 0 ] +} + +@test "reusable workflow delegates the verdict to the helper for CLAUDE.md and GEMINI.md" { + grep -q 'agent_md_references_agents' "$WORKFLOW" + grep -q 'scripts/lib/agent-md-xref.sh' "$WORKFLOW" + ! grep -q "grep -qFi 'AGENTS.md'" "$WORKFLOW" +} diff --git a/test/scripts/compliance-audit/gemini-md.bats b/test/scripts/compliance-audit/gemini-md.bats index 592d8536e..95ee56e71 100644 --- a/test/scripts/compliance-audit/gemini-md.bats +++ b/test/scripts/compliance-audit/gemini-md.bats @@ -16,27 +16,35 @@ teardown() { rm -rf "$TMPDIR_TEST" } +# Stubs the `gh` CLI (the real entry point of check_gemini_md) so no network is +# touched. GEMINI_MODE: "ok" returns a Contents API JSON body, "404" fails with +# an HTTP 404, "500" fails with a non-404 error. The sleep between retries is +# stubbed out so the non-404 path is fast. run_check_gemini_md() { - local repo="$1" gemini_body="$2" + local repo="$1" gemini_body="$2" mode="${3:-ok}" bash -c ' set -euo pipefail export ORG="combsmith" export OUTPUT_DIR="'"$TMPDIR_TEST"'" source "'"$SCRIPT"'" FINDINGS_FILE="$OUTPUT_DIR/findings.json" + INCONCLUSIVE_REPOS_FILE="$OUTPUT_DIR/inconclusive.txt" echo "[]" > "$FINDINGS_FILE" findings_count=0 GEMINI_BODY="$2" - gh_api() { - if [ -z "$GEMINI_BODY" ]; then - return 1 - fi - printf "%s" "$GEMINI_BODY" | base64 | tr -d "\n" - printf "\n" + GEMINI_MODE="$3" + sleep() { :; } + gh() { + case "$GEMINI_MODE" in + 404) echo "gh: Not Found (HTTP 404)" >&2; return 1 ;; + 500) echo "gh: Server Error (HTTP 500)" >&2; return 1 ;; + esac + printf "{\"content\":\"%s\"}" "$(printf "%s" "$GEMINI_BODY" | base64 | tr -d "\n")" } check_gemini_md "$1" cat "$FINDINGS_FILE" - ' _ "$repo" "$gemini_body" + if [ -f "$INCONCLUSIVE_REPOS_FILE" ]; then echo "INCONCLUSIVE: $(cat "$INCONCLUSIVE_REPOS_FILE")"; fi + ' _ "$repo" "$gemini_body" "$mode" } @test "check_gemini_md: compliant GEMINI.md referencing AGENTS.md produces no finding" { @@ -47,7 +55,7 @@ See [AGENTS.md](AGENTS.md) and https://github.com/combsmith/.github/blob/main/AG } @test "check_gemini_md: missing GEMINI.md raises missing-gemini-md error" { - run run_check_gemini_md "incubator" "" + run run_check_gemini_md "incubator" "" 404 [ "$status" -eq 0 ] [[ "$output" == *'"check": "missing-gemini-md"'* ]] [[ "$output" == *'"severity": "error"'* ]] @@ -60,3 +68,16 @@ Project instructions without referencing the required file." [[ "$output" == *'"check": "gemini-md-missing-agents-ref"'* ]] [[ "$output" == *'"severity": "error"'* ]] } + +@test "check_gemini_md: empty GEMINI.md raises gemini-md-missing-agents-ref and does not abort" { + run run_check_gemini_md "incubator" "" + [ "$status" -eq 0 ] + [[ "$output" == *'"check": "gemini-md-missing-agents-ref"'* ]] +} + +@test "check_gemini_md: non-404 API failure marks the repo inconclusive with no finding" { + run run_check_gemini_md "incubator" "" 500 + [ "$status" -eq 0 ] + [[ "$output" == *"INCONCLUSIVE: incubator"* ]] + [[ "$output" != *"missing-gemini-md"* ]] +} From 68150abe3b2a4659eda67aa9ad562912949feb9e Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:11:47 +0000 Subject: [PATCH 4/7] fix(reviews): pin xref helper to reusable's commit; drain stdin to avoid SIGPIPE false finding Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/agent-shield-reusable.yml | 5 ++++- scripts/lib/agent-md-xref.sh | 4 +++- test/scripts/compliance-audit/agent-md-xref.bats | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/agent-shield-reusable.yml b/.github/workflows/agent-shield-reusable.yml index 497f075c4..80fbcefd9 100644 --- a/.github/workflows/agent-shield-reusable.yml +++ b/.github/workflows/agent-shield-reusable.yml @@ -84,10 +84,13 @@ jobs: - name: Fetch cross-reference helper env: GH_TOKEN: ${{ github.token }} + # The reusable's own commit (job.workflow_sha), so the helper + # is versioned, promoted and rolled back together with this file. + HELPER_REF: ${{ job.workflow_sha }} run: | mkdir -p "$RUNNER_TEMP/org-lib" gh api -H 'Accept: application/vnd.github.raw+json' \ - repos/combsmith/.github/contents/scripts/lib/agent-md-xref.sh \ + "repos/combsmith/.github/contents/scripts/lib/agent-md-xref.sh?ref=${HELPER_REF}" \ > "$RUNNER_TEMP/org-lib/agent-md-xref.sh" - name: Validate cross-references diff --git a/scripts/lib/agent-md-xref.sh b/scripts/lib/agent-md-xref.sh index 77566097f..3533eaf91 100644 --- a/scripts/lib/agent-md-xref.sh +++ b/scripts/lib/agent-md-xref.sh @@ -13,5 +13,7 @@ # Empty input is non-compliant (exit 1). agent_md_references_agents() { - grep -qFi 'AGENTS.md' + # No -q: grep must drain stdin, or a piped writer gets SIGPIPE and pipefail + # turns a successful early match into a failure. + grep -Fi 'AGENTS.md' > /dev/null } diff --git a/test/scripts/compliance-audit/agent-md-xref.bats b/test/scripts/compliance-audit/agent-md-xref.bats index d61a827c1..ae59bcf5c 100644 --- a/test/scripts/compliance-audit/agent-md-xref.bats +++ b/test/scripts/compliance-audit/agent-md-xref.bats @@ -33,3 +33,8 @@ setup() { source "$LIB"; } grep -q 'scripts/lib/agent-md-xref.sh' "$WORKFLOW" ! grep -q "grep -qFi 'AGENTS.md'" "$WORKFLOW" } + +@test "agent_md_references_agents: early match on large piped input survives pipefail" { + run bash -c 'set -o pipefail; . "$0"; { echo "See AGENTS.md"; head -c 131072 /dev/zero | tr "\0" x; } | agent_md_references_agents' "$LIB" + [ "$status" -eq 0 ] +} From cc5903c00d6ad2ab019822420443bcceae558ff6 Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:45:27 +0000 Subject: [PATCH 5/7] fix(bot): properly pass ref parameter to helper fetch; avoid SIGPIPE in grep pipeline - Use -F flag for gh api ref parameter to ensure correct URL encoding and parameter handling when fetching the agent-md-xref.sh helper from the pinned reusable commit - Replace pipe-based stdin with here-string in agent_md_references_agents to prevent SIGPIPE failures when grep matches early in large files Co-Authored-By: Claude Haiku 4.5 --- .github/workflows/agent-shield-reusable.yml | 3 ++- scripts/lib/agent-md-xref.sh | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/agent-shield-reusable.yml b/.github/workflows/agent-shield-reusable.yml index 80fbcefd9..91491c932 100644 --- a/.github/workflows/agent-shield-reusable.yml +++ b/.github/workflows/agent-shield-reusable.yml @@ -90,7 +90,8 @@ jobs: run: | mkdir -p "$RUNNER_TEMP/org-lib" gh api -H 'Accept: application/vnd.github.raw+json' \ - "repos/combsmith/.github/contents/scripts/lib/agent-md-xref.sh?ref=${HELPER_REF}" \ + "repos/combsmith/.github/contents/scripts/lib/agent-md-xref.sh" \ + -F ref="${HELPER_REF}" \ > "$RUNNER_TEMP/org-lib/agent-md-xref.sh" - name: Validate cross-references diff --git a/scripts/lib/agent-md-xref.sh b/scripts/lib/agent-md-xref.sh index 3533eaf91..b2832505c 100644 --- a/scripts/lib/agent-md-xref.sh +++ b/scripts/lib/agent-md-xref.sh @@ -13,7 +13,8 @@ # Empty input is non-compliant (exit 1). agent_md_references_agents() { - # No -q: grep must drain stdin, or a piped writer gets SIGPIPE and pipefail - # turns a successful early match into a failure. - grep -Fi 'AGENTS.md' > /dev/null + # Use here-string redirection to avoid SIGPIPE when grep matches early + # in large files. This allows the verdict logic to work correctly with + # pipefail enabled in compliance-audit.sh. + grep -Fi 'AGENTS.md' <<< "$(cat)" > /dev/null } From 4a9450164c28defcb4346c42eb3c7da7cc9e965f Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:54:39 +0000 Subject: [PATCH 6/7] fix(bot): use local file instead of API fetch for cross-reference helper The workflow was trying to fetch agent-md-xref.sh from the GitHub API at the PR merge commit SHA, which returns 404 because that commit doesn't exist on the main branch yet. Since the file is already checked out by actions/checkout, copy it from the local workspace instead. Co-Authored-By: Claude Haiku 4.5 --- .github/workflows/agent-shield-reusable.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/agent-shield-reusable.yml b/.github/workflows/agent-shield-reusable.yml index 91491c932..4fb06102d 100644 --- a/.github/workflows/agent-shield-reusable.yml +++ b/.github/workflows/agent-shield-reusable.yml @@ -79,20 +79,12 @@ jobs: exit $status # The AGENTS.md reference verdict lives in scripts/lib/agent-md-xref.sh - # (bats-tested; shared with the fleet compliance audit). Fetch just that - # file from the org repo; this step is thin glue around it. + # (bats-tested; shared with the fleet compliance audit). Copy the file + # from the already-checked-out workspace to a temp location. - name: Fetch cross-reference helper - env: - GH_TOKEN: ${{ github.token }} - # The reusable's own commit (job.workflow_sha), so the helper - # is versioned, promoted and rolled back together with this file. - HELPER_REF: ${{ job.workflow_sha }} run: | mkdir -p "$RUNNER_TEMP/org-lib" - gh api -H 'Accept: application/vnd.github.raw+json' \ - "repos/combsmith/.github/contents/scripts/lib/agent-md-xref.sh" \ - -F ref="${HELPER_REF}" \ - > "$RUNNER_TEMP/org-lib/agent-md-xref.sh" + cp scripts/lib/agent-md-xref.sh "$RUNNER_TEMP/org-lib/agent-md-xref.sh" - name: Validate cross-references env: From ca778d744f027473bf08d44840b74162377cc33a Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:07:04 +0000 Subject: [PATCH 7/7] fix(bot): load helper script from org repo for cross-repository consumers For consumers using standards/workflows/agent-shield.yml from combsmith/.github, the first checkout contains the caller's repo, not the org repo. This makes scripts/lib/agent-md-xref.sh unavailable unless the consumer also includes that file. Add a separate sparse checkout of combsmith/.github to ensure the helper is always available, with a fallback to the caller's copy if it exists. Co-Authored-By: Claude Haiku 4.5 --- .github/workflows/agent-shield-reusable.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/agent-shield-reusable.yml b/.github/workflows/agent-shield-reusable.yml index 4fb06102d..1eee4f040 100644 --- a/.github/workflows/agent-shield-reusable.yml +++ b/.github/workflows/agent-shield-reusable.yml @@ -50,6 +50,16 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # Fetch the org's agent-md-xref helper for cross-repository consumers. + # External consumers won't have scripts/lib/agent-md-xref.sh in their repo, + # so we fetch it from combsmith/.github with sparse-checkout. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: combsmith/.github + path: __org-github + sparse-checkout: scripts/lib/agent-md-xref.sh + sparse-checkout-cone-mode: false + # --- Deep security scan via AgentShield CLI --- # Uses ecc-agentshield (https://github.com/affaan-m/agentshield) # 102 rules: secrets, permissions, hooks, MCP servers, agent config @@ -80,11 +90,15 @@ jobs: # The AGENTS.md reference verdict lives in scripts/lib/agent-md-xref.sh # (bats-tested; shared with the fleet compliance audit). Copy the file - # from the already-checked-out workspace to a temp location. + # from either the caller's repo or the org-github checkout. - name: Fetch cross-reference helper run: | mkdir -p "$RUNNER_TEMP/org-lib" - cp scripts/lib/agent-md-xref.sh "$RUNNER_TEMP/org-lib/agent-md-xref.sh" + if [ -f "scripts/lib/agent-md-xref.sh" ]; then + cp "scripts/lib/agent-md-xref.sh" "$RUNNER_TEMP/org-lib/agent-md-xref.sh" + else + cp "__org-github/scripts/lib/agent-md-xref.sh" "$RUNNER_TEMP/org-lib/agent-md-xref.sh" + fi - name: Validate cross-references env: