Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
eaacc50
test(strix): require consolidated metadata admission
seonghobae Sep 6, 2026
c5d4686
ci(strix): consolidate metadata admission
seonghobae Sep 6, 2026
726b601
fix(strix): preserve admission shell bytes
seonghobae Sep 6, 2026
3ae7c80
chore(ci): 최신 main을 Strix 통합 브랜치에 반영
seonghobae Sep 6, 2026
c31aa02
merge: preserve Strix metadata consolidation on current main
seonghobae Sep 6, 2026
ee5567f
test(actions): pin concurrency group keys against comment leakage (#1…
seonghobae Sep 6, 2026
6e014c9
fix(actions): coalesce superseded agent mentions while they are queue…
seonghobae Sep 6, 2026
9aad23c
test(actions): parse the concurrency group instead of slicing the blo…
seonghobae Sep 6, 2026
ad0779b
fix(scheduler): skip review dispatch for a head whose merge tree cann…
seonghobae Sep 6, 2026
49eb9e7
test(actions): anchor cancel-in-progress contracts so a flipped flag …
seonghobae Sep 6, 2026
5c60b5d
test(concurrency): pin the cancel flag as a value for the PR-keyed sc…
seonghobae Sep 6, 2026
74224b2
fix(scheduler): match the run name GitHub actually sends for central …
seonghobae Sep 6, 2026
2396ddc
Revert "fix(scheduler): skip review dispatch for a head whose merge t…
seonghobae Sep 6, 2026
bf0bf0a
fix(audit): stop a ruleset drift from disabling the CodeQL coverage d…
seonghobae Sep 6, 2026
c9052e6
fix(audit): count the repositories examined, not the ones supplied (#…
seonghobae Sep 6, 2026
402aca1
Merge origin/main into the strix metadata consolidation
seonghobae Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions .github/workflows/agent-mention-noema-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,26 @@ on:
repository_dispatch:
types: [agent-mention-noema]

concurrency:
# Workflow-level admission, for the same reason strix.yml, noema-review.yml,
# opencode-review.yml and opencode-review-dispatch.yml carry theirs at this level:
# a job-level group is never evaluated while the whole run waits behind the
# organization job ceiling, so a superseded mention keeps its queue slot until a
# runner frees up and only then cancels. At workflow level the older run is
# coalesced while both are still queued, which is where the slot is actually held.
# This workflow has a single job, so the group lives here and nowhere else --
# every workflow in this repository that carries a group at both levels
# (strix.yml, opencode-review-dispatch.yml) gives the two levels DIFFERENT names,
# because a job requesting the group its own run already holds would wait on itself.
group: agent-mention-noema-${{ github.event.client_payload.target_repository }}-${{ github.event.client_payload.pr_number || github.run_id }}
cancel-in-progress: true

permissions:
contents: read

jobs:
validate-and-forward:
if: github.repository == 'ContextualWisdomLab/.github'
concurrency:
group: agent-mention-noema-${{ github.event.client_payload.target_repository }}-${{ github.event.client_payload.pr_number || github.run_id }}
cancel-in-progress: true
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
Expand Down
17 changes: 14 additions & 3 deletions .github/workflows/agent-mention-opencode-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,26 @@ on:
repository_dispatch:
types: [agent-mention-opencode]

concurrency:
# Workflow-level admission, for the same reason strix.yml, noema-review.yml,
# opencode-review.yml and opencode-review-dispatch.yml carry theirs at this level:
# a job-level group is never evaluated while the whole run waits behind the
# organization job ceiling, so a superseded mention keeps its queue slot until a
# runner frees up and only then cancels. At workflow level the older run is
# coalesced while both are still queued, which is where the slot is actually held.
# This workflow has a single job, so the group lives here and nowhere else --
# every workflow in this repository that carries a group at both levels
# (strix.yml, opencode-review-dispatch.yml) gives the two levels DIFFERENT names,
# because a job requesting the group its own run already holds would wait on itself.
group: agent-mention-opencode-${{ github.event.client_payload.target_repository }}-${{ github.event.client_payload.pr_number || github.run_id }}
cancel-in-progress: true

permissions:
contents: read

jobs:
validate-and-forward:
if: github.repository == 'ContextualWisdomLab/.github'
concurrency:
group: agent-mention-opencode-${{ github.event.client_payload.target_repository }}-${{ github.event.client_payload.pr_number || github.run_id }}
cancel-in-progress: true
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
Expand Down
33 changes: 27 additions & 6 deletions .github/workflows/audit-central-ruleset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ jobs:
python3 scripts/ci/audit_central_required_workflows.py --stacked "$stacked_ruleset_json"

- name: Audit organization CodeQL coverage
# Runs even when the ruleset step above failed. Those two audits share a
# job but not a subject: the ruleset step exits 1 on owner-configured
# governance drift, and on 2026-09-06 it did exactly that ("exactly two
# approving reviews are not required", "last-push approval protection is
# disabled"), which silently took this CodeQL coverage detector down with
# it -- every run since 2026-09-04 failed there and never reached this
# step. This step builds its own repository list into its own temp file
# and the step above exports nothing to GITHUB_ENV or GITHUB_OUTPUT, so
# it has no data dependency to lose. The job still fails overall; what
# changes is that a coverage gap is reported instead of hidden behind an
# unrelated failure.
if: always()
env:
ORG_LOGIN: ContextualWisdomLab
ORG_WIDE_CREDENTIAL_AVAILABLE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' }}
Expand Down Expand Up @@ -165,13 +177,21 @@ jobs:
printf '[]\n' >"$coverage_json"
while IFS=$'\t' read -r repository archived; do
default_setup_state=null
# `state` alone is not coverage: a repository can report
# "configured" with an empty `languages` list, which scans nothing
# and produces no analyses (measured 2026-09-07 on life-os, aFIPC
# and inkspan). Collect both fields so the audit can tell those
# apart from a setup that actually covers a language.
default_setup_languages=null
if [ "$archived" != "true" ]; then
default_setup_state_json="$RUNNER_TEMP/codeql-default-setup-${repository//[^A-Za-z0-9_.-]/_}.json"
if gh api "repos/${ORG_LOGIN}/${repository}/code-scanning/default-setup" --jq .state \
>"$default_setup_state_json" 2>/dev/null; then
default_setup_state=$(jq -R '.' "$default_setup_state_json")
default_setup_json="$RUNNER_TEMP/codeql-default-setup-${repository//[^A-Za-z0-9_.-]/_}.json"
if gh api "repos/${ORG_LOGIN}/${repository}/code-scanning/default-setup" \
>"$default_setup_json" 2>/dev/null; then
default_setup_state=$(jq '.state // null' "$default_setup_json")
default_setup_languages=$(jq '.languages // []' "$default_setup_json")
else
default_setup_state=null
default_setup_languages=null
fi
fi

Expand All @@ -187,12 +207,13 @@ jobs:
fi
fi

echo "CODEQL_COVERAGE repository=${repository} archived=${archived} default_setup_state=${default_setup_state} latest_codeql_analysis=${latest_codeql_analysis}"
echo "CODEQL_COVERAGE repository=${repository} archived=${archived} default_setup_state=${default_setup_state} default_setup_languages=${default_setup_languages} latest_codeql_analysis=${latest_codeql_analysis}"
jq --arg name "$repository" \
--argjson archived "$archived" \
--argjson default_setup_state "$default_setup_state" \
--argjson default_setup_languages "$default_setup_languages" \
--argjson latest_codeql_analysis "$latest_codeql_analysis" \
'. + [{name: $name, archived: $archived, default_setup_state: $default_setup_state, latest_codeql_analysis: $latest_codeql_analysis}]' \
'. + [{name: $name, archived: $archived, default_setup_state: $default_setup_state, default_setup_languages: $default_setup_languages, latest_codeql_analysis: $latest_codeql_analysis}]' \
"$coverage_json" >"${coverage_json}.next"
mv "${coverage_json}.next" "$coverage_json"
done < <(jq -r '.[] | [.name, (.archived | tostring)] | @tsv' "$repositories_json")
Expand Down
117 changes: 58 additions & 59 deletions .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,14 @@ permissions:

jobs:
changed-scope:
name: Detect changed scope
# Deliberately keeps the `changed-scope` job id that CLAUDE.md names as the
# required-workflow skip pattern, but this workflow's copy also carries the
# current-head admission that used to live in a separate
# `admit-current-head` job. The display name says so because the check list
# is where the ambiguity bites: sast-semgrep.yml and security-scan.yml both
# publish a job displayed as "Detect changed scope" that does NOT admit, and
# telling them apart from a check list alone cost real time on 2026-09-06.
name: Detect changed scope and admit the current head
# The org ruleset IGNORES every `on:` filter (paths, branches, types) when it
# runs this workflow in another repository, and a trigger-level skip would
# leave `.github`'s classic required contexts Pending forever. Both
Expand All @@ -104,74 +111,20 @@ jobs:
# Fails OPEN: an unreadable, empty, or truncated file list scans everything.
if: github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft')
runs-on: ubuntu-24.04
timeout-minutes: 5
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
outputs:
code: ${{ steps.scope.outputs.code }}
deps: ${{ steps.scope.outputs.deps }}
steps:
- name: Classify changed paths
id: scope
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
PR: ${{ github.event.pull_request.number }}
EXPECTED_FILES: ${{ github.event.pull_request.changed_files }}
shell: bash
run: |
set -uo pipefail
code=true
deps=true
if [ -n "${PR}" ] && [ -n "${EXPECTED_FILES}" ]; then
changed=""
for attempt in 1 2 3; do
if changed="$(gh api --paginate "repos/${REPO}/pulls/${PR}/files?per_page=100" --jq '.[].filename')" && [ -n "$changed" ]; then
break
fi
changed=""
sleep $((attempt * 3))
done
# GitHub caps /pulls/N/files at 3000 entries; a short list would hide
# source files behind a doc-only verdict, so require an exact count.
if [ -n "$changed" ] && [ "$(printf '%s\n' "$changed" | wc -l | tr -d ' ')" = "${EXPECTED_FILES}" ]; then
code=false
deps=false
while IFS= read -r changed_path; do
case "$changed_path" in
*.md|*.markdown|*.rst|*.png|*.jpg|*.jpeg|*.gif|*.webp|*.bmp|*.ico|LICENSE|LICENSE.txt|COPYING|COPYING.txt|NOTICE|NOTICE.txt|.github/ISSUE_TEMPLATE/*) ;;
*) code=true ;;
esac
case "$changed_path" in
requirements*.txt|*/requirements*.txt|pyproject.toml|*/pyproject.toml|uv.lock|*/uv.lock|pylock.*.toml|*/pylock.*.toml|package.json|*/package.json|package-lock.json|*/package-lock.json|pnpm-lock.yaml|*/pnpm-lock.yaml|yarn.lock|*/yarn.lock|Cargo.toml|*/Cargo.toml|Cargo.lock|*/Cargo.lock|go.mod|*/go.mod|go.sum|*/go.sum|pom.xml|*/pom.xml|build.gradle|*/build.gradle|build.gradle.kts|*/build.gradle.kts|DESCRIPTION|*/DESCRIPTION) deps=true ;;
esac
done <<<"$changed"
else
echo "::notice::changed-scope could not read a complete PR file list; scanning everything."
fi
fi
echo "code=${code}" >> "$GITHUB_OUTPUT"
echo "deps=${deps}" >> "$GITHUB_OUTPUT"
echo "changed-scope code=${code} deps=${deps}"

admit-current-head:
name: Admit current pull request head
if: >-
github.event_name != 'pull_request_target' ||
(github.event.action != 'closed' && github.event.action != 'converted_to_draft')
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
admitted: ${{ steps.admission.outputs.admitted }}
target_repository: ${{ steps.admission.outputs.target_repository }}
pr_number: ${{ steps.admission.outputs.pr_number }}
steps:
- name: Verify event metadata against the live pull request
id: admission
timeout-minutes: 5
env:
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
EVENT_NAME: ${{ github.event_name }}
Expand Down Expand Up @@ -213,6 +166,52 @@ jobs:
echo "pr_number=${TARGET_PR_NUMBER}"
} >> "$GITHUB_OUTPUT"


