From 5e284aab889955b1ae229eb81a5b571e5f58c0c7 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Tue, 1 Sep 2026 06:08:15 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20implement=20issue=20#646=20?= =?UTF-8?q?=E2=80=94=20[Phase=203]=20Add=20local=20canonical-AGENTS.md=20s?= =?UTF-8?q?elf-validation=20to=20CI=20(informational)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 34 ++++++++ test/workflows/ci/agents-md-selfcheck.bats | 95 ++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 test/workflows/ci/agents-md-selfcheck.bats diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c611389e3..fbd18ec98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,6 +112,40 @@ jobs: echo "No shell scripts found — skipping" fi + # Informational self-validation of THIS repo's canonical AGENTS.md against the + # standalone structural linter (scripts/agents-md-lint.sh, Phase 2 of epic + # #642). Phase 3 (#646) runs it on every PR in the linter's default + # INFORMATIONAL mode: each structural finding surfaces as a GitHub ::warning:: + # annotation, but the job never fails the build. This job is deliberately NOT a + # branch-protection required status check — promotion to a blocking/required + # check (--mode failing + branch protection) is deferred to Phase 4. Do NOT add + # it to branch protection or pass --mode failing while it remains informational. + # See test/workflows/ci/agents-md-selfcheck.bats. + agents-md-selfcheck: + name: AGENTS.md Self-Check (informational) + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Lint canonical AGENTS.md (informational) + # Default (informational) mode always exits 0 — structural findings + # surface as ::warning:: annotations without failing the build, mirroring + # run-bats.sh's non-fatal-signal convention. Do NOT pass --mode failing: + # promoting this to a blocking/required check is Phase 4 (see job header). + run: | + bash scripts/agents-md-lint.sh AGENTS.md \ + | while IFS=$'\t' read -r severity rule line message; do + [ -n "$rule" ] || continue + if [ "$line" = "-" ] || [ -z "$line" ]; then + echo "::warning file=AGENTS.md::[${severity}] ${rule}: ${message}" + else + echo "::warning file=AGENTS.md,line=${line}::[${severity}] ${rule}: ${message}" + fi + done + agent-security: name: Agent Security Scan runs-on: ubuntu-latest diff --git a/test/workflows/ci/agents-md-selfcheck.bats b/test/workflows/ci/agents-md-selfcheck.bats new file mode 100644 index 000000000..ee2a3ae72 --- /dev/null +++ b/test/workflows/ci/agents-md-selfcheck.bats @@ -0,0 +1,95 @@ +#!/usr/bin/env bats +# Regression guard for the informational AGENTS.md self-check wired into +# .github/workflows/ci.yml (issue #646, Phase 3 of epic #642). +# +# Phase 3 runs the standalone structural linter (scripts/agents-md-lint.sh) +# against THIS repo's own canonical AGENTS.md on every pull request, in +# INFORMATIONAL mode: it annotates each structural finding with a GitHub +# ::warning:: but must never fail the build (promotion to a failing/required +# check is deferred to Phase 4). These tests pin that contract so a future edit +# cannot silently (a) drop the self-check, (b) point it at the wrong file, or +# (c) flip it to failing mode and turn it into a de-facto blocking gate before +# Phase 4 sign-off. + +load 'helpers/setup' + +# Emit the `run:` step block whose text contains the given marker substring. +# Steps in ci.yml start at a 6-space "- " (the `steps:` child indent); every +# deeper-indented line belongs to that step. (Mirrors secret-scan-config.bats.) +step_block_with() { + local marker="$1" + local block="" out="" line="" + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%$'\r'}" + if [[ "$line" == " - "* ]]; then + if [[ "$block" == " - "* ]]; then + [[ "$block" == *"$marker"* ]] && out="$block" + fi + block="$line"$'\n' + else + if [[ -n "$block" ]]; then + block+="$line"$'\n' + fi + fi + done < "$TT_WORKFLOW" + if [[ "$block" == " - "* ]]; then + [[ "$block" == *"$marker"* ]] && out="$block" + fi + printf '%s' "$out" +} + +@test "agents-md-selfcheck: the ci workflow exists" { + [[ -f "$TT_WORKFLOW" ]] +} + +@test "agents-md-selfcheck: the linter script it invokes exists and is executable" { + # The self-check runs the Phase-2 standalone linter; if the path drifts the + # step would fail with file-not-found and red an otherwise-informational job. + [[ -x "${TT_REPO_ROOT}/scripts/agents-md-lint.sh" ]] +} + +@test "agents-md-selfcheck: ci runs the structural linter against this repo's AGENTS.md" { + local block + block="$(step_block_with 'scripts/agents-md-lint.sh')" + [[ -n "$block" ]] + # It must target the repo's own canonical AGENTS.md (the self-validation). + [[ "$block" == *"AGENTS.md"* ]] +} + +@test "agents-md-selfcheck: the self-check stays informational (no --mode failing)" { + # Informational default mode always exits 0; passing --mode failing would turn + # a REQUIRED-level finding into a build failure — that promotion is Phase 4. + # Strip comment lines first: a header comment saying "do NOT pass --mode + # failing" is good documentation and must not trip this assertion, which is + # about the actual invocation. + local block code + block="$(step_block_with 'scripts/agents-md-lint.sh')" + [[ -n "$block" ]] + code="$(printf '%s\n' "$block" | grep -vE '^[[:space:]]*#')" + [[ "$code" == *"scripts/agents-md-lint.sh"* ]] + [[ "$code" != *"--mode failing"* ]] + [[ "$code" != *"--mode=failing"* ]] +} + +@test "agents-md-selfcheck: findings surface as ::warning:: annotations" { + # Mirror run-bats.sh's non-fatal-signal convention: findings annotate without + # failing the build. + local block + block="$(step_block_with 'scripts/agents-md-lint.sh')" + [[ -n "$block" ]] + [[ "$block" == *"::warning"* ]] +} + +@test "agents-md-selfcheck: the self-check itself documents its informational, deferred-to-Phase-4 status" { + # AC #3: no new branch-protection required check while informational. The + # self-check's OWN step block must document that it is informational and that + # promotion to a blocking/required check is deferred to Phase 4, so a reviewer + # editing it later knows the non-blocking behaviour is deliberate. Scoping the + # assertion to the step block (not the whole file) keeps it from passing on an + # unrelated "informational" comment elsewhere in ci.yml. + local block + block="$(step_block_with 'scripts/agents-md-lint.sh')" + [[ -n "$block" ]] + [[ "$block" == *"informational"* ]] + [[ "$block" == *"Phase 4"* ]] +} From db04b2faee5820527dfb8041d0b3bd4736b9616a Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Tue, 1 Sep 2026 06:11:50 +0000 Subject: [PATCH 2/3] fix(reviews): address review comments [skip ci-relay] --- test/workflows/ci/agents-md-selfcheck.bats | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/workflows/ci/agents-md-selfcheck.bats b/test/workflows/ci/agents-md-selfcheck.bats index ee2a3ae72..584b9247d 100644 --- a/test/workflows/ci/agents-md-selfcheck.bats +++ b/test/workflows/ci/agents-md-selfcheck.bats @@ -26,10 +26,15 @@ step_block_with() { [[ "$block" == *"$marker"* ]] && out="$block" fi block="$line"$'\n' - else + elif [[ "$line" =~ ^[[:space:]]{8} ]] || [[ "$line" =~ ^[[:space:]]*$ ]]; then if [[ -n "$block" ]]; then block+="$line"$'\n' fi + else + if [[ "$block" == " - "* ]]; then + [[ "$block" == *"$marker"* ]] && out="$block" + fi + block="" fi done < "$TT_WORKFLOW" if [[ "$block" == " - "* ]]; then From 7f34f901f75f10d5ec65574cb87bd8f1a346969a Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Tue, 1 Sep 2026 06:16:18 +0000 Subject: [PATCH 3/3] fix(tests): tighten agents-md-selfcheck assertions against comment false-positives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strip comment lines before asserting AGENTS.md appears in the linter step, and verify the combined `scripts/agents-md-lint.sh … AGENTS.md` invocation pattern rather than just the script name alone. Co-Authored-By: Claude Sonnet 4.6 --- test/workflows/ci/agents-md-selfcheck.bats | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/workflows/ci/agents-md-selfcheck.bats b/test/workflows/ci/agents-md-selfcheck.bats index 584b9247d..b8dee7abb 100644 --- a/test/workflows/ci/agents-md-selfcheck.bats +++ b/test/workflows/ci/agents-md-selfcheck.bats @@ -54,11 +54,12 @@ step_block_with() { } @test "agents-md-selfcheck: ci runs the structural linter against this repo's AGENTS.md" { - local block + local block code block="$(step_block_with 'scripts/agents-md-lint.sh')" [[ -n "$block" ]] - # It must target the repo's own canonical AGENTS.md (the self-validation). - [[ "$block" == *"AGENTS.md"* ]] + # Strip comments so a comment mentioning AGENTS.md cannot satisfy the assertion. + code="$(printf '%s\n' "$block" | grep -vE '^[[:space:]]*#')" + [[ "$code" == *"AGENTS.md"* ]] } @test "agents-md-selfcheck: the self-check stays informational (no --mode failing)" { @@ -71,7 +72,8 @@ step_block_with() { block="$(step_block_with 'scripts/agents-md-lint.sh')" [[ -n "$block" ]] code="$(printf '%s\n' "$block" | grep -vE '^[[:space:]]*#')" - [[ "$code" == *"scripts/agents-md-lint.sh"* ]] + # Verify the linter is invoked with AGENTS.md as its argument, not just mentioned. + [[ "$code" == *"scripts/agents-md-lint.sh"*"AGENTS.md"* ]] [[ "$code" != *"--mode failing"* ]] [[ "$code" != *"--mode=failing"* ]] }