diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml
index 8b50b918..af31dd6a 100644
--- a/.github/workflows/dogfood-gate.yml
+++ b/.github/workflows/dogfood-gate.yml
@@ -47,7 +47,7 @@ jobs:
- name: Validate A2ML manifests
if: steps.detect.outputs.count > 0
- uses: hyperpolymath/a2ml-validate-action@0f8081cdfa293663ec6b204274b4272da302e564 # main (exit-127 comment fix)
+ uses: hyperpolymath/a2ml-ecosystem/validate-action@aa4b836bd969df2bc58128cb8e3d20bbc88d5e79 # main (exit-127 comment fix)
with:
path: '.'
strict: 'false'
@@ -109,7 +109,7 @@ jobs:
- name: Validate K9 contracts
if: steps.detect.outputs.k9_count > 0
- uses: hyperpolymath/k9-validate-action@91d662ecd1ac1e00875a7d77dff41933e3afe884 # main
+ uses: hyperpolymath/k9-ecosystem/validate-action@89f3c2702f4f650a92aa7411502f38da06abd562 # main
with:
path: '.'
strict: 'false'
diff --git a/lib/rules/structural_drift.ex b/lib/rules/structural_drift.ex
index ed870854..59c984cb 100644
--- a/lib/rules/structural_drift.ex
+++ b/lib/rules/structural_drift.ex
@@ -976,7 +976,11 @@ defmodule Hypatia.Rules.StructuralDrift do
"test/fixtures/",
"tests/fixtures/",
"scripts/fix-scripts/",
- "third_party/"
+ "third_party/",
+ # Archived session notes record the tree AS IT WAS. A path that was
+ # valid when written is not rename drift; "fixing" it would falsify
+ # the record.
+ "docs/archive/"
]
find_files_by_ext(repo_path, [
@@ -989,13 +993,16 @@ defmodule Hypatia.Rules.StructuralDrift do
".twasm"
])
|> Enum.reject(fn rel ->
- rel == "CHANGELOG.md" or Enum.any?(corpus_prefixes, &String.starts_with?(rel, &1))
+ rel == "CHANGELOG.md" or Enum.any?(corpus_prefixes, &String.starts_with?(rel, &1)) or
+ nested_project_doc?(repo_path, rel)
end)
|> Enum.flat_map(fn rel ->
path = Path.join(repo_path, rel)
case File.read(path) do
- {:ok, content} ->
+ {:ok, raw} ->
+ content = strip_full_line_comments(raw, Path.extname(rel))
+
# Negative lookbehind: only a *repo-root-relative* `src/
/`
# is rename-drift. A `src/` preceded by another path segment
# (`vcl-ut/src/bridges/`, `cli/src/commands/`, `echidna/src/rust/`,
@@ -1035,6 +1042,69 @@ defmodule Hypatia.Rules.StructuralDrift do
end
end
+ # Drop whole-line comments before matching. A commented-out example is not a
+ # live claim about the tree.
+ #
+ # Measured 2026-07-29 on gitbot-fleet's .machine_readable/INTENT.contractile,
+ # where SD022 fired on
+ #
+ # ; "src/abi/ - formal proofs, changes require re-verification"
+ #
+ # inside a `; *REMINDER: List areas where LLMs should check...*` block --
+ # template boilerplate, entirely commented out. Same family as the known
+ # unwrap-rule-matches-comments defect.
+ #
+ # Deliberately conservative: only `;` (scheme-shaped contractile/twasm) and
+ # `#` in TOML-shaped files. Markdown/AsciiDoc are left alone, since `#` there
+ # is a heading and may legitimately carry a path.
+ defp strip_full_line_comments(content, ext) do
+ leaders =
+ case ext do
+ e when e in [".contractile", ".twasm"] -> [";"]
+ e when e in [".toml", ".a2ml"] -> ["#", ";"]
+ _ -> []
+ end
+
+ if leaders == [] do
+ content
+ else
+ content
+ |> String.split("\n")
+ |> Enum.reject(fn line ->
+ t = String.trim_leading(line)
+ Enum.any?(leaders, &String.starts_with?(t, &1))
+ end)
+ |> Enum.join("\n")
+ end
+ end
+
+ # A doc inside a NESTED PROJECT ROOT (a subdirectory carrying its own build
+ # manifest) describes THAT project, whose layout is governed by its own
+ # repository -- not by this one. Reporting its paths as this repo's rename
+ # drift is noise the mirror cannot act on.
+ #
+ # Measured 2026-07-29 on gitbot-fleet: `bots/echidnabot/` is a vendored copy
+ # of the standalone echidnabot repo. Its CANONICAL_SOURCE.md documents
+ # `src/abi/` and states in the same table that the ABI namespace is
+ # "owner-managed in standalone; fleet does not need it for deploy". `src/abi/`
+ # exists upstream and is deliberately absent here, so SD022 fired on a doc
+ # that is correct. 10 of 11 findings in that repo were this shape.
+ #
+ # Only subtrees BELOW the repo root count -- a root manifest means the repo
+ # itself is the project, and its drift is in scope.
+ defp nested_project_doc?(repo_path, rel) do
+ manifests = ["Cargo.toml", "mix.exs", "package.json", "go.mod", "pyproject.toml"]
+
+ rel
+ |> Path.dirname()
+ |> Path.split()
+ |> Enum.scan(fn seg, acc -> Path.join(acc, seg) end)
+ |> Enum.reject(&(&1 in [".", ""]))
+ |> Enum.any?(fn dir ->
+ Enum.any?(manifests, &File.exists?(Path.join([repo_path, dir, &1])))
+ end)
+ end
+
# Index every `**/src/` directory in the tracked tree (via the file
# list, so it respects .gitignore and skips _build/deps), returning the set
# of basenames plus the set of real top-level directory names.
diff --git a/lib/rules/workflow_audit.ex b/lib/rules/workflow_audit.ex
index bad71ce1..dbc5dd77 100644
--- a/lib/rules/workflow_audit.ex
+++ b/lib/rules/workflow_audit.ex
@@ -522,8 +522,18 @@ defmodule Hypatia.Rules.WorkflowAudit do
# Actor-controllable expressions. Anchored loosely so we catch the
# expression wherever it appears in a run block — the previous full
# `run: |` block-shape match missed inline `run:` single-liners.
+ # `[\s\S]*?` previously spanned step boundaries: a `run:` in one step and a
+ # context expression in a LATER step's `env:` block matched as though the
+ # value were interpolated into the script. Measured 2026-07-29 on
+ # gitbot-fleet's repo-integrity-guard.yml -- flagged :critical although the
+ # untrusted values are bound via `env:` and consumed as quoted shell vars
+ # through `grep -F`, i.e. exactly the mitigation this rule asks for.
+ #
+ # `[^\n]` per line, and no line may start a new step (`- name:`/`- uses:`)
+ # or open a sibling mapping key at step indent, so the match stays inside
+ # one run block.
run_context_re =
- ~r/run:[\s\S]*?\$\{\{\s*github\.(?:head_ref|event\.pull_request\.(?:title|body|head\.ref)|event\.issue\.(?:title|body)|event\.comment\.body|event\.review\.body|event\.workflow_run\.(?:head_branch|display_title)|event\.commits)[^}]*\}\}/m
+ ~r/run:(?:[^\n]*\n(?!\s*-\s+(?:name|uses|id):|\s{0,10}(?:env|with|if|name|uses):\s))*?[^\n]*\$\{\{\s*github\.(?:head_ref|event\.pull_request\.(?:title|body|head\.ref)|event\.issue\.(?:title|body)|event\.comment\.body|event\.review\.body|event\.workflow_run\.(?:head_branch|display_title)|event\.commits)[^}]*\}\}/m
unsafe_json_payload_re =
~r/-d\s*".*\$\{\{\s*github\.(?:head_ref|event\.pull_request\.(?:title|body|head\.ref)|event\.issue\.(?:title|body)|event\.comment\.body|event\.review\.body|event\.workflow_run\.(?:head_branch|display_title)|event\.commits)/s
@@ -989,7 +999,30 @@ defmodule Hypatia.Rules.WorkflowAudit do
Regex.match?(~r/if:[^\n]*steps\.[A-Za-z0-9_-]+\.outputs\./, step_block) and
String.contains?(stripped, "GITHUB_OUTPUT")
- if Regex.match?(gate_re, step_block) or env_output_gate? do
+ # Third valid idiom, previously unrecognised: a job-level
+ # `env:` binds a presence boolean from the secret and steps gate
+ # on it. Measured 2026-07-29 on gitbot-fleet's instant-sync.yml,
+ # which does exactly this on every step plus an else branch, and
+ # was still reported :high.
+ #
+ # env:
+ # HAS_TOKEN: ${{ secrets.FARM_DISPATCH_TOKEN != '' }}
+ # steps:
+ # - if: env.HAS_TOKEN == 'true'
+ env_boolean_gate? =
+ case Regex.run(
+ ~r/(\w+):\s*\$\{\{\s*secrets\.#{Regex.escape(secret_name)}\s*!=\s*['"]['"]?\s*\}\}/,
+ stripped
+ ) do
+ [_, env_name] ->
+ Regex.match?(~r/if:[^\n]*env\.#{Regex.escape(env_name)}\b/, stripped)
+
+ _ ->
+ false
+ end
+
+ if Regex.match?(gate_re, step_block) or env_output_gate? or
+ env_boolean_gate? do
[]
else
[