- name: Classify changed paths
id: scope
if: steps.admission.outputs.admitted == 'true'
timeout-minutes: 5
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
PR: ${{ github.event.pull_request.number }}
EXPECTED_FILES: ${{ github.event.pull_request.changed_files }}
shell: bash
run: |
set -uo pipefail
code=true
deps=true
if [ -n "${PR}" ] && [ -n "${EXPECTED_FILES}" ]; then
changed=""
for attempt in 1 2 3; do
if changed="$(gh api --paginate "repos/${REPO}/pulls/${PR}/files?per_page=100" --jq '.[].filename')" && [ -n "$changed" ]; then
break
fi
changed=""
sleep $((attempt * 3))
done
# GitHub caps /pulls/N/files at 3000 entries; a short list would hide
# source files behind a doc-only verdict, so require an exact count.
if [ -n "$changed" ] && [ "$(printf '%s\n' "$changed" | wc -l | tr -d ' ')" = "${EXPECTED_FILES}" ]; then
code=false
deps=false
while IFS= read -r changed_path; do
case "$changed_path" in
*.md|*.markdown|*.rst|*.png|*.jpg|*.jpeg|*.gif|*.webp|*.bmp|*.ico|LICENSE|LICENSE.txt|COPYING|COPYING.txt|NOTICE|NOTICE.txt|.github/ISSUE_TEMPLATE/*) ;;
*) code=true ;;
esac
case "$changed_path" in
requirements*.txt|*/requirements*.txt|pyproject.toml|*/pyproject.toml|uv.lock|*/uv.lock|pylock.*.toml|*/pylock.*.toml|package.json|*/package.json|package-lock.json|*/package-lock.json|pnpm-lock.yaml|*/pnpm-lock.yaml|yarn.lock|*/yarn.lock|Cargo.toml|*/Cargo.toml|Cargo.lock|*/Cargo.lock|go.mod|*/go.mod|go.sum|*/go.sum|pom.xml|*/pom.xml|build.gradle|*/build.gradle|build.gradle.kts|*/build.gradle.kts|DESCRIPTION|*/DESCRIPTION) deps=true ;;
esac
done <<<"$changed"
else
echo "::notice::changed-scope could not read a complete PR file list; scanning everything."
fi
fi
echo "code=${code}" >> "$GITHUB_OUTPUT"
echo "deps=${deps}" >> "$GITHUB_OUTPUT"
echo "changed-scope code=${code} deps=${deps}"

