diff --git a/.github/workflows/agent-shield-reusable.yml b/.github/workflows/agent-shield-reusable.yml index c9c84fdb1..49fd19236 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)" @@ -49,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 @@ -121,17 +132,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). Copy the file + # from either the caller's repo or the org-github checkout. + - name: Fetch cross-reference helper + run: | + mkdir -p "$RUNNER_TEMP/org-lib" + 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: 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 + 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 57a30fde0..bf85da4d5 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/** @@ -22,6 +23,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' @@ -33,6 +35,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/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 d521e800a..f57fad3ab 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). @@ -2556,26 +2560,96 @@ 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 - local decoded - decoded=$(echo "$content" | base64 -d 2>/dev/null || echo "") + # 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 "") - if ! echo "$decoded" | grep -qi 'AGENTS\.md'; then + 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 ! 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" fi } +# --------------------------------------------------------------------------- +# Check: GEMINI.md exists and references AGENTS.md +# --------------------------------------------------------------------------- +check_gemini_md() { + local repo="$1" + + 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 [ "$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 + + # 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 "") + + 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 ! 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" + fi +} + # --------------------------------------------------------------------------- # Check: AGENTS.md exists and references org .github/AGENTS.md # --------------------------------------------------------------------------- @@ -3170,7 +3244,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 @@ -3774,6 +3848,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 6bfdbe025..546d894f5 100644 --- a/scripts/compliance-remediate.sh +++ b/scripts/compliance-remediate.sh @@ -913,6 +913,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/scripts/lib/agent-md-xref.sh b/scripts/lib/agent-md-xref.sh new file mode 100644 index 000000000..b2832505c --- /dev/null +++ b/scripts/lib/agent-md-xref.sh @@ -0,0 +1,20 @@ +#!/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() { + # 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 +} 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/agent-md-xref.bats b/test/scripts/compliance-audit/agent-md-xref.bats new file mode 100644 index 000000000..ae59bcf5c --- /dev/null +++ b/test/scripts/compliance-audit/agent-md-xref.bats @@ -0,0 +1,40 @@ +#!/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" +} + +@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 ] +} diff --git a/test/scripts/compliance-audit/gemini-md.bats b/test/scripts/compliance-audit/gemini-md.bats new file mode 100644 index 000000000..95ee56e71 --- /dev/null +++ b/test/scripts/compliance-audit/gemini-md.bats @@ -0,0 +1,83 @@ +#!/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" +} + +# 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" 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" + 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" + 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" { + 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" "" 404 + [ "$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"'* ]] +} + +@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"* ]] +} 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 +}