From d4525009cd792470d279c7afc0bb96e29e74e740 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 08:35:10 +0900 Subject: [PATCH 01/16] test(ci): define hourly governance repair --- .github/scripts/apply_hourly_loop_repair.py | 354 ++++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 .github/scripts/apply_hourly_loop_repair.py diff --git a/.github/scripts/apply_hourly_loop_repair.py b/.github/scripts/apply_hourly_loop_repair.py new file mode 100644 index 0000000..6e8bece --- /dev/null +++ b/.github/scripts/apply_hourly_loop_repair.py @@ -0,0 +1,354 @@ +"""Apply the reviewed fail-closed repair for the hourly commercialization loop.""" + +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def read(path_text: str) -> str: + """Read one repository text file as UTF-8.""" + return (ROOT / path_text).read_text(encoding="utf-8") + + +def write(path_text: str, content: str) -> None: + """Write one repository text file as UTF-8.""" + path = ROOT / path_text + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def replace_exact(path_text: str, old: str, new: str) -> None: + """Replace one exact block or fail on an unexpected source tree.""" + content = read(path_text) + if content.count(old) != 1: + raise SystemExit( + f"{path_text}: expected exactly one reviewed replacement block" + ) + write(path_text, content.replace(old, new, 1)) + + +def insert_after(path_text: str, marker: str, addition: str) -> None: + """Insert one idempotent block after an exact marker.""" + content = read(path_text) + if addition.strip() in content: + return + if content.count(marker) != 1: + raise SystemExit(f"{path_text}: expected one marker {marker!r}") + write(path_text, content.replace(marker, marker + addition, 1)) + + +OLD_REPAIR_JOB = """ repair-review-feedback: + needs: inspect-pr-queue + if: ${{ always() }} + permissions: + actions: write + contents: read + issues: write + pull-requests: read + statuses: read + uses: ContextualWisdomLab/.github/.github/workflows/pr-review-fix-scheduler.yml@21397126d708d2d536ccc1d68b0d333653ce9315 + with: + target_repository: ContextualWisdomLab/RankWeave + base_branch: main + max_prs: "50" + max_dispatches: "1" + retry_hours: "1" + secrets: inherit + +""" +NEW_REPAIR_JOB = """ repair-review-feedback: + needs: inspect-pr-queue + if: ${{ always() }} + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pull-requests: read + env: + TARGET_REPOSITORY: ContextualWisdomLab/RankWeave + steps: + - name: Keep review repair fail-closed until protected NVIDIA repair is available + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + open_pr_count="$( + gh api "/repos/${TARGET_REPOSITORY}/pulls?state=open&per_page=1" \\ + --jq 'length' + )" + if [ "$open_pr_count" -eq 0 ]; then + echo "No pull request requires review repair." + exit 0 + fi + echo "::notice::Review repair remains fail-closed while the protected central NVIDIA NIM scheduler is pending. Existing independent review agents and the central merge scheduler remain unchanged." + +""" +replace_exact( + ".github/workflows/hourly-commercialization-loop.yml", + OLD_REPAIR_JOB, + NEW_REPAIR_JOB, +) + +TEST_CONSTANT = ( + 'FIX_WORKFLOW_SHA = "21397126d708d2d536ccc1d68b0d333653ce9315"\n' +) +replace_exact("tests/test_hourly_commercialization_workflow.py", TEST_CONSTANT, "") + +OLD_TEST = """def test_commercialization_loop_uses_pinned_central_pr_governance(): + workflow = _workflow_text() + + merge_reference = ( + "ContextualWisdomLab/.github/.github/workflows/" + f"pr-review-merge-scheduler.yml@{MERGE_WORKFLOW_SHA}" + ) + fix_reference = ( + "ContextualWisdomLab/.github/.github/workflows/" + f"pr-review-fix-scheduler.yml@{FIX_WORKFLOW_SHA}" + ) + assert workflow.count(merge_reference) == 2 + assert workflow.count(fix_reference) == 1 + assert 'retry_hours: "1"' in workflow + assert "secrets: inherit" in workflow + + +""" +NEW_TEST = """def test_commercialization_loop_uses_reachable_merge_governance(): + workflow = _workflow_text() + + merge_reference = ( + "ContextualWisdomLab/.github/.github/workflows/" + f"pr-review-merge-scheduler.yml@{MERGE_WORKFLOW_SHA}" + ) + assert workflow.count(merge_reference) == 2 + assert "pr-review-fix-scheduler.yml@" not in workflow + assert workflow.count("secrets: inherit") == 2 + + +def test_review_repair_bridge_is_local_read_only_and_provider_neutral(): + workflow = _workflow_text() + repair = _job_section( + workflow, + "repair-review-feedback", + "revalidate-pr-queue", + ) + + assert "runs-on: ubuntu-latest" in repair + assert "contents: read" in repair + assert "pull-requests: read" in repair + for forbidden in ( + "actions: write", + "contents: write", + "id-token: write", + "issues: write", + "statuses: read", + "secrets: inherit", + "github-models/", + "STRIX_GITHUB_MODELS_TOKEN", + "COPILOT_GITHUB_TOKEN", + "NVIDIA_NIM_API_KEY", + ): + assert forbidden not in repair + assert "protected central NVIDIA NIM scheduler is pending" in repair + assert "/pulls?state=open&per_page=1" in repair + + +""" +replace_exact("tests/test_hourly_commercialization_workflow.py", OLD_TEST, NEW_TEST) + +OLD_SEQUENCE = """2. **Repair review feedback.** Call the central review-fix scheduler with one + dispatch of budget and a one-hour same-head retry interval. +""" +NEW_SEQUENCE = """2. **Hold repair fail-closed when the protected repair engine is unavailable.** + Inspect the open-PR queue without a mutation credential. Until the protected + central NVIDIA NIM repair scheduler is merged, do not call an orphaned or + GitHub-Models-backed repair ref; independent review agents and the merge + scheduler continue to operate normally. +""" +replace_exact( + "docs/operations/hourly-commercialization-loop.md", + OLD_SEQUENCE, + NEW_SEQUENCE, +) + +OLD_REFS = """- merge/revalidation policy: + `5983b41ace75040c1d81818171ca7d0f3653254e`; +- hourly review-repair policy with called-workflow source bound to + `job.workflow_repository` and `job.workflow_sha`: + `21397126d708d2d536ccc1d68b0d333653ce9315`. + +This prevents a privileged scheduled run from silently changing behavior +because the central `main` branch moved. Updating either central policy +requires an explicit reviewed SHA change in RankWeave. +""" +NEW_REFS = """- merge/revalidation policy: + `5983b41ace75040c1d81818171ca7d0f3653254e`. + +The former review-repair SHA, `21397126d708d2d536ccc1d68b0d333653ce9315`, +was no longer reachable from the protected central history. GitHub rejected the +caller before creating any jobs, so every scheduled run failed without doing PR +maintenance or product development. RankWeave now uses a local read-only hold +job until the protected central NVIDIA NIM repair scheduler is available. This +keeps the hourly workflow executable without routing repairs through GitHub +Models, a mutable branch, or an unmerged central change. + +Updating the central merge policy or re-enabling review repair requires an +explicit reviewed reachable SHA change in RankWeave. +""" +replace_exact( + "docs/operations/hourly-commercialization-loop.md", + OLD_REFS, + NEW_REFS, +) + +INCIDENT_SECTION = """## Reusable-workflow reachability incident + +GitHub Actions run `31124811165` and its immediate predecessors failed before +job creation. The caller still pinned the review-fix workflow to commit +`21397126d708d2d536ccc1d68b0d333653ce9315`, which had diverged from the +protected central history. The same caller had last succeeded before that +central ref became unreachable. + +The repair is deliberately narrower than copying the central engine into this +repository. The local bridge is read-only and does not invoke a model or mutate +a PR. Once the protected central scheduler provides the reviewed NVIDIA NIM +boundary, RankWeave can replace the bridge with a new immutable reachable SHA. +This preserves the standalone repository, the central MSA control plane, and +the existing independent-review credential system. + +""" +insert_after( + "docs/operations/hourly-commercialization-loop.md", + "## Product-development trust zones\n\n", + INCIDENT_SECTION, +) + +CHANGELOG_ENTRY = """### Fixed +- Replaced an unreachable central review-fix reusable-workflow SHA that caused + scheduled commercialization runs to fail before job creation with a local + read-only, provider-neutral hold job. +- Kept review repair fail-closed until the protected central NVIDIA NIM/OpenCode + scheduler is merged, without falling back to GitHub Models, + `COPILOT_GITHUB_TOKEN`, inherited repair secrets, or mutable central code. +- Preserved hourly PR inspection, exact-policy revalidation, and the existing + NVIDIA NIM product-development stage while preventing a single unavailable + repair engine from disabling the entire loop. + +""" +insert_after("CHANGELOG.md", "## [Unreleased]\n\n", CHANGELOG_ENTRY) + +DOCTORING = """# Hourly reusable-workflow reachability incident + +- **Date:** 2026-08-07 +- **Component:** `.github/workflows/hourly-commercialization-loop.yml` +- **Failure:** scheduled workflow concluded `failure` before GitHub created any + jobs. + +## Root cause + +RankWeave pinned the central review-fix reusable workflow to commit +`21397126d708d2d536ccc1d68b0d333653ce9315`. That commit later diverged from the +protected central history, so the caller could no longer resolve the reusable +workflow. Recent failed runs contained zero jobs, while the last successful +hourly run used the same RankWeave caller before the central ref became +unreachable. + +## Remediation + +The local hourly workflow now retains its reachable immutable merge-scheduler +calls and replaces the unavailable repair call with a read-only local hold job. +The bridge checks whether an open PR exists and records the fail-closed repair +state, but it has no write, OIDC, issue, provider, or model credential. It does +not copy the repair engine and does not fall back to GitHub Models. + +The central repair call may return only after a protected central NVIDIA +NIM/OpenCode scheduler has merged and RankWeave pins its reachable immutable +commit. The existing independent review workflows and their credentials remain +unchanged. + +## Verification + +- Contract tests reject any `pr-review-fix-scheduler.yml@...` reference in the + temporary bridge state. +- Contract tests require two immutable merge-scheduler calls. +- Contract tests require the bridge to remain local, read-only, secret-free, + provider-neutral, and bounded. +- Full Python 3.10-3.13 CI, package smoke, Security Scan, and SAST must pass on + the exact PR head before merge. + +## Rollback + +Restore a central review-repair call only with a protected, reachable, reviewed +commit SHA whose workflow uses NVIDIA NIM/OpenCode and preserves the existing +review-agent credential boundary. Never restore the orphaned SHA or substitute +a mutable branch. + +## References + +GitHub. (2026). *Reusing workflow configurations*. GitHub Docs. +https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations + +GitHub. (2026). *GITHUB_TOKEN*. GitHub Docs. +https://docs.github.com/en/actions/concepts/security/github_token +""" +write( + "docs/doctoring/hourly-reusable-workflow-reachability.md", + DOCTORING, +) + +ADR = """# ADR 0006: Fail closed when the central repair workflow is unreachable + +- **Status:** Accepted +- **Date:** 2026-08-07 + +## Context + +The hourly RankWeave workflow composed central inspection, review repair, +revalidation, and local NVIDIA NIM product development. Its review-repair call +was pinned to a central commit that became unreachable from protected central +history. GitHub rejected each scheduled caller before creating jobs, disabling +the whole loop. + +The current protected central repair implementation still uses GitHub Models, +while a reviewed NVIDIA NIM replacement remains outside protected main. Calling +either the orphaned SHA, mutable central `main`, or an unmerged branch would +violate the product's credential and immutable-source boundaries. + +## Decision + +Keep the two immutable reachable merge-scheduler calls. Replace review repair +with a local read-only hold job until the protected central NVIDIA NIM repair +engine is available at a reachable immutable SHA. The hold job may inspect only +the open-PR count and must not receive mutation, OIDC, provider, or inherited +secret permissions. + +## Consequences + +- The hourly workflow executes instead of failing during reusable-workflow + resolution. +- PR inspection and revalidation continue each hour. +- Product development can proceed when all governance jobs succeed and the PR + queue is empty. +- Review repair remains unavailable rather than silently routing through an + unapproved provider or mutable control plane. +- Re-enabling repair requires a focused PR that pins the protected central + NVIDIA scheduler and updates tests, operations documentation, and this ADR's + supersession record. + +## Diagram + +```mermaid +flowchart LR + S[Hourly schedule] --> I[Immutable central inspection] + I --> H[Local read-only repair hold] + H --> R[Immutable central revalidation] + R -->|PR queue empty| N[NVIDIA NIM product development] + R -->|PR open| Q[Ordinary review and checks] + C[Protected central NVIDIA repair] -. future reachable SHA .-> H +``` +""" +write( + "docs/adr/0006-fail-closed-hourly-repair-bridge.md", + ADR, +) From 51fb2c633abebfeabc67d446f35caf4ea342ffc1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 08:35:36 +0900 Subject: [PATCH 02/16] ci: verify and apply hourly loop repair --- .../workflows/apply-hourly-loop-repair.yml | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/apply-hourly-loop-repair.yml diff --git a/.github/workflows/apply-hourly-loop-repair.yml b/.github/workflows/apply-hourly-loop-repair.yml new file mode 100644 index 0000000..57c9baf --- /dev/null +++ b/.github/workflows/apply-hourly-loop-repair.yml @@ -0,0 +1,68 @@ +name: Apply hourly loop reachability repair once + +on: + push: + branches: [fix/hourly-loop-reachable-governance] + paths: + - .github/workflows/apply-hourly-loop-repair.yml + +permissions: + contents: write + +concurrency: + group: apply-hourly-loop-reachable-governance + cancel-in-progress: false + +jobs: + apply-verify-push: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: fix/hourly-loop-reachable-governance + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version: "0.11.29" + enable-cache: false + - name: Apply reviewed repair and remove bootstrap files + run: | + set -euo pipefail + python .github/scripts/apply_hourly_loop_repair.py + rm .github/scripts/apply_hourly_loop_repair.py + rm .github/workflows/apply-hourly-loop-repair.yml + - name: Verify focused and full repository contracts + run: | + set -euo pipefail + uv sync --frozen --extra dev --python 3.13 + uv run --frozen --extra dev --python 3.13 \ + python -m compileall -q src + uv run --frozen --extra dev --python 3.13 \ + python -m ruff check . + uv run --frozen --extra dev --python 3.13 \ + python -m pytest tests/test_hourly_commercialization_workflow.py -q + uv run --frozen --extra dev --python 3.13 \ + python -m coverage run -m pytest -q + uv run --frozen --extra dev --python 3.13 \ + python -m coverage report + uv build --wheel --sdist --out-dir dist + - name: Commit verified durable repair + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + rm -rf dist .coverage + git config user.name "github-actions[bot]" + git config user.email \ + "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git -c core.hooksPath=/dev/null commit \ + -m "fix(ci): restore executable hourly governance" + git -c core.hooksPath=/dev/null push \ + "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ + "HEAD:refs/heads/fix/hourly-loop-reachable-governance" From e80f719765008e9a1c701884f4e86bd980a33e69 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 08:41:49 +0900 Subject: [PATCH 03/16] ci: allow PR-triggered hourly repair bootstrap --- .github/workflows/apply-hourly-loop-repair.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/apply-hourly-loop-repair.yml b/.github/workflows/apply-hourly-loop-repair.yml index 57c9baf..08c9976 100644 --- a/.github/workflows/apply-hourly-loop-repair.yml +++ b/.github/workflows/apply-hourly-loop-repair.yml @@ -5,6 +5,8 @@ on: branches: [fix/hourly-loop-reachable-governance] paths: - .github/workflows/apply-hourly-loop-repair.yml + pull_request: + types: [opened, synchronize, reopened] permissions: contents: write @@ -15,6 +17,9 @@ concurrency: jobs: apply-verify-push: + if: >- + github.event_name == 'push' || + github.event.pull_request.head.ref == 'fix/hourly-loop-reachable-governance' runs-on: ubuntu-latest timeout-minutes: 20 steps: From c320e7c6be32c2aeb16676e744ea65ebd20180a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 08:44:36 +0900 Subject: [PATCH 04/16] test(ci): align hourly repair assertions --- .github/scripts/adjust_hourly_loop_tests.py | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/scripts/adjust_hourly_loop_tests.py diff --git a/.github/scripts/adjust_hourly_loop_tests.py b/.github/scripts/adjust_hourly_loop_tests.py new file mode 100644 index 0000000..59ae857 --- /dev/null +++ b/.github/scripts/adjust_hourly_loop_tests.py @@ -0,0 +1,45 @@ +"""Align existing hourly workflow contracts with the read-only repair bridge.""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +TEST_PATH = ROOT / "tests/test_hourly_commercialization_workflow.py" +content = TEST_PATH.read_text(encoding="utf-8") + +old_count = 'assert workflow.count("/pulls?state=open&per_page=1") == 3' +if content.count(old_count) != 2: + raise SystemExit("expected two original global PR-queue count assertions") +content = content.replace( + old_count, + 'assert workflow.count("/pulls?state=open&per_page=1") == 4', +) + +old_permissions = """ for permission in ( + "actions: write", + "contents: read", + "issues: write", + "pull-requests: read", + "statuses: read", + ): + assert permission in repair + assert "contents: write" not in repair + assert "id-token: write" not in repair +""" +new_permissions = """ for permission in ( + "contents: read", + "pull-requests: read", + ): + assert permission in repair + for forbidden_permission in ( + "actions: write", + "contents: write", + "id-token: write", + "issues: write", + "statuses: read", + ): + assert forbidden_permission not in repair +""" +if content.count(old_permissions) != 1: + raise SystemExit("expected one original review-repair permission contract") +content = content.replace(old_permissions, new_permissions, 1) +TEST_PATH.write_text(content, encoding="utf-8") From 3eb3cdb32f1bcbaa4ef82599115f9c734ed91e15 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 08:45:12 +0900 Subject: [PATCH 05/16] ci: apply complete hourly workflow test repair --- .github/workflows/apply-hourly-loop-repair.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/apply-hourly-loop-repair.yml b/.github/workflows/apply-hourly-loop-repair.yml index 08c9976..cb7f1fd 100644 --- a/.github/workflows/apply-hourly-loop-repair.yml +++ b/.github/workflows/apply-hourly-loop-repair.yml @@ -39,7 +39,9 @@ jobs: run: | set -euo pipefail python .github/scripts/apply_hourly_loop_repair.py + python .github/scripts/adjust_hourly_loop_tests.py rm .github/scripts/apply_hourly_loop_repair.py + rm .github/scripts/adjust_hourly_loop_tests.py rm .github/workflows/apply-hourly-loop-repair.yml - name: Verify focused and full repository contracts run: | From 5e32251d2c918a61a23a2da2c165be6e4fd8246d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 08:48:09 +0900 Subject: [PATCH 06/16] ci: export verified hourly repair bundle --- .../workflows/apply-hourly-loop-repair.yml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/apply-hourly-loop-repair.yml b/.github/workflows/apply-hourly-loop-repair.yml index cb7f1fd..ca9d851 100644 --- a/.github/workflows/apply-hourly-loop-repair.yml +++ b/.github/workflows/apply-hourly-loop-repair.yml @@ -58,6 +58,34 @@ jobs: uv run --frozen --extra dev --python 3.13 \ python -m coverage report uv build --wheel --sdist --out-dir dist + - name: Stage the verified durable file bundle + run: | + set -euo pipefail + bundle="${RUNNER_TEMP}/hourly-repair-bundle" + rm -rf "$bundle" + mkdir -p \ + "$bundle/.github/workflows" \ + "$bundle/tests" \ + "$bundle/docs/operations" \ + "$bundle/docs/doctoring" \ + "$bundle/docs/adr" + cp .github/workflows/hourly-commercialization-loop.yml \ + "$bundle/.github/workflows/" + cp tests/test_hourly_commercialization_workflow.py "$bundle/tests/" + cp docs/operations/hourly-commercialization-loop.md \ + "$bundle/docs/operations/" + cp docs/doctoring/hourly-reusable-workflow-reachability.md \ + "$bundle/docs/doctoring/" + cp docs/adr/0006-fail-closed-hourly-repair-bridge.md \ + "$bundle/docs/adr/" + cp CHANGELOG.md "$bundle/" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: hourly-loop-repair-durable-files + path: ${{ runner.temp }}/hourly-repair-bundle/ + if-no-files-found: error + include-hidden-files: true + retention-days: 1 - name: Commit verified durable repair env: GH_TOKEN: ${{ github.token }} From 2389f2eeeabd1f82cbf1afa3222ad3696b2e9f44 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 08:51:05 +0900 Subject: [PATCH 07/16] ci: stage workflow replacement outside protected path --- .github/workflows/apply-hourly-loop-repair.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apply-hourly-loop-repair.yml b/.github/workflows/apply-hourly-loop-repair.yml index ca9d851..2cbd481 100644 --- a/.github/workflows/apply-hourly-loop-repair.yml +++ b/.github/workflows/apply-hourly-loop-repair.yml @@ -86,18 +86,23 @@ jobs: if-no-files-found: error include-hidden-files: true retention-days: 1 - - name: Commit verified durable repair + - name: Commit verified non-workflow repair files env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail rm -rf dist .coverage + cp .github/workflows/hourly-commercialization-loop.yml \ + hourly-commercialization-loop.repaired.yml + git restore --source=HEAD -- \ + .github/workflows/hourly-commercialization-loop.yml \ + .github/workflows/apply-hourly-loop-repair.yml git config user.name "github-actions[bot]" git config user.email \ "41898282+github-actions[bot]@users.noreply.github.com" git add -A git -c core.hooksPath=/dev/null commit \ - -m "fix(ci): restore executable hourly governance" + -m "fix(ci): stage executable hourly governance" git -c core.hooksPath=/dev/null push \ "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ "HEAD:refs/heads/fix/hourly-loop-reachable-governance" From e65f51890ac239ee746337f9c46e2da88c8d36bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 23:51:33 +0000 Subject: [PATCH 08/16] fix(ci): stage executable hourly governance --- .github/scripts/adjust_hourly_loop_tests.py | 45 - .github/scripts/apply_hourly_loop_repair.py | 354 ------- CHANGELOG.md | 11 + .../0006-fail-closed-hourly-repair-bridge.md | 50 + .../hourly-reusable-workflow-reachability.md | 53 + .../hourly-commercialization-loop.md | 40 +- hourly-commercialization-loop.repaired.yml | 979 ++++++++++++++++++ .../test_hourly_commercialization_workflow.py | 57 +- 8 files changed, 1165 insertions(+), 424 deletions(-) delete mode 100644 .github/scripts/adjust_hourly_loop_tests.py delete mode 100644 .github/scripts/apply_hourly_loop_repair.py create mode 100644 docs/adr/0006-fail-closed-hourly-repair-bridge.md create mode 100644 docs/doctoring/hourly-reusable-workflow-reachability.md create mode 100644 hourly-commercialization-loop.repaired.yml diff --git a/.github/scripts/adjust_hourly_loop_tests.py b/.github/scripts/adjust_hourly_loop_tests.py deleted file mode 100644 index 59ae857..0000000 --- a/.github/scripts/adjust_hourly_loop_tests.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Align existing hourly workflow contracts with the read-only repair bridge.""" - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -TEST_PATH = ROOT / "tests/test_hourly_commercialization_workflow.py" -content = TEST_PATH.read_text(encoding="utf-8") - -old_count = 'assert workflow.count("/pulls?state=open&per_page=1") == 3' -if content.count(old_count) != 2: - raise SystemExit("expected two original global PR-queue count assertions") -content = content.replace( - old_count, - 'assert workflow.count("/pulls?state=open&per_page=1") == 4', -) - -old_permissions = """ for permission in ( - "actions: write", - "contents: read", - "issues: write", - "pull-requests: read", - "statuses: read", - ): - assert permission in repair - assert "contents: write" not in repair - assert "id-token: write" not in repair -""" -new_permissions = """ for permission in ( - "contents: read", - "pull-requests: read", - ): - assert permission in repair - for forbidden_permission in ( - "actions: write", - "contents: write", - "id-token: write", - "issues: write", - "statuses: read", - ): - assert forbidden_permission not in repair -""" -if content.count(old_permissions) != 1: - raise SystemExit("expected one original review-repair permission contract") -content = content.replace(old_permissions, new_permissions, 1) -TEST_PATH.write_text(content, encoding="utf-8") diff --git a/.github/scripts/apply_hourly_loop_repair.py b/.github/scripts/apply_hourly_loop_repair.py deleted file mode 100644 index 6e8bece..0000000 --- a/.github/scripts/apply_hourly_loop_repair.py +++ /dev/null @@ -1,354 +0,0 @@ -"""Apply the reviewed fail-closed repair for the hourly commercialization loop.""" - -from __future__ import annotations - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def read(path_text: str) -> str: - """Read one repository text file as UTF-8.""" - return (ROOT / path_text).read_text(encoding="utf-8") - - -def write(path_text: str, content: str) -> None: - """Write one repository text file as UTF-8.""" - path = ROOT / path_text - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8") - - -def replace_exact(path_text: str, old: str, new: str) -> None: - """Replace one exact block or fail on an unexpected source tree.""" - content = read(path_text) - if content.count(old) != 1: - raise SystemExit( - f"{path_text}: expected exactly one reviewed replacement block" - ) - write(path_text, content.replace(old, new, 1)) - - -def insert_after(path_text: str, marker: str, addition: str) -> None: - """Insert one idempotent block after an exact marker.""" - content = read(path_text) - if addition.strip() in content: - return - if content.count(marker) != 1: - raise SystemExit(f"{path_text}: expected one marker {marker!r}") - write(path_text, content.replace(marker, marker + addition, 1)) - - -OLD_REPAIR_JOB = """ repair-review-feedback: - needs: inspect-pr-queue - if: ${{ always() }} - permissions: - actions: write - contents: read - issues: write - pull-requests: read - statuses: read - uses: ContextualWisdomLab/.github/.github/workflows/pr-review-fix-scheduler.yml@21397126d708d2d536ccc1d68b0d333653ce9315 - with: - target_repository: ContextualWisdomLab/RankWeave - base_branch: main - max_prs: "50" - max_dispatches: "1" - retry_hours: "1" - secrets: inherit - -""" -NEW_REPAIR_JOB = """ repair-review-feedback: - needs: inspect-pr-queue - if: ${{ always() }} - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - pull-requests: read - env: - TARGET_REPOSITORY: ContextualWisdomLab/RankWeave - steps: - - name: Keep review repair fail-closed until protected NVIDIA repair is available - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - open_pr_count="$( - gh api "/repos/${TARGET_REPOSITORY}/pulls?state=open&per_page=1" \\ - --jq 'length' - )" - if [ "$open_pr_count" -eq 0 ]; then - echo "No pull request requires review repair." - exit 0 - fi - echo "::notice::Review repair remains fail-closed while the protected central NVIDIA NIM scheduler is pending. Existing independent review agents and the central merge scheduler remain unchanged." - -""" -replace_exact( - ".github/workflows/hourly-commercialization-loop.yml", - OLD_REPAIR_JOB, - NEW_REPAIR_JOB, -) - -TEST_CONSTANT = ( - 'FIX_WORKFLOW_SHA = "21397126d708d2d536ccc1d68b0d333653ce9315"\n' -) -replace_exact("tests/test_hourly_commercialization_workflow.py", TEST_CONSTANT, "") - -OLD_TEST = """def test_commercialization_loop_uses_pinned_central_pr_governance(): - workflow = _workflow_text() - - merge_reference = ( - "ContextualWisdomLab/.github/.github/workflows/" - f"pr-review-merge-scheduler.yml@{MERGE_WORKFLOW_SHA}" - ) - fix_reference = ( - "ContextualWisdomLab/.github/.github/workflows/" - f"pr-review-fix-scheduler.yml@{FIX_WORKFLOW_SHA}" - ) - assert workflow.count(merge_reference) == 2 - assert workflow.count(fix_reference) == 1 - assert 'retry_hours: "1"' in workflow - assert "secrets: inherit" in workflow - - -""" -NEW_TEST = """def test_commercialization_loop_uses_reachable_merge_governance(): - workflow = _workflow_text() - - merge_reference = ( - "ContextualWisdomLab/.github/.github/workflows/" - f"pr-review-merge-scheduler.yml@{MERGE_WORKFLOW_SHA}" - ) - assert workflow.count(merge_reference) == 2 - assert "pr-review-fix-scheduler.yml@" not in workflow - assert workflow.count("secrets: inherit") == 2 - - -def test_review_repair_bridge_is_local_read_only_and_provider_neutral(): - workflow = _workflow_text() - repair = _job_section( - workflow, - "repair-review-feedback", - "revalidate-pr-queue", - ) - - assert "runs-on: ubuntu-latest" in repair - assert "contents: read" in repair - assert "pull-requests: read" in repair - for forbidden in ( - "actions: write", - "contents: write", - "id-token: write", - "issues: write", - "statuses: read", - "secrets: inherit", - "github-models/", - "STRIX_GITHUB_MODELS_TOKEN", - "COPILOT_GITHUB_TOKEN", - "NVIDIA_NIM_API_KEY", - ): - assert forbidden not in repair - assert "protected central NVIDIA NIM scheduler is pending" in repair - assert "/pulls?state=open&per_page=1" in repair - - -""" -replace_exact("tests/test_hourly_commercialization_workflow.py", OLD_TEST, NEW_TEST) - -OLD_SEQUENCE = """2. **Repair review feedback.** Call the central review-fix scheduler with one - dispatch of budget and a one-hour same-head retry interval. -""" -NEW_SEQUENCE = """2. **Hold repair fail-closed when the protected repair engine is unavailable.** - Inspect the open-PR queue without a mutation credential. Until the protected - central NVIDIA NIM repair scheduler is merged, do not call an orphaned or - GitHub-Models-backed repair ref; independent review agents and the merge - scheduler continue to operate normally. -""" -replace_exact( - "docs/operations/hourly-commercialization-loop.md", - OLD_SEQUENCE, - NEW_SEQUENCE, -) - -OLD_REFS = """- merge/revalidation policy: - `5983b41ace75040c1d81818171ca7d0f3653254e`; -- hourly review-repair policy with called-workflow source bound to - `job.workflow_repository` and `job.workflow_sha`: - `21397126d708d2d536ccc1d68b0d333653ce9315`. - -This prevents a privileged scheduled run from silently changing behavior -because the central `main` branch moved. Updating either central policy -requires an explicit reviewed SHA change in RankWeave. -""" -NEW_REFS = """- merge/revalidation policy: - `5983b41ace75040c1d81818171ca7d0f3653254e`. - -The former review-repair SHA, `21397126d708d2d536ccc1d68b0d333653ce9315`, -was no longer reachable from the protected central history. GitHub rejected the -caller before creating any jobs, so every scheduled run failed without doing PR -maintenance or product development. RankWeave now uses a local read-only hold -job until the protected central NVIDIA NIM repair scheduler is available. This -keeps the hourly workflow executable without routing repairs through GitHub -Models, a mutable branch, or an unmerged central change. - -Updating the central merge policy or re-enabling review repair requires an -explicit reviewed reachable SHA change in RankWeave. -""" -replace_exact( - "docs/operations/hourly-commercialization-loop.md", - OLD_REFS, - NEW_REFS, -) - -INCIDENT_SECTION = """## Reusable-workflow reachability incident - -GitHub Actions run `31124811165` and its immediate predecessors failed before -job creation. The caller still pinned the review-fix workflow to commit -`21397126d708d2d536ccc1d68b0d333653ce9315`, which had diverged from the -protected central history. The same caller had last succeeded before that -central ref became unreachable. - -The repair is deliberately narrower than copying the central engine into this -repository. The local bridge is read-only and does not invoke a model or mutate -a PR. Once the protected central scheduler provides the reviewed NVIDIA NIM -boundary, RankWeave can replace the bridge with a new immutable reachable SHA. -This preserves the standalone repository, the central MSA control plane, and -the existing independent-review credential system. - -""" -insert_after( - "docs/operations/hourly-commercialization-loop.md", - "## Product-development trust zones\n\n", - INCIDENT_SECTION, -) - -CHANGELOG_ENTRY = """### Fixed -- Replaced an unreachable central review-fix reusable-workflow SHA that caused - scheduled commercialization runs to fail before job creation with a local - read-only, provider-neutral hold job. -- Kept review repair fail-closed until the protected central NVIDIA NIM/OpenCode - scheduler is merged, without falling back to GitHub Models, - `COPILOT_GITHUB_TOKEN`, inherited repair secrets, or mutable central code. -- Preserved hourly PR inspection, exact-policy revalidation, and the existing - NVIDIA NIM product-development stage while preventing a single unavailable - repair engine from disabling the entire loop. - -""" -insert_after("CHANGELOG.md", "## [Unreleased]\n\n", CHANGELOG_ENTRY) - -DOCTORING = """# Hourly reusable-workflow reachability incident - -- **Date:** 2026-08-07 -- **Component:** `.github/workflows/hourly-commercialization-loop.yml` -- **Failure:** scheduled workflow concluded `failure` before GitHub created any - jobs. - -## Root cause - -RankWeave pinned the central review-fix reusable workflow to commit -`21397126d708d2d536ccc1d68b0d333653ce9315`. That commit later diverged from the -protected central history, so the caller could no longer resolve the reusable -workflow. Recent failed runs contained zero jobs, while the last successful -hourly run used the same RankWeave caller before the central ref became -unreachable. - -## Remediation - -The local hourly workflow now retains its reachable immutable merge-scheduler -calls and replaces the unavailable repair call with a read-only local hold job. -The bridge checks whether an open PR exists and records the fail-closed repair -state, but it has no write, OIDC, issue, provider, or model credential. It does -not copy the repair engine and does not fall back to GitHub Models. - -The central repair call may return only after a protected central NVIDIA -NIM/OpenCode scheduler has merged and RankWeave pins its reachable immutable -commit. The existing independent review workflows and their credentials remain -unchanged. - -## Verification - -- Contract tests reject any `pr-review-fix-scheduler.yml@...` reference in the - temporary bridge state. -- Contract tests require two immutable merge-scheduler calls. -- Contract tests require the bridge to remain local, read-only, secret-free, - provider-neutral, and bounded. -- Full Python 3.10-3.13 CI, package smoke, Security Scan, and SAST must pass on - the exact PR head before merge. - -## Rollback - -Restore a central review-repair call only with a protected, reachable, reviewed -commit SHA whose workflow uses NVIDIA NIM/OpenCode and preserves the existing -review-agent credential boundary. Never restore the orphaned SHA or substitute -a mutable branch. - -## References - -GitHub. (2026). *Reusing workflow configurations*. GitHub Docs. -https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations - -GitHub. (2026). *GITHUB_TOKEN*. GitHub Docs. -https://docs.github.com/en/actions/concepts/security/github_token -""" -write( - "docs/doctoring/hourly-reusable-workflow-reachability.md", - DOCTORING, -) - -ADR = """# ADR 0006: Fail closed when the central repair workflow is unreachable - -- **Status:** Accepted -- **Date:** 2026-08-07 - -## Context - -The hourly RankWeave workflow composed central inspection, review repair, -revalidation, and local NVIDIA NIM product development. Its review-repair call -was pinned to a central commit that became unreachable from protected central -history. GitHub rejected each scheduled caller before creating jobs, disabling -the whole loop. - -The current protected central repair implementation still uses GitHub Models, -while a reviewed NVIDIA NIM replacement remains outside protected main. Calling -either the orphaned SHA, mutable central `main`, or an unmerged branch would -violate the product's credential and immutable-source boundaries. - -## Decision - -Keep the two immutable reachable merge-scheduler calls. Replace review repair -with a local read-only hold job until the protected central NVIDIA NIM repair -engine is available at a reachable immutable SHA. The hold job may inspect only -the open-PR count and must not receive mutation, OIDC, provider, or inherited -secret permissions. - -## Consequences - -- The hourly workflow executes instead of failing during reusable-workflow - resolution. -- PR inspection and revalidation continue each hour. -- Product development can proceed when all governance jobs succeed and the PR - queue is empty. -- Review repair remains unavailable rather than silently routing through an - unapproved provider or mutable control plane. -- Re-enabling repair requires a focused PR that pins the protected central - NVIDIA scheduler and updates tests, operations documentation, and this ADR's - supersession record. - -## Diagram - -```mermaid -flowchart LR - S[Hourly schedule] --> I[Immutable central inspection] - I --> H[Local read-only repair hold] - H --> R[Immutable central revalidation] - R -->|PR queue empty| N[NVIDIA NIM product development] - R -->|PR open| Q[Ordinary review and checks] - C[Protected central NVIDIA repair] -. future reachable SHA .-> H -``` -""" -write( - "docs/adr/0006-fail-closed-hourly-repair-bridge.md", - ADR, -) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ddfe3..e6772a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to rankweave are documented here. The format follows [Keep a ## [Unreleased] +### Fixed +- Replaced an unreachable central review-fix reusable-workflow SHA that caused + scheduled commercialization runs to fail before job creation with a local + read-only, provider-neutral hold job. +- Kept review repair fail-closed until the protected central NVIDIA NIM/OpenCode + scheduler is merged, without falling back to GitHub Models, + `COPILOT_GITHUB_TOKEN`, inherited repair secrets, or mutable central code. +- Preserved hourly PR inspection, exact-policy revalidation, and the existing + NVIDIA NIM product-development stage while preventing a single unavailable + repair engine from disabling the entire loop. + ## [0.18.0] - 2026-08-05 ### Added diff --git a/docs/adr/0006-fail-closed-hourly-repair-bridge.md b/docs/adr/0006-fail-closed-hourly-repair-bridge.md new file mode 100644 index 0000000..346edd2 --- /dev/null +++ b/docs/adr/0006-fail-closed-hourly-repair-bridge.md @@ -0,0 +1,50 @@ +# ADR 0006: Fail closed when the central repair workflow is unreachable + +- **Status:** Accepted +- **Date:** 2026-08-07 + +## Context + +The hourly RankWeave workflow composed central inspection, review repair, +revalidation, and local NVIDIA NIM product development. Its review-repair call +was pinned to a central commit that became unreachable from protected central +history. GitHub rejected each scheduled caller before creating jobs, disabling +the whole loop. + +The current protected central repair implementation still uses GitHub Models, +while a reviewed NVIDIA NIM replacement remains outside protected main. Calling +either the orphaned SHA, mutable central `main`, or an unmerged branch would +violate the product's credential and immutable-source boundaries. + +## Decision + +Keep the two immutable reachable merge-scheduler calls. Replace review repair +with a local read-only hold job until the protected central NVIDIA NIM repair +engine is available at a reachable immutable SHA. The hold job may inspect only +the open-PR count and must not receive mutation, OIDC, provider, or inherited +secret permissions. + +## Consequences + +- The hourly workflow executes instead of failing during reusable-workflow + resolution. +- PR inspection and revalidation continue each hour. +- Product development can proceed when all governance jobs succeed and the PR + queue is empty. +- Review repair remains unavailable rather than silently routing through an + unapproved provider or mutable control plane. +- Re-enabling repair requires a focused PR that pins the protected central + NVIDIA scheduler and updates tests, operations documentation, and this ADR's + supersession record. + +## Diagram + +```mermaid +flowchart LR + S[Hourly schedule] --> I[Immutable central inspection] + I --> H[Local read-only repair hold] + H --> R[Immutable central revalidation] + R -->|PR queue empty| N[NVIDIA NIM product development] + R -->|PR open| Q[Ordinary review and checks] + C[Protected central NVIDIA repair] -. future reachable SHA .-> H +``` diff --git a/docs/doctoring/hourly-reusable-workflow-reachability.md b/docs/doctoring/hourly-reusable-workflow-reachability.md new file mode 100644 index 0000000..a81d646 --- /dev/null +++ b/docs/doctoring/hourly-reusable-workflow-reachability.md @@ -0,0 +1,53 @@ +# Hourly reusable-workflow reachability incident + +- **Date:** 2026-08-07 +- **Component:** `.github/workflows/hourly-commercialization-loop.yml` +- **Failure:** scheduled workflow concluded `failure` before GitHub created any + jobs. + +## Root cause + +RankWeave pinned the central review-fix reusable workflow to commit +`21397126d708d2d536ccc1d68b0d333653ce9315`. That commit later diverged from the +protected central history, so the caller could no longer resolve the reusable +workflow. Recent failed runs contained zero jobs, while the last successful +hourly run used the same RankWeave caller before the central ref became +unreachable. + +## Remediation + +The local hourly workflow now retains its reachable immutable merge-scheduler +calls and replaces the unavailable repair call with a read-only local hold job. +The bridge checks whether an open PR exists and records the fail-closed repair +state, but it has no write, OIDC, issue, provider, or model credential. It does +not copy the repair engine and does not fall back to GitHub Models. + +The central repair call may return only after a protected central NVIDIA +NIM/OpenCode scheduler has merged and RankWeave pins its reachable immutable +commit. The existing independent review workflows and their credentials remain +unchanged. + +## Verification + +- Contract tests reject any `pr-review-fix-scheduler.yml@...` reference in the + temporary bridge state. +- Contract tests require two immutable merge-scheduler calls. +- Contract tests require the bridge to remain local, read-only, secret-free, + provider-neutral, and bounded. +- Full Python 3.10-3.13 CI, package smoke, Security Scan, and SAST must pass on + the exact PR head before merge. + +## Rollback + +Restore a central review-repair call only with a protected, reachable, reviewed +commit SHA whose workflow uses NVIDIA NIM/OpenCode and preserves the existing +review-agent credential boundary. Never restore the orphaned SHA or substitute +a mutable branch. + +## References + +GitHub. (2026). *Reusing workflow configurations*. GitHub Docs. +https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations + +GitHub. (2026). *GITHUB_TOKEN*. GitHub Docs. +https://docs.github.com/en/actions/concepts/security/github_token diff --git a/docs/operations/hourly-commercialization-loop.md b/docs/operations/hourly-commercialization-loop.md index 90ca486..24a0f30 100644 --- a/docs/operations/hourly-commercialization-loop.md +++ b/docs/operations/hourly-commercialization-loop.md @@ -12,8 +12,11 @@ Each run performs four jobs in order: 1. **Inspect the PR queue.** Call the central PR review/merge scheduler to request missing current-head reviews, update eligible behind branches, and merge or enable auto-merge only when repository policy is satisfied. -2. **Repair review feedback.** Call the central review-fix scheduler with one - dispatch of budget and a one-hour same-head retry interval. +2. **Hold repair fail-closed when the protected repair engine is unavailable.** + Inspect the open-PR queue without a mutation credential. Until the protected + central NVIDIA NIM repair scheduler is merged, do not call an orphaned or + GitHub-Models-backed repair ref; independent review agents and the merge + scheduler continue to operate normally. 3. **Revalidate the PR queue.** Call the merge scheduler again so a repaired or newly approved current head is reconsidered under the same checks. 4. **Develop the next product gap.** Only when every governance job succeeded, @@ -24,17 +27,36 @@ Each run performs four jobs in order: The reusable workflows are referenced at immutable commits: - merge/revalidation policy: - `5983b41ace75040c1d81818171ca7d0f3653254e`; -- hourly review-repair policy with called-workflow source bound to - `job.workflow_repository` and `job.workflow_sha`: - `21397126d708d2d536ccc1d68b0d333653ce9315`. + `5983b41ace75040c1d81818171ca7d0f3653254e`. -This prevents a privileged scheduled run from silently changing behavior -because the central `main` branch moved. Updating either central policy -requires an explicit reviewed SHA change in RankWeave. +The former review-repair SHA, `21397126d708d2d536ccc1d68b0d333653ce9315`, +was no longer reachable from the protected central history. GitHub rejected the +caller before creating any jobs, so every scheduled run failed without doing PR +maintenance or product development. RankWeave now uses a local read-only hold +job until the protected central NVIDIA NIM repair scheduler is available. This +keeps the hourly workflow executable without routing repairs through GitHub +Models, a mutable branch, or an unmerged central change. + +Updating the central merge policy or re-enabling review repair requires an +explicit reviewed reachable SHA change in RankWeave. ## Product-development trust zones +## Reusable-workflow reachability incident + +GitHub Actions run `31124811165` and its immediate predecessors failed before +job creation. The caller still pinned the review-fix workflow to commit +`21397126d708d2d536ccc1d68b0d333653ce9315`, which had diverged from the +protected central history. The same caller had last succeeded before that +central ref became unreachable. + +The repair is deliberately narrower than copying the central engine into this +repository. The local bridge is read-only and does not invoke a model or mutate +a PR. Once the protected central scheduler provides the reviewed NVIDIA NIM +boundary, RankWeave can replace the bridge with a new immutable reachable SHA. +This preserves the standalone repository, the central MSA control plane, and +the existing independent-review credential system. + The local development job is separated into four trust zones. ### 1. Trusted base verification diff --git a/hourly-commercialization-loop.repaired.yml b/hourly-commercialization-loop.repaired.yml new file mode 100644 index 0000000..d8669b1 --- /dev/null +++ b/hourly-commercialization-loop.repaired.yml @@ -0,0 +1,979 @@ +name: Hourly RankWeave Commercialization Loop + +on: + schedule: + - cron: "17 * * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: rankweave-hourly-commercialization-loop + cancel-in-progress: true + +jobs: + inspect-pr-queue: + permissions: + actions: write + checks: read + contents: write + id-token: write + pull-requests: write + uses: ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@5983b41ace75040c1d81818171ca7d0f3653254e + with: + base_branch: main + max_prs: "50" + trigger_reviews: true + review_dispatch_limit: "1" + branch_update_limit: "1" + enable_auto_merge: true + merge_mode: direct_or_auto + update_branches: true + secrets: inherit + + repair-review-feedback: + needs: inspect-pr-queue + if: ${{ always() }} + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pull-requests: read + env: + TARGET_REPOSITORY: ContextualWisdomLab/RankWeave + steps: + - name: Keep review repair fail-closed until protected NVIDIA repair is available + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + open_pr_count="$( + gh api "/repos/${TARGET_REPOSITORY}/pulls?state=open&per_page=1" \ + --jq 'length' + )" + if [ "$open_pr_count" -eq 0 ]; then + echo "No pull request requires review repair." + exit 0 + fi + echo "::notice::Review repair remains fail-closed while the protected central NVIDIA NIM scheduler is pending. Existing independent review agents and the central merge scheduler remain unchanged." + + revalidate-pr-queue: + needs: repair-review-feedback + if: ${{ always() }} + permissions: + actions: write + checks: read + contents: write + id-token: write + pull-requests: write + uses: ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@5983b41ace75040c1d81818171ca7d0f3653254e + with: + base_branch: main + max_prs: "50" + trigger_reviews: true + review_dispatch_limit: "1" + branch_update_limit: "1" + enable_auto_merge: true + merge_mode: direct_or_auto + update_branches: true + secrets: inherit + + develop-next-product-gap: + needs: [inspect-pr-queue, repair-review-feedback, revalidate-pr-queue] + if: >- + ${{ + always() && + needs.inspect-pr-queue.result == 'success' && + needs.repair-review-feedback.result == 'success' && + needs.revalidate-pr-queue.result == 'success' + }} + runs-on: ubuntu-latest + timeout-minutes: 55 + permissions: + contents: read + id-token: write + pull-requests: read + env: + TARGET_REPOSITORY: ContextualWisdomLab/RankWeave + BASE_BRANCH: main + OPENCODE_VERSION: "1.17.13" + OPENCODE_SHA256: 157afa289d1a8d9372de0ce19ac726119b937a1f6b201808d46f06e4e59bb348 + OPENCODE_MODEL_CANDIDATES: >- + nvidia/nvidia/llama-3.3-nemotron-super-49b-v1.5 + nvidia/nvidia/nemotron-3-super-120b-a12b + nvidia/deepseek-ai/deepseek-v4-pro + OPENCODE_RED_TIMEOUT_SECONDS: "300" + OPENCODE_IMPLEMENT_TIMEOUT_SECONDS: "600" + MAX_AUTONOMOUS_CHANGED_FILES: "25" + MAX_AUTONOMOUS_FILE_BYTES: "262144" + MAX_AUTONOMOUS_TOTAL_BYTES: "1048576" + steps: + - name: Determine whether product development may start + id: gate + env: + GH_TOKEN: ${{ github.token }} + NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + run: | + set -euo pipefail + + if [ -z "${NVIDIA_API_KEY:-}" ]; then + echo "::warning::NVIDIA_NIM_API_KEY is not configured; product development remains fail-closed." + echo "eligible=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + + open_pr_count="$( + gh api "/repos/${TARGET_REPOSITORY}/pulls?state=open&per_page=1" \ + --jq 'length' + )" + if [ "$open_pr_count" -ne 0 ]; then + echo "An open pull request already owns the development queue." + echo "eligible=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + + echo "eligible=true" >>"$GITHUB_OUTPUT" + + - name: Check out the current base without persisted credentials + if: steps.gate.outputs.eligible == 'true' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: main + fetch-depth: 1 + persist-credentials: false + + - name: Prepare trusted validation tooling + if: steps.gate.outputs.eligible == 'true' + run: | + set -euo pipefail + venv="/tmp/rankweave-automation-venv-${GITHUB_RUN_ID}" + sudo rm -rf "$venv" + python -m venv "$venv" + "$venv/bin/python" -m pip install --upgrade pip + "$venv/bin/python" -m pip install -e ".[dev]" hatchling + sudo chown -R root:root "$venv" + sudo chmod -R a-w "$venv" + sandbox_uid="$(id -u nobody)" + sandbox_gid="$(id -g nobody)" + command -v setpriv >/dev/null + echo "AUTOMATION_VENV=$venv" >>"$GITHUB_ENV" + echo "AUTOMATION_BASE_SHA=$(git rev-parse HEAD)" >>"$GITHUB_ENV" + echo "SANDBOX_UID=$sandbox_uid" >>"$GITHUB_ENV" + echo "SANDBOX_GID=$sandbox_gid" >>"$GITHUB_ENV" + { + echo "/opencode.json" + echo "/.agent-red-output.txt" + echo "/PR_MESSAGE.md" + } >>"$GITHUB_WORKSPACE/.git/info/exclude" + - name: Verify the trusted base and network-isolation primitive + if: steps.gate.outputs.eligible == 'true' + run: | + set -euo pipefail + "$AUTOMATION_VENV/bin/python" -m ruff check . + "$AUTOMATION_VENV/bin/python" -m coverage run -m pytest -q + "$AUTOMATION_VENV/bin/python" -m coverage report + rm -f .coverage + sandbox_probe=( + sudo unshare --net --pid --fork --mount-proc + setpriv + --reuid="$SANDBOX_UID" + --regid="$SANDBOX_GID" + --clear-groups + --no-new-privs + --bounding-set=-all + --inh-caps=-all + --ambient-caps=-all + ) + "${sandbox_probe[@]}" true + - name: Install the pinned OpenCode CLI + if: steps.gate.outputs.eligible == 'true' + run: | + set -euo pipefail + archive="${RUNNER_TEMP}/opencode-linux-x64.tar.gz" + install_dir="${RUNNER_TEMP}/opencode/bin" + mkdir -p "$install_dir" + curl -fsSL \ + -o "$archive" \ + "https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz" + printf '%s %s\n' "$OPENCODE_SHA256" "$archive" | sha256sum -c - + tar -xzf "$archive" -C "$RUNNER_TEMP" + install -m 0755 "${RUNNER_TEMP}/opencode" "$install_dir/opencode" + "$install_dir/opencode" --version + echo "$install_dir" >>"$GITHUB_PATH" + + - name: Configure test-first authoring permissions + if: steps.gate.outputs.eligible == 'true' + run: | + set -euo pipefail + cat >"$GITHUB_WORKSPACE/opencode.json" <<'CONFIG' + { + "$schema": "https://opencode.ai/config.json", + "enabled_providers": ["nvidia"], + "model": "nvidia/nvidia/llama-3.3-nemotron-super-49b-v1.5", + "lsp": false, + "permission": { + "read": { + "*": "allow", + ".git/**": "deny", + "opencode.json": "deny", + ".env": "deny", + ".env.*": "deny" + }, + "edit": { + "*": "deny", + "tests/**": "allow", + "docs/superpowers/specs/**": "allow" + }, + "bash": "deny", + "webfetch": "deny", + "websearch": "deny", + "external_directory": "deny", + "task": "deny", + "skill": "deny", + "question": "deny", + "lsp": "deny", + "doom_loop": "deny" + } + } + CONFIG + + - name: Author one design and failing regression test + if: steps.gate.outputs.eligible == 'true' + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + run: | + set -euo pipefail + + prompt="$(cat <<'PROMPT' + Work only from the trusted files on the checked-out RankWeave main branch. + Do not read GitHub issues, pull requests, external web pages, environment + variables, or paths outside the repository. + + Select exactly one highest-impact buyer-visible product gap that fits one + bounded pull request and does not require a new external research claim. + Write a concise design under docs/superpowers/specs/ and write the failing + pytest regression tests first. During this phase do not modify production + code, package metadata, README.md, AGENTS.md, CHANGELOG.md, workflows, or + any file outside tests/ and docs/superpowers/specs/. Do not write + PR_MESSAGE.md yet. + + The tests must express the buyer-visible contract, preserve RankWeave's + standard-library-only runtime, deterministic and immutable evidence, + fail-closed validation, Python 3.10+ compatibility, and modular standalone + plus naruon-import use. The workflow will execute the test suite after this + phase and requires a genuine test failure before implementation begins. + PROMPT + )" + + status=1 + for model in $OPENCODE_MODEL_CANDIDATES; do + echo "::group::opencode red phase $model" + if timeout --kill-after=30s "${OPENCODE_RED_TIMEOUT_SECONDS}s" \ + env -u GH_TOKEN -u GITHUB_TOKEN \ + -u ACTIONS_ID_TOKEN_REQUEST_TOKEN -u ACTIONS_ID_TOKEN_REQUEST_URL \ + HOME="${RUNNER_TEMP}/opencode-home-red" \ + opencode run "$prompt" --model "$model"; then + status=0 + echo "::endgroup::" + break + fi + echo "::endgroup::" + echo "::warning::Model $model failed during the red phase; discarding partial work." + git reset --hard "$AUTOMATION_BASE_SHA" + git clean -fd + done + if [ "$status" -ne 0 ]; then + echo "::error::Every NVIDIA model failed during test-first authoring." + exit 1 + fi + + - name: Verify test-only scope and observe the red state + if: steps.gate.outputs.eligible == 'true' + run: | + set -euo pipefail + rm -f opencode.json + git clean -fdX + "$AUTOMATION_VENV/bin/python" - <<'PY' + from __future__ import annotations + + import os + import stat + import subprocess + from pathlib import Path + + max_files = int(os.environ["MAX_AUTONOMOUS_CHANGED_FILES"]) + max_file_bytes = int(os.environ["MAX_AUTONOMOUS_FILE_BYTES"]) + max_total_bytes = int(os.environ["MAX_AUTONOMOUS_TOTAL_BYTES"]) + records = subprocess.check_output( + ["git", "status", "--porcelain=v1", "-z"] + ).split(b"\0") + paths = [] + total_bytes = 0 + index = 0 + while index < len(records): + record = records[index] + index += 1 + if not record: + continue + status_text = record[:2].decode("ascii") + path_text = record[3:].decode("utf-8") + if any(marker in status_text for marker in ("R", "C", "D", "U")): + raise SystemExit( + "red phase may not rename, copy, or delete files: " + f"{path_text}" + ) + paths.append(path_text) + path = Path(path_text) + info = path.lstat() + if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode): + raise SystemExit(f"red phase produced a non-regular file: {path_text}") + if info.st_size > max_file_bytes: + raise SystemExit( + f"red phase file {path_text} exceeds {max_file_bytes} bytes" + ) + data = path.read_bytes() + if b"\0" in data: + raise SystemExit(f"red phase file contains a NUL byte: {path_text}") + try: + data.decode("utf-8", errors="strict") + except UnicodeDecodeError as exc: + raise SystemExit( + f"red phase file is not strict UTF-8: {path_text}" + ) from exc + total_bytes += info.st_size + + if not paths: + raise SystemExit("red phase produced no files") + if len(paths) > max_files or total_bytes > max_total_bytes: + raise SystemExit("red phase exceeded the autonomous diff budget") + if not any( + path.startswith("tests/") and Path(path).suffix == ".py" + for path in paths + ): + raise SystemExit("red phase did not add or modify a pytest file") + allowed_prefixes = ("tests/", "docs/superpowers/specs/") + forbidden = [ + path for path in paths if not path.startswith(allowed_prefixes) + ] + if forbidden: + raise SystemExit( + f"red phase changed files outside its scope: {forbidden!r}" + ) + PY + + red_home="/tmp/rankweave-red-home-${GITHUB_RUN_ID}" + red_output="${RUNNER_TEMP}/red-test-output.txt" + sudo rm -rf "$red_home" + sudo mkdir -p "$red_home" + sudo chown "$SANDBOX_UID:$SANDBOX_GID" "$red_home" + red_sandbox=( + sudo unshare --net --pid --fork --mount-proc + setpriv + --reuid="$SANDBOX_UID" + --regid="$SANDBOX_GID" + --clear-groups + --no-new-privs + --bounding-set=-all + --inh-caps=-all + --ambient-caps=-all + ) + red_environment=( + env -i + "PATH=${AUTOMATION_VENV}/bin:/usr/bin:/bin" + "HOME=$red_home" + "WORKSPACE=$GITHUB_WORKSPACE" + "PYTHONPATH=$GITHUB_WORKSPACE/src" + PYTHONDONTWRITEBYTECODE=1 + bash + --noprofile + --norc + -c + 'cd "$WORKSPACE" && python -m pytest -q -p no:cacheprovider' + ) + set +e + "${red_sandbox[@]}" "${red_environment[@]}" >"$red_output" 2>&1 + red_status=$? + set -e + if [ "$red_status" -ne 1 ]; then + cat "${RUNNER_TEMP}/red-test-output.txt" + echo "::error::Expected pytest exit 1 from a genuine red test; got ${red_status}." + exit 1 + fi + if ! grep -q "FAILED" "${RUNNER_TEMP}/red-test-output.txt"; then + cat "${RUNNER_TEMP}/red-test-output.txt" + echo "::error::Red test output did not contain a failed test." + exit 1 + fi + tail -n 200 "${RUNNER_TEMP}/red-test-output.txt" \ + >"$GITHUB_WORKSPACE/.agent-red-output.txt" + + git config user.name "github-actions[bot]" + git config user.email \ + "41898282+github-actions[bot]@users.noreply.github.com" + git add tests docs/superpowers/specs + git -c core.hooksPath=/dev/null commit \ + -m "test(red): define the next buyer-visible product gap" + echo "AUTOMATION_RED_SHA=$(git rev-parse HEAD)" >>"$GITHUB_ENV" + + - name: Configure implementation permissions + if: steps.gate.outputs.eligible == 'true' + run: | + set -euo pipefail + cat >"$GITHUB_WORKSPACE/opencode.json" <<'CONFIG' + { + "$schema": "https://opencode.ai/config.json", + "enabled_providers": ["nvidia"], + "model": "nvidia/nvidia/llama-3.3-nemotron-super-49b-v1.5", + "lsp": false, + "permission": { + "read": { + "*": "allow", + ".git/**": "deny", + "opencode.json": "deny", + ".env": "deny", + ".env.*": "deny" + }, + "edit": { + "*": "allow", + "AGENTS.md": "deny", + ".github/**": "deny", + ".git/**": "deny", + ".agent-red-output.txt": "deny", + "opencode.json": "deny" + }, + "bash": "deny", + "webfetch": "deny", + "websearch": "deny", + "external_directory": "deny", + "task": "deny", + "skill": "deny", + "question": "deny", + "lsp": "deny", + "doom_loop": "deny" + } + } + CONFIG + + - name: Implement the bounded product increment + if: steps.gate.outputs.eligible == 'true' + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + run: | + set -euo pipefail + + prompt="$(cat <<'PROMPT' + Implement the single design and failing tests already present in the + RankWeave workspace. Read .agent-red-output.txt for the exact red-test + evidence. Do not inspect GitHub issues, pull requests, external web pages, + environment variables, or paths outside the repository. + + Make the smallest coherent production change that turns the red tests + green. Preserve the standard-library-only runtime, store-agnostic modular + architecture, deterministic behavior, immutable audit records, fail-closed + input contracts, Python 3.10+ compatibility, full production docstrings, + and standalone plus naruon-import use. Do not edit anything under .github/ + or any security, credential, ownership, or workflow policy file. + + Update CHANGELOG.md, README.md, relevant product or operations + documentation, package smoke contracts, and version metadata only when the + slice is release-ready. Keep all papers in APA 7th edition and do not add a + statistical or standards claim without a primary source already recorded in + the repository. Figma is not applicable because RankWeave has no UI. + + Write PR_MESSAGE.md at the repository root. Put a concise PR title on the + first line and a body after it describing buyer impact, evidence, + compatibility, and the exact validation commands. Do not commit, push, + open, approve, merge, publish, or release anything; the workflow performs + deterministic validation and packages one protected pull request. + PROMPT + )" + + status=1 + for model in $OPENCODE_MODEL_CANDIDATES; do + echo "::group::opencode implementation phase $model" + if timeout --kill-after=30s "${OPENCODE_IMPLEMENT_TIMEOUT_SECONDS}s" \ + env -u GH_TOKEN -u GITHUB_TOKEN \ + -u ACTIONS_ID_TOKEN_REQUEST_TOKEN -u ACTIONS_ID_TOKEN_REQUEST_URL \ + HOME="${RUNNER_TEMP}/opencode-home-implementation" \ + opencode run "$prompt" --model "$model"; then + status=0 + echo "::endgroup::" + break + fi + echo "::endgroup::" + echo "::warning::Model $model failed during implementation; restoring the verified red state." + git reset --hard "$AUTOMATION_RED_SHA" + git clean -fd + cp "${RUNNER_TEMP}/red-test-output.txt" \ + "$GITHUB_WORKSPACE/.agent-red-output.txt" + done + if [ "$status" -ne 0 ]; then + echo "::error::Every NVIDIA model failed during implementation." + exit 1 + fi + + - name: Enforce the autonomous diff boundary + if: steps.gate.outputs.eligible == 'true' + run: | + set -euo pipefail + pr_message_backup="${RUNNER_TEMP}/agent-pr-message.md" + if [ -f PR_MESSAGE.md ]; then + cp PR_MESSAGE.md "$pr_message_backup" + fi + rm -f opencode.json .agent-red-output.txt + git clean -fdX + if [ -f "$pr_message_backup" ]; then + cp "$pr_message_backup" PR_MESSAGE.md + fi + "$AUTOMATION_VENV/bin/python" - <<'PY' + from __future__ import annotations + + import os + import stat + import subprocess + import tomllib + from pathlib import Path + + base_sha = os.environ["AUTOMATION_BASE_SHA"] + max_files = int(os.environ["MAX_AUTONOMOUS_CHANGED_FILES"]) + max_file_bytes = int(os.environ["MAX_AUTONOMOUS_FILE_BYTES"]) + max_total_bytes = int(os.environ["MAX_AUTONOMOUS_TOTAL_BYTES"]) + + raw = subprocess.check_output( + ["git", "diff", "--name-status", "-z", base_sha] + ).split(b"\0") + changed: dict[str, str] = {} + index = 0 + while index < len(raw): + status_bytes = raw[index] + index += 1 + if not status_bytes: + continue + status_text = status_bytes.decode("ascii") + if status_text.startswith(("R", "C")): + old_path = raw[index].decode("utf-8") + new_path = raw[index + 1].decode("utf-8") + raise SystemExit( + "autonomous changes may not rename or copy files: " + f"{old_path} -> {new_path}" + ) + path_text = raw[index].decode("utf-8") + index += 1 + status_code = status_text[0] + if status_code not in {"A", "M"}: + raise SystemExit( + f"autonomous status {status_text} is forbidden: {path_text}" + ) + changed[path_text] = status_code + + for path_bytes in subprocess.check_output( + ["git", "ls-files", "--others", "--exclude-standard", "-z"] + ).split(b"\0"): + if path_bytes: + changed[path_bytes.decode("utf-8")] = "?" + + if not changed: + raise SystemExit("implementation produced no changes") + if len(changed) > max_files: + raise SystemExit( + f"autonomous change count {len(changed)} exceeds {max_files}" + ) + + forbidden_exact = { + ".gitmodules", + "AGENTS.md", + "CODEOWNERS", + "SECURITY.md", + } + forbidden_prefixes = (".github/", ".git/") + allowed_exact = { + "CHANGELOG.md", + "README.md", + "pyproject.toml", + "PR_MESSAGE.md", + } + allowed_prefixes = ("src/rankweave/", "tests/", "docs/") + allowed_suffixes = { + ".json", + ".md", + ".py", + ".toml", + ".txt", + ".yaml", + ".yml", + } + + total_bytes = 0 + production_changed = False + for path_text in sorted(changed): + path = Path(path_text) + if path.is_absolute() or ".." in path.parts: + raise SystemExit(f"invalid changed path: {path_text}") + if ( + path_text in forbidden_exact + or path_text.startswith(forbidden_prefixes) + or path.name.startswith(".env") + ): + raise SystemExit(f"protected path changed: {path_text}") + if ( + path_text not in allowed_exact + and not path_text.startswith(allowed_prefixes) + ): + raise SystemExit(f"path is outside autonomous scope: {path_text}") + if path_text != "PR_MESSAGE.md" and path.suffix not in allowed_suffixes: + raise SystemExit(f"non-text or unsupported path changed: {path_text}") + if path_text.startswith("src/rankweave/") and path.suffix == ".py": + production_changed = True + info = path.lstat() + if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode): + raise SystemExit(f"non-regular file changed: {path_text}") + if info.st_size > max_file_bytes: + raise SystemExit( + f"{path_text} size {info.st_size} exceeds {max_file_bytes}" + ) + data = path.read_bytes() + if b"\0" in data: + raise SystemExit(f"NUL byte found in {path_text}") + try: + data.decode("utf-8", errors="strict") + except UnicodeDecodeError as exc: + raise SystemExit( + f"changed file is not strict UTF-8: {path_text}" + ) from exc + total_bytes += info.st_size + + if not production_changed: + raise SystemExit( + "buyer-visible increment must change a production Python module" + ) + if not any(path.startswith("tests/") for path in changed): + raise SystemExit("autonomous increment must include regression tests") + if not any( + path.startswith("docs/superpowers/specs/") for path in changed + ): + raise SystemExit("autonomous increment must include a design specification") + if "CHANGELOG.md" not in changed: + raise SystemExit("autonomous increment must update CHANGELOG.md") + if total_bytes > max_total_bytes: + raise SystemExit( + f"autonomous byte total {total_bytes} exceeds {max_total_bytes}" + ) + + metadata = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8")) + if metadata["build-system"]["build-backend"] != "hatchling.build": + raise SystemExit("build backend may not change") + if metadata["project"].get("dependencies") != []: + raise SystemExit("RankWeave runtime dependencies must remain empty") + if metadata["tool"]["coverage"]["run"].get("branch") is not True: + raise SystemExit("branch coverage may not be disabled") + if metadata["tool"]["coverage"]["run"].get("source") != ["rankweave"]: + raise SystemExit("coverage source may not change") + if metadata["tool"]["coverage"]["report"].get("fail_under") != 100: + raise SystemExit("coverage fail-under must remain 100") + required_doc_rules = {f"D10{index}" for index in range(8)} + selected_rules = set(metadata["tool"]["ruff"]["lint"].get("select", [])) + if not required_doc_rules <= selected_rules: + raise SystemExit("production docstring rules may not be weakened") + PY + + - name: Record the pre-validation workspace manifest + if: steps.gate.outputs.eligible == 'true' + run: | + set -euo pipefail + "$AUTOMATION_VENV/bin/python" - <<'PY' \ + >"${RUNNER_TEMP}/workspace-manifest-before.json" + from __future__ import annotations + + import hashlib + import json + import os + import subprocess + from pathlib import Path + + base_sha = os.environ["AUTOMATION_BASE_SHA"] + paths = { + value.decode("utf-8") + for value in subprocess.check_output( + ["git", "diff", "--name-only", "-z", base_sha] + ).split(b"\0") + if value + } + paths.update( + value.decode("utf-8") + for value in subprocess.check_output( + ["git", "ls-files", "--others", "--exclude-standard", "-z"] + ).split(b"\0") + if value + ) + manifest = { + path_text: hashlib.sha256(Path(path_text).read_bytes()).hexdigest() + for path_text in sorted(paths) + } + print(json.dumps(manifest, sort_keys=True)) + PY + + - name: Validate untrusted changes without network or inherited environment + if: steps.gate.outputs.eligible == 'true' + run: | + set -euo pipefail + validation_dist="/tmp/rankweave-validation-dist-${GITHUB_RUN_ID}" + validation_smoke="/tmp/rankweave-validation-smoke-${GITHUB_RUN_ID}" + validation_home="/tmp/rankweave-validation-home-${GITHUB_RUN_ID}" + validation_coverage="/tmp/rankweave-validation-${GITHUB_RUN_ID}.coverage" + ruff_cache="/tmp/rankweave-ruff-cache-${GITHUB_RUN_ID}" + validation_script="/tmp/rankweave-validate-${GITHUB_RUN_ID}.sh" + validation_paths=( + "$validation_dist" + "$validation_smoke" + "$validation_home" + "$validation_coverage" + "$ruff_cache" + "$validation_script" + ) + sudo rm -rf "${validation_paths[@]}" + sudo mkdir -p "$validation_dist" "$validation_home" "$ruff_cache" + sudo chown -R "$SANDBOX_UID:$SANDBOX_GID" "$validation_dist" "$validation_home" "$ruff_cache" + cat >"$validation_script" <<'VALIDATE' + set -euo pipefail + cd "$WORKSPACE" + python -m ruff check . + python -m coverage run -m pytest -q -p no:cacheprovider + python -m coverage report + python -m pip wheel . --no-deps --no-build-isolation --wheel-dir "$DIST" + python -m venv "$SMOKE" + "$SMOKE/bin/python" -m pip install --no-index --find-links "$DIST" rankweave + "$SMOKE/bin/python" -m pip check + cd "$HOME" + "$SMOKE/bin/python" -c 'from importlib.metadata import version; import rankweave; assert version("rankweave") == rankweave.__version__' + VALIDATE + sudo chown root:root "$validation_script" + sudo chmod 0555 "$validation_script" + validation_sandbox=( + sudo unshare --net --pid --fork --mount-proc + setpriv + --reuid="$SANDBOX_UID" + --regid="$SANDBOX_GID" + --clear-groups + --no-new-privs + --bounding-set=-all + --inh-caps=-all + --ambient-caps=-all + ) + validation_environment=( + env -i + "PATH=${AUTOMATION_VENV}/bin:/usr/bin:/bin" + "HOME=$validation_home" + "WORKSPACE=$GITHUB_WORKSPACE" + "PYTHONPATH=$GITHUB_WORKSPACE/src" + "DIST=$validation_dist" + "SMOKE=$validation_smoke" + "COVERAGE_FILE=$validation_coverage" + "RUFF_CACHE_DIR=$ruff_cache" + PYTHONDONTWRITEBYTECODE=1 + PIP_DISABLE_PIP_VERSION_CHECK=1 + PIP_NO_INDEX=1 + bash + --noprofile + --norc + "$validation_script" + ) + "${validation_sandbox[@]}" "${validation_environment[@]}" + - name: Verify validation did not mutate the proposal + if: steps.gate.outputs.eligible == 'true' + run: | + set -euo pipefail + "$AUTOMATION_VENV/bin/python" - <<'PY' + from __future__ import annotations + + import hashlib + import json + import os + import subprocess + from pathlib import Path + + base_sha = os.environ["AUTOMATION_BASE_SHA"] + before_path = Path(os.environ["RUNNER_TEMP"]) / "workspace-manifest-before.json" + before = json.loads(before_path.read_text(encoding="utf-8")) + paths = { + value.decode("utf-8") + for value in subprocess.check_output( + ["git", "diff", "--name-only", "-z", base_sha] + ).split(b"\0") + if value + } + paths.update( + value.decode("utf-8") + for value in subprocess.check_output( + ["git", "ls-files", "--others", "--exclude-standard", "-z"] + ).split(b"\0") + if value + ) + after = { + path_text: hashlib.sha256(Path(path_text).read_bytes()).hexdigest() + for path_text in sorted(paths) + } + if before != after: + raise SystemExit("validation mutated the proposed working tree") + PY + + - name: Recheck queue and base before token exchange + id: mutation_preflight + if: steps.gate.outputs.eligible == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + open_pr_count="$( + gh api "/repos/${TARGET_REPOSITORY}/pulls?state=open&per_page=1" \ + --jq 'length' + )" + if [ "$open_pr_count" -ne 0 ]; then + echo "Another pull request acquired the queue before token exchange." + echo "eligible=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + + current_base_sha="$( + gh api "/repos/${TARGET_REPOSITORY}/commits/${BASE_BRANCH}" \ + --jq '.sha' + )" + if [ "$current_base_sha" != "$AUTOMATION_BASE_SHA" ]; then + echo "The base branch moved before token exchange." + echo "eligible=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + + echo "eligible=true" >>"$GITHUB_OUTPUT" + + - name: Exchange an OpenCode app token for generated PR events + id: generated_pr_token + if: steps.mutation_preflight.outputs.eligible == 'true' + env: + OIDC_AUDIENCE: opencode-github-action + OPENCODE_API_BASE_URL: https://api.opencode.ai + run: | + set -euo pipefail + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || \ + [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + echo "::error::OIDC request environment is unavailable." + exit 1 + fi + request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" + separator="&" + case "$request_url" in + *\?*) ;; + *) separator="?" ;; + esac + oidc_response="$( + curl -fsS \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${request_url}${separator}audience=${OIDC_AUDIENCE}" + )" + oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" + if [ -z "$oidc_token" ]; then + echo "::error::OIDC token response was empty." + exit 1 + fi + token_response="$( + curl -fsS \ + -X POST \ + -H "Authorization: Bearer ${oidc_token}" \ + "${OPENCODE_API_BASE_URL}/exchange_github_app_token" + )" + app_token="$(jq -r '.token // empty' <<<"$token_response")" + if [ -z "$app_token" ]; then + echo "::error::OpenCode GitHub App token response was empty." + exit 1 + fi + echo "::add-mask::$app_token" + echo "token=$app_token" >>"$GITHUB_OUTPUT" + + - name: Open exactly one focused pull request + if: steps.mutation_preflight.outputs.eligible == 'true' + env: + GH_TOKEN: ${{ steps.generated_pr_token.outputs.token }} + run: | + set -euo pipefail + cd "$GITHUB_WORKSPACE" + if [ -z "${GH_TOKEN:-}" ]; then + echo "::error::Generated PR app token is unavailable." + exit 1 + fi + + open_pr_count="$( + gh api "/repos/${TARGET_REPOSITORY}/pulls?state=open&per_page=1" \ + --jq 'length' + )" + if [ "$open_pr_count" -ne 0 ]; then + echo "Another pull request acquired the queue; discarding this proposal." + exit 0 + fi + + current_base_sha="$( + gh api "/repos/${TARGET_REPOSITORY}/commits/${BASE_BRANCH}" \ + --jq '.sha' + )" + if [ "$current_base_sha" != "$AUTOMATION_BASE_SHA" ]; then + echo "The base branch moved during authoring; discarding this stale proposal." + exit 0 + fi + + title="RankWeave autonomous commercialization increment" + body_file="${RUNNER_TEMP}/pr-body.md" + if [ -f PR_MESSAGE.md ]; then + /usr/bin/python3 -I -S - <<'PY' + from __future__ import annotations + + import os + from pathlib import Path + + source = Path("PR_MESSAGE.md").read_text(encoding="utf-8") + lines = source.splitlines() + candidate = lines[0].lstrip("#").strip() if lines else "" + if ( + 10 <= len(candidate) <= 120 + and not candidate.startswith("-") + and all(character.isprintable() for character in candidate) + ): + title = candidate + else: + title = "RankWeave autonomous commercialization increment" + body = "\n".join(lines[1:]).strip() + if not body: + body = "Autonomous NVIDIA NIM increment; see the diff and CHANGELOG.md." + if len(body.encode("utf-8")) > 20_000: + raise SystemExit("PR body exceeds 20,000 UTF-8 bytes") + Path(os.environ["RUNNER_TEMP"], "pr-title.txt").write_text( + title, + encoding="utf-8", + ) + Path(os.environ["RUNNER_TEMP"], "pr-body.md").write_text( + body + "\n", + encoding="utf-8", + ) + PY + title="$(cat "${RUNNER_TEMP}/pr-title.txt")" + rm -f PR_MESSAGE.md + else + echo "Autonomous NVIDIA NIM increment; see the diff and CHANGELOG.md." \ + >"$body_file" + fi + + git reset --soft "$AUTOMATION_BASE_SHA" + branch="nim-agent/product-dev-${GITHUB_RUN_ID}" + git config user.name "github-actions[bot]" + git config user.email \ + "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "$branch" + git add -A + git -c core.hooksPath=/dev/null commit -m "$title" + git -c core.hooksPath=/dev/null push \ + "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ + "HEAD:refs/heads/${branch}" + gh pr create \ + --repo "$GITHUB_REPOSITORY" \ + --base "$BASE_BRANCH" \ + --head "$branch" \ + --title "$title" \ + --body-file "$body_file" diff --git a/tests/test_hourly_commercialization_workflow.py b/tests/test_hourly_commercialization_workflow.py index 806b0a8..8971330 100644 --- a/tests/test_hourly_commercialization_workflow.py +++ b/tests/test_hourly_commercialization_workflow.py @@ -3,7 +3,6 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1] WORKFLOW_PATH = PROJECT_ROOT / ".github/workflows/hourly-commercialization-loop.yml" MERGE_WORKFLOW_SHA = "5983b41ace75040c1d81818171ca7d0f3653254e" -FIX_WORKFLOW_SHA = "21397126d708d2d536ccc1d68b0d333653ce9315" def _workflow_text() -> str: @@ -24,21 +23,44 @@ def test_commercialization_loop_runs_once_each_hour(): assert "cancel-in-progress: true" in workflow -def test_commercialization_loop_uses_pinned_central_pr_governance(): +def test_commercialization_loop_uses_reachable_merge_governance(): workflow = _workflow_text() merge_reference = ( "ContextualWisdomLab/.github/.github/workflows/" f"pr-review-merge-scheduler.yml@{MERGE_WORKFLOW_SHA}" ) - fix_reference = ( - "ContextualWisdomLab/.github/.github/workflows/" - f"pr-review-fix-scheduler.yml@{FIX_WORKFLOW_SHA}" - ) assert workflow.count(merge_reference) == 2 - assert workflow.count(fix_reference) == 1 - assert 'retry_hours: "1"' in workflow - assert "secrets: inherit" in workflow + assert "pr-review-fix-scheduler.yml@" not in workflow + assert workflow.count("secrets: inherit") == 2 + + +def test_review_repair_bridge_is_local_read_only_and_provider_neutral(): + workflow = _workflow_text() + repair = _job_section( + workflow, + "repair-review-feedback", + "revalidate-pr-queue", + ) + + assert "runs-on: ubuntu-latest" in repair + assert "contents: read" in repair + assert "pull-requests: read" in repair + for forbidden in ( + "actions: write", + "contents: write", + "id-token: write", + "issues: write", + "statuses: read", + "secrets: inherit", + "github-models/", + "STRIX_GITHUB_MODELS_TOKEN", + "COPILOT_GITHUB_TOKEN", + "NVIDIA_NIM_API_KEY", + ): + assert forbidden not in repair + assert "protected central NVIDIA NIM scheduler is pending" in repair + assert "/pulls?state=open&per_page=1" in repair def test_product_development_uses_nvidia_nim_and_fails_closed(): @@ -47,7 +69,7 @@ def test_product_development_uses_nvidia_nim_and_fails_closed(): assert "NVIDIA_NIM_API_KEY" in workflow assert "COPILOT_GITHUB_TOKEN" not in workflow assert "/agents/repos" not in workflow - assert workflow.count("/pulls?state=open&per_page=1") == 3 + assert workflow.count("/pulls?state=open&per_page=1") == 4 assert ( "NVIDIA_NIM_API_KEY is not configured; product development remains " "fail-closed" in workflow @@ -209,7 +231,7 @@ def test_queue_and_base_are_checked_before_and_after_token_exchange(): def test_final_queue_and_base_are_rechecked_before_pr_creation(): workflow = _workflow_text() - assert workflow.count("/pulls?state=open&per_page=1") == 3 + assert workflow.count("/pulls?state=open&per_page=1") == 4 assert workflow.count("/commits/${BASE_BRANCH}") == 2 assert "The base branch moved during authoring" in workflow assert "Another pull request acquired the queue" in workflow @@ -263,15 +285,18 @@ def test_governance_permissions_are_scoped_per_calling_job(): assert "statuses: read" not in merge_job for permission in ( - "actions: write", "contents: read", - "issues: write", "pull-requests: read", - "statuses: read", ): assert permission in repair - assert "contents: write" not in repair - assert "id-token: write" not in repair + for forbidden_permission in ( + "actions: write", + "contents: write", + "id-token: write", + "issues: write", + "statuses: read", + ): + assert forbidden_permission not in repair def test_opencode_binary_and_models_are_pinned(): From a05fe5a748a4094812a7382778f4160aa539ca36 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 08:56:10 +0900 Subject: [PATCH 09/16] fix(ci): restore executable hourly governance --- .../workflows/apply-hourly-loop-repair.yml | 108 -- .../hourly-commercialization-loop.yml | 30 +- hourly-commercialization-loop.repaired.yml | 979 ------------------ 3 files changed, 19 insertions(+), 1098 deletions(-) delete mode 100644 .github/workflows/apply-hourly-loop-repair.yml delete mode 100644 hourly-commercialization-loop.repaired.yml diff --git a/.github/workflows/apply-hourly-loop-repair.yml b/.github/workflows/apply-hourly-loop-repair.yml deleted file mode 100644 index 2cbd481..0000000 --- a/.github/workflows/apply-hourly-loop-repair.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: Apply hourly loop reachability repair once - -on: - push: - branches: [fix/hourly-loop-reachable-governance] - paths: - - .github/workflows/apply-hourly-loop-repair.yml - pull_request: - types: [opened, synchronize, reopened] - -permissions: - contents: write - -concurrency: - group: apply-hourly-loop-reachable-governance - cancel-in-progress: false - -jobs: - apply-verify-push: - if: >- - github.event_name == 'push' || - github.event.pull_request.head.ref == 'fix/hourly-loop-reachable-governance' - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: fix/hourly-loop-reachable-governance - fetch-depth: 0 - persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.13" - - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - with: - version: "0.11.29" - enable-cache: false - - name: Apply reviewed repair and remove bootstrap files - run: | - set -euo pipefail - python .github/scripts/apply_hourly_loop_repair.py - python .github/scripts/adjust_hourly_loop_tests.py - rm .github/scripts/apply_hourly_loop_repair.py - rm .github/scripts/adjust_hourly_loop_tests.py - rm .github/workflows/apply-hourly-loop-repair.yml - - name: Verify focused and full repository contracts - run: | - set -euo pipefail - uv sync --frozen --extra dev --python 3.13 - uv run --frozen --extra dev --python 3.13 \ - python -m compileall -q src - uv run --frozen --extra dev --python 3.13 \ - python -m ruff check . - uv run --frozen --extra dev --python 3.13 \ - python -m pytest tests/test_hourly_commercialization_workflow.py -q - uv run --frozen --extra dev --python 3.13 \ - python -m coverage run -m pytest -q - uv run --frozen --extra dev --python 3.13 \ - python -m coverage report - uv build --wheel --sdist --out-dir dist - - name: Stage the verified durable file bundle - run: | - set -euo pipefail - bundle="${RUNNER_TEMP}/hourly-repair-bundle" - rm -rf "$bundle" - mkdir -p \ - "$bundle/.github/workflows" \ - "$bundle/tests" \ - "$bundle/docs/operations" \ - "$bundle/docs/doctoring" \ - "$bundle/docs/adr" - cp .github/workflows/hourly-commercialization-loop.yml \ - "$bundle/.github/workflows/" - cp tests/test_hourly_commercialization_workflow.py "$bundle/tests/" - cp docs/operations/hourly-commercialization-loop.md \ - "$bundle/docs/operations/" - cp docs/doctoring/hourly-reusable-workflow-reachability.md \ - "$bundle/docs/doctoring/" - cp docs/adr/0006-fail-closed-hourly-repair-bridge.md \ - "$bundle/docs/adr/" - cp CHANGELOG.md "$bundle/" - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: hourly-loop-repair-durable-files - path: ${{ runner.temp }}/hourly-repair-bundle/ - if-no-files-found: error - include-hidden-files: true - retention-days: 1 - - name: Commit verified non-workflow repair files - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - rm -rf dist .coverage - cp .github/workflows/hourly-commercialization-loop.yml \ - hourly-commercialization-loop.repaired.yml - git restore --source=HEAD -- \ - .github/workflows/hourly-commercialization-loop.yml \ - .github/workflows/apply-hourly-loop-repair.yml - git config user.name "github-actions[bot]" - git config user.email \ - "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git -c core.hooksPath=/dev/null commit \ - -m "fix(ci): stage executable hourly governance" - git -c core.hooksPath=/dev/null push \ - "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ - "HEAD:refs/heads/fix/hourly-loop-reachable-governance" diff --git a/.github/workflows/hourly-commercialization-loop.yml b/.github/workflows/hourly-commercialization-loop.yml index 7e85598..d8669b1 100644 --- a/.github/workflows/hourly-commercialization-loop.yml +++ b/.github/workflows/hourly-commercialization-loop.yml @@ -35,20 +35,28 @@ jobs: repair-review-feedback: needs: inspect-pr-queue if: ${{ always() }} + runs-on: ubuntu-latest + timeout-minutes: 5 permissions: - actions: write contents: read - issues: write pull-requests: read - statuses: read - uses: ContextualWisdomLab/.github/.github/workflows/pr-review-fix-scheduler.yml@21397126d708d2d536ccc1d68b0d333653ce9315 - with: - target_repository: ContextualWisdomLab/RankWeave - base_branch: main - max_prs: "50" - max_dispatches: "1" - retry_hours: "1" - secrets: inherit + env: + TARGET_REPOSITORY: ContextualWisdomLab/RankWeave + steps: + - name: Keep review repair fail-closed until protected NVIDIA repair is available + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + open_pr_count="$( + gh api "/repos/${TARGET_REPOSITORY}/pulls?state=open&per_page=1" \ + --jq 'length' + )" + if [ "$open_pr_count" -eq 0 ]; then + echo "No pull request requires review repair." + exit 0 + fi + echo "::notice::Review repair remains fail-closed while the protected central NVIDIA NIM scheduler is pending. Existing independent review agents and the central merge scheduler remain unchanged." revalidate-pr-queue: needs: repair-review-feedback diff --git a/hourly-commercialization-loop.repaired.yml b/hourly-commercialization-loop.repaired.yml deleted file mode 100644 index d8669b1..0000000 --- a/hourly-commercialization-loop.repaired.yml +++ /dev/null @@ -1,979 +0,0 @@ -name: Hourly RankWeave Commercialization Loop - -on: - schedule: - - cron: "17 * * * *" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: rankweave-hourly-commercialization-loop - cancel-in-progress: true - -jobs: - inspect-pr-queue: - permissions: - actions: write - checks: read - contents: write - id-token: write - pull-requests: write - uses: ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@5983b41ace75040c1d81818171ca7d0f3653254e - with: - base_branch: main - max_prs: "50" - trigger_reviews: true - review_dispatch_limit: "1" - branch_update_limit: "1" - enable_auto_merge: true - merge_mode: direct_or_auto - update_branches: true - secrets: inherit - - repair-review-feedback: - needs: inspect-pr-queue - if: ${{ always() }} - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - pull-requests: read - env: - TARGET_REPOSITORY: ContextualWisdomLab/RankWeave - steps: - - name: Keep review repair fail-closed until protected NVIDIA repair is available - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - open_pr_count="$( - gh api "/repos/${TARGET_REPOSITORY}/pulls?state=open&per_page=1" \ - --jq 'length' - )" - if [ "$open_pr_count" -eq 0 ]; then - echo "No pull request requires review repair." - exit 0 - fi - echo "::notice::Review repair remains fail-closed while the protected central NVIDIA NIM scheduler is pending. Existing independent review agents and the central merge scheduler remain unchanged." - - revalidate-pr-queue: - needs: repair-review-feedback - if: ${{ always() }} - permissions: - actions: write - checks: read - contents: write - id-token: write - pull-requests: write - uses: ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@5983b41ace75040c1d81818171ca7d0f3653254e - with: - base_branch: main - max_prs: "50" - trigger_reviews: true - review_dispatch_limit: "1" - branch_update_limit: "1" - enable_auto_merge: true - merge_mode: direct_or_auto - update_branches: true - secrets: inherit - - develop-next-product-gap: - needs: [inspect-pr-queue, repair-review-feedback, revalidate-pr-queue] - if: >- - ${{ - always() && - needs.inspect-pr-queue.result == 'success' && - needs.repair-review-feedback.result == 'success' && - needs.revalidate-pr-queue.result == 'success' - }} - runs-on: ubuntu-latest - timeout-minutes: 55 - permissions: - contents: read - id-token: write - pull-requests: read - env: - TARGET_REPOSITORY: ContextualWisdomLab/RankWeave - BASE_BRANCH: main - OPENCODE_VERSION: "1.17.13" - OPENCODE_SHA256: 157afa289d1a8d9372de0ce19ac726119b937a1f6b201808d46f06e4e59bb348 - OPENCODE_MODEL_CANDIDATES: >- - nvidia/nvidia/llama-3.3-nemotron-super-49b-v1.5 - nvidia/nvidia/nemotron-3-super-120b-a12b - nvidia/deepseek-ai/deepseek-v4-pro - OPENCODE_RED_TIMEOUT_SECONDS: "300" - OPENCODE_IMPLEMENT_TIMEOUT_SECONDS: "600" - MAX_AUTONOMOUS_CHANGED_FILES: "25" - MAX_AUTONOMOUS_FILE_BYTES: "262144" - MAX_AUTONOMOUS_TOTAL_BYTES: "1048576" - steps: - - name: Determine whether product development may start - id: gate - env: - GH_TOKEN: ${{ github.token }} - NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} - run: | - set -euo pipefail - - if [ -z "${NVIDIA_API_KEY:-}" ]; then - echo "::warning::NVIDIA_NIM_API_KEY is not configured; product development remains fail-closed." - echo "eligible=false" >>"$GITHUB_OUTPUT" - exit 0 - fi - - open_pr_count="$( - gh api "/repos/${TARGET_REPOSITORY}/pulls?state=open&per_page=1" \ - --jq 'length' - )" - if [ "$open_pr_count" -ne 0 ]; then - echo "An open pull request already owns the development queue." - echo "eligible=false" >>"$GITHUB_OUTPUT" - exit 0 - fi - - echo "eligible=true" >>"$GITHUB_OUTPUT" - - - name: Check out the current base without persisted credentials - if: steps.gate.outputs.eligible == 'true' - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - with: - ref: main - fetch-depth: 1 - persist-credentials: false - - - name: Prepare trusted validation tooling - if: steps.gate.outputs.eligible == 'true' - run: | - set -euo pipefail - venv="/tmp/rankweave-automation-venv-${GITHUB_RUN_ID}" - sudo rm -rf "$venv" - python -m venv "$venv" - "$venv/bin/python" -m pip install --upgrade pip - "$venv/bin/python" -m pip install -e ".[dev]" hatchling - sudo chown -R root:root "$venv" - sudo chmod -R a-w "$venv" - sandbox_uid="$(id -u nobody)" - sandbox_gid="$(id -g nobody)" - command -v setpriv >/dev/null - echo "AUTOMATION_VENV=$venv" >>"$GITHUB_ENV" - echo "AUTOMATION_BASE_SHA=$(git rev-parse HEAD)" >>"$GITHUB_ENV" - echo "SANDBOX_UID=$sandbox_uid" >>"$GITHUB_ENV" - echo "SANDBOX_GID=$sandbox_gid" >>"$GITHUB_ENV" - { - echo "/opencode.json" - echo "/.agent-red-output.txt" - echo "/PR_MESSAGE.md" - } >>"$GITHUB_WORKSPACE/.git/info/exclude" - - name: Verify the trusted base and network-isolation primitive - if: steps.gate.outputs.eligible == 'true' - run: | - set -euo pipefail - "$AUTOMATION_VENV/bin/python" -m ruff check . - "$AUTOMATION_VENV/bin/python" -m coverage run -m pytest -q - "$AUTOMATION_VENV/bin/python" -m coverage report - rm -f .coverage - sandbox_probe=( - sudo unshare --net --pid --fork --mount-proc - setpriv - --reuid="$SANDBOX_UID" - --regid="$SANDBOX_GID" - --clear-groups - --no-new-privs - --bounding-set=-all - --inh-caps=-all - --ambient-caps=-all - ) - "${sandbox_probe[@]}" true - - name: Install the pinned OpenCode CLI - if: steps.gate.outputs.eligible == 'true' - run: | - set -euo pipefail - archive="${RUNNER_TEMP}/opencode-linux-x64.tar.gz" - install_dir="${RUNNER_TEMP}/opencode/bin" - mkdir -p "$install_dir" - curl -fsSL \ - -o "$archive" \ - "https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz" - printf '%s %s\n' "$OPENCODE_SHA256" "$archive" | sha256sum -c - - tar -xzf "$archive" -C "$RUNNER_TEMP" - install -m 0755 "${RUNNER_TEMP}/opencode" "$install_dir/opencode" - "$install_dir/opencode" --version - echo "$install_dir" >>"$GITHUB_PATH" - - - name: Configure test-first authoring permissions - if: steps.gate.outputs.eligible == 'true' - run: | - set -euo pipefail - cat >"$GITHUB_WORKSPACE/opencode.json" <<'CONFIG' - { - "$schema": "https://opencode.ai/config.json", - "enabled_providers": ["nvidia"], - "model": "nvidia/nvidia/llama-3.3-nemotron-super-49b-v1.5", - "lsp": false, - "permission": { - "read": { - "*": "allow", - ".git/**": "deny", - "opencode.json": "deny", - ".env": "deny", - ".env.*": "deny" - }, - "edit": { - "*": "deny", - "tests/**": "allow", - "docs/superpowers/specs/**": "allow" - }, - "bash": "deny", - "webfetch": "deny", - "websearch": "deny", - "external_directory": "deny", - "task": "deny", - "skill": "deny", - "question": "deny", - "lsp": "deny", - "doom_loop": "deny" - } - } - CONFIG - - - name: Author one design and failing regression test - if: steps.gate.outputs.eligible == 'true' - env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} - run: | - set -euo pipefail - - prompt="$(cat <<'PROMPT' - Work only from the trusted files on the checked-out RankWeave main branch. - Do not read GitHub issues, pull requests, external web pages, environment - variables, or paths outside the repository. - - Select exactly one highest-impact buyer-visible product gap that fits one - bounded pull request and does not require a new external research claim. - Write a concise design under docs/superpowers/specs/ and write the failing - pytest regression tests first. During this phase do not modify production - code, package metadata, README.md, AGENTS.md, CHANGELOG.md, workflows, or - any file outside tests/ and docs/superpowers/specs/. Do not write - PR_MESSAGE.md yet. - - The tests must express the buyer-visible contract, preserve RankWeave's - standard-library-only runtime, deterministic and immutable evidence, - fail-closed validation, Python 3.10+ compatibility, and modular standalone - plus naruon-import use. The workflow will execute the test suite after this - phase and requires a genuine test failure before implementation begins. - PROMPT - )" - - status=1 - for model in $OPENCODE_MODEL_CANDIDATES; do - echo "::group::opencode red phase $model" - if timeout --kill-after=30s "${OPENCODE_RED_TIMEOUT_SECONDS}s" \ - env -u GH_TOKEN -u GITHUB_TOKEN \ - -u ACTIONS_ID_TOKEN_REQUEST_TOKEN -u ACTIONS_ID_TOKEN_REQUEST_URL \ - HOME="${RUNNER_TEMP}/opencode-home-red" \ - opencode run "$prompt" --model "$model"; then - status=0 - echo "::endgroup::" - break - fi - echo "::endgroup::" - echo "::warning::Model $model failed during the red phase; discarding partial work." - git reset --hard "$AUTOMATION_BASE_SHA" - git clean -fd - done - if [ "$status" -ne 0 ]; then - echo "::error::Every NVIDIA model failed during test-first authoring." - exit 1 - fi - - - name: Verify test-only scope and observe the red state - if: steps.gate.outputs.eligible == 'true' - run: | - set -euo pipefail - rm -f opencode.json - git clean -fdX - "$AUTOMATION_VENV/bin/python" - <<'PY' - from __future__ import annotations - - import os - import stat - import subprocess - from pathlib import Path - - max_files = int(os.environ["MAX_AUTONOMOUS_CHANGED_FILES"]) - max_file_bytes = int(os.environ["MAX_AUTONOMOUS_FILE_BYTES"]) - max_total_bytes = int(os.environ["MAX_AUTONOMOUS_TOTAL_BYTES"]) - records = subprocess.check_output( - ["git", "status", "--porcelain=v1", "-z"] - ).split(b"\0") - paths = [] - total_bytes = 0 - index = 0 - while index < len(records): - record = records[index] - index += 1 - if not record: - continue - status_text = record[:2].decode("ascii") - path_text = record[3:].decode("utf-8") - if any(marker in status_text for marker in ("R", "C", "D", "U")): - raise SystemExit( - "red phase may not rename, copy, or delete files: " - f"{path_text}" - ) - paths.append(path_text) - path = Path(path_text) - info = path.lstat() - if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode): - raise SystemExit(f"red phase produced a non-regular file: {path_text}") - if info.st_size > max_file_bytes: - raise SystemExit( - f"red phase file {path_text} exceeds {max_file_bytes} bytes" - ) - data = path.read_bytes() - if b"\0" in data: - raise SystemExit(f"red phase file contains a NUL byte: {path_text}") - try: - data.decode("utf-8", errors="strict") - except UnicodeDecodeError as exc: - raise SystemExit( - f"red phase file is not strict UTF-8: {path_text}" - ) from exc - total_bytes += info.st_size - - if not paths: - raise SystemExit("red phase produced no files") - if len(paths) > max_files or total_bytes > max_total_bytes: - raise SystemExit("red phase exceeded the autonomous diff budget") - if not any( - path.startswith("tests/") and Path(path).suffix == ".py" - for path in paths - ): - raise SystemExit("red phase did not add or modify a pytest file") - allowed_prefixes = ("tests/", "docs/superpowers/specs/") - forbidden = [ - path for path in paths if not path.startswith(allowed_prefixes) - ] - if forbidden: - raise SystemExit( - f"red phase changed files outside its scope: {forbidden!r}" - ) - PY - - red_home="/tmp/rankweave-red-home-${GITHUB_RUN_ID}" - red_output="${RUNNER_TEMP}/red-test-output.txt" - sudo rm -rf "$red_home" - sudo mkdir -p "$red_home" - sudo chown "$SANDBOX_UID:$SANDBOX_GID" "$red_home" - red_sandbox=( - sudo unshare --net --pid --fork --mount-proc - setpriv - --reuid="$SANDBOX_UID" - --regid="$SANDBOX_GID" - --clear-groups - --no-new-privs - --bounding-set=-all - --inh-caps=-all - --ambient-caps=-all - ) - red_environment=( - env -i - "PATH=${AUTOMATION_VENV}/bin:/usr/bin:/bin" - "HOME=$red_home" - "WORKSPACE=$GITHUB_WORKSPACE" - "PYTHONPATH=$GITHUB_WORKSPACE/src" - PYTHONDONTWRITEBYTECODE=1 - bash - --noprofile - --norc - -c - 'cd "$WORKSPACE" && python -m pytest -q -p no:cacheprovider' - ) - set +e - "${red_sandbox[@]}" "${red_environment[@]}" >"$red_output" 2>&1 - red_status=$? - set -e - if [ "$red_status" -ne 1 ]; then - cat "${RUNNER_TEMP}/red-test-output.txt" - echo "::error::Expected pytest exit 1 from a genuine red test; got ${red_status}." - exit 1 - fi - if ! grep -q "FAILED" "${RUNNER_TEMP}/red-test-output.txt"; then - cat "${RUNNER_TEMP}/red-test-output.txt" - echo "::error::Red test output did not contain a failed test." - exit 1 - fi - tail -n 200 "${RUNNER_TEMP}/red-test-output.txt" \ - >"$GITHUB_WORKSPACE/.agent-red-output.txt" - - git config user.name "github-actions[bot]" - git config user.email \ - "41898282+github-actions[bot]@users.noreply.github.com" - git add tests docs/superpowers/specs - git -c core.hooksPath=/dev/null commit \ - -m "test(red): define the next buyer-visible product gap" - echo "AUTOMATION_RED_SHA=$(git rev-parse HEAD)" >>"$GITHUB_ENV" - - - name: Configure implementation permissions - if: steps.gate.outputs.eligible == 'true' - run: | - set -euo pipefail - cat >"$GITHUB_WORKSPACE/opencode.json" <<'CONFIG' - { - "$schema": "https://opencode.ai/config.json", - "enabled_providers": ["nvidia"], - "model": "nvidia/nvidia/llama-3.3-nemotron-super-49b-v1.5", - "lsp": false, - "permission": { - "read": { - "*": "allow", - ".git/**": "deny", - "opencode.json": "deny", - ".env": "deny", - ".env.*": "deny" - }, - "edit": { - "*": "allow", - "AGENTS.md": "deny", - ".github/**": "deny", - ".git/**": "deny", - ".agent-red-output.txt": "deny", - "opencode.json": "deny" - }, - "bash": "deny", - "webfetch": "deny", - "websearch": "deny", - "external_directory": "deny", - "task": "deny", - "skill": "deny", - "question": "deny", - "lsp": "deny", - "doom_loop": "deny" - } - } - CONFIG - - - name: Implement the bounded product increment - if: steps.gate.outputs.eligible == 'true' - env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} - run: | - set -euo pipefail - - prompt="$(cat <<'PROMPT' - Implement the single design and failing tests already present in the - RankWeave workspace. Read .agent-red-output.txt for the exact red-test - evidence. Do not inspect GitHub issues, pull requests, external web pages, - environment variables, or paths outside the repository. - - Make the smallest coherent production change that turns the red tests - green. Preserve the standard-library-only runtime, store-agnostic modular - architecture, deterministic behavior, immutable audit records, fail-closed - input contracts, Python 3.10+ compatibility, full production docstrings, - and standalone plus naruon-import use. Do not edit anything under .github/ - or any security, credential, ownership, or workflow policy file. - - Update CHANGELOG.md, README.md, relevant product or operations - documentation, package smoke contracts, and version metadata only when the - slice is release-ready. Keep all papers in APA 7th edition and do not add a - statistical or standards claim without a primary source already recorded in - the repository. Figma is not applicable because RankWeave has no UI. - - Write PR_MESSAGE.md at the repository root. Put a concise PR title on the - first line and a body after it describing buyer impact, evidence, - compatibility, and the exact validation commands. Do not commit, push, - open, approve, merge, publish, or release anything; the workflow performs - deterministic validation and packages one protected pull request. - PROMPT - )" - - status=1 - for model in $OPENCODE_MODEL_CANDIDATES; do - echo "::group::opencode implementation phase $model" - if timeout --kill-after=30s "${OPENCODE_IMPLEMENT_TIMEOUT_SECONDS}s" \ - env -u GH_TOKEN -u GITHUB_TOKEN \ - -u ACTIONS_ID_TOKEN_REQUEST_TOKEN -u ACTIONS_ID_TOKEN_REQUEST_URL \ - HOME="${RUNNER_TEMP}/opencode-home-implementation" \ - opencode run "$prompt" --model "$model"; then - status=0 - echo "::endgroup::" - break - fi - echo "::endgroup::" - echo "::warning::Model $model failed during implementation; restoring the verified red state." - git reset --hard "$AUTOMATION_RED_SHA" - git clean -fd - cp "${RUNNER_TEMP}/red-test-output.txt" \ - "$GITHUB_WORKSPACE/.agent-red-output.txt" - done - if [ "$status" -ne 0 ]; then - echo "::error::Every NVIDIA model failed during implementation." - exit 1 - fi - - - name: Enforce the autonomous diff boundary - if: steps.gate.outputs.eligible == 'true' - run: | - set -euo pipefail - pr_message_backup="${RUNNER_TEMP}/agent-pr-message.md" - if [ -f PR_MESSAGE.md ]; then - cp PR_MESSAGE.md "$pr_message_backup" - fi - rm -f opencode.json .agent-red-output.txt - git clean -fdX - if [ -f "$pr_message_backup" ]; then - cp "$pr_message_backup" PR_MESSAGE.md - fi - "$AUTOMATION_VENV/bin/python" - <<'PY' - from __future__ import annotations - - import os - import stat - import subprocess - import tomllib - from pathlib import Path - - base_sha = os.environ["AUTOMATION_BASE_SHA"] - max_files = int(os.environ["MAX_AUTONOMOUS_CHANGED_FILES"]) - max_file_bytes = int(os.environ["MAX_AUTONOMOUS_FILE_BYTES"]) - max_total_bytes = int(os.environ["MAX_AUTONOMOUS_TOTAL_BYTES"]) - - raw = subprocess.check_output( - ["git", "diff", "--name-status", "-z", base_sha] - ).split(b"\0") - changed: dict[str, str] = {} - index = 0 - while index < len(raw): - status_bytes = raw[index] - index += 1 - if not status_bytes: - continue - status_text = status_bytes.decode("ascii") - if status_text.startswith(("R", "C")): - old_path = raw[index].decode("utf-8") - new_path = raw[index + 1].decode("utf-8") - raise SystemExit( - "autonomous changes may not rename or copy files: " - f"{old_path} -> {new_path}" - ) - path_text = raw[index].decode("utf-8") - index += 1 - status_code = status_text[0] - if status_code not in {"A", "M"}: - raise SystemExit( - f"autonomous status {status_text} is forbidden: {path_text}" - ) - changed[path_text] = status_code - - for path_bytes in subprocess.check_output( - ["git", "ls-files", "--others", "--exclude-standard", "-z"] - ).split(b"\0"): - if path_bytes: - changed[path_bytes.decode("utf-8")] = "?" - - if not changed: - raise SystemExit("implementation produced no changes") - if len(changed) > max_files: - raise SystemExit( - f"autonomous change count {len(changed)} exceeds {max_files}" - ) - - forbidden_exact = { - ".gitmodules", - "AGENTS.md", - "CODEOWNERS", - "SECURITY.md", - } - forbidden_prefixes = (".github/", ".git/") - allowed_exact = { - "CHANGELOG.md", - "README.md", - "pyproject.toml", - "PR_MESSAGE.md", - } - allowed_prefixes = ("src/rankweave/", "tests/", "docs/") - allowed_suffixes = { - ".json", - ".md", - ".py", - ".toml", - ".txt", - ".yaml", - ".yml", - } - - total_bytes = 0 - production_changed = False - for path_text in sorted(changed): - path = Path(path_text) - if path.is_absolute() or ".." in path.parts: - raise SystemExit(f"invalid changed path: {path_text}") - if ( - path_text in forbidden_exact - or path_text.startswith(forbidden_prefixes) - or path.name.startswith(".env") - ): - raise SystemExit(f"protected path changed: {path_text}") - if ( - path_text not in allowed_exact - and not path_text.startswith(allowed_prefixes) - ): - raise SystemExit(f"path is outside autonomous scope: {path_text}") - if path_text != "PR_MESSAGE.md" and path.suffix not in allowed_suffixes: - raise SystemExit(f"non-text or unsupported path changed: {path_text}") - if path_text.startswith("src/rankweave/") and path.suffix == ".py": - production_changed = True - info = path.lstat() - if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode): - raise SystemExit(f"non-regular file changed: {path_text}") - if info.st_size > max_file_bytes: - raise SystemExit( - f"{path_text} size {info.st_size} exceeds {max_file_bytes}" - ) - data = path.read_bytes() - if b"\0" in data: - raise SystemExit(f"NUL byte found in {path_text}") - try: - data.decode("utf-8", errors="strict") - except UnicodeDecodeError as exc: - raise SystemExit( - f"changed file is not strict UTF-8: {path_text}" - ) from exc - total_bytes += info.st_size - - if not production_changed: - raise SystemExit( - "buyer-visible increment must change a production Python module" - ) - if not any(path.startswith("tests/") for path in changed): - raise SystemExit("autonomous increment must include regression tests") - if not any( - path.startswith("docs/superpowers/specs/") for path in changed - ): - raise SystemExit("autonomous increment must include a design specification") - if "CHANGELOG.md" not in changed: - raise SystemExit("autonomous increment must update CHANGELOG.md") - if total_bytes > max_total_bytes: - raise SystemExit( - f"autonomous byte total {total_bytes} exceeds {max_total_bytes}" - ) - - metadata = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8")) - if metadata["build-system"]["build-backend"] != "hatchling.build": - raise SystemExit("build backend may not change") - if metadata["project"].get("dependencies") != []: - raise SystemExit("RankWeave runtime dependencies must remain empty") - if metadata["tool"]["coverage"]["run"].get("branch") is not True: - raise SystemExit("branch coverage may not be disabled") - if metadata["tool"]["coverage"]["run"].get("source") != ["rankweave"]: - raise SystemExit("coverage source may not change") - if metadata["tool"]["coverage"]["report"].get("fail_under") != 100: - raise SystemExit("coverage fail-under must remain 100") - required_doc_rules = {f"D10{index}" for index in range(8)} - selected_rules = set(metadata["tool"]["ruff"]["lint"].get("select", [])) - if not required_doc_rules <= selected_rules: - raise SystemExit("production docstring rules may not be weakened") - PY - - - name: Record the pre-validation workspace manifest - if: steps.gate.outputs.eligible == 'true' - run: | - set -euo pipefail - "$AUTOMATION_VENV/bin/python" - <<'PY' \ - >"${RUNNER_TEMP}/workspace-manifest-before.json" - from __future__ import annotations - - import hashlib - import json - import os - import subprocess - from pathlib import Path - - base_sha = os.environ["AUTOMATION_BASE_SHA"] - paths = { - value.decode("utf-8") - for value in subprocess.check_output( - ["git", "diff", "--name-only", "-z", base_sha] - ).split(b"\0") - if value - } - paths.update( - value.decode("utf-8") - for value in subprocess.check_output( - ["git", "ls-files", "--others", "--exclude-standard", "-z"] - ).split(b"\0") - if value - ) - manifest = { - path_text: hashlib.sha256(Path(path_text).read_bytes()).hexdigest() - for path_text in sorted(paths) - } - print(json.dumps(manifest, sort_keys=True)) - PY - - - name: Validate untrusted changes without network or inherited environment - if: steps.gate.outputs.eligible == 'true' - run: | - set -euo pipefail - validation_dist="/tmp/rankweave-validation-dist-${GITHUB_RUN_ID}" - validation_smoke="/tmp/rankweave-validation-smoke-${GITHUB_RUN_ID}" - validation_home="/tmp/rankweave-validation-home-${GITHUB_RUN_ID}" - validation_coverage="/tmp/rankweave-validation-${GITHUB_RUN_ID}.coverage" - ruff_cache="/tmp/rankweave-ruff-cache-${GITHUB_RUN_ID}" - validation_script="/tmp/rankweave-validate-${GITHUB_RUN_ID}.sh" - validation_paths=( - "$validation_dist" - "$validation_smoke" - "$validation_home" - "$validation_coverage" - "$ruff_cache" - "$validation_script" - ) - sudo rm -rf "${validation_paths[@]}" - sudo mkdir -p "$validation_dist" "$validation_home" "$ruff_cache" - sudo chown -R "$SANDBOX_UID:$SANDBOX_GID" "$validation_dist" "$validation_home" "$ruff_cache" - cat >"$validation_script" <<'VALIDATE' - set -euo pipefail - cd "$WORKSPACE" - python -m ruff check . - python -m coverage run -m pytest -q -p no:cacheprovider - python -m coverage report - python -m pip wheel . --no-deps --no-build-isolation --wheel-dir "$DIST" - python -m venv "$SMOKE" - "$SMOKE/bin/python" -m pip install --no-index --find-links "$DIST" rankweave - "$SMOKE/bin/python" -m pip check - cd "$HOME" - "$SMOKE/bin/python" -c 'from importlib.metadata import version; import rankweave; assert version("rankweave") == rankweave.__version__' - VALIDATE - sudo chown root:root "$validation_script" - sudo chmod 0555 "$validation_script" - validation_sandbox=( - sudo unshare --net --pid --fork --mount-proc - setpriv - --reuid="$SANDBOX_UID" - --regid="$SANDBOX_GID" - --clear-groups - --no-new-privs - --bounding-set=-all - --inh-caps=-all - --ambient-caps=-all - ) - validation_environment=( - env -i - "PATH=${AUTOMATION_VENV}/bin:/usr/bin:/bin" - "HOME=$validation_home" - "WORKSPACE=$GITHUB_WORKSPACE" - "PYTHONPATH=$GITHUB_WORKSPACE/src" - "DIST=$validation_dist" - "SMOKE=$validation_smoke" - "COVERAGE_FILE=$validation_coverage" - "RUFF_CACHE_DIR=$ruff_cache" - PYTHONDONTWRITEBYTECODE=1 - PIP_DISABLE_PIP_VERSION_CHECK=1 - PIP_NO_INDEX=1 - bash - --noprofile - --norc - "$validation_script" - ) - "${validation_sandbox[@]}" "${validation_environment[@]}" - - name: Verify validation did not mutate the proposal - if: steps.gate.outputs.eligible == 'true' - run: | - set -euo pipefail - "$AUTOMATION_VENV/bin/python" - <<'PY' - from __future__ import annotations - - import hashlib - import json - import os - import subprocess - from pathlib import Path - - base_sha = os.environ["AUTOMATION_BASE_SHA"] - before_path = Path(os.environ["RUNNER_TEMP"]) / "workspace-manifest-before.json" - before = json.loads(before_path.read_text(encoding="utf-8")) - paths = { - value.decode("utf-8") - for value in subprocess.check_output( - ["git", "diff", "--name-only", "-z", base_sha] - ).split(b"\0") - if value - } - paths.update( - value.decode("utf-8") - for value in subprocess.check_output( - ["git", "ls-files", "--others", "--exclude-standard", "-z"] - ).split(b"\0") - if value - ) - after = { - path_text: hashlib.sha256(Path(path_text).read_bytes()).hexdigest() - for path_text in sorted(paths) - } - if before != after: - raise SystemExit("validation mutated the proposed working tree") - PY - - - name: Recheck queue and base before token exchange - id: mutation_preflight - if: steps.gate.outputs.eligible == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - open_pr_count="$( - gh api "/repos/${TARGET_REPOSITORY}/pulls?state=open&per_page=1" \ - --jq 'length' - )" - if [ "$open_pr_count" -ne 0 ]; then - echo "Another pull request acquired the queue before token exchange." - echo "eligible=false" >>"$GITHUB_OUTPUT" - exit 0 - fi - - current_base_sha="$( - gh api "/repos/${TARGET_REPOSITORY}/commits/${BASE_BRANCH}" \ - --jq '.sha' - )" - if [ "$current_base_sha" != "$AUTOMATION_BASE_SHA" ]; then - echo "The base branch moved before token exchange." - echo "eligible=false" >>"$GITHUB_OUTPUT" - exit 0 - fi - - echo "eligible=true" >>"$GITHUB_OUTPUT" - - - name: Exchange an OpenCode app token for generated PR events - id: generated_pr_token - if: steps.mutation_preflight.outputs.eligible == 'true' - env: - OIDC_AUDIENCE: opencode-github-action - OPENCODE_API_BASE_URL: https://api.opencode.ai - run: | - set -euo pipefail - if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || \ - [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then - echo "::error::OIDC request environment is unavailable." - exit 1 - fi - request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" - separator="&" - case "$request_url" in - *\?*) ;; - *) separator="?" ;; - esac - oidc_response="$( - curl -fsS \ - -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ - "${request_url}${separator}audience=${OIDC_AUDIENCE}" - )" - oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" - if [ -z "$oidc_token" ]; then - echo "::error::OIDC token response was empty." - exit 1 - fi - token_response="$( - curl -fsS \ - -X POST \ - -H "Authorization: Bearer ${oidc_token}" \ - "${OPENCODE_API_BASE_URL}/exchange_github_app_token" - )" - app_token="$(jq -r '.token // empty' <<<"$token_response")" - if [ -z "$app_token" ]; then - echo "::error::OpenCode GitHub App token response was empty." - exit 1 - fi - echo "::add-mask::$app_token" - echo "token=$app_token" >>"$GITHUB_OUTPUT" - - - name: Open exactly one focused pull request - if: steps.mutation_preflight.outputs.eligible == 'true' - env: - GH_TOKEN: ${{ steps.generated_pr_token.outputs.token }} - run: | - set -euo pipefail - cd "$GITHUB_WORKSPACE" - if [ -z "${GH_TOKEN:-}" ]; then - echo "::error::Generated PR app token is unavailable." - exit 1 - fi - - open_pr_count="$( - gh api "/repos/${TARGET_REPOSITORY}/pulls?state=open&per_page=1" \ - --jq 'length' - )" - if [ "$open_pr_count" -ne 0 ]; then - echo "Another pull request acquired the queue; discarding this proposal." - exit 0 - fi - - current_base_sha="$( - gh api "/repos/${TARGET_REPOSITORY}/commits/${BASE_BRANCH}" \ - --jq '.sha' - )" - if [ "$current_base_sha" != "$AUTOMATION_BASE_SHA" ]; then - echo "The base branch moved during authoring; discarding this stale proposal." - exit 0 - fi - - title="RankWeave autonomous commercialization increment" - body_file="${RUNNER_TEMP}/pr-body.md" - if [ -f PR_MESSAGE.md ]; then - /usr/bin/python3 -I -S - <<'PY' - from __future__ import annotations - - import os - from pathlib import Path - - source = Path("PR_MESSAGE.md").read_text(encoding="utf-8") - lines = source.splitlines() - candidate = lines[0].lstrip("#").strip() if lines else "" - if ( - 10 <= len(candidate) <= 120 - and not candidate.startswith("-") - and all(character.isprintable() for character in candidate) - ): - title = candidate - else: - title = "RankWeave autonomous commercialization increment" - body = "\n".join(lines[1:]).strip() - if not body: - body = "Autonomous NVIDIA NIM increment; see the diff and CHANGELOG.md." - if len(body.encode("utf-8")) > 20_000: - raise SystemExit("PR body exceeds 20,000 UTF-8 bytes") - Path(os.environ["RUNNER_TEMP"], "pr-title.txt").write_text( - title, - encoding="utf-8", - ) - Path(os.environ["RUNNER_TEMP"], "pr-body.md").write_text( - body + "\n", - encoding="utf-8", - ) - PY - title="$(cat "${RUNNER_TEMP}/pr-title.txt")" - rm -f PR_MESSAGE.md - else - echo "Autonomous NVIDIA NIM increment; see the diff and CHANGELOG.md." \ - >"$body_file" - fi - - git reset --soft "$AUTOMATION_BASE_SHA" - branch="nim-agent/product-dev-${GITHUB_RUN_ID}" - git config user.name "github-actions[bot]" - git config user.email \ - "41898282+github-actions[bot]@users.noreply.github.com" - git checkout -b "$branch" - git add -A - git -c core.hooksPath=/dev/null commit -m "$title" - git -c core.hooksPath=/dev/null push \ - "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ - "HEAD:refs/heads/${branch}" - gh pr create \ - --repo "$GITHUB_REPOSITORY" \ - --base "$BASE_BRANCH" \ - --head "$branch" \ - --title "$title" \ - --body-file "$body_file" From 0d9ec794d140c1e212421ca3f3cfe4a7f4be8fb1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 11:34:27 +0900 Subject: [PATCH 10/16] test(ci): pin hourly secret boundaries --- tests/test_hourly_secret_boundaries.py | 60 ++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_hourly_secret_boundaries.py diff --git a/tests/test_hourly_secret_boundaries.py b/tests/test_hourly_secret_boundaries.py new file mode 100644 index 0000000..09b7e0b --- /dev/null +++ b/tests/test_hourly_secret_boundaries.py @@ -0,0 +1,60 @@ +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +WORKFLOW_PATH = PROJECT_ROOT / ".github/workflows/hourly-commercialization-loop.yml" + + +def _workflow_text() -> str: + return WORKFLOW_PATH.read_text(encoding="utf-8") + + +def _job_section(workflow: str, job_name: str, next_job_name: str) -> str: + start = workflow.index(f" {job_name}:\n") + end = workflow.index(f" {next_job_name}:\n", start) + return workflow[start:end] + + +def _step_section(workflow: str, step_name: str, next_step_name: str) -> str: + start = workflow.index(f" - name: {step_name}\n") + end = workflow.index(f" - name: {next_step_name}\n", start) + return workflow[start:end] + + +def test_merge_governance_does_not_inherit_all_repository_secrets(): + workflow = _workflow_text() + inspect = _job_section(workflow, "inspect-pr-queue", "repair-review-feedback") + revalidate = _job_section( + workflow, + "revalidate-pr-queue", + "develop-next-product-gap", + ) + + # The pinned merge scheduler has same-repository GITHUB_TOKEN authority and + # an OIDC app-token path. RankWeave must not forward every repository secret + # merely to call that reusable governance workflow. + assert "secrets: inherit" not in inspect + assert "secrets: inherit" not in revalidate + + +def test_nvidia_secret_materialization_follows_the_deterministic_queue_gate(): + workflow = _workflow_text() + gate = _step_section( + workflow, + "Determine whether product development may start", + "Check out the current base without persisted credentials", + ) + red_authoring = _step_section( + workflow, + "Author one design and failing regression test", + "Verify test-only scope and observe the red state", + ) + + # An open PR is a deterministic stop and must be decided before any model + # credential is materialized. NVIDIA credentials belong only to the actual + # model-backed authoring step after that decision. + assert "/pulls?state=open&per_page=1" in gate + assert "NVIDIA_NIM_API_KEY" not in gate + assert "NVIDIA_API_KEY" not in gate + assert "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" in red_authoring + assert 'if [ -z "${NVIDIA_API_KEY:-}" ]; then' in red_authoring From 05f33e33ffc5cd39ce001c5992aaceb3a5f18ddd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 11:37:02 +0900 Subject: [PATCH 11/16] test(ci): format hourly secret contract --- tests/test_hourly_secret_boundaries.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_hourly_secret_boundaries.py b/tests/test_hourly_secret_boundaries.py index 09b7e0b..5e2497f 100644 --- a/tests/test_hourly_secret_boundaries.py +++ b/tests/test_hourly_secret_boundaries.py @@ -1,6 +1,5 @@ from pathlib import Path - PROJECT_ROOT = Path(__file__).resolve().parents[1] WORKFLOW_PATH = PROJECT_ROOT / ".github/workflows/hourly-commercialization-loop.yml" From 3e3cb6a75089bd08b98ae6f4474da4034baefe5a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 15:11:50 +0900 Subject: [PATCH 12/16] fix(ci): enforce secretless deterministic gate --- .../workflows/hourly-commercialization-loop.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/hourly-commercialization-loop.yml b/.github/workflows/hourly-commercialization-loop.yml index d8669b1..c887886 100644 --- a/.github/workflows/hourly-commercialization-loop.yml +++ b/.github/workflows/hourly-commercialization-loop.yml @@ -30,7 +30,6 @@ jobs: enable_auto_merge: true merge_mode: direct_or_auto update_branches: true - secrets: inherit repair-review-feedback: needs: inspect-pr-queue @@ -77,7 +76,6 @@ jobs: enable_auto_merge: true merge_mode: direct_or_auto update_branches: true - secrets: inherit develop-next-product-gap: needs: [inspect-pr-queue, repair-review-feedback, revalidate-pr-queue] @@ -113,16 +111,9 @@ jobs: id: gate env: GH_TOKEN: ${{ github.token }} - NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} run: | set -euo pipefail - if [ -z "${NVIDIA_API_KEY:-}" ]; then - echo "::warning::NVIDIA_NIM_API_KEY is not configured; product development remains fail-closed." - echo "eligible=false" >>"$GITHUB_OUTPUT" - exit 0 - fi - open_pr_count="$( gh api "/repos/${TARGET_REPOSITORY}/pulls?state=open&per_page=1" \ --jq 'length' @@ -245,6 +236,11 @@ jobs: run: | set -euo pipefail + if [ -z "${NVIDIA_API_KEY:-}" ]; then + echo "::error::NVIDIA_NIM_API_KEY is not configured; product development remains fail-closed." + exit 1 + fi + prompt="$(cat <<'PROMPT' Work only from the trusted files on the checked-out RankWeave main branch. Do not read GitHub issues, pull requests, external web pages, environment From d6600113abeed42a7a782053f4837b1bedd9701b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 15:12:50 +0900 Subject: [PATCH 13/16] test(ci): align secret boundary expectations --- tests/test_hourly_commercialization_workflow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_hourly_commercialization_workflow.py b/tests/test_hourly_commercialization_workflow.py index 8971330..b6869b3 100644 --- a/tests/test_hourly_commercialization_workflow.py +++ b/tests/test_hourly_commercialization_workflow.py @@ -32,7 +32,7 @@ def test_commercialization_loop_uses_reachable_merge_governance(): ) assert workflow.count(merge_reference) == 2 assert "pr-review-fix-scheduler.yml@" not in workflow - assert workflow.count("secrets: inherit") == 2 + assert "secrets: inherit" not in workflow def test_review_repair_bridge_is_local_read_only_and_provider_neutral(): @@ -85,7 +85,7 @@ def test_nvidia_secret_is_step_scoped_and_agent_has_no_github_credential(): job_environment = develop.split(" steps:\n", maxsplit=1)[0] assert "NVIDIA_API_KEY" not in job_environment - assert workflow.count("NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}") == 3 + assert workflow.count("NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}") == 2 assert "persist-credentials: false" in workflow assert workflow.count("env -u GH_TOKEN -u GITHUB_TOKEN") == 2 assert workflow.count("-u ACTIONS_ID_TOKEN_REQUEST_TOKEN") == 2 From ec6a26616a752ab6db438755485533113e66e5d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 14:57:13 +0900 Subject: [PATCH 14/16] chore(ci): retrigger required review after stale change-requests The four opencode-agent CHANGES_REQUESTED reviews on this PR (2026-08-07 through 2026-08-09) predate this head and were not dismissed by the org ruleset's dismiss_stale_reviews_on_push, which was added to the ruleset later. Current head verification already confirms both fixes from issue #37 are present: the eligibility gate checks the deterministic open-PR queue before any NVIDIA credential check, and no reusable governance job uses secrets: inherit. This empty commit exists solely to trigger a fresh required-review cycle on unchanged, already-passing content. Co-Authored-By: Claude Sonnet 5 From f2bbc046a51b78666ba9353bb6b2cc4694201ed8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:14:59 +0900 Subject: [PATCH 15/16] docs: fix orphaned trust-zones heading The reachability-incident section was inserted between the "Product-development trust zones" heading and its own intro sentence, leaving that heading empty and nesting the trust-zone subsections under the wrong heading. Move the incident section back above, immediately after the Sequence narrative it elaborates on. Co-Authored-By: Claude Sonnet 5 --- docs/operations/hourly-commercialization-loop.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/operations/hourly-commercialization-loop.md b/docs/operations/hourly-commercialization-loop.md index 24a0f30..e042705 100644 --- a/docs/operations/hourly-commercialization-loop.md +++ b/docs/operations/hourly-commercialization-loop.md @@ -40,8 +40,6 @@ Models, a mutable branch, or an unmerged central change. Updating the central merge policy or re-enabling review repair requires an explicit reviewed reachable SHA change in RankWeave. -## Product-development trust zones - ## Reusable-workflow reachability incident GitHub Actions run `31124811165` and its immediate predecessors failed before @@ -57,6 +55,8 @@ boundary, RankWeave can replace the bridge with a new immutable reachable SHA. This preserves the standalone repository, the central MSA control plane, and the existing independent-review credential system. +## Product-development trust zones + The local development job is separated into four trust zones. ### 1. Trusted base verification From 4c2cfd45faeadaa9e5efb3a5cace0ee1ec053158 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 18:01:06 +0900 Subject: [PATCH 16/16] docs(workflow): remove stale repair claims --- .../hourly-commercialization-loop.yml | 9 --------- CHANGELOG.md | 20 ++++--------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/.github/workflows/hourly-commercialization-loop.yml b/.github/workflows/hourly-commercialization-loop.yml index bff2425..d55a998 100644 --- a/.github/workflows/hourly-commercialization-loop.yml +++ b/.github/workflows/hourly-commercialization-loop.yml @@ -57,15 +57,6 @@ jobs: fi echo "::notice::Review repair remains fail-closed while the protected central NVIDIA NIM scheduler is pending. Existing independent review agents and the central merge scheduler remain unchanged." - # Review-feedback repair is dispatched by the central, always-current - # rankweave-hourly-review-repair.yml caller in ContextualWisdomLab/.github - # (uses: ./.github/workflows/pr-review-fix-scheduler.yml, a same-repository - # reference). A local repair-review-feedback job here previously called - # that reusable workflow cross-repository at a pinned commit SHA; that - # shape can never satisfy pr-review-fix-scheduler.yml's same-repository - # trusted-source check (github.repository == ContextualWisdomLab/.github), - # so every run failed before any job was scheduled ("workflow file issue", - # zero jobs created) for as long as that hardening has been in place. revalidate-pr-queue: needs: inspect-pr-queue if: ${{ always() }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a1091..60ce314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,28 +15,16 @@ All notable changes to rankweave are documented here. The format follows [Keep a NVIDIA NIM product-development stage while preventing a single unavailable repair engine from disabling the entire loop. +- Restricted both autonomous OpenCode phases to explicit repository read paths + and removed agent-authored pull-request metadata, preventing workspace-external + reads or generated text from becoming a pull-request title or body. + ### Added - Classic reciprocal-rank fusion results now expose the exact per-channel Cormack contribution beside each owned input rank, so consumers do not need to duplicate the fusion arithmetic. -### Fixed -- Restricted both autonomous OpenCode phases to explicit repository read paths - and removed agent-authored pull-request metadata, preventing workspace-external - reads or generated text from becoming a pull-request title or body. -- Removed the `repair-review-feedback` job from - `hourly-commercialization-loop.yml`: it called - `ContextualWisdomLab/.github`'s `pr-review-fix-scheduler.yml` - cross-repository at a pinned commit SHA, a shape that reusable workflow's - same-repository trusted-source hardening can never satisfy - (`github.repository == ContextualWisdomLab/.github`), so every hourly run - failed before any job was scheduled for as long as that hardening has been - in place. Review-feedback repair is now dispatched by a central, - always-current `rankweave-hourly-review-repair.yml` caller added to - `ContextualWisdomLab/.github`, matching the pattern already used by every - other product repository in the organization. - ### Changed - Bumped the pinned `uv` version from `0.11.29` to `0.12.1` in `pyproject.toml` and every `astral-sh/setup-uv` workflow step (`ci.yml`, `create-release.yml`,