cancel-superseded-pr-runs:
if: >-
github.event_name == 'pull_request_target' &&
Expand Down Expand Up @@ -340,8 +339,8 @@ jobs:
done

strix:
needs: [changed-scope, admit-current-head]
if: needs.changed-scope.outputs.code == 'true' && needs.admit-current-head.outputs.admitted == 'true'
needs: [changed-scope]
if: needs.changed-scope.outputs.code == 'true' && needs.changed-scope.outputs.admitted == 'true'
# Large, actively-growing repositories (e.g. contextual-orchestrator) can
# legitimately require well over two hours to scan -- this org's own
# standing operating directive accepts that central OpenCode/Strix/Noema
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
- Raised `hourly-review-repair.yml`'s discovery ceiling from 50 to 200 while rotating deterministic 50-PR deep-inspection windows by hourly run number. The scheduler hydrates only the selected window and stops immediately after its single dispatch, preserving access to newer PRs without quadrupling expensive review/check/comment work. See `docs/doctoring/hourly-review-repair-single-file-consolidation.md`'s 2026-09-03 follow-up.

## [Unreleased]
- Consolidate Strix live-head admission and changed-scope classification into
one bounded read-only metadata job before the security scan.
- Include merge-scheduler entrypoint, core, and regression-test changes in
the existing runtime-quality workflow's trigger and suite selector. Scheduler
workflow edits retain queue checks and also select the full review-repair
Expand Down
Loading
Loading