diff --git a/.github/workflows/gh-aw-docs-applies-to-sweep.lock.yml b/.github/workflows/gh-aw-docs-applies-to-sweep.lock.yml
index 5517437..ae9ac91 100644
--- a/.github/workflows/gh-aw-docs-applies-to-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-applies-to-sweep.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"49fc698656605a712ffdaefc8d87e037819ff94851c8b57484fe821eea7f4cfc","body_hash":"36685caba0d54b111be0cfdddbf486df2ae7f35b01d8a1223a8376b2c27adabb","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"1c8588b32e769a4904816be8d148e3993b1ecc1c101ff8e5e812fd11f8cbf533","body_hash":"985d467aa98b59b4a74692803f730d70dcc53ce812838a85d01ceb3f91351c18","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_PLUGINS_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"},{"repo":"microsoft/apm-action","sha":"b48dd081eb0050f6d7f32d0e7caa0a59a2d419fd","version":"v1.7.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}
# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -30,6 +30,7 @@
#
# Resolved workflow manifest:
# Imports:
+# - gh-aw-fragments/findings-contract.md
# - gh-aw-fragments/formatting.md
# - gh-aw-fragments/mcp-pagination.md
# - gh-aw-fragments/rigor.md
@@ -110,6 +111,11 @@ on:
description: Approximate pages per rotating slice; controls shard count N = ceil(total/batch-size)
required: false
type: string
+ target-files:
+ default: ""
+ description: "Optional newline- or comma-separated list of docs-root-relative file paths to sweep. When set, overrides target-path and scope-mode: the sweep processes exactly these files."
+ required: false
+ type: string
target-path:
default: ""
description: Optional docs-root-relative directory to sweep recursively. Accepts a leading slash.
@@ -335,20 +341,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_133656d743b08bef_EOF'
+ cat << 'GH_AW_PROMPT_2fd01e7e79c802d7_EOF'
- GH_AW_PROMPT_133656d743b08bef_EOF
+ GH_AW_PROMPT_2fd01e7e79c802d7_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_133656d743b08bef_EOF'
+ cat << 'GH_AW_PROMPT_2fd01e7e79c802d7_EOF'
Tools: create_issue, missing_tool, missing_data, noop
- GH_AW_PROMPT_133656d743b08bef_EOF
+ GH_AW_PROMPT_2fd01e7e79c802d7_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
- cat << 'GH_AW_PROMPT_133656d743b08bef_EOF'
+ cat << 'GH_AW_PROMPT_2fd01e7e79c802d7_EOF'
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -377,9 +383,9 @@ jobs:
{{/if}}
- GH_AW_PROMPT_133656d743b08bef_EOF
+ GH_AW_PROMPT_2fd01e7e79c802d7_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_133656d743b08bef_EOF'
+ cat << 'GH_AW_PROMPT_2fd01e7e79c802d7_EOF'
## Formatting Guidelines
@@ -420,6 +426,42 @@ jobs:
If you see `MCP tool response exceeds maximum allowed tokens`, retry with a smaller `perPage` value (halve it).
+ ## Findings contract
+
+ These rules apply to every finding this sweep emits. Sweep output is consumed by humans and, increasingly, by AI fix-agents that may act on it without a human in the loop. A finding that is uncertain, or that looks authoritative but is wrong, is worse than no finding at all.
+
+ ### Finding-type allowlist
+
+ Emit only the `category` values enumerated in this workflow's "Build the findings list" step. That enumeration is a closed allowlist:
+
+ - Never invent, rename, pluralize, or otherwise vary a category string. If a finding does not map cleanly to an allowlisted category, drop it.
+ - A finding is valid only if applying its `suggested_fix` would change the page's rendered output or its published metadata. Drop no-op findings whose fix a reader would never see — for example, adding a marker the docs toolchain already generates automatically.
+ - If you spot something real that has no allowlisted category, describe it in the issue body's **Notes** section as prose. Do not smuggle it in as a finding under an invented category.
+
+ ### Per-finding confidence
+
+ Add a `confidence` field to every finding, set to exactly one of `high`, `medium`, or `low`. Judge confidence on how safe the finding is to act on *without* human verification — this is a separate axis from `severity`, which measures impact:
+
+ - `high` — the problem and the fix are objective and verifiable from the evidence in front of you: a missing required field, a tool-flagged issue with a single unambiguous correction, a directly quoted contradiction. A fix-agent could apply the `suggested_fix` verbatim without judgment.
+ - `medium` — the finding is well-supported, but the fix involves wording choices, or depends on a repository convention you could not fully verify this run. A human should confirm the fix before it lands.
+ - `low` — the finding is plausible but rests on partial evidence, subjective judgment, or an assumption about intent or convention you could not confirm. If you cannot justify at least `low`, drop the finding rather than filing it.
+
+ When a finding's evidence traces back to text you did not verify — for example, terminology copied from an issue or PR description rather than confirmed against the code or the published docs — cap its confidence at `low` and say so in the `evidence`.
+
+ Include `confidence` in the YAML schema for every finding, alongside `severity`. Keep the existing sort order (by `severity` first); do not reorder by confidence.
+
+ ### Human-review gate
+
+ If the capped findings list contains **any** finding with `confidence: medium` or `confidence: low`:
+
+ 1. Add the label `needs-human-review` to your `create_issue` call, in addition to the labels the workflow adds automatically. This marks the issue as not safe to auto-action and keeps it out of the `good-for-ai` delegation track.
+ 2. Immediately below the `## Findings ()` heading and before the YAML block, add this callout verbatim:
+
+ > [!WARNING]
+ > This issue contains medium- or low-confidence findings. Review them before acting — auto-applying sweep output without verification risks putting incorrect content into the docs. Findings marked `confidence: high` are safe to delegate to a fix-agent; `medium` and `low` need a human sign-off first.
+
+ If every finding is `confidence: high`, do not add the label or the callout: the issue is safe to delegate as-is.
+
# Docs `applies_to` sweep agent
You are an `applies_to` validator for an Elastic documentation repository. Your job is to audit the `applies_to` frontmatter key on a deterministically-selected slice of pages and emit a single labeled fix-issue with structured findings.
@@ -483,7 +525,9 @@ jobs:
- `inconsistent-applies-to` — `applies_to` contradicts other frontmatter (e.g., `products:` says one thing, `applies_to:` another).
- `outdated-applies-to` — references a deployment or lifecycle value that is deprecated per the verified reference.
- For each finding extract `file`, `line`, `category`, `severity`, `evidence`, and `suggested_fix` when you can produce a concrete YAML snippet confidently.
+ These five are the complete category allowlist for this sweep (see the **Findings contract**).
+
+ For each finding extract `file`, `line`, `category`, `severity`, `confidence`, `evidence`, and `suggested_fix` when you can produce a concrete YAML snippet confidently. Set `confidence` per the **Findings contract**: syntax and value errors verified against the published reference are usually `high`; dimension-choice or convention judgments are usually `medium`.
Strip the `/tmp/gh-aw/sweep-data/scope/` prefix from `file` so paths are repo-relative.
@@ -497,6 +541,9 @@ jobs:
- Shard mode with `target_path`: `"No applies_to issues under / in shard / ( pages)"`.
- Full mode with `target_path`: `"No applies_to issues under / ( pages)"`.
- Full mode without `target_path`: `"No applies_to issues in full sweep ( pages)"`.
+ - Files mode: `"No applies_to issues in the requested file list ( files)"`.
+
+ **Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; audit exactly those files and ignore `target_path`/shard framing. Use an explicit-file-list description in the title (`file list — pages`) and scope-summary (`Explicit file list · of requested files in scope.`).
## Output: fix-issue body
@@ -526,6 +573,7 @@ jobs:
line: 4
category: invalid-applies-to-value
severity: high
+ confidence: high
evidence: "applies_to.deployment.ess uses an unrecognized deployment key; use `ech` for Elastic Cloud Hosted"
suggested_fix: |
applies_to:
@@ -535,6 +583,7 @@ jobs:
line: 1
category: missing-applies-to
severity: high
+ confidence: high
evidence: "frontmatter has no applies_to key"
```
@@ -548,7 +597,7 @@ jobs:
```
- Keep the YAML block parseable. Use the literal `|` block scalar for multi-line `suggested_fix` values.
+ Keep the YAML block parseable — every entry must have `file`, `line`, `category`, `severity`, `confidence`, `evidence`. Use the literal `|` block scalar for multi-line `suggested_fix` values.
## What to skip
@@ -558,7 +607,7 @@ jobs:
__GH_AW_EXPR_49B959F1__
- GH_AW_PROMPT_133656d743b08bef_EOF
+ GH_AW_PROMPT_2fd01e7e79c802d7_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -744,9 +793,10 @@ jobs:
DOCS_ROOT: ${{ inputs.docs-root }}
SCOPE_MODE: ${{ inputs.scope-mode }}
TARGET_BATCH: ${{ inputs.target-batch-size }}
+ TARGET_FILES: ${{ inputs.target-files }}
TARGET_PATH: ${{ inputs.target-path }}
name: Compute sweep targets
- run: "set -eu\nmkdir -p /tmp/gh-aw/sweep-data/scope\n\nTARGET_PATH_CLEAN=${TARGET_PATH#/}\nTARGET_PATH_CLEAN=${TARGET_PATH_CLEAN%/}\nDOCS_ROOT_CLEAN=${DOCS_ROOT%/}\nSCOPE_ROOT=\"$DOCS_ROOT\"\nREQUESTED_SCOPE_MODE=\"$SCOPE_MODE\"\nSELECTION_MODE=\"shard\"\n\ncase \"$REQUESTED_SCOPE_MODE\" in\n auto|full|shard) ;;\n *)\n echo \"scope-mode '$REQUESTED_SCOPE_MODE' must be one of: auto, full, shard\"\n exit 1\n ;;\nesac\n\nif [ -n \"$TARGET_PATH_CLEAN\" ]; then\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n SCOPE_ROOT=\"$TARGET_PATH_CLEAN\"\n else\n SCOPE_ROOT=\"$DOCS_ROOT_CLEAN/$TARGET_PATH_CLEAN\"\n fi\nfi\n\nif [ \"$REQUESTED_SCOPE_MODE\" = \"auto\" ]; then\n if [ -n \"$TARGET_PATH_CLEAN\" ]; then\n SELECTION_MODE=\"full\"\n else\n SELECTION_MODE=\"shard\"\n fi\nelse\n SELECTION_MODE=\"$REQUESTED_SCOPE_MODE\"\nfi\n\nif [ ! -d \"$SCOPE_ROOT\" ]; then\n echo \"scope root '$SCOPE_ROOT' does not exist; producing empty scope\"\n : > /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"total\":0,\"shard_n\":1,\"shard_slot\":0,\"shard_count\":0,\"recent_count\":0,\"in_scope_count\":0,\"iso_week\":\"'\"$(date +%G-W%V)\"'\",\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\"}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\nelse\n if [ \"$TOTAL\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n\n : > /tmp/gh-aw/sweep-data/shard.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/shard.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\n\n git log --since='2 days ago' --name-only --pretty=format: -- \"$DOCS_ROOT/*.md\" \"$DOCS_ROOT/**/*.md\" 2>/dev/null \\\n | grep -E '\\.md$' \\\n | sort -u > /tmp/gh-aw/sweep-data/recent.txt || true\n\n # Cap the recently-changed pass: if a corpus-wide rebase or migration\n # touched far more pages than one slice, fall back to slice-only so\n # rotation actually rotates.\n RECENT_RAW=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\n RECENT_LIMIT=$(( TARGET_BATCH * 2 ))\n if [ \"$RECENT_RAW\" -gt \"$RECENT_LIMIT\" ]; then\n echo \"recently-changed pass produced $RECENT_RAW pages (>2x target batch $TARGET_BATCH); disabling for this run\"\n : > /tmp/gh-aw/sweep-data/recent.txt\n fi\nfi\n\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nelse\n sort -u /tmp/gh-aw/sweep-data/shard.txt /tmp/gh-aw/sweep-data/recent.txt \\\n | grep -v '^$' > /tmp/gh-aw/sweep-data/in-scope.txt || true\nfi\n\nwhile IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\ndone < /tmp/gh-aw/sweep-data/in-scope.txt\n\nSHARD_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/shard.txt | tr -d ' ')\nRECENT_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n\n REQUESTED_COUNT=0\n printf '%s' \"$TARGET_FILES\" | tr ',' '\\n' > /tmp/gh-aw/sweep-data/target-files.raw\n while IFS= read -r raw || [ -n \"$raw\" ]; do\n entry=$(printf '%s' \"$raw\" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')\n [ -z \"$entry\" ] && continue\n REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))\n entry=${entry#/}\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n resolved=\"$entry\"\n else\n case \"$entry\" in\n \"$DOCS_ROOT_CLEAN\"/*) resolved=\"$entry\" ;;\n *) resolved=\"$DOCS_ROOT_CLEAN/$entry\" ;;\n esac\n fi\n if [ -f \"$resolved\" ]; then\n echo \"$resolved\" >> /tmp/gh-aw/sweep-data/all.txt\n else\n echo \"target-files: '$resolved' not found; skipping\"\n fi\n done < /tmp/gh-aw/sweep-data/target-files.raw\n\n sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt\n cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt\n\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\n done < /tmp/gh-aw/sweep-data/in-scope.txt\n\n IN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"total\":0,\"shard_n\":1,\"shard_slot\":0,\"shard_count\":0,\"recent_count\":0,\"in_scope_count\":0,\"iso_week\":\"'\"$(date +%G-W%V)\"'\",\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\"}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\nelse\n if [ \"$TOTAL\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n\n : > /tmp/gh-aw/sweep-data/shard.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/shard.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\n\n git log --since='2 days ago' --name-only --pretty=format: -- \"$DOCS_ROOT/*.md\" \"$DOCS_ROOT/**/*.md\" 2>/dev/null \\\n | grep -E '\\.md$' \\\n | sort -u > /tmp/gh-aw/sweep-data/recent.txt || true\n\n # Cap the recently-changed pass: if a corpus-wide rebase or migration\n # touched far more pages than one slice, fall back to slice-only so\n # rotation actually rotates.\n RECENT_RAW=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\n RECENT_LIMIT=$(( TARGET_BATCH * 2 ))\n if [ \"$RECENT_RAW\" -gt \"$RECENT_LIMIT\" ]; then\n echo \"recently-changed pass produced $RECENT_RAW pages (>2x target batch $TARGET_BATCH); disabling for this run\"\n : > /tmp/gh-aw/sweep-data/recent.txt\n fi\nfi\n\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nelse\n sort -u /tmp/gh-aw/sweep-data/shard.txt /tmp/gh-aw/sweep-data/recent.txt \\\n | grep -v '^$' > /tmp/gh-aw/sweep-data/in-scope.txt || true\nfi\n\nwhile IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\ndone < /tmp/gh-aw/sweep-data/in-scope.txt\n\nSHARD_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/shard.txt | tr -d ' ')\nRECENT_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt
+ : > /tmp/gh-aw/sweep-data/shard.txt
+ : > /tmp/gh-aw/sweep-data/recent.txt
+ : > /tmp/gh-aw/sweep-data/in-scope.txt
+
+ REQUESTED_COUNT=0
+ printf '%s' "$TARGET_FILES" | tr ',' '\n' > /tmp/gh-aw/sweep-data/target-files.raw
+ while IFS= read -r raw || [ -n "$raw" ]; do
+ entry=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
+ [ -z "$entry" ] && continue
+ REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))
+ entry=${entry#/}
+ if [ "$DOCS_ROOT_CLEAN" = "." ] || [ -z "$DOCS_ROOT_CLEAN" ]; then
+ resolved="$entry"
+ else
+ case "$entry" in
+ "$DOCS_ROOT_CLEAN"/*) resolved="$entry" ;;
+ *) resolved="$DOCS_ROOT_CLEAN/$entry" ;;
+ esac
+ fi
+ if [ -f "$resolved" ]; then
+ echo "$resolved" >> /tmp/gh-aw/sweep-data/all.txt
+ else
+ echo "target-files: '$resolved' not found; skipping"
+ fi
+ done < /tmp/gh-aw/sweep-data/target-files.raw
+
+ sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt
+ cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt
+
+ while IFS= read -r f; do
+ [ -z "$f" ] && continue
+ [ ! -f "$f" ] && continue
+ mkdir -p "/tmp/gh-aw/sweep-data/scope/$(dirname "$f")"
+ cp "$f" "/tmp/gh-aw/sweep-data/scope/$f"
+ done < /tmp/gh-aw/sweep-data/in-scope.txt
+
+ IN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')
+ cat > /tmp/gh-aw/sweep-data/stats.json < in shard / ( pages)"`.
- Full mode with `target_path`: `"No applies_to issues under / ( pages)"`.
- Full mode without `target_path`: `"No applies_to issues in full sweep ( pages)"`.
+- Files mode: `"No applies_to issues in the requested file list ( files)"`.
+
+**Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; audit exactly those files and ignore `target_path`/shard framing. Use an explicit-file-list description in the title (`file list — pages`) and scope-summary (`Explicit file list · of requested files in scope.`).
## Output: fix-issue body
@@ -355,6 +430,7 @@ Use one of these scope-summary lines:
line: 4
category: invalid-applies-to-value
severity: high
+ confidence: high
evidence: "applies_to.deployment.ess uses an unrecognized deployment key; use `ech` for Elastic Cloud Hosted"
suggested_fix: |
applies_to:
@@ -364,6 +440,7 @@ Use one of these scope-summary lines:
line: 1
category: missing-applies-to
severity: high
+ confidence: high
evidence: "frontmatter has no applies_to key"
```
@@ -377,7 +454,7 @@ Use one of these scope-summary lines:
```
-Keep the YAML block parseable. Use the literal `|` block scalar for multi-line `suggested_fix` values.
+Keep the YAML block parseable — every entry must have `file`, `line`, `category`, `severity`, `confidence`, `evidence`. Use the literal `|` block scalar for multi-line `suggested_fix` values.
## What to skip
diff --git a/.github/workflows/gh-aw-docs-coherence-sweep.lock.yml b/.github/workflows/gh-aw-docs-coherence-sweep.lock.yml
index 95880b0..18f6738 100644
--- a/.github/workflows/gh-aw-docs-coherence-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-coherence-sweep.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d151a22e66f75be58f7477486a2e58c8a44b984ec52ccb98756e400faa01fcc9","body_hash":"cbaaeab4fd57132cc07f8db62e3706bb6cec77795a283b6536916514169f2673","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"10dd7841ea7c0799f58573d6d2db6e2c32c95b136091b253f05f77bb47839b39","body_hash":"2105c836ec996b9dbac8851d52f0fc7ca551badee9375f3301264847e5c75947","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}
# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -31,6 +31,7 @@
#
# Resolved workflow manifest:
# Imports:
+# - gh-aw-fragments/findings-contract.md
# - gh-aw-fragments/formatting.md
# - gh-aw-fragments/mcp-pagination.md
# - gh-aw-fragments/rigor.md
@@ -112,6 +113,11 @@ on:
description: Approximate pages per rotating slice. Coherence is expensive (MCP + LLM comparisons per page) — keep this small.
required: false
type: string
+ target-files:
+ default: ""
+ description: "Optional newline- or comma-separated list of docs-root-relative file paths to sweep. When set, overrides target-path and scope-mode: the sweep processes exactly these files."
+ required: false
+ type: string
target-path:
default: ""
description: Optional docs-root-relative directory to sweep recursively. Accepts a leading slash.
@@ -336,20 +342,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_dcbdb508cfe14d01_EOF'
+ cat << 'GH_AW_PROMPT_7ca42d78f09b15b0_EOF'
- GH_AW_PROMPT_dcbdb508cfe14d01_EOF
+ GH_AW_PROMPT_7ca42d78f09b15b0_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_dcbdb508cfe14d01_EOF'
+ cat << 'GH_AW_PROMPT_7ca42d78f09b15b0_EOF'
Tools: create_issue, missing_tool, missing_data, noop
- GH_AW_PROMPT_dcbdb508cfe14d01_EOF
+ GH_AW_PROMPT_7ca42d78f09b15b0_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
- cat << 'GH_AW_PROMPT_dcbdb508cfe14d01_EOF'
+ cat << 'GH_AW_PROMPT_7ca42d78f09b15b0_EOF'
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -378,9 +384,9 @@ jobs:
{{/if}}
- GH_AW_PROMPT_dcbdb508cfe14d01_EOF
+ GH_AW_PROMPT_7ca42d78f09b15b0_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_dcbdb508cfe14d01_EOF'
+ cat << 'GH_AW_PROMPT_7ca42d78f09b15b0_EOF'
## Formatting Guidelines
@@ -421,6 +427,42 @@ jobs:
If you see `MCP tool response exceeds maximum allowed tokens`, retry with a smaller `perPage` value (halve it).
+ ## Findings contract
+
+ These rules apply to every finding this sweep emits. Sweep output is consumed by humans and, increasingly, by AI fix-agents that may act on it without a human in the loop. A finding that is uncertain, or that looks authoritative but is wrong, is worse than no finding at all.
+
+ ### Finding-type allowlist
+
+ Emit only the `category` values enumerated in this workflow's "Build the findings list" step. That enumeration is a closed allowlist:
+
+ - Never invent, rename, pluralize, or otherwise vary a category string. If a finding does not map cleanly to an allowlisted category, drop it.
+ - A finding is valid only if applying its `suggested_fix` would change the page's rendered output or its published metadata. Drop no-op findings whose fix a reader would never see — for example, adding a marker the docs toolchain already generates automatically.
+ - If you spot something real that has no allowlisted category, describe it in the issue body's **Notes** section as prose. Do not smuggle it in as a finding under an invented category.
+
+ ### Per-finding confidence
+
+ Add a `confidence` field to every finding, set to exactly one of `high`, `medium`, or `low`. Judge confidence on how safe the finding is to act on *without* human verification — this is a separate axis from `severity`, which measures impact:
+
+ - `high` — the problem and the fix are objective and verifiable from the evidence in front of you: a missing required field, a tool-flagged issue with a single unambiguous correction, a directly quoted contradiction. A fix-agent could apply the `suggested_fix` verbatim without judgment.
+ - `medium` — the finding is well-supported, but the fix involves wording choices, or depends on a repository convention you could not fully verify this run. A human should confirm the fix before it lands.
+ - `low` — the finding is plausible but rests on partial evidence, subjective judgment, or an assumption about intent or convention you could not confirm. If you cannot justify at least `low`, drop the finding rather than filing it.
+
+ When a finding's evidence traces back to text you did not verify — for example, terminology copied from an issue or PR description rather than confirmed against the code or the published docs — cap its confidence at `low` and say so in the `evidence`.
+
+ Include `confidence` in the YAML schema for every finding, alongside `severity`. Keep the existing sort order (by `severity` first); do not reorder by confidence.
+
+ ### Human-review gate
+
+ If the capped findings list contains **any** finding with `confidence: medium` or `confidence: low`:
+
+ 1. Add the label `needs-human-review` to your `create_issue` call, in addition to the labels the workflow adds automatically. This marks the issue as not safe to auto-action and keeps it out of the `good-for-ai` delegation track.
+ 2. Immediately below the `## Findings ()` heading and before the YAML block, add this callout verbatim:
+
+ > [!WARNING]
+ > This issue contains medium- or low-confidence findings. Review them before acting — auto-applying sweep output without verification risks putting incorrect content into the docs. Findings marked `confidence: high` are safe to delegate to a fix-agent; `medium` and `low` need a human sign-off first.
+
+ If every finding is `confidence: high`, do not add the label or the callout: the issue is safe to delegate as-is.
+
# Docs coherence sweep agent
You are a coherence reviewer for an Elastic documentation repository. Your job is to compare each in-scope page against published Elastic docs that cover overlapping topics, and flag two things that hurt search and AI-assistant quality:
@@ -467,8 +509,9 @@ jobs:
- `file` — the in-scope repo-relative path (strip `/tmp/gh-aw/sweep-data/scope/`).
- `line` — line number of the conflicting/overlapping passage in the in-scope file.
- - `category` — `duplicate-content`, `near-duplicate`, or `contradictory-content`.
+ - `category` — `duplicate-content`, `near-duplicate`, or `contradictory-content`. These three are the complete category allowlist for this sweep (see the **Findings contract**).
- `severity` — `high` for `contradictory-content`; `medium` for `duplicate-content`; `low` for `near-duplicate`.
+ - `confidence` — per the **Findings contract**: `high` only for a `contradictory-content` finding that quotes the exact disagreeing values on both pages; `medium` for `duplicate-content`; `low` for `near-duplicate` and for any comparison that rests on a judgment about how much two pages overlap.
- `evidence` — quote the disagreeing passage from the in-scope file in 1–2 short sentences, name the related published page by its URL.
- `related_url` — the published URL of the other page.
- `suggested_fix` — concrete remediation (consolidate, redirect, cross-link, or fix the contradiction). Be specific about which page should be the source of truth.
@@ -485,6 +528,9 @@ jobs:
- Shard mode with `target_path`: `"No coherence findings under / in shard / ( pages)"`.
- Full mode with `target_path`: `"No coherence findings under / ( pages)"`.
- Full mode without `target_path`: `"No coherence findings in full sweep ( pages)"`.
+ - Files mode: `"No coherence findings in the requested file list ( files)"`.
+
+ **Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; audit exactly those files and ignore `target_path`/shard framing. Use an explicit-file-list description in the title (`file list — findings`) and scope-summary (`Explicit file list · of requested files in scope.`).
If the MCP server is unreachable or returns errors for more than half the pages, abort by calling `noop` with `"elastic-docs MCP unavailable; skipping coherence sweep"` rather than emitting unreliable findings.
@@ -514,6 +560,7 @@ jobs:
line: 12
category: contradictory-content
severity: high
+ confidence: high
evidence: "states 'default refresh interval is 1s' but related published page says 30s"
related_url: https://www.elastic.co/docs/elasticsearch/reference/refresh-intervals
suggested_fix: |
@@ -522,6 +569,7 @@ jobs:
line: 1
category: duplicate-content
severity: medium
+ confidence: medium
evidence: "the entire 'Configure data views' section duplicates the published page nearly verbatim"
related_url: https://www.elastic.co/docs/kibana/data-views/configure
suggested_fix: |
@@ -530,6 +578,7 @@ jobs:
line: 47
category: near-duplicate
severity: low
+ confidence: low
evidence: "the 'Performance tuning' subsection overlaps with two paragraphs of the related page; rest of this page is unique"
related_url: https://www.elastic.co/docs/elasticsearch/performance/tuning
suggested_fix: |
@@ -558,7 +607,7 @@ jobs:
__GH_AW_EXPR_49B959F1__
- GH_AW_PROMPT_dcbdb508cfe14d01_EOF
+ GH_AW_PROMPT_7ca42d78f09b15b0_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -734,9 +783,10 @@ jobs:
DOCS_ROOT: ${{ inputs.docs-root }}
SCOPE_MODE: ${{ inputs.scope-mode }}
TARGET_BATCH: ${{ inputs.target-batch-size }}
+ TARGET_FILES: ${{ inputs.target-files }}
TARGET_PATH: ${{ inputs.target-path }}
name: Compute sweep targets
- run: "set -eu\nmkdir -p /tmp/gh-aw/sweep-data/scope\n\nTARGET_PATH_CLEAN=${TARGET_PATH#/}\nTARGET_PATH_CLEAN=${TARGET_PATH_CLEAN%/}\nDOCS_ROOT_CLEAN=${DOCS_ROOT%/}\nSCOPE_ROOT=\"$DOCS_ROOT\"\nREQUESTED_SCOPE_MODE=\"$SCOPE_MODE\"\nSELECTION_MODE=\"shard\"\n\ncase \"$REQUESTED_SCOPE_MODE\" in\n auto|full|shard) ;;\n *)\n echo \"scope-mode '$REQUESTED_SCOPE_MODE' must be one of: auto, full, shard\"\n exit 1\n ;;\nesac\n\nif [ -n \"$TARGET_PATH_CLEAN\" ]; then\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n SCOPE_ROOT=\"$TARGET_PATH_CLEAN\"\n else\n SCOPE_ROOT=\"$DOCS_ROOT_CLEAN/$TARGET_PATH_CLEAN\"\n fi\nfi\n\nif [ \"$REQUESTED_SCOPE_MODE\" = \"auto\" ]; then\n if [ -n \"$TARGET_PATH_CLEAN\" ]; then\n SELECTION_MODE=\"full\"\n else\n SELECTION_MODE=\"shard\"\n fi\nelse\n SELECTION_MODE=\"$REQUESTED_SCOPE_MODE\"\nfi\n\nif [ ! -d \"$SCOPE_ROOT\" ]; then\n echo \"scope root '$SCOPE_ROOT' does not exist; producing empty scope\"\n : > /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"total\":0,\"shard_n\":1,\"shard_slot\":0,\"shard_count\":0,\"recent_count\":0,\"in_scope_count\":0,\"iso_week\":\"'\"$(date +%G-W%V)\"'\",\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\"}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\nelse\n if [ \"$TOTAL\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n\n : > /tmp/gh-aw/sweep-data/shard.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/shard.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\n\n git log --since='2 days ago' --name-only --pretty=format: -- \"$DOCS_ROOT/*.md\" \"$DOCS_ROOT/**/*.md\" 2>/dev/null \\\n | grep -E '\\.md$' \\\n | sort -u > /tmp/gh-aw/sweep-data/recent.txt || true\n\n # Cap the recently-changed pass: if a corpus-wide rebase or migration\n # touched far more pages than one slice, fall back to slice-only so\n # rotation actually rotates.\n RECENT_RAW=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\n RECENT_LIMIT=$(( TARGET_BATCH * 2 ))\n if [ \"$RECENT_RAW\" -gt \"$RECENT_LIMIT\" ]; then\n echo \"recently-changed pass produced $RECENT_RAW pages (>2x target batch $TARGET_BATCH); disabling for this run\"\n : > /tmp/gh-aw/sweep-data/recent.txt\n fi\nfi\n\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nelse\n sort -u /tmp/gh-aw/sweep-data/shard.txt /tmp/gh-aw/sweep-data/recent.txt \\\n | grep -v '^$' > /tmp/gh-aw/sweep-data/in-scope.txt || true\nfi\n\nwhile IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\ndone < /tmp/gh-aw/sweep-data/in-scope.txt\n\nSHARD_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/shard.txt | tr -d ' ')\nRECENT_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n\n REQUESTED_COUNT=0\n printf '%s' \"$TARGET_FILES\" | tr ',' '\\n' > /tmp/gh-aw/sweep-data/target-files.raw\n while IFS= read -r raw || [ -n \"$raw\" ]; do\n entry=$(printf '%s' \"$raw\" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')\n [ -z \"$entry\" ] && continue\n REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))\n entry=${entry#/}\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n resolved=\"$entry\"\n else\n case \"$entry\" in\n \"$DOCS_ROOT_CLEAN\"/*) resolved=\"$entry\" ;;\n *) resolved=\"$DOCS_ROOT_CLEAN/$entry\" ;;\n esac\n fi\n if [ -f \"$resolved\" ]; then\n echo \"$resolved\" >> /tmp/gh-aw/sweep-data/all.txt\n else\n echo \"target-files: '$resolved' not found; skipping\"\n fi\n done < /tmp/gh-aw/sweep-data/target-files.raw\n\n sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt\n cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt\n\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\n done < /tmp/gh-aw/sweep-data/in-scope.txt\n\n IN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"total\":0,\"shard_n\":1,\"shard_slot\":0,\"shard_count\":0,\"recent_count\":0,\"in_scope_count\":0,\"iso_week\":\"'\"$(date +%G-W%V)\"'\",\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\"}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\nelse\n if [ \"$TOTAL\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n\n : > /tmp/gh-aw/sweep-data/shard.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/shard.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\n\n git log --since='2 days ago' --name-only --pretty=format: -- \"$DOCS_ROOT/*.md\" \"$DOCS_ROOT/**/*.md\" 2>/dev/null \\\n | grep -E '\\.md$' \\\n | sort -u > /tmp/gh-aw/sweep-data/recent.txt || true\n\n # Cap the recently-changed pass: if a corpus-wide rebase or migration\n # touched far more pages than one slice, fall back to slice-only so\n # rotation actually rotates.\n RECENT_RAW=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\n RECENT_LIMIT=$(( TARGET_BATCH * 2 ))\n if [ \"$RECENT_RAW\" -gt \"$RECENT_LIMIT\" ]; then\n echo \"recently-changed pass produced $RECENT_RAW pages (>2x target batch $TARGET_BATCH); disabling for this run\"\n : > /tmp/gh-aw/sweep-data/recent.txt\n fi\nfi\n\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nelse\n sort -u /tmp/gh-aw/sweep-data/shard.txt /tmp/gh-aw/sweep-data/recent.txt \\\n | grep -v '^$' > /tmp/gh-aw/sweep-data/in-scope.txt || true\nfi\n\nwhile IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\ndone < /tmp/gh-aw/sweep-data/in-scope.txt\n\nSHARD_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/shard.txt | tr -d ' ')\nRECENT_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt
+ : > /tmp/gh-aw/sweep-data/shard.txt
+ : > /tmp/gh-aw/sweep-data/recent.txt
+ : > /tmp/gh-aw/sweep-data/in-scope.txt
+
+ REQUESTED_COUNT=0
+ printf '%s' "$TARGET_FILES" | tr ',' '\n' > /tmp/gh-aw/sweep-data/target-files.raw
+ while IFS= read -r raw || [ -n "$raw" ]; do
+ entry=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
+ [ -z "$entry" ] && continue
+ REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))
+ entry=${entry#/}
+ if [ "$DOCS_ROOT_CLEAN" = "." ] || [ -z "$DOCS_ROOT_CLEAN" ]; then
+ resolved="$entry"
+ else
+ case "$entry" in
+ "$DOCS_ROOT_CLEAN"/*) resolved="$entry" ;;
+ *) resolved="$DOCS_ROOT_CLEAN/$entry" ;;
+ esac
+ fi
+ if [ -f "$resolved" ]; then
+ echo "$resolved" >> /tmp/gh-aw/sweep-data/all.txt
+ else
+ echo "target-files: '$resolved' not found; skipping"
+ fi
+ done < /tmp/gh-aw/sweep-data/target-files.raw
+
+ sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt
+ cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt
+
+ while IFS= read -r f; do
+ [ -z "$f" ] && continue
+ [ ! -f "$f" ] && continue
+ mkdir -p "/tmp/gh-aw/sweep-data/scope/$(dirname "$f")"
+ cp "$f" "/tmp/gh-aw/sweep-data/scope/$f"
+ done < /tmp/gh-aw/sweep-data/in-scope.txt
+
+ IN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')
+ cat > /tmp/gh-aw/sweep-data/stats.json < in shard / ( pages)"`.
- Full mode with `target_path`: `"No coherence findings under / ( pages)"`.
- Full mode without `target_path`: `"No coherence findings in full sweep ( pages)"`.
+- Files mode: `"No coherence findings in the requested file list ( files)"`.
+
+**Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; audit exactly those files and ignore `target_path`/shard framing. Use an explicit-file-list description in the title (`file list — findings`) and scope-summary (`Explicit file list · of requested files in scope.`).
If the MCP server is unreachable or returns errors for more than half the pages, abort by calling `noop` with `"elastic-docs MCP unavailable; skipping coherence sweep"` rather than emitting unreliable findings.
@@ -344,6 +418,7 @@ Use one of these scope-summary lines:
line: 12
category: contradictory-content
severity: high
+ confidence: high
evidence: "states 'default refresh interval is 1s' but related published page says 30s"
related_url: https://www.elastic.co/docs/elasticsearch/reference/refresh-intervals
suggested_fix: |
@@ -352,6 +427,7 @@ Use one of these scope-summary lines:
line: 1
category: duplicate-content
severity: medium
+ confidence: medium
evidence: "the entire 'Configure data views' section duplicates the published page nearly verbatim"
related_url: https://www.elastic.co/docs/kibana/data-views/configure
suggested_fix: |
@@ -360,6 +436,7 @@ Use one of these scope-summary lines:
line: 47
category: near-duplicate
severity: low
+ confidence: low
evidence: "the 'Performance tuning' subsection overlaps with two paragraphs of the related page; rest of this page is unique"
related_url: https://www.elastic.co/docs/elasticsearch/performance/tuning
suggested_fix: |
diff --git a/.github/workflows/gh-aw-docs-frontmatter-sweep.lock.yml b/.github/workflows/gh-aw-docs-frontmatter-sweep.lock.yml
index 571a7d4..d1368ed 100644
--- a/.github/workflows/gh-aw-docs-frontmatter-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-frontmatter-sweep.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"1c2971b2a9e2c51593fd2460677b275da93f4cdb6381687086953c61f271db52","body_hash":"b14914e91299745794c626ed014f016afc67c6aee7521b6debca49f8044021b9","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"28d008630ac14137665a0fd78f083cfed8a7962897f9bfd3d03e06ca1c2efc6f","body_hash":"9888177849234fea12068e9e9c995e127bdbcace10e9976ee10d334ffcb88df4","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_PLUGINS_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"},{"repo":"microsoft/apm-action","sha":"b48dd081eb0050f6d7f32d0e7caa0a59a2d419fd","version":"v1.7.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}
# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -31,6 +31,7 @@
#
# Resolved workflow manifest:
# Imports:
+# - gh-aw-fragments/findings-contract.md
# - gh-aw-fragments/formatting.md
# - gh-aw-fragments/mcp-pagination.md
# - gh-aw-fragments/rigor.md
@@ -111,6 +112,11 @@ on:
description: Approximate pages per Tier 2 rotating slice; controls shard count N = ceil(total/batch-size)
required: false
type: string
+ target-files:
+ default: ""
+ description: "Optional newline- or comma-separated list of docs-root-relative file paths to sweep. When set, overrides target-path and scope-mode: the sweep processes exactly these files."
+ required: false
+ type: string
target-path:
default: ""
description: Optional docs-root-relative directory to sweep recursively. Accepts a leading slash.
@@ -336,20 +342,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_706803d725ab80ca_EOF'
+ cat << 'GH_AW_PROMPT_746f077cb8210b87_EOF'
- GH_AW_PROMPT_706803d725ab80ca_EOF
+ GH_AW_PROMPT_746f077cb8210b87_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_706803d725ab80ca_EOF'
+ cat << 'GH_AW_PROMPT_746f077cb8210b87_EOF'
Tools: create_issue, missing_tool, missing_data, noop
- GH_AW_PROMPT_706803d725ab80ca_EOF
+ GH_AW_PROMPT_746f077cb8210b87_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
- cat << 'GH_AW_PROMPT_706803d725ab80ca_EOF'
+ cat << 'GH_AW_PROMPT_746f077cb8210b87_EOF'
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -378,9 +384,9 @@ jobs:
{{/if}}
- GH_AW_PROMPT_706803d725ab80ca_EOF
+ GH_AW_PROMPT_746f077cb8210b87_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_706803d725ab80ca_EOF'
+ cat << 'GH_AW_PROMPT_746f077cb8210b87_EOF'
## Formatting Guidelines
@@ -421,6 +427,42 @@ jobs:
If you see `MCP tool response exceeds maximum allowed tokens`, retry with a smaller `perPage` value (halve it).
+ ## Findings contract
+
+ These rules apply to every finding this sweep emits. Sweep output is consumed by humans and, increasingly, by AI fix-agents that may act on it without a human in the loop. A finding that is uncertain, or that looks authoritative but is wrong, is worse than no finding at all.
+
+ ### Finding-type allowlist
+
+ Emit only the `category` values enumerated in this workflow's "Build the findings list" step. That enumeration is a closed allowlist:
+
+ - Never invent, rename, pluralize, or otherwise vary a category string. If a finding does not map cleanly to an allowlisted category, drop it.
+ - A finding is valid only if applying its `suggested_fix` would change the page's rendered output or its published metadata. Drop no-op findings whose fix a reader would never see — for example, adding a marker the docs toolchain already generates automatically.
+ - If you spot something real that has no allowlisted category, describe it in the issue body's **Notes** section as prose. Do not smuggle it in as a finding under an invented category.
+
+ ### Per-finding confidence
+
+ Add a `confidence` field to every finding, set to exactly one of `high`, `medium`, or `low`. Judge confidence on how safe the finding is to act on *without* human verification — this is a separate axis from `severity`, which measures impact:
+
+ - `high` — the problem and the fix are objective and verifiable from the evidence in front of you: a missing required field, a tool-flagged issue with a single unambiguous correction, a directly quoted contradiction. A fix-agent could apply the `suggested_fix` verbatim without judgment.
+ - `medium` — the finding is well-supported, but the fix involves wording choices, or depends on a repository convention you could not fully verify this run. A human should confirm the fix before it lands.
+ - `low` — the finding is plausible but rests on partial evidence, subjective judgment, or an assumption about intent or convention you could not confirm. If you cannot justify at least `low`, drop the finding rather than filing it.
+
+ When a finding's evidence traces back to text you did not verify — for example, terminology copied from an issue or PR description rather than confirmed against the code or the published docs — cap its confidence at `low` and say so in the `evidence`.
+
+ Include `confidence` in the YAML schema for every finding, alongside `severity`. Keep the existing sort order (by `severity` first); do not reorder by confidence.
+
+ ### Human-review gate
+
+ If the capped findings list contains **any** finding with `confidence: medium` or `confidence: low`:
+
+ 1. Add the label `needs-human-review` to your `create_issue` call, in addition to the labels the workflow adds automatically. This marks the issue as not safe to auto-action and keeps it out of the `good-for-ai` delegation track.
+ 2. Immediately below the `## Findings ()` heading and before the YAML block, add this callout verbatim:
+
+ > [!WARNING]
+ > This issue contains medium- or low-confidence findings. Review them before acting — auto-applying sweep output without verification risks putting incorrect content into the docs. Findings marked `confidence: high` are safe to delegate to a fix-agent; `medium` and `low` need a human sign-off first.
+
+ If every finding is `confidence: high`, do not add the label or the callout: the issue is safe to delegate as-is.
+
# Docs frontmatter sweep agent
You are a frontmatter quality reviewer for an Elastic documentation repository. Your job is to audit the frontmatter (`---` block at the top of each `.md` file) of a deterministically-selected slice of pages, and emit a single labeled fix-issue with structured findings that a human (and later, a fix-agent) can act on.
@@ -452,6 +494,9 @@ jobs:
- Full mode without `target_path`: `Empty full sweep for (0 pages)`.
- Shard mode with `target_path`: `Empty subtree shard for / (shard /, 0 pages)`.
- Shard mode without `target_path`: `All files in this rotation are unaudited (shard /, 0 pages)`.
+ - Files mode (`selection_mode` is `files`): `Empty file list (0 of requested files found)`.
+
+ **Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; audit exactly those files and ignore `target_path`/shard framing. In the title, scope-summary, and `noop` messages, describe the scope as an explicit file list — for example title `file list — pages`, scope-summary `Explicit file list · of requested files in scope.`, and no-findings `noop` `No high-confidence frontmatter issues in the requested file list ( files)`.
## Step 1: Audit the frontmatter
@@ -484,8 +529,9 @@ jobs:
- `file` — the original repository-relative path (strip the `/tmp/gh-aw/sweep-data/scope/` prefix from any scoped file path).
- `line` — `1` for missing/invalid frontmatter keys (frontmatter starts at line 1); for description-quality findings use the line of the `description:` key.
- - `category` — one of: `missing-description`, `weak-description`, `description-too-long`, `missing-products`, `missing-navigation-title`. **Do not emit `missing-applies-to` or `invalid-applies-to`** — those belong to `gh-aw-docs-applies-to-sweep`. If another source suggests them, drop them silently.
+ - `category` — one of: `missing-description`, `weak-description`, `description-too-long`, `missing-products`, `missing-navigation-title`. These five are the complete category allowlist for this sweep (see the **Findings contract**). **Do not emit `missing-applies-to` or `invalid-applies-to`** — those belong to `gh-aw-docs-applies-to-sweep`. If another source suggests them, drop them silently.
- `severity` — `high` for missing required fields; `medium` for weak/long/invalid; `low` for nits.
+ - `confidence` — `high`, `medium`, or `low` per the **Findings contract**. Missing required-field findings verified from the file are usually `high`; description-quality rewrites that involve wording judgment are usually `medium`.
- `evidence` — one short sentence quoting or naming the exact problem.
- `suggested_fix` — concrete YAML snippet ready to paste into the file's frontmatter when you can produce one confidently. For audit-only findings, or a missing field with no verified value, omit `suggested_fix`.
@@ -534,6 +580,7 @@ jobs:
line: 1
category: missing-description
severity: high
+ confidence: high
evidence: "frontmatter has no `description` field"
suggested_fix: |
description: "How to configure X for Y use cases."
@@ -541,6 +588,7 @@ jobs:
line: 1
category: weak-description
severity: medium
+ confidence: medium
evidence: "description is generic ('Learn about X')"
suggested_fix: |
description: ""
@@ -556,7 +604,7 @@ jobs:
```
- Keep the YAML block parseable — every entry must have `file`, `line`, `category`, `severity`, `evidence`. Use the literal `|` block scalar for multi-line `suggested_fix` values. Do not include comments inside the YAML block.
+ Keep the YAML block parseable — every entry must have `file`, `line`, `category`, `severity`, `confidence`, `evidence`. Use the literal `|` block scalar for multi-line `suggested_fix` values. Do not include comments inside the YAML block.
## What to skip
@@ -567,7 +615,7 @@ jobs:
__GH_AW_EXPR_49B959F1__
- GH_AW_PROMPT_706803d725ab80ca_EOF
+ GH_AW_PROMPT_746f077cb8210b87_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -753,9 +801,10 @@ jobs:
DOCS_ROOT: ${{ inputs.docs-root }}
SCOPE_MODE: ${{ inputs.scope-mode }}
TARGET_BATCH: ${{ inputs.target-batch-size }}
+ TARGET_FILES: ${{ inputs.target-files }}
TARGET_PATH: ${{ inputs.target-path }}
name: Compute sweep targets
- run: "set -eu\nmkdir -p /tmp/gh-aw/sweep-data/scope\n\nTARGET_PATH_CLEAN=${TARGET_PATH#/}\nTARGET_PATH_CLEAN=${TARGET_PATH_CLEAN%/}\nDOCS_ROOT_CLEAN=${DOCS_ROOT%/}\nSCOPE_ROOT=\"$DOCS_ROOT\"\nREQUESTED_SCOPE_MODE=\"$SCOPE_MODE\"\nSELECTION_MODE=\"shard\"\n\ncase \"$REQUESTED_SCOPE_MODE\" in\n auto|full|shard) ;;\n *)\n echo \"scope-mode '$REQUESTED_SCOPE_MODE' must be one of: auto, full, shard\"\n exit 1\n ;;\nesac\n\nif [ -n \"$TARGET_PATH_CLEAN\" ]; then\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n SCOPE_ROOT=\"$TARGET_PATH_CLEAN\"\n else\n SCOPE_ROOT=\"$DOCS_ROOT_CLEAN/$TARGET_PATH_CLEAN\"\n fi\nfi\n\nif [ \"$REQUESTED_SCOPE_MODE\" = \"auto\" ]; then\n if [ -n \"$TARGET_PATH_CLEAN\" ]; then\n SELECTION_MODE=\"full\"\n else\n SELECTION_MODE=\"shard\"\n fi\nelse\n SELECTION_MODE=\"$REQUESTED_SCOPE_MODE\"\nfi\n\nif [ ! -d \"$SCOPE_ROOT\" ]; then\n echo \"scope root '$SCOPE_ROOT' does not exist; producing empty scope\"\n : > /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"total\":0,\"shard_n\":1,\"shard_slot\":0,\"shard_count\":0,\"recent_count\":0,\"in_scope_count\":0,\"iso_week\":\"'\"$(date +%G-W%V)\"'\",\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\"}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\nelse\n if [ \"$TOTAL\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n\n : > /tmp/gh-aw/sweep-data/shard.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/shard.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\n\n git log --since='2 days ago' --name-only --pretty=format: -- \"$DOCS_ROOT/*.md\" \"$DOCS_ROOT/**/*.md\" 2>/dev/null \\\n | grep -E '\\.md$' \\\n | sort -u > /tmp/gh-aw/sweep-data/recent.txt || true\n\n # Cap the recently-changed pass: if a corpus-wide rebase or migration\n # touched far more pages than one slice, fall back to slice-only so\n # rotation actually rotates.\n RECENT_RAW=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\n RECENT_LIMIT=$(( TARGET_BATCH * 2 ))\n if [ \"$RECENT_RAW\" -gt \"$RECENT_LIMIT\" ]; then\n echo \"recently-changed pass produced $RECENT_RAW pages (>2x target batch $TARGET_BATCH); disabling for this run\"\n : > /tmp/gh-aw/sweep-data/recent.txt\n fi\nfi\n\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nelse\n sort -u /tmp/gh-aw/sweep-data/shard.txt /tmp/gh-aw/sweep-data/recent.txt \\\n | grep -v '^$' > /tmp/gh-aw/sweep-data/in-scope.txt || true\nfi\n\nwhile IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\ndone < /tmp/gh-aw/sweep-data/in-scope.txt\n\nSHARD_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/shard.txt | tr -d ' ')\nRECENT_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n\n REQUESTED_COUNT=0\n printf '%s' \"$TARGET_FILES\" | tr ',' '\\n' > /tmp/gh-aw/sweep-data/target-files.raw\n while IFS= read -r raw || [ -n \"$raw\" ]; do\n entry=$(printf '%s' \"$raw\" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')\n [ -z \"$entry\" ] && continue\n REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))\n entry=${entry#/}\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n resolved=\"$entry\"\n else\n case \"$entry\" in\n \"$DOCS_ROOT_CLEAN\"/*) resolved=\"$entry\" ;;\n *) resolved=\"$DOCS_ROOT_CLEAN/$entry\" ;;\n esac\n fi\n if [ -f \"$resolved\" ]; then\n echo \"$resolved\" >> /tmp/gh-aw/sweep-data/all.txt\n else\n echo \"target-files: '$resolved' not found; skipping\"\n fi\n done < /tmp/gh-aw/sweep-data/target-files.raw\n\n sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt\n cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt\n\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\n done < /tmp/gh-aw/sweep-data/in-scope.txt\n\n IN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"total\":0,\"shard_n\":1,\"shard_slot\":0,\"shard_count\":0,\"recent_count\":0,\"in_scope_count\":0,\"iso_week\":\"'\"$(date +%G-W%V)\"'\",\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\"}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\nelse\n if [ \"$TOTAL\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n\n : > /tmp/gh-aw/sweep-data/shard.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/shard.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\n\n git log --since='2 days ago' --name-only --pretty=format: -- \"$DOCS_ROOT/*.md\" \"$DOCS_ROOT/**/*.md\" 2>/dev/null \\\n | grep -E '\\.md$' \\\n | sort -u > /tmp/gh-aw/sweep-data/recent.txt || true\n\n # Cap the recently-changed pass: if a corpus-wide rebase or migration\n # touched far more pages than one slice, fall back to slice-only so\n # rotation actually rotates.\n RECENT_RAW=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\n RECENT_LIMIT=$(( TARGET_BATCH * 2 ))\n if [ \"$RECENT_RAW\" -gt \"$RECENT_LIMIT\" ]; then\n echo \"recently-changed pass produced $RECENT_RAW pages (>2x target batch $TARGET_BATCH); disabling for this run\"\n : > /tmp/gh-aw/sweep-data/recent.txt\n fi\nfi\n\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nelse\n sort -u /tmp/gh-aw/sweep-data/shard.txt /tmp/gh-aw/sweep-data/recent.txt \\\n | grep -v '^$' > /tmp/gh-aw/sweep-data/in-scope.txt || true\nfi\n\nwhile IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\ndone < /tmp/gh-aw/sweep-data/in-scope.txt\n\nSHARD_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/shard.txt | tr -d ' ')\nRECENT_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt
+ : > /tmp/gh-aw/sweep-data/shard.txt
+ : > /tmp/gh-aw/sweep-data/recent.txt
+ : > /tmp/gh-aw/sweep-data/in-scope.txt
+
+ REQUESTED_COUNT=0
+ printf '%s' "$TARGET_FILES" | tr ',' '\n' > /tmp/gh-aw/sweep-data/target-files.raw
+ while IFS= read -r raw || [ -n "$raw" ]; do
+ entry=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
+ [ -z "$entry" ] && continue
+ REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))
+ entry=${entry#/}
+ if [ "$DOCS_ROOT_CLEAN" = "." ] || [ -z "$DOCS_ROOT_CLEAN" ]; then
+ resolved="$entry"
+ else
+ case "$entry" in
+ "$DOCS_ROOT_CLEAN"/*) resolved="$entry" ;;
+ *) resolved="$DOCS_ROOT_CLEAN/$entry" ;;
+ esac
+ fi
+ if [ -f "$resolved" ]; then
+ echo "$resolved" >> /tmp/gh-aw/sweep-data/all.txt
+ else
+ echo "target-files: '$resolved' not found; skipping"
+ fi
+ done < /tmp/gh-aw/sweep-data/target-files.raw
+
+ sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt
+ cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt
+
+ while IFS= read -r f; do
+ [ -z "$f" ] && continue
+ [ ! -f "$f" ] && continue
+ mkdir -p "/tmp/gh-aw/sweep-data/scope/$(dirname "$f")"
+ cp "$f" "/tmp/gh-aw/sweep-data/scope/$f"
+ done < /tmp/gh-aw/sweep-data/in-scope.txt
+
+ IN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')
+ cat > /tmp/gh-aw/sweep-data/stats.json < (0 pages)`.
- Shard mode with `target_path`: `Empty subtree shard for / (shard /, 0 pages)`.
- Shard mode without `target_path`: `All files in this rotation are unaudited (shard /, 0 pages)`.
+- Files mode (`selection_mode` is `files`): `Empty file list (0 of requested files found)`.
+
+**Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; audit exactly those files and ignore `target_path`/shard framing. In the title, scope-summary, and `noop` messages, describe the scope as an explicit file list — for example title `file list — pages`, scope-summary `Explicit file list · of requested files in scope.`, and no-findings `noop` `No high-confidence frontmatter issues in the requested file list ( files)`.
## Step 1: Audit the frontmatter
@@ -314,8 +387,9 @@ For each finding, extract:
- `file` — the original repository-relative path (strip the `/tmp/gh-aw/sweep-data/scope/` prefix from any scoped file path).
- `line` — `1` for missing/invalid frontmatter keys (frontmatter starts at line 1); for description-quality findings use the line of the `description:` key.
-- `category` — one of: `missing-description`, `weak-description`, `description-too-long`, `missing-products`, `missing-navigation-title`. **Do not emit `missing-applies-to` or `invalid-applies-to`** — those belong to `gh-aw-docs-applies-to-sweep`. If another source suggests them, drop them silently.
+- `category` — one of: `missing-description`, `weak-description`, `description-too-long`, `missing-products`, `missing-navigation-title`. These five are the complete category allowlist for this sweep (see the **Findings contract**). **Do not emit `missing-applies-to` or `invalid-applies-to`** — those belong to `gh-aw-docs-applies-to-sweep`. If another source suggests them, drop them silently.
- `severity` — `high` for missing required fields; `medium` for weak/long/invalid; `low` for nits.
+- `confidence` — `high`, `medium`, or `low` per the **Findings contract**. Missing required-field findings verified from the file are usually `high`; description-quality rewrites that involve wording judgment are usually `medium`.
- `evidence` — one short sentence quoting or naming the exact problem.
- `suggested_fix` — concrete YAML snippet ready to paste into the file's frontmatter when you can produce one confidently. For audit-only findings, or a missing field with no verified value, omit `suggested_fix`.
@@ -364,6 +438,7 @@ Use one of these scope-summary lines:
line: 1
category: missing-description
severity: high
+ confidence: high
evidence: "frontmatter has no `description` field"
suggested_fix: |
description: "How to configure X for Y use cases."
@@ -371,6 +446,7 @@ Use one of these scope-summary lines:
line: 1
category: weak-description
severity: medium
+ confidence: medium
evidence: "description is generic ('Learn about X')"
suggested_fix: |
description: ""
@@ -386,7 +462,7 @@ Use one of these scope-summary lines:
```
-Keep the YAML block parseable — every entry must have `file`, `line`, `category`, `severity`, `evidence`. Use the literal `|` block scalar for multi-line `suggested_fix` values. Do not include comments inside the YAML block.
+Keep the YAML block parseable — every entry must have `file`, `line`, `category`, `severity`, `confidence`, `evidence`. Use the literal `|` block scalar for multi-line `suggested_fix` values. Do not include comments inside the YAML block.
## What to skip
diff --git a/.github/workflows/gh-aw-docs-issue-scope.lock.yml b/.github/workflows/gh-aw-docs-issue-scope.lock.yml
index 247ab9d..2ca8e7b 100644
--- a/.github/workflows/gh-aw-docs-issue-scope.lock.yml
+++ b/.github/workflows/gh-aw-docs-issue-scope.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4f56d7ca9e8462584cb2cb6f78e08ec4f3bd63db39820ddb492c62af16f87928","body_hash":"6548a734e96cbe7fc5a24cb3276bb9dbce6ec2d7c69272cada451bc0e5f5e87c","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"2d66adf6a27245a325f294e97bae422675be34a7ba885b9dea2c5a4d9e45b2de","body_hash":"a00eff1cc7139ab210a9a703d6a655ebd325755bb492823388ebdec42e54e5b9","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_PLUGINS_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"},{"repo":"microsoft/apm-action","sha":"b48dd081eb0050f6d7f32d0e7caa0a59a2d419fd","version":"v1.7.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}
# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -302,20 +302,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_493b3278c5c97a40_EOF'
+ cat << 'GH_AW_PROMPT_deacf603f00727d0_EOF'
- GH_AW_PROMPT_493b3278c5c97a40_EOF
+ GH_AW_PROMPT_deacf603f00727d0_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_493b3278c5c97a40_EOF'
+ cat << 'GH_AW_PROMPT_deacf603f00727d0_EOF'
Tools: add_comment, update_issue, missing_tool, missing_data, noop
- GH_AW_PROMPT_493b3278c5c97a40_EOF
+ GH_AW_PROMPT_deacf603f00727d0_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
- cat << 'GH_AW_PROMPT_493b3278c5c97a40_EOF'
+ cat << 'GH_AW_PROMPT_deacf603f00727d0_EOF'
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -344,9 +344,9 @@ jobs:
{{/if}}
- GH_AW_PROMPT_493b3278c5c97a40_EOF
+ GH_AW_PROMPT_deacf603f00727d0_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_493b3278c5c97a40_EOF'
+ cat << 'GH_AW_PROMPT_deacf603f00727d0_EOF'
## Formatting Guidelines
@@ -531,11 +531,13 @@ jobs:
- **Review only**
- **No action**
- Also assign a confidence level:
+ Also assign a confidence level to every recommendation. Because this workflow runs with `min-integrity: none` — it must read issues and PRs from public contributors — treat the issue text and linked descriptions as *unverified input*, not as trusted fact:
- - **High** — strong evidence from the issue, linked code, and existing docs structure
- - **Medium** — likely correct, but some ambiguity remains
- - **Low** — tentative recommendation based on partial evidence
+ - **High** — cross-checked evidence: the linked code, the existing docs structure, and the issue text agree, and any product terminology or feature name is confirmed against the code or the published docs.
+ - **Medium** — likely correct, but some ambiguity remains, or part of the evidence could not be cross-checked.
+ - **Low** — tentative: based on partial evidence, or resting on a claim, feature name, or terminology that appears only in the issue or PR description and could not be verified against the code or published docs.
+
+ Never restate unverified terminology as established fact. When a term or capability comes only from the issue or PR author, attribute it ("the issue describes a *reporting capability*") rather than asserting it, and mark that recommendation **Low**. Propagating a contributor's incorrect phrasing into a docs recommendation is the failure this confidence signal exists to prevent.
## Step 4: Publish findings
@@ -589,6 +591,10 @@ jobs:
```
+ If any recommendation in the table is **Low** confidence, add this caveat line immediately under the table so a reader (or a downstream agent) does not act on it unverified:
+
+ > ⚠️ Low-confidence rows rest on claims or terminology from the issue or linked PR that could not be verified against the code or published docs. Confirm before acting.
+
Keep the managed block concise. Avoid long per-page writeups. Prefer short tables, short recommendations, and brief notes that mention content-type fit or section role only when it materially affects the recommendation.
On reruns for the same issue, update the existing managed block in place. Do not append a second copy when the issue body already contains one valid `docs-issue-scope:start` / `docs-issue-scope:end` marker pair.
@@ -639,7 +645,7 @@ jobs:
__GH_AW_EXPR_49B959F1__
- GH_AW_PROMPT_493b3278c5c97a40_EOF
+ GH_AW_PROMPT_deacf603f00727d0_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
diff --git a/.github/workflows/gh-aw-docs-issue-scope.md b/.github/workflows/gh-aw-docs-issue-scope.md
index 4ec7820..dff91c4 100644
--- a/.github/workflows/gh-aw-docs-issue-scope.md
+++ b/.github/workflows/gh-aw-docs-issue-scope.md
@@ -220,11 +220,13 @@ For each recommendation, classify the action as one of:
- **Review only**
- **No action**
-Also assign a confidence level:
+Also assign a confidence level to every recommendation. Because this workflow runs with `min-integrity: none` — it must read issues and PRs from public contributors — treat the issue text and linked descriptions as *unverified input*, not as trusted fact:
-- **High** — strong evidence from the issue, linked code, and existing docs structure
-- **Medium** — likely correct, but some ambiguity remains
-- **Low** — tentative recommendation based on partial evidence
+- **High** — cross-checked evidence: the linked code, the existing docs structure, and the issue text agree, and any product terminology or feature name is confirmed against the code or the published docs.
+- **Medium** — likely correct, but some ambiguity remains, or part of the evidence could not be cross-checked.
+- **Low** — tentative: based on partial evidence, or resting on a claim, feature name, or terminology that appears only in the issue or PR description and could not be verified against the code or published docs.
+
+Never restate unverified terminology as established fact. When a term or capability comes only from the issue or PR author, attribute it ("the issue describes a *reporting capability*") rather than asserting it, and mark that recommendation **Low**. Propagating a contributor's incorrect phrasing into a docs recommendation is the failure this confidence signal exists to prevent.
## Step 4: Publish findings
@@ -278,6 +280,10 @@ Use this exact body-block format inside the managed markers:
```
+If any recommendation in the table is **Low** confidence, add this caveat line immediately under the table so a reader (or a downstream agent) does not act on it unverified:
+
+> ⚠️ Low-confidence rows rest on claims or terminology from the issue or linked PR that could not be verified against the code or published docs. Confirm before acting.
+
Keep the managed block concise. Avoid long per-page writeups. Prefer short tables, short recommendations, and brief notes that mention content-type fit or section role only when it materially affects the recommendation.
On reruns for the same issue, update the existing managed block in place. Do not append a second copy when the issue body already contains one valid `docs-issue-scope:start` / `docs-issue-scope:end` marker pair.
diff --git a/.github/workflows/gh-aw-docs-openings-sweep.lock.yml b/.github/workflows/gh-aw-docs-openings-sweep.lock.yml
index b65eb66..9a1911c 100644
--- a/.github/workflows/gh-aw-docs-openings-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-openings-sweep.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"68373b56b144265044bb8053b7d6d6e0cce26d9c8152f3758e07543c83975469","body_hash":"87a8e2d287582f10b25f2056ff38e3efd0c9933a39eccfe202b182821710f00a","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e3b02c470e4dbfa867bb0faa206ee3d5896658a636029b38ee5b54dac43824ff","body_hash":"23356929aabe8abaecd6bf74357d2adc16812d128cfc71d5acc9cab341d01091","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_PLUGINS_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"},{"repo":"microsoft/apm-action","sha":"b48dd081eb0050f6d7f32d0e7caa0a59a2d419fd","version":"v1.7.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}
# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -30,6 +30,7 @@
#
# Resolved workflow manifest:
# Imports:
+# - gh-aw-fragments/findings-contract.md
# - gh-aw-fragments/formatting.md
# - gh-aw-fragments/mcp-pagination.md
# - gh-aw-fragments/rigor.md
@@ -110,6 +111,11 @@ on:
description: Approximate pages per rotating slice; controls shard count N = ceil(total/batch-size)
required: false
type: string
+ target-files:
+ default: ""
+ description: "Optional newline- or comma-separated list of docs-root-relative file paths to sweep. When set, overrides target-path and scope-mode: the sweep processes exactly these files."
+ required: false
+ type: string
target-path:
default: ""
description: Optional docs-root-relative directory to sweep recursively. Accepts a leading slash.
@@ -335,20 +341,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_0b576116a652cb19_EOF'
+ cat << 'GH_AW_PROMPT_e14140b6fc216bfa_EOF'
- GH_AW_PROMPT_0b576116a652cb19_EOF
+ GH_AW_PROMPT_e14140b6fc216bfa_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_0b576116a652cb19_EOF'
+ cat << 'GH_AW_PROMPT_e14140b6fc216bfa_EOF'
Tools: create_issue, missing_tool, missing_data, noop
- GH_AW_PROMPT_0b576116a652cb19_EOF
+ GH_AW_PROMPT_e14140b6fc216bfa_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
- cat << 'GH_AW_PROMPT_0b576116a652cb19_EOF'
+ cat << 'GH_AW_PROMPT_e14140b6fc216bfa_EOF'
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -377,9 +383,9 @@ jobs:
{{/if}}
- GH_AW_PROMPT_0b576116a652cb19_EOF
+ GH_AW_PROMPT_e14140b6fc216bfa_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_0b576116a652cb19_EOF'
+ cat << 'GH_AW_PROMPT_e14140b6fc216bfa_EOF'
## Formatting Guidelines
@@ -420,6 +426,42 @@ jobs:
If you see `MCP tool response exceeds maximum allowed tokens`, retry with a smaller `perPage` value (halve it).
+ ## Findings contract
+
+ These rules apply to every finding this sweep emits. Sweep output is consumed by humans and, increasingly, by AI fix-agents that may act on it without a human in the loop. A finding that is uncertain, or that looks authoritative but is wrong, is worse than no finding at all.
+
+ ### Finding-type allowlist
+
+ Emit only the `category` values enumerated in this workflow's "Build the findings list" step. That enumeration is a closed allowlist:
+
+ - Never invent, rename, pluralize, or otherwise vary a category string. If a finding does not map cleanly to an allowlisted category, drop it.
+ - A finding is valid only if applying its `suggested_fix` would change the page's rendered output or its published metadata. Drop no-op findings whose fix a reader would never see — for example, adding a marker the docs toolchain already generates automatically.
+ - If you spot something real that has no allowlisted category, describe it in the issue body's **Notes** section as prose. Do not smuggle it in as a finding under an invented category.
+
+ ### Per-finding confidence
+
+ Add a `confidence` field to every finding, set to exactly one of `high`, `medium`, or `low`. Judge confidence on how safe the finding is to act on *without* human verification — this is a separate axis from `severity`, which measures impact:
+
+ - `high` — the problem and the fix are objective and verifiable from the evidence in front of you: a missing required field, a tool-flagged issue with a single unambiguous correction, a directly quoted contradiction. A fix-agent could apply the `suggested_fix` verbatim without judgment.
+ - `medium` — the finding is well-supported, but the fix involves wording choices, or depends on a repository convention you could not fully verify this run. A human should confirm the fix before it lands.
+ - `low` — the finding is plausible but rests on partial evidence, subjective judgment, or an assumption about intent or convention you could not confirm. If you cannot justify at least `low`, drop the finding rather than filing it.
+
+ When a finding's evidence traces back to text you did not verify — for example, terminology copied from an issue or PR description rather than confirmed against the code or the published docs — cap its confidence at `low` and say so in the `evidence`.
+
+ Include `confidence` in the YAML schema for every finding, alongside `severity`. Keep the existing sort order (by `severity` first); do not reorder by confidence.
+
+ ### Human-review gate
+
+ If the capped findings list contains **any** finding with `confidence: medium` or `confidence: low`:
+
+ 1. Add the label `needs-human-review` to your `create_issue` call, in addition to the labels the workflow adds automatically. This marks the issue as not safe to auto-action and keeps it out of the `good-for-ai` delegation track.
+ 2. Immediately below the `## Findings ()` heading and before the YAML block, add this callout verbatim:
+
+ > [!WARNING]
+ > This issue contains medium- or low-confidence findings. Review them before acting — auto-applying sweep output without verification risks putting incorrect content into the docs. Findings marked `confidence: high` are safe to delegate to a fix-agent; `medium` and `low` need a human sign-off first.
+
+ If every finding is `confidence: high`, do not add the label or the callout: the issue is safe to delegate as-is.
+
# Docs page-openings sweep agent
You are a page-opening reviewer for an Elastic documentation repository. Your job is to audit the opening of a deterministically-selected slice of pages — H1 specificity, opening paragraph quality, and "Before you begin" appropriateness — and emit a single labeled fix-issue with structured findings.
@@ -455,7 +497,6 @@ jobs:
- The page should have exactly one clear H1 near the top after frontmatter.
- The H1 should be discoverable, specific, unique, and include product, feature, or task context. Generic titles such as "Overview", "Introduction", "Guide", "Configuration", or "Settings" are findings only when the surrounding page does not make the topic clear in the heading itself.
- Use content-type-appropriate H1 patterns: tutorials often start with "Get started with...", how-to pages use action verbs such as "Configure..." or "Troubleshoot...", reference pages use labels such as "[Feature] settings" or "[API] reference", explanation pages can use "How [feature] works", and overview pages can use the feature name when the page is a landing page.
- - If nearby pages in the same docs area consistently use explicit anchor suffixes in H1s, flag a missing H1 anchor on pages that violate that local convention.
- The opening paragraph should immediately follow the H1 unless an important or warning admonition must remain first. It should explain what the page covers within the first two sentences, front-load the important information, and convey purpose, value, and scope in 2-4 complete sentences.
- The opening should not repeat the frontmatter `description`, duplicate the next paragraph, use fragments instead of sentences, or bury the page purpose after a long setup.
- Tutorials should define the feature, explain how it works, and state what the tutorial covers. How-to pages should define the feature or task, explain what it does, and state the value. Reference pages should define the subject and state its purpose. Explanation pages should establish context and state the concepts covered. Overview pages should state what the feature is, its value, and key capabilities.
@@ -492,16 +533,17 @@ jobs:
- `missing-h1` — file has no `# Heading` line.
- `vague-h1` — H1 is generic ("Overview", "Introduction", "Guide", "About") without product/feature context, or is a common word that doesn't indicate the page topic.
- - `missing-h1-anchor` — H1 lacks the `[anchor-id]` suffix where the repo's convention requires one.
- `weak-opening` — opening paragraph is empty, exceeds 4 sentences, or fails to convey what the page covers within the first 2 sentences.
- `missing-before-you-begin` — task/how-to page that omits a prerequisites section even though the steps require prior access, permissions, setup, sample data, or product state.
- `inadequate-navigation-title` — `navigation_title` is missing or duplicates the H1 verbatim when a shorter form is needed.
+ These five are the complete category allowlist for this sweep (see the **Findings contract**). In particular, do not emit a "missing H1 anchor" finding: docs-builder auto-generates a default anchor for every heading, and an H1 is unique per page, so adding a custom `[anchor-id]` suffix to an H1 changes nothing a reader or a link ever sees.
+
For each finding extract:
- `file` — repo-relative path (strip `/tmp/gh-aw/sweep-data/scope/`).
- `line` — line number of the affected element in the original file (H1 line for H1 findings, opening-paragraph start for opening findings, `navigation_title:` line for nav-title findings).
- - `category`, `severity` (`high` for missing/vague-H1; `medium` for weak-opening; `low` for nav-title nits), `evidence`, `suggested_fix`.
+ - `category`, `severity` (`high` for missing/vague-H1; `medium` for weak-opening; `low` for nav-title nits), `confidence` (per the **Findings contract**), `evidence`, `suggested_fix`.
- `suggested_fix` should be a concrete replacement (e.g., a one-line H1, a 2–4 sentence opening paragraph, or a YAML snippet for navigation_title).
## Step 3: Sort and cap
@@ -514,6 +556,9 @@ jobs:
- Shard mode with `target_path`: `"No high-confidence opening issues under / in shard / ( pages)"`.
- Full mode with `target_path`: `"No high-confidence opening issues under / ( pages)"`.
- Full mode without `target_path`: `"No high-confidence opening issues in full sweep ( pages)"`.
+ - Files mode: `"No high-confidence opening issues in the requested file list ( files)"`.
+
+ **Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; audit exactly those files and ignore `target_path`/shard framing. Use an explicit-file-list description in the title (`file list — pages`) and scope-summary (`Explicit file list · of requested files in scope.`).
**Drop vague `suggested_fix` values**: do not emit `suggested_fix` if the only thing you can produce is generic prose like "improve clarity" or "consider rewording". Either propose a concrete replacement or omit the `suggested_fix` field entirely — vague advice wastes an author's time.
@@ -545,13 +590,15 @@ jobs:
line: 7
category: vague-h1
severity: high
+ confidence: medium
evidence: "H1 is 'Overview' — no product or feature context"
suggested_fix: |
- # Configure data views in Kibana [configure-data-views]
+ # Configure data views in Kibana
- file: docs/bar.md
line: 9
category: weak-opening
severity: medium
+ confidence: medium
evidence: "first paragraph is one sentence and does not say what the page covers"
suggested_fix: |
Data views are saved searches that point to one or more indices and define
@@ -579,7 +626,7 @@ jobs:
__GH_AW_EXPR_49B959F1__
- GH_AW_PROMPT_0b576116a652cb19_EOF
+ GH_AW_PROMPT_e14140b6fc216bfa_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -765,9 +812,10 @@ jobs:
DOCS_ROOT: ${{ inputs.docs-root }}
SCOPE_MODE: ${{ inputs.scope-mode }}
TARGET_BATCH: ${{ inputs.target-batch-size }}
+ TARGET_FILES: ${{ inputs.target-files }}
TARGET_PATH: ${{ inputs.target-path }}
name: Compute sweep targets
- run: "set -eu\nmkdir -p /tmp/gh-aw/sweep-data/scope\n\nTARGET_PATH_CLEAN=${TARGET_PATH#/}\nTARGET_PATH_CLEAN=${TARGET_PATH_CLEAN%/}\nDOCS_ROOT_CLEAN=${DOCS_ROOT%/}\nSCOPE_ROOT=\"$DOCS_ROOT\"\nREQUESTED_SCOPE_MODE=\"$SCOPE_MODE\"\nSELECTION_MODE=\"shard\"\n\ncase \"$REQUESTED_SCOPE_MODE\" in\n auto|full|shard) ;;\n *)\n echo \"scope-mode '$REQUESTED_SCOPE_MODE' must be one of: auto, full, shard\"\n exit 1\n ;;\nesac\n\nif [ -n \"$TARGET_PATH_CLEAN\" ]; then\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n SCOPE_ROOT=\"$TARGET_PATH_CLEAN\"\n else\n SCOPE_ROOT=\"$DOCS_ROOT_CLEAN/$TARGET_PATH_CLEAN\"\n fi\nfi\n\nif [ \"$REQUESTED_SCOPE_MODE\" = \"auto\" ]; then\n if [ -n \"$TARGET_PATH_CLEAN\" ]; then\n SELECTION_MODE=\"full\"\n else\n SELECTION_MODE=\"shard\"\n fi\nelse\n SELECTION_MODE=\"$REQUESTED_SCOPE_MODE\"\nfi\n\nif [ ! -d \"$SCOPE_ROOT\" ]; then\n echo \"scope root '$SCOPE_ROOT' does not exist; producing empty scope\"\n : > /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"total\":0,\"shard_n\":1,\"shard_slot\":0,\"shard_count\":0,\"recent_count\":0,\"in_scope_count\":0,\"iso_week\":\"'\"$(date +%G-W%V)\"'\",\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\"}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\nelse\n if [ \"$TOTAL\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n\n : > /tmp/gh-aw/sweep-data/shard.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/shard.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\n\n git log --since='2 days ago' --name-only --pretty=format: -- \"$DOCS_ROOT/*.md\" \"$DOCS_ROOT/**/*.md\" 2>/dev/null \\\n | grep -E '\\.md$' \\\n | sort -u > /tmp/gh-aw/sweep-data/recent.txt || true\n\n # Cap the recently-changed pass: if a corpus-wide rebase or migration\n # touched far more pages than one slice, fall back to slice-only so\n # rotation actually rotates.\n RECENT_RAW=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\n RECENT_LIMIT=$(( TARGET_BATCH * 2 ))\n if [ \"$RECENT_RAW\" -gt \"$RECENT_LIMIT\" ]; then\n echo \"recently-changed pass produced $RECENT_RAW pages (>2x target batch $TARGET_BATCH); disabling for this run\"\n : > /tmp/gh-aw/sweep-data/recent.txt\n fi\nfi\n\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nelse\n sort -u /tmp/gh-aw/sweep-data/shard.txt /tmp/gh-aw/sweep-data/recent.txt \\\n | grep -v '^$' > /tmp/gh-aw/sweep-data/in-scope.txt || true\nfi\n\nwhile IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\ndone < /tmp/gh-aw/sweep-data/in-scope.txt\n\nSHARD_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/shard.txt | tr -d ' ')\nRECENT_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n\n REQUESTED_COUNT=0\n printf '%s' \"$TARGET_FILES\" | tr ',' '\\n' > /tmp/gh-aw/sweep-data/target-files.raw\n while IFS= read -r raw || [ -n \"$raw\" ]; do\n entry=$(printf '%s' \"$raw\" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')\n [ -z \"$entry\" ] && continue\n REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))\n entry=${entry#/}\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n resolved=\"$entry\"\n else\n case \"$entry\" in\n \"$DOCS_ROOT_CLEAN\"/*) resolved=\"$entry\" ;;\n *) resolved=\"$DOCS_ROOT_CLEAN/$entry\" ;;\n esac\n fi\n if [ -f \"$resolved\" ]; then\n echo \"$resolved\" >> /tmp/gh-aw/sweep-data/all.txt\n else\n echo \"target-files: '$resolved' not found; skipping\"\n fi\n done < /tmp/gh-aw/sweep-data/target-files.raw\n\n sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt\n cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt\n\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\n done < /tmp/gh-aw/sweep-data/in-scope.txt\n\n IN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"total\":0,\"shard_n\":1,\"shard_slot\":0,\"shard_count\":0,\"recent_count\":0,\"in_scope_count\":0,\"iso_week\":\"'\"$(date +%G-W%V)\"'\",\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\"}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\nelse\n if [ \"$TOTAL\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n\n : > /tmp/gh-aw/sweep-data/shard.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/shard.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\n\n git log --since='2 days ago' --name-only --pretty=format: -- \"$DOCS_ROOT/*.md\" \"$DOCS_ROOT/**/*.md\" 2>/dev/null \\\n | grep -E '\\.md$' \\\n | sort -u > /tmp/gh-aw/sweep-data/recent.txt || true\n\n # Cap the recently-changed pass: if a corpus-wide rebase or migration\n # touched far more pages than one slice, fall back to slice-only so\n # rotation actually rotates.\n RECENT_RAW=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\n RECENT_LIMIT=$(( TARGET_BATCH * 2 ))\n if [ \"$RECENT_RAW\" -gt \"$RECENT_LIMIT\" ]; then\n echo \"recently-changed pass produced $RECENT_RAW pages (>2x target batch $TARGET_BATCH); disabling for this run\"\n : > /tmp/gh-aw/sweep-data/recent.txt\n fi\nfi\n\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nelse\n sort -u /tmp/gh-aw/sweep-data/shard.txt /tmp/gh-aw/sweep-data/recent.txt \\\n | grep -v '^$' > /tmp/gh-aw/sweep-data/in-scope.txt || true\nfi\n\nwhile IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\ndone < /tmp/gh-aw/sweep-data/in-scope.txt\n\nSHARD_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/shard.txt | tr -d ' ')\nRECENT_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt
+ : > /tmp/gh-aw/sweep-data/shard.txt
+ : > /tmp/gh-aw/sweep-data/recent.txt
+ : > /tmp/gh-aw/sweep-data/in-scope.txt
+
+ REQUESTED_COUNT=0
+ printf '%s' "$TARGET_FILES" | tr ',' '\n' > /tmp/gh-aw/sweep-data/target-files.raw
+ while IFS= read -r raw || [ -n "$raw" ]; do
+ entry=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
+ [ -z "$entry" ] && continue
+ REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))
+ entry=${entry#/}
+ if [ "$DOCS_ROOT_CLEAN" = "." ] || [ -z "$DOCS_ROOT_CLEAN" ]; then
+ resolved="$entry"
+ else
+ case "$entry" in
+ "$DOCS_ROOT_CLEAN"/*) resolved="$entry" ;;
+ *) resolved="$DOCS_ROOT_CLEAN/$entry" ;;
+ esac
+ fi
+ if [ -f "$resolved" ]; then
+ echo "$resolved" >> /tmp/gh-aw/sweep-data/all.txt
+ else
+ echo "target-files: '$resolved' not found; skipping"
+ fi
+ done < /tmp/gh-aw/sweep-data/target-files.raw
+
+ sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt
+ cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt
+
+ while IFS= read -r f; do
+ [ -z "$f" ] && continue
+ [ ! -f "$f" ] && continue
+ mkdir -p "/tmp/gh-aw/sweep-data/scope/$(dirname "$f")"
+ cp "$f" "/tmp/gh-aw/sweep-data/scope/$f"
+ done < /tmp/gh-aw/sweep-data/in-scope.txt
+
+ IN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')
+ cat > /tmp/gh-aw/sweep-data/stats.json < in shard / ( pages)"`.
- Full mode with `target_path`: `"No high-confidence opening issues under / ( pages)"`.
- Full mode without `target_path`: `"No high-confidence opening issues in full sweep ( pages)"`.
+- Files mode: `"No high-confidence opening issues in the requested file list ( files)"`.
+
+**Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; audit exactly those files and ignore `target_path`/shard framing. Use an explicit-file-list description in the title (`file list — pages`) and scope-summary (`Explicit file list · of requested files in scope.`).
**Drop vague `suggested_fix` values**: do not emit `suggested_fix` if the only thing you can produce is generic prose like "improve clarity" or "consider rewording". Either propose a concrete replacement or omit the `suggested_fix` field entirely — vague advice wastes an author's time.
@@ -379,13 +452,15 @@ Use one of these scope-summary lines:
line: 7
category: vague-h1
severity: high
+ confidence: medium
evidence: "H1 is 'Overview' — no product or feature context"
suggested_fix: |
- # Configure data views in Kibana [configure-data-views]
+ # Configure data views in Kibana
- file: docs/bar.md
line: 9
category: weak-opening
severity: medium
+ confidence: medium
evidence: "first paragraph is one sentence and does not say what the page covers"
suggested_fix: |
Data views are saved searches that point to one or more indices and define
diff --git a/.github/workflows/gh-aw-docs-staleness-sweep.lock.yml b/.github/workflows/gh-aw-docs-staleness-sweep.lock.yml
index 00eef42..8c509c4 100644
--- a/.github/workflows/gh-aw-docs-staleness-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-staleness-sweep.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"77dc5a1966e2dc37609bf96ca1aaf388f9b750f96f066a16e6cba0e4348a890b","body_hash":"e03bc1ff2c849e9d17e5ff1879c0670cf07e1c1cb2e9d59019ada0509faffed8","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"1f8d0a4b0f033c187e02f2605ca5bf765deb8310f82a5177dd781a3847b9165c","body_hash":"48905234cf56371180cc394cc8a67d310ac77174daf01424a9fa2f8a441d0d93","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}
# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -31,6 +31,7 @@
#
# Resolved workflow manifest:
# Imports:
+# - gh-aw-fragments/findings-contract.md
# - gh-aw-fragments/formatting.md
# - gh-aw-fragments/mcp-pagination.md
# - gh-aw-fragments/rigor.md
@@ -122,6 +123,11 @@ on:
description: Approximate pages per rotating slice; controls shard count N = ceil(total/batch-size)
required: false
type: string
+ target-files:
+ default: ""
+ description: "Optional newline- or comma-separated list of docs-root-relative file paths to sweep. When set, overrides target-path and scope-mode: the sweep processes exactly these files."
+ required: false
+ type: string
target-path:
default: ""
description: Optional docs-root-relative directory to sweep recursively. Accepts a leading slash.
@@ -345,20 +351,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_859500b3fa1848bf_EOF'
+ cat << 'GH_AW_PROMPT_33df8da1c518607c_EOF'
- GH_AW_PROMPT_859500b3fa1848bf_EOF
+ GH_AW_PROMPT_33df8da1c518607c_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_859500b3fa1848bf_EOF'
+ cat << 'GH_AW_PROMPT_33df8da1c518607c_EOF'
Tools: create_issue, missing_tool, missing_data, noop
- GH_AW_PROMPT_859500b3fa1848bf_EOF
+ GH_AW_PROMPT_33df8da1c518607c_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
- cat << 'GH_AW_PROMPT_859500b3fa1848bf_EOF'
+ cat << 'GH_AW_PROMPT_33df8da1c518607c_EOF'
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -387,9 +393,9 @@ jobs:
{{/if}}
- GH_AW_PROMPT_859500b3fa1848bf_EOF
+ GH_AW_PROMPT_33df8da1c518607c_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_859500b3fa1848bf_EOF'
+ cat << 'GH_AW_PROMPT_33df8da1c518607c_EOF'
## Formatting Guidelines
@@ -430,6 +436,42 @@ jobs:
If you see `MCP tool response exceeds maximum allowed tokens`, retry with a smaller `perPage` value (halve it).
+ ## Findings contract
+
+ These rules apply to every finding this sweep emits. Sweep output is consumed by humans and, increasingly, by AI fix-agents that may act on it without a human in the loop. A finding that is uncertain, or that looks authoritative but is wrong, is worse than no finding at all.
+
+ ### Finding-type allowlist
+
+ Emit only the `category` values enumerated in this workflow's "Build the findings list" step. That enumeration is a closed allowlist:
+
+ - Never invent, rename, pluralize, or otherwise vary a category string. If a finding does not map cleanly to an allowlisted category, drop it.
+ - A finding is valid only if applying its `suggested_fix` would change the page's rendered output or its published metadata. Drop no-op findings whose fix a reader would never see — for example, adding a marker the docs toolchain already generates automatically.
+ - If you spot something real that has no allowlisted category, describe it in the issue body's **Notes** section as prose. Do not smuggle it in as a finding under an invented category.
+
+ ### Per-finding confidence
+
+ Add a `confidence` field to every finding, set to exactly one of `high`, `medium`, or `low`. Judge confidence on how safe the finding is to act on *without* human verification — this is a separate axis from `severity`, which measures impact:
+
+ - `high` — the problem and the fix are objective and verifiable from the evidence in front of you: a missing required field, a tool-flagged issue with a single unambiguous correction, a directly quoted contradiction. A fix-agent could apply the `suggested_fix` verbatim without judgment.
+ - `medium` — the finding is well-supported, but the fix involves wording choices, or depends on a repository convention you could not fully verify this run. A human should confirm the fix before it lands.
+ - `low` — the finding is plausible but rests on partial evidence, subjective judgment, or an assumption about intent or convention you could not confirm. If you cannot justify at least `low`, drop the finding rather than filing it.
+
+ When a finding's evidence traces back to text you did not verify — for example, terminology copied from an issue or PR description rather than confirmed against the code or the published docs — cap its confidence at `low` and say so in the `evidence`.
+
+ Include `confidence` in the YAML schema for every finding, alongside `severity`. Keep the existing sort order (by `severity` first); do not reorder by confidence.
+
+ ### Human-review gate
+
+ If the capped findings list contains **any** finding with `confidence: medium` or `confidence: low`:
+
+ 1. Add the label `needs-human-review` to your `create_issue` call, in addition to the labels the workflow adds automatically. This marks the issue as not safe to auto-action and keeps it out of the `good-for-ai` delegation track.
+ 2. Immediately below the `## Findings ()` heading and before the YAML block, add this callout verbatim:
+
+ > [!WARNING]
+ > This issue contains medium- or low-confidence findings. Review them before acting — auto-applying sweep output without verification risks putting incorrect content into the docs. Findings marked `confidence: high` are safe to delegate to a fix-agent; `medium` and `low` need a human sign-off first.
+
+ If every finding is `confidence: high`, do not add the label or the callout: the issue is safe to delegate as-is.
+
# Docs staleness sweep agent
You are a staleness reviewer for an Elastic documentation repository. Your job is to flag pages, screenshots, external links, and version mentions that have likely gone out of date — and emit a single labeled fix-issue with structured findings.
@@ -457,7 +499,7 @@ jobs:
- **`broken-external-link`** — every entry in `fail_map`. `file` is the source markdown; `line` is the line number from lychee (if present); `evidence` is the URL plus the failure reason.
- Group all of these into the findings list directly; no LLM judgment needed. Apply the Rigor standards — drop anything where the evidence isn't concrete (a missing `line`, an ambiguous file path, etc.) rather than guessing.
+ Group all of these into the findings list directly; no LLM judgment needed. Apply the Rigor standards — drop anything where the evidence isn't concrete (a missing `line`, an ambiguous file path, etc.) rather than guessing. These three categories are deterministic — set `confidence: high` on every one (see the **Findings contract**).
## Step 2: Add version-mention findings via MCP
@@ -471,7 +513,7 @@ jobs:
- The version mentioned is below the published "supported" range (i.e., end-of-life), AND
- The page is not itself a release-notes / changelog / upgrade-from-old-version page (those legitimately reference EOL versions).
- Category: `unsupported-version-mention`. `evidence` cites the version token and the support-matrix source.
+ Category: `unsupported-version-mention`. `evidence` cites the version token and the support-matrix source. Set `confidence: medium` when the support-matrix evidence is unambiguous, `low` when the page context leaves doubt (per the **Findings contract**); this is the only non-deterministic category this sweep emits. The complete category allowlist is `stale-content`, `stale-screenshot`, `broken-external-link`, and `unsupported-version-mention` — never emit any other category string.
Do not invent versions or speculate about the support matrix when the MCP server doesn't return a clean answer. Skip the finding instead.
@@ -491,6 +533,9 @@ jobs:
- Shard mode with `target_path`: `"No staleness findings under / in shard / ( pages)"`.
- Full mode with `target_path`: `"No staleness findings under / ( pages)"`.
- Full mode without `target_path`: `"No staleness findings in full sweep ( pages)"`.
+ - Files mode: `"No staleness findings in the requested file list ( files)"`.
+
+ **Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; audit exactly those files and ignore `target_path`/shard framing. Use an explicit-file-list description in the title (`file list — findings`) and scope-summary (`Explicit file list · of requested files in scope.`).
## Output: fix-issue body
@@ -519,6 +564,7 @@ jobs:
line: 1
category: stale-content
severity: medium
+ confidence: high
evidence: "last commit 2023-01-15 (38 months ago); threshold is 24 months"
suggested_fix: |
review and refresh; either update content or move to archive
@@ -526,6 +572,7 @@ jobs:
line: 17
category: stale-screenshot
severity: low
+ confidence: high
evidence: "image images/bar-ui.png last modified 12 months before page; gap exceeds 6-month threshold"
suggested_fix: |
re-capture screenshot reflecting the current UI
@@ -533,6 +580,7 @@ jobs:
line: 42
category: broken-external-link
severity: high
+ confidence: high
evidence: "https://example.com/old-spec returned 404 (lychee)"
suggested_fix: |
replace link or cite a current source
@@ -540,6 +588,7 @@ jobs:
line: 9
category: unsupported-version-mention
severity: medium
+ confidence: medium
evidence: "references Elastic Stack 7.17 — past EOL per support matrix at https://www.elastic.co/support/eol"
suggested_fix: |
update to a currently-supported version or move guidance to upgrade docs
@@ -568,7 +617,7 @@ jobs:
__GH_AW_EXPR_49B959F1__
- GH_AW_PROMPT_859500b3fa1848bf_EOF
+ GH_AW_PROMPT_33df8da1c518607c_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -741,9 +790,10 @@ jobs:
DOCS_ROOT: ${{ inputs.docs-root }}
SCOPE_MODE: ${{ inputs.scope-mode }}
TARGET_BATCH: ${{ inputs.target-batch-size }}
+ TARGET_FILES: ${{ inputs.target-files }}
TARGET_PATH: ${{ inputs.target-path }}
name: Compute sweep targets
- run: "set -eu\nmkdir -p /tmp/gh-aw/sweep-data/scope\n\nTARGET_PATH_CLEAN=${TARGET_PATH#/}\nTARGET_PATH_CLEAN=${TARGET_PATH_CLEAN%/}\nDOCS_ROOT_CLEAN=${DOCS_ROOT%/}\nSCOPE_ROOT=\"$DOCS_ROOT\"\nREQUESTED_SCOPE_MODE=\"$SCOPE_MODE\"\nSELECTION_MODE=\"shard\"\n\ncase \"$REQUESTED_SCOPE_MODE\" in\n auto|full|shard) ;;\n *)\n echo \"scope-mode '$REQUESTED_SCOPE_MODE' must be one of: auto, full, shard\"\n exit 1\n ;;\nesac\n\nif [ -n \"$TARGET_PATH_CLEAN\" ]; then\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n SCOPE_ROOT=\"$TARGET_PATH_CLEAN\"\n else\n SCOPE_ROOT=\"$DOCS_ROOT_CLEAN/$TARGET_PATH_CLEAN\"\n fi\nfi\n\nif [ \"$REQUESTED_SCOPE_MODE\" = \"auto\" ]; then\n if [ -n \"$TARGET_PATH_CLEAN\" ]; then\n SELECTION_MODE=\"full\"\n else\n SELECTION_MODE=\"shard\"\n fi\nelse\n SELECTION_MODE=\"$REQUESTED_SCOPE_MODE\"\nfi\n\nif [ ! -d \"$SCOPE_ROOT\" ]; then\n echo \"scope root '$SCOPE_ROOT' does not exist; producing empty scope\"\n : > /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"total\":0,\"shard_n\":1,\"shard_slot\":0,\"shard_count\":0,\"recent_count\":0,\"in_scope_count\":0,\"iso_week\":\"'\"$(date +%G-W%V)\"'\",\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\"}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\nelse\n if [ \"$TOTAL\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n\n : > /tmp/gh-aw/sweep-data/shard.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/shard.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\n\n git log --since='2 days ago' --name-only --pretty=format: -- \"$DOCS_ROOT/*.md\" \"$DOCS_ROOT/**/*.md\" 2>/dev/null \\\n | grep -E '\\.md$' \\\n | sort -u > /tmp/gh-aw/sweep-data/recent.txt || true\n\n # Cap the recently-changed pass: if a corpus-wide rebase or migration\n # touched far more pages than one slice, fall back to slice-only so\n # rotation actually rotates.\n RECENT_RAW=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\n RECENT_LIMIT=$(( TARGET_BATCH * 2 ))\n if [ \"$RECENT_RAW\" -gt \"$RECENT_LIMIT\" ]; then\n echo \"recently-changed pass produced $RECENT_RAW pages (>2x target batch $TARGET_BATCH); disabling for this run\"\n : > /tmp/gh-aw/sweep-data/recent.txt\n fi\nfi\n\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nelse\n sort -u /tmp/gh-aw/sweep-data/shard.txt /tmp/gh-aw/sweep-data/recent.txt \\\n | grep -v '^$' > /tmp/gh-aw/sweep-data/in-scope.txt || true\nfi\n\nwhile IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\ndone < /tmp/gh-aw/sweep-data/in-scope.txt\n\nSHARD_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/shard.txt | tr -d ' ')\nRECENT_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n\n REQUESTED_COUNT=0\n printf '%s' \"$TARGET_FILES\" | tr ',' '\\n' > /tmp/gh-aw/sweep-data/target-files.raw\n while IFS= read -r raw || [ -n \"$raw\" ]; do\n entry=$(printf '%s' \"$raw\" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')\n [ -z \"$entry\" ] && continue\n REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))\n entry=${entry#/}\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n resolved=\"$entry\"\n else\n case \"$entry\" in\n \"$DOCS_ROOT_CLEAN\"/*) resolved=\"$entry\" ;;\n *) resolved=\"$DOCS_ROOT_CLEAN/$entry\" ;;\n esac\n fi\n if [ -f \"$resolved\" ]; then\n echo \"$resolved\" >> /tmp/gh-aw/sweep-data/all.txt\n else\n echo \"target-files: '$resolved' not found; skipping\"\n fi\n done < /tmp/gh-aw/sweep-data/target-files.raw\n\n sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt\n cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt\n\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\n done < /tmp/gh-aw/sweep-data/in-scope.txt\n\n IN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"total\":0,\"shard_n\":1,\"shard_slot\":0,\"shard_count\":0,\"recent_count\":0,\"in_scope_count\":0,\"iso_week\":\"'\"$(date +%G-W%V)\"'\",\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\"}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\nelse\n if [ \"$TOTAL\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n\n : > /tmp/gh-aw/sweep-data/shard.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/shard.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\n\n git log --since='2 days ago' --name-only --pretty=format: -- \"$DOCS_ROOT/*.md\" \"$DOCS_ROOT/**/*.md\" 2>/dev/null \\\n | grep -E '\\.md$' \\\n | sort -u > /tmp/gh-aw/sweep-data/recent.txt || true\n\n # Cap the recently-changed pass: if a corpus-wide rebase or migration\n # touched far more pages than one slice, fall back to slice-only so\n # rotation actually rotates.\n RECENT_RAW=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\n RECENT_LIMIT=$(( TARGET_BATCH * 2 ))\n if [ \"$RECENT_RAW\" -gt \"$RECENT_LIMIT\" ]; then\n echo \"recently-changed pass produced $RECENT_RAW pages (>2x target batch $TARGET_BATCH); disabling for this run\"\n : > /tmp/gh-aw/sweep-data/recent.txt\n fi\nfi\n\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nelse\n sort -u /tmp/gh-aw/sweep-data/shard.txt /tmp/gh-aw/sweep-data/recent.txt \\\n | grep -v '^$' > /tmp/gh-aw/sweep-data/in-scope.txt || true\nfi\n\nwhile IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\ndone < /tmp/gh-aw/sweep-data/in-scope.txt\n\nSHARD_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/shard.txt | tr -d ' ')\nRECENT_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt
+ : > /tmp/gh-aw/sweep-data/shard.txt
+ : > /tmp/gh-aw/sweep-data/recent.txt
+ : > /tmp/gh-aw/sweep-data/in-scope.txt
+
+ REQUESTED_COUNT=0
+ printf '%s' "$TARGET_FILES" | tr ',' '\n' > /tmp/gh-aw/sweep-data/target-files.raw
+ while IFS= read -r raw || [ -n "$raw" ]; do
+ entry=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
+ [ -z "$entry" ] && continue
+ REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))
+ entry=${entry#/}
+ if [ "$DOCS_ROOT_CLEAN" = "." ] || [ -z "$DOCS_ROOT_CLEAN" ]; then
+ resolved="$entry"
+ else
+ case "$entry" in
+ "$DOCS_ROOT_CLEAN"/*) resolved="$entry" ;;
+ *) resolved="$DOCS_ROOT_CLEAN/$entry" ;;
+ esac
+ fi
+ if [ -f "$resolved" ]; then
+ echo "$resolved" >> /tmp/gh-aw/sweep-data/all.txt
+ else
+ echo "target-files: '$resolved' not found; skipping"
+ fi
+ done < /tmp/gh-aw/sweep-data/target-files.raw
+
+ sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt
+ cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt
+
+ while IFS= read -r f; do
+ [ -z "$f" ] && continue
+ [ ! -f "$f" ] && continue
+ mkdir -p "/tmp/gh-aw/sweep-data/scope/$(dirname "$f")"
+ cp "$f" "/tmp/gh-aw/sweep-data/scope/$f"
+ done < /tmp/gh-aw/sweep-data/in-scope.txt
+
+ IN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')
+ cat > /tmp/gh-aw/sweep-data/stats.json < in shard / ( pages)"`.
- Full mode with `target_path`: `"No staleness findings under / ( pages)"`.
- Full mode without `target_path`: `"No staleness findings in full sweep ( pages)"`.
+- Files mode: `"No staleness findings in the requested file list ( files)"`.
+
+**Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; audit exactly those files and ignore `target_path`/shard framing. Use an explicit-file-list description in the title (`file list — findings`) and scope-summary (`Explicit file list · of requested files in scope.`).
## Output: fix-issue body
@@ -504,6 +577,7 @@ Thresholds: stale_content_months=, stale_image_min_gap_months=.
line: 1
category: stale-content
severity: medium
+ confidence: high
evidence: "last commit 2023-01-15 (38 months ago); threshold is 24 months"
suggested_fix: |
review and refresh; either update content or move to archive
@@ -511,6 +585,7 @@ Thresholds: stale_content_months=, stale_image_min_gap_months=.
line: 17
category: stale-screenshot
severity: low
+ confidence: high
evidence: "image images/bar-ui.png last modified 12 months before page; gap exceeds 6-month threshold"
suggested_fix: |
re-capture screenshot reflecting the current UI
@@ -518,6 +593,7 @@ Thresholds: stale_content_months=, stale_image_min_gap_months=.
line: 42
category: broken-external-link
severity: high
+ confidence: high
evidence: "https://example.com/old-spec returned 404 (lychee)"
suggested_fix: |
replace link or cite a current source
@@ -525,6 +601,7 @@ Thresholds: stale_content_months=, stale_image_min_gap_months=.
line: 9
category: unsupported-version-mention
severity: medium
+ confidence: medium
evidence: "references Elastic Stack 7.17 — past EOL per support matrix at https://www.elastic.co/support/eol"
suggested_fix: |
update to a currently-supported version or move guidance to upgrade docs
diff --git a/.github/workflows/gh-aw-docs-style-sweep.lock.yml b/.github/workflows/gh-aw-docs-style-sweep.lock.yml
index 64eb2ab..6234a67 100644
--- a/.github/workflows/gh-aw-docs-style-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-style-sweep.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ef086b0c29c84372cc65ee3a74a03ee74077ef3682f5f7f50456bc301112fb64","body_hash":"56566f811c30a29e22d09d6beaf78cedb9dcfb06e690e9ab9fb27124c83571ca","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a7254286df1e48c1fdff486b60bcc23bdf9d7075ce208461565824784c65d1e2","body_hash":"e88b6d4a955e77b6b3a30cebd89d69266592e1a67abd2035dc1e9105670d7a29","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_PLUGINS_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"},{"repo":"microsoft/apm-action","sha":"b48dd081eb0050f6d7f32d0e7caa0a59a2d419fd","version":"v1.7.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}
# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -30,6 +30,7 @@
#
# Resolved workflow manifest:
# Imports:
+# - gh-aw-fragments/findings-contract.md
# - gh-aw-fragments/formatting.md
# - gh-aw-fragments/mcp-pagination.md
# - gh-aw-fragments/rigor.md
@@ -110,6 +111,11 @@ on:
description: Approximate pages per rotating slice; controls shard count N = ceil(total/batch-size)
required: false
type: string
+ target-files:
+ default: ""
+ description: "Optional newline- or comma-separated list of docs-root-relative file paths to sweep. When set, overrides target-path and scope-mode: the sweep processes exactly these files."
+ required: false
+ type: string
target-path:
default: ""
description: Optional docs-root-relative directory to sweep recursively. Accepts a leading slash.
@@ -335,20 +341,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_2f876853975ceb21_EOF'
+ cat << 'GH_AW_PROMPT_975be611fd4c9fce_EOF'
- GH_AW_PROMPT_2f876853975ceb21_EOF
+ GH_AW_PROMPT_975be611fd4c9fce_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_2f876853975ceb21_EOF'
+ cat << 'GH_AW_PROMPT_975be611fd4c9fce_EOF'
Tools: create_issue, missing_tool, missing_data, noop
- GH_AW_PROMPT_2f876853975ceb21_EOF
+ GH_AW_PROMPT_975be611fd4c9fce_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
- cat << 'GH_AW_PROMPT_2f876853975ceb21_EOF'
+ cat << 'GH_AW_PROMPT_975be611fd4c9fce_EOF'
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -377,9 +383,9 @@ jobs:
{{/if}}
- GH_AW_PROMPT_2f876853975ceb21_EOF
+ GH_AW_PROMPT_975be611fd4c9fce_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_2f876853975ceb21_EOF'
+ cat << 'GH_AW_PROMPT_975be611fd4c9fce_EOF'
## Formatting Guidelines
@@ -420,6 +426,42 @@ jobs:
If you see `MCP tool response exceeds maximum allowed tokens`, retry with a smaller `perPage` value (halve it).
+ ## Findings contract
+
+ These rules apply to every finding this sweep emits. Sweep output is consumed by humans and, increasingly, by AI fix-agents that may act on it without a human in the loop. A finding that is uncertain, or that looks authoritative but is wrong, is worse than no finding at all.
+
+ ### Finding-type allowlist
+
+ Emit only the `category` values enumerated in this workflow's "Build the findings list" step. That enumeration is a closed allowlist:
+
+ - Never invent, rename, pluralize, or otherwise vary a category string. If a finding does not map cleanly to an allowlisted category, drop it.
+ - A finding is valid only if applying its `suggested_fix` would change the page's rendered output or its published metadata. Drop no-op findings whose fix a reader would never see — for example, adding a marker the docs toolchain already generates automatically.
+ - If you spot something real that has no allowlisted category, describe it in the issue body's **Notes** section as prose. Do not smuggle it in as a finding under an invented category.
+
+ ### Per-finding confidence
+
+ Add a `confidence` field to every finding, set to exactly one of `high`, `medium`, or `low`. Judge confidence on how safe the finding is to act on *without* human verification — this is a separate axis from `severity`, which measures impact:
+
+ - `high` — the problem and the fix are objective and verifiable from the evidence in front of you: a missing required field, a tool-flagged issue with a single unambiguous correction, a directly quoted contradiction. A fix-agent could apply the `suggested_fix` verbatim without judgment.
+ - `medium` — the finding is well-supported, but the fix involves wording choices, or depends on a repository convention you could not fully verify this run. A human should confirm the fix before it lands.
+ - `low` — the finding is plausible but rests on partial evidence, subjective judgment, or an assumption about intent or convention you could not confirm. If you cannot justify at least `low`, drop the finding rather than filing it.
+
+ When a finding's evidence traces back to text you did not verify — for example, terminology copied from an issue or PR description rather than confirmed against the code or the published docs — cap its confidence at `low` and say so in the `evidence`.
+
+ Include `confidence` in the YAML schema for every finding, alongside `severity`. Keep the existing sort order (by `severity` first); do not reorder by confidence.
+
+ ### Human-review gate
+
+ If the capped findings list contains **any** finding with `confidence: medium` or `confidence: low`:
+
+ 1. Add the label `needs-human-review` to your `create_issue` call, in addition to the labels the workflow adds automatically. This marks the issue as not safe to auto-action and keeps it out of the `good-for-ai` delegation track.
+ 2. Immediately below the `## Findings ()` heading and before the YAML block, add this callout verbatim:
+
+ > [!WARNING]
+ > This issue contains medium- or low-confidence findings. Review them before acting — auto-applying sweep output without verification risks putting incorrect content into the docs. Findings marked `confidence: high` are safe to delegate to a fix-agent; `medium` and `low` need a human sign-off first.
+
+ If every finding is `confidence: high`, do not add the label or the callout: the issue is safe to delegate as-is.
+
# Docs style sweep agent
You are a style-guide reviewer for an Elastic documentation repository. Your job is to format Vale's findings (already produced by a deterministic pre-step) into the structured YAML schema below, applying light filtering and category mapping. You may add high-confidence manual findings for style-guide areas Vale does not fully cover, especially Formatting and UI writing. **Vale has already run** — you are not invoking any skill.
@@ -491,8 +533,9 @@ jobs:
- `file` — repo-relative (strip `/tmp/gh-aw/sweep-data/scope/`).
- `line` — exact line number from Vale's output.
- - `category` — one of the strings above.
+ - `category` — one of the strings above; that list is the complete category allowlist for this sweep (see the **Findings contract**).
- `severity` — `high` for changes-meaning issues; `medium` for clear style violations; `low` for nits.
+ - `confidence` — `high`, `medium`, or `low` per the **Findings contract**. Vale-sourced findings with a single deterministic replacement are usually `high`; manual style judgments you added are usually `medium`.
- `evidence` — short quote of the offending text plus the rule name (e.g., `"'in order to' — Elastic.WordList rule"`).
- `suggested_fix` — concrete replacement text or short directive (e.g., `to`).
@@ -506,6 +549,9 @@ jobs:
- Shard mode with `target_path`: `"No high-confidence style issues under / in shard / ( pages)"`.
- Full mode with `target_path`: `"No high-confidence style issues under / ( pages)"`.
- Full mode without `target_path`: `"No high-confidence style issues in full sweep ( pages)"`.
+ - Files mode: `"No high-confidence style issues in the requested file list ( files)"`.
+
+ **Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; audit exactly those files and ignore `target_path`/shard framing. Use an explicit-file-list description in the title (`file list — pages`) and scope-summary (`Explicit file list · of requested files in scope.`).
Drop low-severity findings when the cap is already reached with medium/high — prioritize impact.
@@ -535,6 +581,7 @@ jobs:
line: 42
category: word-choice
severity: medium
+ confidence: high
evidence: "'in order to' — Elastic.WordList prefers 'to'"
suggested_fix: |
to
@@ -542,6 +589,7 @@ jobs:
line: 17
category: voice-tone
severity: medium
+ confidence: medium
evidence: "second-person inconsistency: 'we recommend' inside a how-to"
suggested_fix: |
Use this approach when ...
@@ -567,7 +615,7 @@ jobs:
__GH_AW_EXPR_49B959F1__
- GH_AW_PROMPT_2f876853975ceb21_EOF
+ GH_AW_PROMPT_975be611fd4c9fce_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -753,9 +801,10 @@ jobs:
DOCS_ROOT: ${{ inputs.docs-root }}
SCOPE_MODE: ${{ inputs.scope-mode }}
TARGET_BATCH: ${{ inputs.target-batch-size }}
+ TARGET_FILES: ${{ inputs.target-files }}
TARGET_PATH: ${{ inputs.target-path }}
name: Compute sweep targets
- run: "set -eu\nmkdir -p /tmp/gh-aw/sweep-data/scope\n\nTARGET_PATH_CLEAN=${TARGET_PATH#/}\nTARGET_PATH_CLEAN=${TARGET_PATH_CLEAN%/}\nDOCS_ROOT_CLEAN=${DOCS_ROOT%/}\nSCOPE_ROOT=\"$DOCS_ROOT\"\nREQUESTED_SCOPE_MODE=\"$SCOPE_MODE\"\nSELECTION_MODE=\"shard\"\n\ncase \"$REQUESTED_SCOPE_MODE\" in\n auto|full|shard) ;;\n *)\n echo \"scope-mode '$REQUESTED_SCOPE_MODE' must be one of: auto, full, shard\"\n exit 1\n ;;\nesac\n\nif [ -n \"$TARGET_PATH_CLEAN\" ]; then\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n SCOPE_ROOT=\"$TARGET_PATH_CLEAN\"\n else\n SCOPE_ROOT=\"$DOCS_ROOT_CLEAN/$TARGET_PATH_CLEAN\"\n fi\nfi\n\nif [ \"$REQUESTED_SCOPE_MODE\" = \"auto\" ]; then\n if [ -n \"$TARGET_PATH_CLEAN\" ]; then\n SELECTION_MODE=\"full\"\n else\n SELECTION_MODE=\"shard\"\n fi\nelse\n SELECTION_MODE=\"$REQUESTED_SCOPE_MODE\"\nfi\n\nif [ ! -d \"$SCOPE_ROOT\" ]; then\n echo \"scope root '$SCOPE_ROOT' does not exist; producing empty scope\"\n : > /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"total\":0,\"shard_n\":1,\"shard_slot\":0,\"shard_count\":0,\"recent_count\":0,\"in_scope_count\":0,\"iso_week\":\"'\"$(date +%G-W%V)\"'\",\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\"}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\nelse\n if [ \"$TOTAL\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n\n : > /tmp/gh-aw/sweep-data/shard.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/shard.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\n\n git log --since='2 days ago' --name-only --pretty=format: -- \"$DOCS_ROOT/*.md\" \"$DOCS_ROOT/**/*.md\" 2>/dev/null \\\n | grep -E '\\.md$' \\\n | sort -u > /tmp/gh-aw/sweep-data/recent.txt || true\n\n # Cap the recently-changed pass: if a corpus-wide rebase or migration\n # touched far more pages than one slice, fall back to slice-only so\n # rotation actually rotates.\n RECENT_RAW=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\n RECENT_LIMIT=$(( TARGET_BATCH * 2 ))\n if [ \"$RECENT_RAW\" -gt \"$RECENT_LIMIT\" ]; then\n echo \"recently-changed pass produced $RECENT_RAW pages (>2x target batch $TARGET_BATCH); disabling for this run\"\n : > /tmp/gh-aw/sweep-data/recent.txt\n fi\nfi\n\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nelse\n sort -u /tmp/gh-aw/sweep-data/shard.txt /tmp/gh-aw/sweep-data/recent.txt \\\n | grep -v '^$' > /tmp/gh-aw/sweep-data/in-scope.txt || true\nfi\n\nwhile IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\ndone < /tmp/gh-aw/sweep-data/in-scope.txt\n\nSHARD_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/shard.txt | tr -d ' ')\nRECENT_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n\n REQUESTED_COUNT=0\n printf '%s' \"$TARGET_FILES\" | tr ',' '\\n' > /tmp/gh-aw/sweep-data/target-files.raw\n while IFS= read -r raw || [ -n \"$raw\" ]; do\n entry=$(printf '%s' \"$raw\" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')\n [ -z \"$entry\" ] && continue\n REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))\n entry=${entry#/}\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n resolved=\"$entry\"\n else\n case \"$entry\" in\n \"$DOCS_ROOT_CLEAN\"/*) resolved=\"$entry\" ;;\n *) resolved=\"$DOCS_ROOT_CLEAN/$entry\" ;;\n esac\n fi\n if [ -f \"$resolved\" ]; then\n echo \"$resolved\" >> /tmp/gh-aw/sweep-data/all.txt\n else\n echo \"target-files: '$resolved' not found; skipping\"\n fi\n done < /tmp/gh-aw/sweep-data/target-files.raw\n\n sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt\n cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt\n\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\n done < /tmp/gh-aw/sweep-data/in-scope.txt\n\n IN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"total\":0,\"shard_n\":1,\"shard_slot\":0,\"shard_count\":0,\"recent_count\":0,\"in_scope_count\":0,\"iso_week\":\"'\"$(date +%G-W%V)\"'\",\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\"}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/shard.txt\n : > /tmp/gh-aw/sweep-data/recent.txt\nelse\n if [ \"$TOTAL\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n\n : > /tmp/gh-aw/sweep-data/shard.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/shard.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\n\n git log --since='2 days ago' --name-only --pretty=format: -- \"$DOCS_ROOT/*.md\" \"$DOCS_ROOT/**/*.md\" 2>/dev/null \\\n | grep -E '\\.md$' \\\n | sort -u > /tmp/gh-aw/sweep-data/recent.txt || true\n\n # Cap the recently-changed pass: if a corpus-wide rebase or migration\n # touched far more pages than one slice, fall back to slice-only so\n # rotation actually rotates.\n RECENT_RAW=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\n RECENT_LIMIT=$(( TARGET_BATCH * 2 ))\n if [ \"$RECENT_RAW\" -gt \"$RECENT_LIMIT\" ]; then\n echo \"recently-changed pass produced $RECENT_RAW pages (>2x target batch $TARGET_BATCH); disabling for this run\"\n : > /tmp/gh-aw/sweep-data/recent.txt\n fi\nfi\n\nif [ \"$SELECTION_MODE\" = \"full\" ]; then\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nelse\n sort -u /tmp/gh-aw/sweep-data/shard.txt /tmp/gh-aw/sweep-data/recent.txt \\\n | grep -v '^$' > /tmp/gh-aw/sweep-data/in-scope.txt || true\nfi\n\nwhile IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [ ! -f \"$f\" ] && continue\n mkdir -p \"/tmp/gh-aw/sweep-data/scope/$(dirname \"$f\")\"\n cp \"$f\" \"/tmp/gh-aw/sweep-data/scope/$f\"\ndone < /tmp/gh-aw/sweep-data/in-scope.txt\n\nSHARD_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/shard.txt | tr -d ' ')\nRECENT_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/recent.txt | tr -d ' ')\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt
+ : > /tmp/gh-aw/sweep-data/shard.txt
+ : > /tmp/gh-aw/sweep-data/recent.txt
+ : > /tmp/gh-aw/sweep-data/in-scope.txt
+
+ REQUESTED_COUNT=0
+ printf '%s' "$TARGET_FILES" | tr ',' '\n' > /tmp/gh-aw/sweep-data/target-files.raw
+ while IFS= read -r raw || [ -n "$raw" ]; do
+ entry=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
+ [ -z "$entry" ] && continue
+ REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))
+ entry=${entry#/}
+ if [ "$DOCS_ROOT_CLEAN" = "." ] || [ -z "$DOCS_ROOT_CLEAN" ]; then
+ resolved="$entry"
+ else
+ case "$entry" in
+ "$DOCS_ROOT_CLEAN"/*) resolved="$entry" ;;
+ *) resolved="$DOCS_ROOT_CLEAN/$entry" ;;
+ esac
+ fi
+ if [ -f "$resolved" ]; then
+ echo "$resolved" >> /tmp/gh-aw/sweep-data/all.txt
+ else
+ echo "target-files: '$resolved' not found; skipping"
+ fi
+ done < /tmp/gh-aw/sweep-data/target-files.raw
+
+ sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt
+ cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt
+
+ while IFS= read -r f; do
+ [ -z "$f" ] && continue
+ [ ! -f "$f" ] && continue
+ mkdir -p "/tmp/gh-aw/sweep-data/scope/$(dirname "$f")"
+ cp "$f" "/tmp/gh-aw/sweep-data/scope/$f"
+ done < /tmp/gh-aw/sweep-data/in-scope.txt
+
+ IN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')
+ cat > /tmp/gh-aw/sweep-data/stats.json < in shard / ( pages)"`.
- Full mode with `target_path`: `"No high-confidence style issues under / ( pages)"`.
- Full mode without `target_path`: `"No high-confidence style issues in full sweep ( pages)"`.
+- Files mode: `"No high-confidence style issues in the requested file list ( files)"`.
+
+**Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; audit exactly those files and ignore `target_path`/shard framing. Use an explicit-file-list description in the title (`file list — pages`) and scope-summary (`Explicit file list · of requested files in scope.`).
Drop low-severity findings when the cap is already reached with medium/high — prioritize impact.
@@ -424,6 +498,7 @@ Use one of these scope-summary lines:
line: 42
category: word-choice
severity: medium
+ confidence: high
evidence: "'in order to' — Elastic.WordList prefers 'to'"
suggested_fix: |
to
@@ -431,6 +506,7 @@ Use one of these scope-summary lines:
line: 17
category: voice-tone
severity: medium
+ confidence: medium
evidence: "second-person inconsistency: 'we recommend' inside a how-to"
suggested_fix: |
Use this approach when ...
diff --git a/.github/workflows/gh-aw-docs-typos-sweep.lock.yml b/.github/workflows/gh-aw-docs-typos-sweep.lock.yml
index 6479c7d..bad5970 100644
--- a/.github/workflows/gh-aw-docs-typos-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-typos-sweep.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"20071ba546b202f44585855446d275fbbe371992b2a3dfda53ba15008a782e42","body_hash":"afa31842d60df735eb31048d186b3ed8a05fd8430c677fb421899c674fe62c72","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"81ca0aa5978a5721673f362029baf56bd87fc5b72411fb0866542d202164b270","body_hash":"df37c1a7d13227b0879302a29aa54e4ef9815af58fe22e30afd07b195082446f","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"claude-sonnet-5","engine_versions":{"copilot":"1.0.73"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/setup-python","sha":"ece7cb06caefa5fff74198d8649806c4678c61a1","version":"v6.3.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}
# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -30,6 +30,7 @@
#
# Resolved workflow manifest:
# Imports:
+# - gh-aw-fragments/findings-contract.md
# - gh-aw-fragments/formatting.md
# - gh-aw-fragments/rigor.md
#
@@ -111,6 +112,11 @@ on:
description: Approximate pages per rotating slice when scope-mode resolves to shard
required: false
type: string
+ target-files:
+ default: ""
+ description: "Optional newline- or comma-separated list of docs-root-relative file paths to sweep. When set, overrides target-path and scope-mode: the sweep processes exactly these files."
+ required: false
+ type: string
target-path:
default: ""
description: Optional docs-root-relative directory to sweep recursively. Accepts a leading slash.
@@ -334,20 +340,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_af0764ebb5d9f6ef_EOF'
+ cat << 'GH_AW_PROMPT_3c57cef25726a2f9_EOF'
- GH_AW_PROMPT_af0764ebb5d9f6ef_EOF
+ GH_AW_PROMPT_3c57cef25726a2f9_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_af0764ebb5d9f6ef_EOF'
+ cat << 'GH_AW_PROMPT_3c57cef25726a2f9_EOF'
Tools: create_issue, missing_tool, missing_data, noop
- GH_AW_PROMPT_af0764ebb5d9f6ef_EOF
+ GH_AW_PROMPT_3c57cef25726a2f9_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
- cat << 'GH_AW_PROMPT_af0764ebb5d9f6ef_EOF'
+ cat << 'GH_AW_PROMPT_3c57cef25726a2f9_EOF'
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -376,9 +382,9 @@ jobs:
{{/if}}
- GH_AW_PROMPT_af0764ebb5d9f6ef_EOF
+ GH_AW_PROMPT_3c57cef25726a2f9_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_af0764ebb5d9f6ef_EOF'
+ cat << 'GH_AW_PROMPT_3c57cef25726a2f9_EOF'
## Formatting Guidelines
@@ -399,6 +405,42 @@ jobs:
- Before filing any issue or opening any PR, re-read your own output as a skeptical reviewer. Ask: "Would a senior engineer on this team find this useful, or would they close it immediately?" If the answer is "close," call `noop` instead.
- Only report findings you would confidently defend in a code review. If you feel the need to hedge with "might," "could," or "possibly," the finding is not ready to file.
+ ## Findings contract
+
+ These rules apply to every finding this sweep emits. Sweep output is consumed by humans and, increasingly, by AI fix-agents that may act on it without a human in the loop. A finding that is uncertain, or that looks authoritative but is wrong, is worse than no finding at all.
+
+ ### Finding-type allowlist
+
+ Emit only the `category` values enumerated in this workflow's "Build the findings list" step. That enumeration is a closed allowlist:
+
+ - Never invent, rename, pluralize, or otherwise vary a category string. If a finding does not map cleanly to an allowlisted category, drop it.
+ - A finding is valid only if applying its `suggested_fix` would change the page's rendered output or its published metadata. Drop no-op findings whose fix a reader would never see — for example, adding a marker the docs toolchain already generates automatically.
+ - If you spot something real that has no allowlisted category, describe it in the issue body's **Notes** section as prose. Do not smuggle it in as a finding under an invented category.
+
+ ### Per-finding confidence
+
+ Add a `confidence` field to every finding, set to exactly one of `high`, `medium`, or `low`. Judge confidence on how safe the finding is to act on *without* human verification — this is a separate axis from `severity`, which measures impact:
+
+ - `high` — the problem and the fix are objective and verifiable from the evidence in front of you: a missing required field, a tool-flagged issue with a single unambiguous correction, a directly quoted contradiction. A fix-agent could apply the `suggested_fix` verbatim without judgment.
+ - `medium` — the finding is well-supported, but the fix involves wording choices, or depends on a repository convention you could not fully verify this run. A human should confirm the fix before it lands.
+ - `low` — the finding is plausible but rests on partial evidence, subjective judgment, or an assumption about intent or convention you could not confirm. If you cannot justify at least `low`, drop the finding rather than filing it.
+
+ When a finding's evidence traces back to text you did not verify — for example, terminology copied from an issue or PR description rather than confirmed against the code or the published docs — cap its confidence at `low` and say so in the `evidence`.
+
+ Include `confidence` in the YAML schema for every finding, alongside `severity`. Keep the existing sort order (by `severity` first); do not reorder by confidence.
+
+ ### Human-review gate
+
+ If the capped findings list contains **any** finding with `confidence: medium` or `confidence: low`:
+
+ 1. Add the label `needs-human-review` to your `create_issue` call, in addition to the labels the workflow adds automatically. This marks the issue as not safe to auto-action and keeps it out of the `good-for-ai` delegation track.
+ 2. Immediately below the `## Findings ()` heading and before the YAML block, add this callout verbatim:
+
+ > [!WARNING]
+ > This issue contains medium- or low-confidence findings. Review them before acting — auto-applying sweep output without verification risks putting incorrect content into the docs. Findings marked `confidence: high` are safe to delegate to a fix-agent; `medium` and `low` need a human sign-off first.
+
+ If every finding is `confidence: high`, do not add the label or the callout: the issue is safe to delegate as-is.
+
# Docs typos sweep agent
You are a deterministic typos-finding formatter. The detection has already happened — `codespell` ran in a pre-step against the docs root. Your only job is to convert the raw output into a labeled fix-issue with a clean structured findings list.
@@ -434,12 +476,15 @@ jobs:
- `typo` — codespell flagged a misspelling with a single confident correction.
- `ambiguous-typo` — codespell offered multiple corrections; the right one depends on context.
+ These two are the complete category allowlist for this sweep (see the **Findings contract**).
+
For each finding produce:
- `file` — repo-relative path (codespell already emits these correctly).
- `line` — line number from codespell.
- `category` — `typo` or `ambiguous-typo`.
- `severity` — always `low` (typos are unambiguous fixes regardless of impact).
+ - `confidence` — per the **Findings contract**: `high` for `typo` (a single deterministic correction), `low` for `ambiguous-typo` (the right correction depends on context and needs human judgment).
- `evidence` — `"'' — codespell suggests: "`.
- `suggested_fix` — the chosen correction (omit for `ambiguous-typo`).
@@ -471,6 +516,9 @@ jobs:
- Full mode with `target_path`: `path / — typos across files`.
- Shard mode without `target_path`: `shard / — typos across files`.
- Shard mode with `target_path`: `path / — shard / — typos across files`.
+ - Files mode (`selection_mode` is `files`): `file list — typos across files`.
+
+ **Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; scan exactly those files. Use the scope-summary line `Scanned of requested files; codespell raised raw findings, retained after filtering.`
```markdown
Generated by `gh-aw-docs-typos-sweep` for `__GH_AW_EXPR_E6F7E87E__` on .
@@ -489,6 +537,7 @@ jobs:
line: 42
category: typo
severity: low
+ confidence: high
evidence: "'teh' — codespell suggests: the"
suggested_fix: |
the
@@ -496,6 +545,7 @@ jobs:
line: 9
category: ambiguous-typo
severity: low
+ confidence: low
evidence: "'ambigous' — codespell suggests: ambiguous, ambiguously"
```
@@ -511,7 +561,7 @@ jobs:
__GH_AW_EXPR_49B959F1__
- GH_AW_PROMPT_af0764ebb5d9f6ef_EOF
+ GH_AW_PROMPT_3c57cef25726a2f9_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -691,9 +741,10 @@ jobs:
DOCS_ROOT: ${{ inputs.docs-root }}
SCOPE_MODE: ${{ inputs.scope-mode }}
TARGET_BATCH: ${{ inputs.target-batch-size }}
+ TARGET_FILES: ${{ inputs.target-files }}
TARGET_PATH: ${{ inputs.target-path }}
name: Run codespell
- run: "set -u\nmkdir -p /tmp/gh-aw/sweep-data\n\nTARGET_PATH_CLEAN=${TARGET_PATH#/}\nTARGET_PATH_CLEAN=${TARGET_PATH_CLEAN%/}\nDOCS_ROOT_CLEAN=${DOCS_ROOT%/}\nSCOPE_ROOT=\"$DOCS_ROOT\"\nREQUESTED_SCOPE_MODE=\"$SCOPE_MODE\"\nSELECTION_MODE=\"full\"\n\ncase \"$REQUESTED_SCOPE_MODE\" in\n auto|full|shard) ;;\n *)\n echo \"scope-mode '$REQUESTED_SCOPE_MODE' must be one of: auto, full, shard\"\n exit 1\n ;;\nesac\n\nif [ -n \"$TARGET_PATH_CLEAN\" ]; then\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n SCOPE_ROOT=\"$TARGET_PATH_CLEAN\"\n else\n SCOPE_ROOT=\"$DOCS_ROOT_CLEAN/$TARGET_PATH_CLEAN\"\n fi\nfi\n\nif [ \"$REQUESTED_SCOPE_MODE\" = \"auto\" ]; then\n SELECTION_MODE=\"full\"\nelse\n SELECTION_MODE=\"$REQUESTED_SCOPE_MODE\"\nfi\n\nif [ ! -d \"$SCOPE_ROOT\" ]; then\n echo \"scope root '$SCOPE_ROOT' does not exist; producing empty output\"\n : > /tmp/gh-aw/sweep-data/codespell.out\n : > /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"shard_n\":1,\"shard_slot\":0,\"in_scope_count\":0,\"total_md\":0,\"finding_count\":0}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL_MD=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\n\nif [ \"$SELECTION_MODE\" = \"shard\" ]; then\n if [ \"$TOTAL_MD\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL_MD + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/in-scope.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\nelse\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nfi\n\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\n# codespell exits non-zero when misspellings are found. Capture and continue.\nset +e\nif [ ! -s /tmp/gh-aw/sweep-data/in-scope.txt ]; then\n : > /tmp/gh-aw/sweep-data/codespell.out\n RC=0\nelse\n codespell \\\n --quiet-level=2 \\\n $CODESPELL_ARGS \\\n $(cat /tmp/gh-aw/sweep-data/in-scope.txt) \\\n > /tmp/gh-aw/sweep-data/codespell.out 2>&1\n RC=$?\nfi\nset -e\n\nFINDING_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/codespell.out | tr -d ' ')\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n\n REQUESTED_COUNT=0\n printf '%s' \"$TARGET_FILES\" | tr ',' '\\n' > /tmp/gh-aw/sweep-data/target-files.raw\n while IFS= read -r raw || [ -n \"$raw\" ]; do\n entry=$(printf '%s' \"$raw\" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')\n [ -z \"$entry\" ] && continue\n REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))\n entry=${entry#/}\n if [ \"$DOCS_ROOT_CLEAN\" = \".\" ] || [ -z \"$DOCS_ROOT_CLEAN\" ]; then\n resolved=\"$entry\"\n else\n case \"$entry\" in\n \"$DOCS_ROOT_CLEAN\"/*) resolved=\"$entry\" ;;\n *) resolved=\"$DOCS_ROOT_CLEAN/$entry\" ;;\n esac\n fi\n if [ -f \"$resolved\" ]; then\n echo \"$resolved\" >> /tmp/gh-aw/sweep-data/all.txt\n else\n echo \"target-files: '$resolved' not found; skipping\"\n fi\n done < /tmp/gh-aw/sweep-data/target-files.raw\n\n sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt\n cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt\n TOTAL_MD=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\n IN_SCOPE_COUNT=\"$TOTAL_MD\"\n\n set +e\n if [ ! -s /tmp/gh-aw/sweep-data/in-scope.txt ]; then\n : > /tmp/gh-aw/sweep-data/codespell.out\n RC=0\n else\n codespell \\\n --quiet-level=2 \\\n $CODESPELL_ARGS \\\n $(cat /tmp/gh-aw/sweep-data/in-scope.txt) \\\n > /tmp/gh-aw/sweep-data/codespell.out 2>&1\n RC=$?\n fi\n set -e\n\n FINDING_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/codespell.out | tr -d ' ')\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/codespell.out\n : > /tmp/gh-aw/sweep-data/all.txt\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n echo '{\"docs_root\":\"'\"$DOCS_ROOT\"'\",\"scope_root\":\"'\"$SCOPE_ROOT\"'\",\"target_path\":\"'\"$TARGET_PATH_CLEAN\"'\",\"scope_mode\":\"'\"$REQUESTED_SCOPE_MODE\"'\",\"selection_mode\":\"'\"$SELECTION_MODE\"'\",\"shard_n\":1,\"shard_slot\":0,\"in_scope_count\":0,\"total_md\":0,\"finding_count\":0}' > /tmp/gh-aw/sweep-data/stats.json\n exit 0\nfi\n\nfind \"$SCOPE_ROOT\" -type f -name '*.md' \\\n -not -path '*/node_modules/*' \\\n -not -path '*/.git/*' \\\n | sort > /tmp/gh-aw/sweep-data/all.txt\n\nTOTAL_MD=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')\n\nif [ \"$SELECTION_MODE\" = \"shard\" ]; then\n if [ \"$TOTAL_MD\" -eq 0 ]; then\n N=1\n else\n N=$(( (TOTAL_MD + TARGET_BATCH - 1) / TARGET_BATCH ))\n fi\n ISO_WEEK_NUM=$(date +%V | sed 's/^0//')\n SLOT=$(( ISO_WEEK_NUM % N ))\n : > /tmp/gh-aw/sweep-data/in-scope.txt\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n HEX=$(printf '%s' \"$f\" | shasum -a 256 | cut -c1-4)\n HASH_NUM=$(( 16#$HEX ))\n if [ $((HASH_NUM % N)) -eq \"$SLOT\" ]; then\n echo \"$f\" >> /tmp/gh-aw/sweep-data/in-scope.txt\n fi\n done < /tmp/gh-aw/sweep-data/all.txt\nelse\n N=1\n SLOT=0\n cp /tmp/gh-aw/sweep-data/all.txt /tmp/gh-aw/sweep-data/in-scope.txt\nfi\n\nIN_SCOPE_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/in-scope.txt | tr -d ' ')\n\n# codespell exits non-zero when misspellings are found. Capture and continue.\nset +e\nif [ ! -s /tmp/gh-aw/sweep-data/in-scope.txt ]; then\n : > /tmp/gh-aw/sweep-data/codespell.out\n RC=0\nelse\n codespell \\\n --quiet-level=2 \\\n $CODESPELL_ARGS \\\n $(cat /tmp/gh-aw/sweep-data/in-scope.txt) \\\n > /tmp/gh-aw/sweep-data/codespell.out 2>&1\n RC=$?\nfi\nset -e\n\nFINDING_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/codespell.out | tr -d ' ')\ncat > /tmp/gh-aw/sweep-data/stats.json < /tmp/gh-aw/sweep-data/all.txt
+ : > /tmp/gh-aw/sweep-data/in-scope.txt
+
+ REQUESTED_COUNT=0
+ printf '%s' "$TARGET_FILES" | tr ',' '\n' > /tmp/gh-aw/sweep-data/target-files.raw
+ while IFS= read -r raw || [ -n "$raw" ]; do
+ entry=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
+ [ -z "$entry" ] && continue
+ REQUESTED_COUNT=$(( REQUESTED_COUNT + 1 ))
+ entry=${entry#/}
+ if [ "$DOCS_ROOT_CLEAN" = "." ] || [ -z "$DOCS_ROOT_CLEAN" ]; then
+ resolved="$entry"
+ else
+ case "$entry" in
+ "$DOCS_ROOT_CLEAN"/*) resolved="$entry" ;;
+ *) resolved="$DOCS_ROOT_CLEAN/$entry" ;;
+ esac
+ fi
+ if [ -f "$resolved" ]; then
+ echo "$resolved" >> /tmp/gh-aw/sweep-data/all.txt
+ else
+ echo "target-files: '$resolved' not found; skipping"
+ fi
+ done < /tmp/gh-aw/sweep-data/target-files.raw
+
+ sort -u /tmp/gh-aw/sweep-data/all.txt > /tmp/gh-aw/sweep-data/in-scope.txt
+ cp /tmp/gh-aw/sweep-data/in-scope.txt /tmp/gh-aw/sweep-data/all.txt
+ TOTAL_MD=$(wc -l < /tmp/gh-aw/sweep-data/all.txt | tr -d ' ')
+ IN_SCOPE_COUNT="$TOTAL_MD"
+
+ set +e
+ if [ ! -s /tmp/gh-aw/sweep-data/in-scope.txt ]; then
+ : > /tmp/gh-aw/sweep-data/codespell.out
+ RC=0
+ else
+ codespell \
+ --quiet-level=2 \
+ $CODESPELL_ARGS \
+ $(cat /tmp/gh-aw/sweep-data/in-scope.txt) \
+ > /tmp/gh-aw/sweep-data/codespell.out 2>&1
+ RC=$?
+ fi
+ set -e
+
+ FINDING_COUNT=$(wc -l < /tmp/gh-aw/sweep-data/codespell.out | tr -d ' ')
+ cat > /tmp/gh-aw/sweep-data/stats.json <' — codespell suggests: "`.
- `suggested_fix` — the chosen correction (omit for `ambiguous-typo`).
@@ -295,6 +376,9 @@ Title body depends on the selection mode:
- Full mode with `target_path`: `path / — typos across files`.
- Shard mode without `target_path`: `shard / — typos across files`.
- Shard mode with `target_path`: `path / — shard / — typos across files`.
+- Files mode (`selection_mode` is `files`): `file list — typos across files`.
+
+**Files mode**: when `selection_mode` is `files`, the caller supplied an explicit file list via `target-files`; scan exactly those files. Use the scope-summary line `Scanned of requested files; codespell raised raw findings, retained after filtering.`
```markdown
Generated by `gh-aw-docs-typos-sweep` for `${{ inputs.source-repo || github.repository }}` on .
@@ -313,6 +397,7 @@ Use one of these scope-summary lines:
line: 42
category: typo
severity: low
+ confidence: high
evidence: "'teh' — codespell suggests: the"
suggested_fix: |
the
@@ -320,6 +405,7 @@ Use one of these scope-summary lines:
line: 9
category: ambiguous-typo
severity: low
+ confidence: low
evidence: "'ambigous' — codespell suggests: ambiguous, ambiguously"
```
diff --git a/.github/workflows/gh-aw-fragments/findings-contract.md b/.github/workflows/gh-aw-fragments/findings-contract.md
new file mode 100644
index 0000000..f4700b1
--- /dev/null
+++ b/.github/workflows/gh-aw-fragments/findings-contract.md
@@ -0,0 +1,35 @@
+## Findings contract
+
+These rules apply to every finding this sweep emits. Sweep output is consumed by humans and, increasingly, by AI fix-agents that may act on it without a human in the loop. A finding that is uncertain, or that looks authoritative but is wrong, is worse than no finding at all.
+
+### Finding-type allowlist
+
+Emit only the `category` values enumerated in this workflow's "Build the findings list" step. That enumeration is a closed allowlist:
+
+- Never invent, rename, pluralize, or otherwise vary a category string. If a finding does not map cleanly to an allowlisted category, drop it.
+- A finding is valid only if applying its `suggested_fix` would change the page's rendered output or its published metadata. Drop no-op findings whose fix a reader would never see — for example, adding a marker the docs toolchain already generates automatically.
+- If you spot something real that has no allowlisted category, describe it in the issue body's **Notes** section as prose. Do not smuggle it in as a finding under an invented category.
+
+### Per-finding confidence
+
+Add a `confidence` field to every finding, set to exactly one of `high`, `medium`, or `low`. Judge confidence on how safe the finding is to act on *without* human verification — this is a separate axis from `severity`, which measures impact:
+
+- `high` — the problem and the fix are objective and verifiable from the evidence in front of you: a missing required field, a tool-flagged issue with a single unambiguous correction, a directly quoted contradiction. A fix-agent could apply the `suggested_fix` verbatim without judgment.
+- `medium` — the finding is well-supported, but the fix involves wording choices, or depends on a repository convention you could not fully verify this run. A human should confirm the fix before it lands.
+- `low` — the finding is plausible but rests on partial evidence, subjective judgment, or an assumption about intent or convention you could not confirm. If you cannot justify at least `low`, drop the finding rather than filing it.
+
+When a finding's evidence traces back to text you did not verify — for example, terminology copied from an issue or PR description rather than confirmed against the code or the published docs — cap its confidence at `low` and say so in the `evidence`.
+
+Include `confidence` in the YAML schema for every finding, alongside `severity`. Keep the existing sort order (by `severity` first); do not reorder by confidence.
+
+### Human-review gate
+
+If the capped findings list contains **any** finding with `confidence: medium` or `confidence: low`:
+
+1. Add the label `needs-human-review` to your `create_issue` call, in addition to the labels the workflow adds automatically. This marks the issue as not safe to auto-action and keeps it out of the `good-for-ai` delegation track.
+2. Immediately below the `## Findings ()` heading and before the YAML block, add this callout verbatim:
+
+ > [!WARNING]
+ > This issue contains medium- or low-confidence findings. Review them before acting — auto-applying sweep output without verification risks putting incorrect content into the docs. Findings marked `confidence: high` are safe to delegate to a fix-agent; `medium` and `low` need a human sign-off first.
+
+If every finding is `confidence: high`, do not add the label or the callout: the issue is safe to delegate as-is.
diff --git a/.github/workflows/gh-aw-fragments/size-logic.md b/.github/workflows/gh-aw-fragments/size-logic.md
index 512137e..5aa8047 100644
--- a/.github/workflows/gh-aw-fragments/size-logic.md
+++ b/.github/workflows/gh-aw-fragments/size-logic.md
@@ -108,6 +108,9 @@ Decide whether to apply the `good-for-ai` label. Apply it only when **all** of t
- There are no blocking human-only steps (e.g. design sign-off, credentials an agent cannot
obtain).
- The effort is `hours` or `weeks: <1`.
+- The issue is **not** labeled `needs-human-review`. That label marks findings (for example, from
+ a docs quality sweep) that a human must sign off on before any automated action. Never put such
+ an issue on the `good-for-ai` track, regardless of effort.
Otherwise, do not apply it.
diff --git a/.github/workflows/gh-aw-issue-size.lock.yml b/.github/workflows/gh-aw-issue-size.lock.yml
index cca6d32..e0fa52b 100644
--- a/.github/workflows/gh-aw-issue-size.lock.yml
+++ b/.github/workflows/gh-aw-issue-size.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a0aa6ab9525d39986c4da8917f52a378253f88e0fff2d071a3278087698bf5da","body_hash":"b32e5b54b843217ced0a556768024541496a60cced7d7fd79158fe91c5de8ab2","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"gpt-5-mini","engine_versions":{"copilot":"1.0.73"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a0aa6ab9525d39986c4da8917f52a378253f88e0fff2d071a3278087698bf5da","body_hash":"5b249c5d03ee4aafc46098de04017cde70324b69b191abe61b9086761699694d","compiler_version":"v0.83.1","agent_id":"copilot","agent_model":"gpt-5-mini","engine_versions":{"copilot":"1.0.73"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}
# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -324,20 +324,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_6cb3ae9b7747f500_EOF'
+ cat << 'GH_AW_PROMPT_a2cceecca291d497_EOF'
- GH_AW_PROMPT_6cb3ae9b7747f500_EOF
+ GH_AW_PROMPT_a2cceecca291d497_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_6cb3ae9b7747f500_EOF'
+ cat << 'GH_AW_PROMPT_a2cceecca291d497_EOF'
Tools: add_comment, add_labels(max:2), missing_tool, missing_data, noop
- GH_AW_PROMPT_6cb3ae9b7747f500_EOF
+ GH_AW_PROMPT_a2cceecca291d497_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
- cat << 'GH_AW_PROMPT_6cb3ae9b7747f500_EOF'
+ cat << 'GH_AW_PROMPT_a2cceecca291d497_EOF'
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -366,9 +366,9 @@ jobs:
{{/if}}
- GH_AW_PROMPT_6cb3ae9b7747f500_EOF
+ GH_AW_PROMPT_a2cceecca291d497_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_6cb3ae9b7747f500_EOF'
+ cat << 'GH_AW_PROMPT_a2cceecca291d497_EOF'
## Formatting Guidelines
@@ -519,6 +519,9 @@ jobs:
- There are no blocking human-only steps (e.g. design sign-off, credentials an agent cannot
obtain).
- The effort is `hours` or `weeks: <1`.
+ - The issue is **not** labeled `needs-human-review`. That label marks findings (for example, from
+ a docs quality sweep) that a human must sign off on before any automated action. Never put such
+ an issue on the `good-for-ai` track, regardless of effort.
Otherwise, do not apply it.
@@ -574,7 +577,7 @@ jobs:
__GH_AW_EXPR_49B959F1__
- GH_AW_PROMPT_6cb3ae9b7747f500_EOF
+ GH_AW_PROMPT_a2cceecca291d497_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
diff --git a/agentic-workflows/README.md b/agentic-workflows/README.md
index 6f6013b..1575108 100644
--- a/agentic-workflows/README.md
+++ b/agentic-workflows/README.md
@@ -32,4 +32,6 @@ These workflows use `COPILOT_GITHUB_TOKEN` for authentication. Pass `secrets.COP
Skill imports are workflow-specific. Some workflows install APM skills from `elastic/elastic-docs-skills`, while others intentionally rely only on embedded rules and deterministic pre-steps.
-For the sweep family, `docs-root` defines the default corpus, `target-path` narrows that corpus to one subtree under the root, and `scope-mode` controls whether the matched set is scanned in full or sharded.
+For the sweep family, `docs-root` defines the default corpus, `target-path` narrows that corpus to one subtree under the root, and `scope-mode` controls whether the matched set is scanned in full or sharded. `target-files` overrides both to sweep an explicit list of files (ideal for post-merge "check only what changed" runs).
+
+Every sweep finding carries a `confidence` rating (`high` / `medium` / `low`) alongside its `severity`. A fix-issue that contains any medium- or low-confidence finding is labeled `needs-human-review` and kept off the `good-for-ai` auto-delegation track, so uncertain output is not applied to the docs without a human sign-off.
diff --git a/agentic-workflows/docs-applies-to-sweep/README.md b/agentic-workflows/docs-applies-to-sweep/README.md
index 2ca2e42..af30691 100644
--- a/agentic-workflows/docs-applies-to-sweep/README.md
+++ b/agentic-workflows/docs-applies-to-sweep/README.md
@@ -24,6 +24,7 @@ Add `copilot-requests: write` to the caller job `permissions:` block — no secr
|-------|------|----------|---------|-------------|
| `docs-root` | string | No | `docs/` | Root directory to sweep. |
| `target-path` | string | No | `""` | Optional `docs-root`-relative directory to sweep recursively. Accepts a leading slash, such as `/solutions/observability`. |
+| `target-files` | string | No | `""` | Newline- or comma-separated list of `docs-root`-relative file paths to sweep. When set, overrides `target-path` and `scope-mode` — the sweep processes exactly these files. |
| `scope-mode` | string | No | `auto` | Scope behavior for the matched markdown files. `auto` preserves the existing behavior, `full` scans all matched files, and `shard` shards within the matched set. |
| `target-batch-size` | string | No | `100` | Pages per slice; controls shard count `N`. |
| `max-per-fix-issue` | string | No | `20` | Findings cap per fix-issue. |
@@ -37,6 +38,10 @@ Add `copilot-requests: write` to the caller job `permissions:` block — no secr
| `noop` | — | — |
| `create-issue` | 1 | `docs-quality-sweep`, `docs-fix:applies-to` |
+When any finding in the issue is `medium`- or `low`-confidence, the sweep also adds a `needs-human-review` label and a review-before-acting callout above the findings. Issues where every finding is `high`-confidence carry no such label and are safe to delegate to a fix-agent.
+
+Each finding carries a `confidence` field (`high`/`medium`/`low`) that signals how safe it is to act on without human verification — a separate axis from `severity`.
+
## How it works
1. Pre-step enumerates `*.md` under the matched scope (`docs-root`, optionally narrowed by `target-path`), then either scans them all or computes the rotating slice (`hash(path) mod N == iso_week mod N`) plus pages modified in the last 7 days based on `scope-mode`.
diff --git a/agentic-workflows/docs-applies-to-sweep/example.yml b/agentic-workflows/docs-applies-to-sweep/example.yml
index f2e9a9e..baa4526 100644
--- a/agentic-workflows/docs-applies-to-sweep/example.yml
+++ b/agentic-workflows/docs-applies-to-sweep/example.yml
@@ -14,6 +14,10 @@ on:
description: "Optional docs-root-relative directory to sweep recursively (accepts a leading slash)"
required: false
default: ""
+ target-files:
+ description: "Newline- or comma-separated list of docs-root-relative files to sweep. Overrides target-path and scope-mode."
+ required: false
+ default: ""
scope-mode:
description: "How to scope the matched markdown files: auto, full, or shard"
required: false
@@ -42,6 +46,7 @@ jobs:
source-repo: ${{ inputs.source-repo }}
docs-root: ${{ inputs.docs-root }}
target-path: ${{ inputs.target-path }}
+ target-files: ${{ inputs.target-files }}
scope-mode: ${{ inputs.scope-mode }}
target-batch-size: ${{ inputs.target-batch-size }}
max-per-fix-issue: ${{ inputs.max-per-fix-issue }}
diff --git a/agentic-workflows/docs-coherence-sweep/README.md b/agentic-workflows/docs-coherence-sweep/README.md
index f7cb636..0afef09 100644
--- a/agentic-workflows/docs-coherence-sweep/README.md
+++ b/agentic-workflows/docs-coherence-sweep/README.md
@@ -31,6 +31,7 @@ Add `copilot-requests: write` to the caller job `permissions:` block. Also confi
|-------|---------|-------------|
| `docs-root` | `docs/` | Root directory to sweep. |
| `target-path` | `""` | Optional `docs-root`-relative directory to sweep recursively. Accepts a leading slash, such as `/solutions/observability`. |
+| `target-files` | `""` | Newline- or comma-separated list of `docs-root`-relative file paths to sweep. When set, overrides `target-path` and `scope-mode` — the sweep processes exactly these files. |
| `scope-mode` | `auto` | Scope behavior for the matched markdown files. `auto` preserves the existing behavior, `full` scans all matched files, and `shard` shards within the matched set. |
| `target-batch-size` | `50` | Pages per slice. Smaller than other sweeps because each comparison is expensive. |
| `max-per-fix-issue` | `20` | Findings cap per fix-issue. |
@@ -45,6 +46,10 @@ Add `copilot-requests: write` to the caller job `permissions:` block. Also confi
| `noop` | — | — |
| `create-issue` | 1 | `docs-quality-sweep`, `docs-fix:coherence` (filed in `elastic/docs-content-internal`) |
+When any finding in the issue is `medium`- or `low`-confidence, the sweep also adds a `needs-human-review` label and a review-before-acting callout above the findings. Issues where every finding is `high`-confidence carry no such label and are safe to delegate to a fix-agent.
+
+Each finding carries a `confidence` field (`high`/`medium`/`low`) that signals how safe it is to act on without human verification — a separate axis from `severity`.
+
## How it works
1. Pre-step enumerates `*.md` under the matched scope (`docs-root`, optionally narrowed by `target-path`), then either scans them all or computes the rotating slice plus recently-changed pages based on `scope-mode`.
diff --git a/agentic-workflows/docs-coherence-sweep/example.yml b/agentic-workflows/docs-coherence-sweep/example.yml
index 7e221f2..65408b2 100644
--- a/agentic-workflows/docs-coherence-sweep/example.yml
+++ b/agentic-workflows/docs-coherence-sweep/example.yml
@@ -14,6 +14,10 @@ on:
description: "Optional docs-root-relative directory to sweep recursively (accepts a leading slash)"
required: false
default: ""
+ target-files:
+ description: "Newline- or comma-separated list of docs-root-relative files to sweep. Overrides target-path and scope-mode."
+ required: false
+ default: ""
scope-mode:
description: "How to scope the matched markdown files: auto, full, or shard"
required: false
@@ -42,6 +46,7 @@ jobs:
source-repo: ${{ inputs.source-repo }}
docs-root: ${{ inputs.docs-root }}
target-path: ${{ inputs.target-path }}
+ target-files: ${{ inputs.target-files }}
scope-mode: ${{ inputs.scope-mode }}
target-batch-size: ${{ inputs.target-batch-size }}
max-per-fix-issue: ${{ inputs.max-per-fix-issue }}
diff --git a/agentic-workflows/docs-frontmatter-sweep/README.md b/agentic-workflows/docs-frontmatter-sweep/README.md
index b2db268..7c73ead 100644
--- a/agentic-workflows/docs-frontmatter-sweep/README.md
+++ b/agentic-workflows/docs-frontmatter-sweep/README.md
@@ -27,6 +27,7 @@ Issues are filed in the **calling repo** (where the workflow runs). Install this
| `source-repo` | string | No | `""` (calling repo) | Repository to scan, as `owner/repo`. Set this when the workflow runs in a triage repo (e.g., `docs-content-internal`) but should audit a separate docs repo (e.g., `docs-content`). The example template defaults to `elastic/docs-content`. |
| `docs-root` | string | No | `docs/` | Root directory to sweep within the source repo. Set to `.` for repos where docs live at the repo root. |
| `target-path` | string | No | `""` | Optional `docs-root`-relative directory to sweep recursively. Accepts a leading slash, such as `/solutions/observability`. |
+| `target-files` | string | No | `""` | Newline- or comma-separated list of `docs-root`-relative file paths to sweep. When set, overrides `target-path` and `scope-mode` — the sweep processes exactly these files. |
| `scope-mode` | string | No | `auto` | Scope behavior for the matched markdown files. `auto` preserves the existing behavior, `full` scans all matched files, and `shard` shards within the matched set. |
| `target-batch-size` | string | No | `100` | Approximate pages per slice; controls shard count `N = ceil(total/batch-size)`. |
| `max-per-fix-issue` | string | No | `20` | Cap on findings per fix-issue. Overflow surfaces in the next sweep. |
@@ -40,6 +41,8 @@ Issues are filed in the **calling repo** (where the workflow runs). Install this
| `noop` | — | — | No high-confidence findings in this slice. |
| `create-issue` | 1 | `docs-quality-sweep`, `docs-fix:frontmatter` | Fix-issue with structured YAML findings. |
+When any finding in the issue is `medium`- or `low`-confidence, the sweep also adds a `needs-human-review` label and a review-before-acting callout above the findings. Issues where every finding is `high`-confidence carry no such label and are safe to delegate to a fix-agent.
+
## How it works
1. A pre-step enumerates `*.md` under the matched scope (`docs-root`, optionally narrowed by `target-path`), then either scans them all or computes a deterministic shard `(hash(path) mod N == iso_week mod N)` based on `scope-mode`.
@@ -66,11 +69,14 @@ The issue body contains a fenced YAML block with one entry per finding:
line: 1
category: missing-description
severity: high
+ confidence: high
evidence: "frontmatter has no `description` field"
suggested_fix: |
description: "How to configure X for Y use cases."
```
+Each finding carries a `confidence` field (`high`/`medium`/`low`) that signals how safe it is to act on without human verification — a separate axis from `severity`.
+
Categories: `missing-description`, `weak-description`, `description-too-long`, `missing-products`, `missing-navigation-title`.
The workflow may use Elastic docs MCP for targeted published authoring guidance, but it does not use MCP as a blanket replacement for local frontmatter evidence.
diff --git a/agentic-workflows/docs-frontmatter-sweep/example.yml b/agentic-workflows/docs-frontmatter-sweep/example.yml
index 2ff8b67..ccf38ee 100644
--- a/agentic-workflows/docs-frontmatter-sweep/example.yml
+++ b/agentic-workflows/docs-frontmatter-sweep/example.yml
@@ -14,6 +14,10 @@ on:
description: "Optional docs-root-relative directory to sweep recursively (accepts a leading slash)"
required: false
default: ""
+ target-files:
+ description: "Newline- or comma-separated list of docs-root-relative files to sweep. Overrides target-path and scope-mode."
+ required: false
+ default: ""
scope-mode:
description: "How to scope the matched markdown files: auto, full, or shard"
required: false
@@ -42,6 +46,7 @@ jobs:
source-repo: ${{ inputs.source-repo }}
docs-root: ${{ inputs.docs-root }}
target-path: ${{ inputs.target-path }}
+ target-files: ${{ inputs.target-files }}
scope-mode: ${{ inputs.scope-mode }}
target-batch-size: ${{ inputs.target-batch-size }}
max-per-fix-issue: ${{ inputs.max-per-fix-issue }}
diff --git a/agentic-workflows/docs-issue-scope/README.md b/agentic-workflows/docs-issue-scope/README.md
index 3f3b7a4..dfa63d0 100644
--- a/agentic-workflows/docs-issue-scope/README.md
+++ b/agentic-workflows/docs-issue-scope/README.md
@@ -37,6 +37,8 @@ Add `copilot-requests: write` to the caller job `permissions:` block — no secr
This workflow explicitly sets `tools.github.min-integrity: none` so it can scope docs work from public community issues in public repositories. Treat issue and comment content as untrusted input, and rely on the workflow prompt and safe outputs to keep the analysis constrained.
+Because of this, every recommendation in the managed block carries a **Confidence** rating (High / Medium / Low). Terminology or capabilities that appear only in the issue or a linked PR — and cannot be verified against the code or the published docs — are attributed ("the issue describes …") rather than asserted as fact, and marked **Low**, with a caveat line under the recommendations table. This is the signal that prevents a contributor's incorrect phrasing from propagating into a docs recommendation.
+
## Managed issue block
When `/docs-issue-scope` runs on an issue, the workflow prefers to maintain a bot-managed block between:
diff --git a/agentic-workflows/docs-openings-sweep/README.md b/agentic-workflows/docs-openings-sweep/README.md
index bdd4034..dc7fd24 100644
--- a/agentic-workflows/docs-openings-sweep/README.md
+++ b/agentic-workflows/docs-openings-sweep/README.md
@@ -24,6 +24,7 @@ Add `copilot-requests: write` to the caller job `permissions:` block — no secr
|-------|------|----------|---------|-------------|
| `docs-root` | string | No | `docs/` | Root directory to sweep. |
| `target-path` | string | No | `""` | Optional `docs-root`-relative directory to sweep recursively. Accepts a leading slash, such as `/solutions/observability`. |
+| `target-files` | string | No | `""` | Newline- or comma-separated list of `docs-root`-relative file paths to sweep. When set, overrides `target-path` and `scope-mode` — the sweep processes exactly these files. |
| `scope-mode` | string | No | `auto` | Scope behavior for the matched markdown files. `auto` preserves the existing behavior, `full` scans all matched files, and `shard` shards within the matched set. |
| `target-batch-size` | string | No | `100` | Pages per slice. |
| `max-per-fix-issue` | string | No | `20` | Findings cap per fix-issue. |
@@ -37,11 +38,15 @@ Add `copilot-requests: write` to the caller job `permissions:` block — no secr
| `noop` | — | — |
| `create-issue` | 1 | `docs-quality-sweep`, `docs-fix:openings` |
+When any finding in the issue is `medium`- or `low`-confidence, the sweep also adds a `needs-human-review` label and a review-before-acting callout above the findings. Issues where every finding is `high`-confidence carry no such label and are safe to delegate to a fix-agent.
+
+Each finding carries a `confidence` field (`high`/`medium`/`low`) that signals how safe it is to act on without human verification — a separate axis from `severity`.
+
## How it works
1. Pre-step enumerates `*.md` under the matched scope (`docs-root`, optionally narrowed by `target-path`), then either scans them all or computes the rotating slice plus recently-changed pages based on `scope-mode`.
2. The agent reads the copied slice and applies embedded checks for content type, H1 specificity, opening paragraph quality, task prerequisites, substitutions, UI/technical formatting in openings, and navigation titles.
-3. Categories: `missing-h1`, `vague-h1`, `missing-h1-anchor`, `weak-opening`, `missing-before-you-begin`, `inadequate-navigation-title`.
+3. Categories: `missing-h1`, `vague-h1`, `weak-opening`, `missing-before-you-begin`, `inadequate-navigation-title`.
4. The agent may use Elastic docs MCP to compare sibling page titles when H1 specificity needs published-doc context.
5. The workflow does not edit files or push changes — only the structured findings are emitted in the fix-issue.
diff --git a/agentic-workflows/docs-openings-sweep/example.yml b/agentic-workflows/docs-openings-sweep/example.yml
index 91eed66..267222c 100644
--- a/agentic-workflows/docs-openings-sweep/example.yml
+++ b/agentic-workflows/docs-openings-sweep/example.yml
@@ -14,6 +14,10 @@ on:
description: "Optional docs-root-relative directory to sweep recursively (accepts a leading slash)"
required: false
default: ""
+ target-files:
+ description: "Newline- or comma-separated list of docs-root-relative files to sweep. Overrides target-path and scope-mode."
+ required: false
+ default: ""
scope-mode:
description: "How to scope the matched markdown files: auto, full, or shard"
required: false
@@ -42,6 +46,7 @@ jobs:
source-repo: ${{ inputs.source-repo }}
docs-root: ${{ inputs.docs-root }}
target-path: ${{ inputs.target-path }}
+ target-files: ${{ inputs.target-files }}
scope-mode: ${{ inputs.scope-mode }}
target-batch-size: ${{ inputs.target-batch-size }}
max-per-fix-issue: ${{ inputs.max-per-fix-issue }}
diff --git a/agentic-workflows/docs-quality-sweep/README.md b/agentic-workflows/docs-quality-sweep/README.md
index ceeff51..b60b9f4 100644
--- a/agentic-workflows/docs-quality-sweep/README.md
+++ b/agentic-workflows/docs-quality-sweep/README.md
@@ -53,6 +53,7 @@ gh workflow run docs-quality-sweep.yml \
| `source-repo` | `elastic/docs-content` | Repository to scan, as `owner/repo`. Set to empty to scan the calling repo. |
| `docs-root` | `.` | Root directory inside the source repo. `.` works for repos where docs live at the root (e.g., `elastic/docs-content`). Set to `docs/` for repos with a `docs/` subtree. |
| `target-path` | `""` | Optional `docs-root`-relative directory to sweep recursively. Accepts a leading slash, such as `/solutions/observability`. |
+| `target-files` | `""` | Newline- or comma-separated list of `docs-root`-relative file paths. When set, overrides `target-path` and `scope-mode` — each sweep processes exactly these files. Ideal for post-merge "check only what changed" runs. |
| `scope-mode` | `auto` | Scope behavior for the matched markdown files. `auto` preserves the existing behavior, `full` scans all matched files, and `shard` shards within the matched set. |
| `target-batch-size` | `100` | Approximate pages per rotating slice when `scope-mode` resolves to `shard`. |
| `max-per-fix-issue` | `20` | Cap on findings per fix-issue — overflow surfaces in the next sweep. |
@@ -76,6 +77,23 @@ Each sweep opens its own labeled fix-issue **in the calling repo** (or calls `no
All issues also carry the parent label `docs-quality-sweep`. Sweep issues stay open until maintainers close them or a fixing PR resolves them.
+## Confidence and the human-review gate
+
+Every finding a sweep emits carries a `confidence` field — `high`, `medium`, or `low` — that signals how safe it is to act on *without* human verification. This is a separate axis from `severity`, which measures impact:
+
+- `high` — objective and verifiable from the evidence; a fix-agent can apply the suggested fix without judgment.
+- `medium` — well-supported, but the fix involves wording or convention choices a human should confirm.
+- `low` — plausible but resting on partial evidence or unverified assumptions.
+
+When a fix-issue contains **any** medium- or low-confidence finding, the sweep adds a `needs-human-review` label and a review-before-acting callout above the findings.
+
+**Intended agent workflow:**
+
+- A fix-issue **without** `needs-human-review` (every finding is high-confidence) is safe to delegate to a fix-agent — this is the `good-for-ai` track.
+- A fix-issue **with** `needs-human-review` needs a human sign-off before any automated action. The `/size` workflow will not put the `good-for-ai` label on an issue that carries `needs-human-review`, so it stays off the auto-delegation track.
+
+This gate exists because sweep output is increasingly read by AI agents that act on it directly. Signaling confidence and gating low-confidence findings keeps incorrect content from being auto-applied to the docs.
+
## Skill mapping
The orchestrator does not import APM skills directly. Each child sweep owns its own skill mapping so only strong workflow-to-skill matches are installed, and workflows without a strong public `elastic-docs-skills` match can keep relying on embedded rules and deterministic pre-steps.
@@ -113,6 +131,20 @@ gh workflow run docs-quality-sweep.yml \
-f target-batch-size=100
```
+## Running against a specific file list
+
+Pass `target-files` to sweep only the files you name — it overrides `target-path` and `scope-mode`. This is the precise, low-cost option for "check only the files I just changed" (e.g. a post-merge trigger that already knows the changed paths):
+
+```bash
+gh workflow run docs-quality-sweep.yml \
+ -f sweeps=frontmatter,style,typos \
+ -f source-repo=elastic/docs-content \
+ -f docs-root=. \
+ -f target-files="solutions/observability/apm/index.md, solutions/observability/logs/index.md"
+```
+
+Newline-separated also works. Cost scales with the number of files you pass, not with the size of any directory.
+
## Adding a schedule
Once manual runs look right, append a cron trigger:
diff --git a/agentic-workflows/docs-quality-sweep/example.yml b/agentic-workflows/docs-quality-sweep/example.yml
index 34b5de4..418b394 100644
--- a/agentic-workflows/docs-quality-sweep/example.yml
+++ b/agentic-workflows/docs-quality-sweep/example.yml
@@ -31,6 +31,10 @@ on:
description: "Optional docs-root-relative directory to sweep recursively (accepts a leading slash)"
required: false
default: ""
+ target-files:
+ description: "Newline- or comma-separated list of docs-root-relative files to sweep. Overrides target-path and scope-mode."
+ required: false
+ default: ""
scope-mode:
description: "How to scope the matched markdown files: auto, full, or shard"
required: false
@@ -51,6 +55,7 @@ jobs:
SRC: ${{ inputs.source-repo }}
ROOT: ${{ inputs.docs-root }}
TARGET_PATH: ${{ inputs.target-path }}
+ TARGET_FILES: ${{ inputs.target-files }}
SCOPE_MODE: ${{ inputs.scope-mode }}
run: |
set -e
@@ -67,6 +72,7 @@ jobs:
-f source-repo="$SRC" \
-f docs-root="$ROOT" \
-f target-path="$TARGET_PATH" \
+ -f target-files="$TARGET_FILES" \
-f scope-mode="$SCOPE_MODE")"
dispatched_names+=("$name")
dispatched_urls+=("$run_url")
diff --git a/agentic-workflows/docs-staleness-sweep/README.md b/agentic-workflows/docs-staleness-sweep/README.md
index 49035e5..7954d51 100644
--- a/agentic-workflows/docs-staleness-sweep/README.md
+++ b/agentic-workflows/docs-staleness-sweep/README.md
@@ -33,6 +33,7 @@ Add `copilot-requests: write` to the caller job `permissions:` block. Also confi
|-------|---------|-------------|
| `docs-root` | `docs/` | Root directory to sweep. |
| `target-path` | `""` | Optional `docs-root`-relative directory to sweep recursively. Accepts a leading slash, such as `/solutions/observability`. |
+| `target-files` | `""` | Newline- or comma-separated list of `docs-root`-relative file paths to sweep. When set, overrides `target-path` and `scope-mode` — the sweep processes exactly these files. |
| `scope-mode` | `auto` | Scope behavior for the matched markdown files. `auto` preserves the existing behavior, `full` scans all matched files, and `shard` shards within the matched set. |
| `target-batch-size` | `100` | Approximate pages per slice. |
| `max-per-fix-issue` | `30` | Findings cap per fix-issue. |
@@ -49,6 +50,10 @@ Add `copilot-requests: write` to the caller job `permissions:` block. Also confi
| `noop` | — | — |
| `create-issue` | 1 | `docs-quality-sweep`, `docs-fix:staleness` (filed in `elastic/docs-content-internal`) |
+When any finding in the issue is `medium`- or `low`-confidence, the sweep also adds a `needs-human-review` label and a review-before-acting callout above the findings. Issues where every finding is `high`-confidence carry no such label and are safe to delegate to a fix-agent.
+
+Each finding carries a `confidence` field (`high`/`medium`/`low`) that signals how safe it is to act on without human verification — a separate axis from `severity`.
+
## How it works
1. **Pre-step 1** — enumerate `*.md` under the matched scope (`docs-root`, optionally narrowed by `target-path`), then either scan them all or compute the rotating slice plus pages modified in the last 7 days based on `scope-mode`.
diff --git a/agentic-workflows/docs-staleness-sweep/example.yml b/agentic-workflows/docs-staleness-sweep/example.yml
index 95a497d..ad486cd 100644
--- a/agentic-workflows/docs-staleness-sweep/example.yml
+++ b/agentic-workflows/docs-staleness-sweep/example.yml
@@ -14,6 +14,10 @@ on:
description: "Optional docs-root-relative directory to sweep recursively (accepts a leading slash)"
required: false
default: ""
+ target-files:
+ description: "Newline- or comma-separated list of docs-root-relative files to sweep. Overrides target-path and scope-mode."
+ required: false
+ default: ""
scope-mode:
description: "How to scope the matched markdown files: auto, full, or shard"
required: false
@@ -46,6 +50,7 @@ jobs:
source-repo: ${{ inputs.source-repo }}
docs-root: ${{ inputs.docs-root }}
target-path: ${{ inputs.target-path }}
+ target-files: ${{ inputs.target-files }}
scope-mode: ${{ inputs.scope-mode }}
target-batch-size: ${{ inputs.target-batch-size }}
max-per-fix-issue: ${{ inputs.max-per-fix-issue }}
diff --git a/agentic-workflows/docs-style-sweep/README.md b/agentic-workflows/docs-style-sweep/README.md
index 17485ed..35aa248 100644
--- a/agentic-workflows/docs-style-sweep/README.md
+++ b/agentic-workflows/docs-style-sweep/README.md
@@ -24,6 +24,7 @@ Add `copilot-requests: write` to the caller job `permissions:` block — no secr
|-------|------|----------|---------|-------------|
| `docs-root` | string | No | `docs/` | Root directory to sweep. |
| `target-path` | string | No | `""` | Optional `docs-root`-relative directory to sweep recursively. Accepts a leading slash, such as `/solutions/observability`. |
+| `target-files` | string | No | `""` | Newline- or comma-separated list of `docs-root`-relative file paths to sweep. When set, overrides `target-path` and `scope-mode` — the sweep processes exactly these files. |
| `scope-mode` | string | No | `auto` | Scope behavior for the matched markdown files. `auto` preserves the existing behavior, `full` scans all matched files, and `shard` shards within the matched set. |
| `target-batch-size` | string | No | `100` | Pages per slice. |
| `max-per-fix-issue` | string | No | `20` | Findings cap per fix-issue. |
@@ -37,6 +38,10 @@ Add `copilot-requests: write` to the caller job `permissions:` block — no secr
| `noop` | — | — |
| `create-issue` | 1 | `docs-quality-sweep`, `docs-fix:style` |
+When any finding in the issue is `medium`- or `low`-confidence, the sweep also adds a `needs-human-review` label and a review-before-acting callout above the findings. Issues where every finding is `high`-confidence carry no such label and are safe to delegate to a fix-agent.
+
+Each finding carries a `confidence` field (`high`/`medium`/`low`) that signals how safe it is to act on without human verification — a separate axis from `severity`.
+
## How it works
1. Pre-step enumerates `*.md` under the matched scope (`docs-root`, optionally narrowed by `target-path`), then either scans them all or computes the rotating slice plus recently-changed pages based on `scope-mode`.
diff --git a/agentic-workflows/docs-style-sweep/example.yml b/agentic-workflows/docs-style-sweep/example.yml
index 03607f7..74dca03 100644
--- a/agentic-workflows/docs-style-sweep/example.yml
+++ b/agentic-workflows/docs-style-sweep/example.yml
@@ -14,6 +14,10 @@ on:
description: "Optional docs-root-relative directory to sweep recursively (accepts a leading slash)"
required: false
default: ""
+ target-files:
+ description: "Newline- or comma-separated list of docs-root-relative files to sweep. Overrides target-path and scope-mode."
+ required: false
+ default: ""
scope-mode:
description: "How to scope the matched markdown files: auto, full, or shard"
required: false
@@ -42,6 +46,7 @@ jobs:
source-repo: ${{ inputs.source-repo }}
docs-root: ${{ inputs.docs-root }}
target-path: ${{ inputs.target-path }}
+ target-files: ${{ inputs.target-files }}
scope-mode: ${{ inputs.scope-mode }}
target-batch-size: ${{ inputs.target-batch-size }}
max-per-fix-issue: ${{ inputs.max-per-fix-issue }}
diff --git a/agentic-workflows/docs-typos-sweep/README.md b/agentic-workflows/docs-typos-sweep/README.md
index 9167c2b..d52392b 100644
--- a/agentic-workflows/docs-typos-sweep/README.md
+++ b/agentic-workflows/docs-typos-sweep/README.md
@@ -26,6 +26,7 @@ Add `copilot-requests: write` to the caller job `permissions:` block — no secr
|-------|------|----------|---------|-------------|
| `docs-root` | string | No | `docs/` | Root directory to scan. |
| `target-path` | string | No | `""` | Optional `docs-root`-relative directory to scan recursively. Accepts a leading slash, such as `/solutions/observability`. |
+| `target-files` | string | No | `""` | Newline- or comma-separated list of `docs-root`-relative file paths to scan. When set, overrides `target-path` and `scope-mode` — the sweep processes exactly these files. |
| `scope-mode` | string | No | `auto` | Scope behavior for the matched markdown files. `auto` preserves the existing behavior, `full` scans all matched files, and `shard` shards within the matched set. |
| `target-batch-size` | string | No | `100` | Approximate pages per rotating slice when `scope-mode=shard`. |
| `max-per-fix-issue` | string | No | `50` | Cap on findings per fix-issue. |
@@ -40,6 +41,10 @@ Add `copilot-requests: write` to the caller job `permissions:` block — no secr
| `noop` | — | — |
| `create-issue` | 1 | `docs-quality-sweep`, `docs-fix:typos` |
+When any finding in the issue is `medium`- or `low`-confidence, the sweep also adds a `needs-human-review` label and a review-before-acting callout above the findings. Issues where every finding is `high`-confidence carry no such label and are safe to delegate to a fix-agent.
+
+Each finding carries a `confidence` field (`high`/`medium`/`low`) that signals how safe it is to act on without human verification — a separate axis from `severity`.
+
## How it works
1. Pre-step installs `codespell`, enumerates `*.md` under the matched scope (`docs-root`, optionally narrowed by `target-path`), and either scans them all or scans one rotating shard based on `scope-mode`, capturing output to `/tmp/gh-aw/sweep-data/codespell.out`.
diff --git a/agentic-workflows/docs-typos-sweep/example.yml b/agentic-workflows/docs-typos-sweep/example.yml
index a2f11d3..d939dc5 100644
--- a/agentic-workflows/docs-typos-sweep/example.yml
+++ b/agentic-workflows/docs-typos-sweep/example.yml
@@ -14,6 +14,10 @@ on:
description: "Optional docs-root-relative directory to scan recursively (accepts a leading slash)"
required: false
default: ""
+ target-files:
+ description: "Newline- or comma-separated list of docs-root-relative files to scan. Overrides target-path and scope-mode."
+ required: false
+ default: ""
scope-mode:
description: "How to scope the matched markdown files: auto, full, or shard"
required: false
@@ -46,6 +50,7 @@ jobs:
source-repo: ${{ inputs.source-repo }}
docs-root: ${{ inputs.docs-root }}
target-path: ${{ inputs.target-path }}
+ target-files: ${{ inputs.target-files }}
scope-mode: ${{ inputs.scope-mode }}
target-batch-size: ${{ inputs.target-batch-size }}
max-per-fix-issue: ${{ inputs.max-per-fix-issue }}