diff --git a/.github/workflows/hourly-nvidia-nim-review-repair.yml b/.github/workflows/hourly-nvidia-nim-review-repair.yml index f63b1ce5a..562e6cf1b 100644 --- a/.github/workflows/hourly-nvidia-nim-review-repair.yml +++ b/.github/workflows/hourly-nvidia-nim-review-repair.yml @@ -10,6 +10,7 @@ on: - .github/workflows/contextual-orchestrator-hourly-review-repair.yml - .github/workflows/clearfolio-hourly-review-repair.yml - .github/workflows/disksage-hourly-review-repair.yml + - .github/workflows/lineageweave-hourly-review-repair.yml - .github/workflows/inkspan-hourly-review-repair.yml - .github/workflows/fast-mlsirm-hourly-review-repair.yml - .github/workflows/github-hourly-review-repair.yml @@ -24,6 +25,7 @@ on: - scripts/ci/pr_review_autofix_context.py - tests/test_bandscope_hourly_review_caller.py - tests/test_disksage_hourly_review_caller.py + - tests/test_lineageweave_hourly_review_caller.py - tests/test_inkspan_hourly_review_caller.py - tests/test_fast_mlsirm_hourly_review_caller.py - tests/test_github_hourly_conflict_repair.py @@ -52,6 +54,7 @@ on: - docs/doctoring/clearfolio-hourly-review-caller.md - docs/doctoring/conflict-control-evidence-isolation.md - docs/doctoring/disksage-hourly-review-caller.md + - docs/doctoring/lineageweave-hourly-review-caller.md - docs/doctoring/inkspan-hourly-review-caller.md - docs/doctoring/fast-mlsirm-hourly-review-caller.md - docs/doctoring/github-hourly-conflict-repair.md @@ -72,6 +75,7 @@ on: - .github/workflows/contextual-orchestrator-hourly-review-repair.yml - .github/workflows/clearfolio-hourly-review-repair.yml - .github/workflows/disksage-hourly-review-repair.yml + - .github/workflows/lineageweave-hourly-review-repair.yml - .github/workflows/inkspan-hourly-review-repair.yml - .github/workflows/fast-mlsirm-hourly-review-repair.yml - .github/workflows/github-hourly-review-repair.yml @@ -86,6 +90,7 @@ on: - scripts/ci/pr_review_autofix_context.py - tests/test_bandscope_hourly_review_caller.py - tests/test_disksage_hourly_review_caller.py + - tests/test_lineageweave_hourly_review_caller.py - tests/test_inkspan_hourly_review_caller.py - tests/test_fast_mlsirm_hourly_review_caller.py - tests/test_github_hourly_conflict_repair.py @@ -114,6 +119,7 @@ on: - docs/doctoring/clearfolio-hourly-review-caller.md - docs/doctoring/conflict-control-evidence-isolation.md - docs/doctoring/disksage-hourly-review-caller.md + - docs/doctoring/lineageweave-hourly-review-caller.md - docs/doctoring/inkspan-hourly-review-caller.md - docs/doctoring/fast-mlsirm-hourly-review-caller.md - docs/doctoring/github-hourly-conflict-repair.md @@ -174,6 +180,7 @@ jobs: tests/test_pr_review_conflict_scope.py \ tests/test_bandscope_hourly_review_caller.py \ tests/test_disksage_hourly_review_caller.py \ + tests/test_lineageweave_hourly_review_caller.py \ tests/test_inkspan_hourly_review_caller.py \ tests/test_fast_mlsirm_hourly_review_caller.py \ tests/test_github_hourly_conflict_repair.py \ diff --git a/.github/workflows/lineageweave-hourly-review-repair.yml b/.github/workflows/lineageweave-hourly-review-repair.yml new file mode 100644 index 000000000..fe246dd48 --- /dev/null +++ b/.github/workflows/lineageweave-hourly-review-repair.yml @@ -0,0 +1,33 @@ +name: LineageWeave Hourly Review Repair + +on: + schedule: + # Minute 4 is LineageWeave's reserved heartbeat; minute 56 belongs to Inkspan. + - cron: "4 * * * *" + +concurrency: + group: lineageweave-hourly-review-repair + # Preserve a legitimate long-running root-cause analysis across heartbeats. + cancel-in-progress: false + +permissions: + contents: read + +jobs: + dispatch-review-repair: + permissions: + contents: read + id-token: write + uses: ./.github/workflows/pr-review-fix-scheduler.yml + with: + target_repository: ContextualWisdomLab/LineageWeave + # LineageWeave uses stacked PRs; repair reviewed heads on every base. + base_branch: "*" + max_prs: "50" + max_dispatches: "1" + # Strix, coverage-source-tree, and the full backend suite can exceed one + # hour on a single head; do not redispatch for the same head sooner. + retry_hours: "2" + secrets: + PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} + OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/pr-review-autofix.yml b/.github/workflows/pr-review-autofix.yml index acafaea91..b187d8267 100644 --- a/.github/workflows/pr-review-autofix.yml +++ b/.github/workflows/pr-review-autofix.yml @@ -222,6 +222,15 @@ jobs: install -m 0755 "${RUNNER_TEMP}/opencode" "${install_dir}/opencode" echo "$install_dir" >>"$GITHUB_PATH" + - name: Checkout pinned contextual-orchestrator gateway + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ContextualWisdomLab/contextual-orchestrator + ref: 838b3de160c341a6f36bf588ae9fcc09989c040c + fetch-depth: 1 + persist-credentials: false + path: trusted-contextual-orchestrator + - name: Collect review feedback context env: GH_TOKEN: ${{ steps.target_app_token.outputs.token || github.token }} @@ -279,9 +288,9 @@ jobs: EOF jq -n --arg workspace "$TARGET_WORKSPACE" '{ "$schema": "https://opencode.ai/config.json", - "model": "nvidia-nim/mistralai/mistral-small-4-119b-2603", - "small_model": "nvidia-nim/nvidia/nemotron-3-nano-30b-a3b", - "enabled_providers": ["nvidia-nim"], + "model": "contextual-orchestrator/contextual-orchestrator", + "small_model": "contextual-orchestrator/contextual-orchestrator", + "enabled_providers": ["contextual-orchestrator"], "permission": { "edit": { "*": "allow", @@ -306,7 +315,7 @@ jobs: "ci-autofix": { "description": "Conservative CI pull request review autofix agent", "mode": "primary", - "model": "nvidia-nim/mistralai/mistral-small-4-119b-2603", + "model": "contextual-orchestrator/contextual-orchestrator", "reasoningEffort": "high", "prompt": "{file:./autofix-prompt.md}", "steps": 12, @@ -333,32 +342,23 @@ jobs: } }, "provider": { - "nvidia-nim": { + "contextual-orchestrator": { "npm": "@ai-sdk/openai-compatible", - "name": "NVIDIA NIM", + "name": "Contextual Orchestrator", "options": { - "baseURL": "https://integrate.api.nvidia.com/v1", - "apiKey": "{env:NVIDIA_API_KEY}" + "baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}", + "apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}" }, "models": { - "mistralai/mistral-small-4-119b-2603": { - "name": "Mistral Small 4 119B 2603", + "contextual-orchestrator": { + "name": "Contextual Orchestrator (auto-discovered)", "tool_call": true, "reasoning": true, "options": { "reasoningEffort": "high" }, "limit": { - "context": 128000, - "output": 4096 - } - }, - "nvidia/nemotron-3-nano-30b-a3b": { - "name": "Nemotron 3 Nano 30B A3B", - "tool_call": true, - "reasoning": true, - "limit": { - "context": 128000, + "context": 200000, "output": 32768 } } @@ -370,18 +370,79 @@ jobs: - name: Run OpenCode review autofix if: env.RESOLVE_CONFLICT != 'true' env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} - MODEL: nvidia-nim/mistralai/mistral-small-4-119b-2603 + BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} + NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + MODEL: contextual-orchestrator/contextual-orchestrator SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" OPENCODE_AUTOFIX_WORKDIR: ${{ runner.temp }}/opencode-autofix-project run: | set -euo pipefail - if [ -z "${NVIDIA_API_KEY:-}" ]; then - echo "::error::NVIDIA_NIM_API_KEY is required for scheduled OpenCode autofix." + contextual_gateway_pid="" + cleanup_contextual_gateway() { + if [ -n "$contextual_gateway_pid" ]; then + kill "$contextual_gateway_pid" 2>/dev/null || true + for _ in 1 2 3 4 5; do + if ! kill -0 "$contextual_gateway_pid" 2>/dev/null; then + wait "$contextual_gateway_pid" 2>/dev/null || true + contextual_gateway_pid="" + return + fi + sleep 1 + done + kill -KILL "$contextual_gateway_pid" 2>/dev/null || true + wait "$contextual_gateway_pid" 2>/dev/null || true + contextual_gateway_pid="" + fi + } + trap cleanup_contextual_gateway EXIT + gateway_license="$GITHUB_WORKSPACE/trusted-contextual-orchestrator/LICENSE" + if [ "$(sed -n '1p' "$gateway_license")" != "MIT License" ] || + [ "$(git hash-object --no-filters "$gateway_license")" != "591bbf197b355e60604618c8a8a50bc5a839b204" ]; then + echo "::error::Pinned contextual-orchestrator license identity did not match the reviewed MIT source." + exit 1 + fi + contextual_gateway_token="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')" + contextual_gateway_base_url="http://127.0.0.1:18080/v1" + echo "::add-mask::$contextual_gateway_token" + env -i \ + PATH="$PATH" HOME="$HOME" \ + PYTHONPATH="$GITHUB_WORKSPACE/trusted-contextual-orchestrator" \ + BYTEZ_API_KEY="${BYTEZ_API_KEY:-}" \ + NVIDIA_NIM_API_KEY="${NVIDIA_NIM_API_KEY:-}" \ + NVIDIA_NIM_API_KEY_SUB="${NVIDIA_NIM_API_KEY_SUB:-}" \ + OPENROUTER_API_KEY="${OPENROUTER_API_KEY:-}" \ + OPENAI_API_KEY="${OPENAI_API_KEY:-}" \ + CONTEXTUAL_ORCHESTRATOR_TOKEN="$contextual_gateway_token" \ + GITHUB_ENV=/dev/null GITHUB_OUTPUT=/dev/null GITHUB_PATH=/dev/null \ + GITHUB_STEP_SUMMARY=/dev/null GITHUB_STATE=/dev/null BASH_ENV=/dev/null \ + python3 -m contextual_orchestrator.review_gateway \ + --host 127.0.0.1 --port 18080 \ + >"${RUNNER_TEMP}/contextual-orchestrator-autofix.log" 2>&1 & + contextual_gateway_pid=$! + models_file="$(mktemp "${RUNNER_TEMP}/contextual-orchestrator-models.XXXXXX")" + gateway_ready=false + for _ in {1..60}; do + if curl -fsS --max-time 3 "${contextual_gateway_base_url%/v1}/healthz" >/dev/null && + curl -fsS --max-time 3 -H "Authorization: Bearer ${contextual_gateway_token}" \ + -o "$models_file" "${contextual_gateway_base_url}/models" && + python3 -c 'import json,sys; data=json.load(open(sys.argv[1], encoding="utf-8")).get("data", []); raise SystemExit(0 if any(str(item.get("id") or "").strip() for item in data if isinstance(item, dict)) else 1)' "$models_file"; then + gateway_ready=true + break + fi + sleep 1 + done + if [ "$gateway_ready" != "true" ]; then + echo "::error::Pinned contextual-orchestrator gateway did not expose a usable auto-discovered model catalog." exit 1 fi + export CONTEXTUAL_ORCHESTRATOR_TOKEN="$contextual_gateway_token" + export CONTEXTUAL_ORCHESTRATOR_BASE_URL="$contextual_gateway_base_url" + unset BYTEZ_API_KEY NVIDIA_NIM_API_KEY NVIDIA_NIM_API_KEY_SUB OPENROUTER_API_KEY OPENAI_API_KEY prompt_file="${RUNNER_TEMP}/opencode-autofix-prompt.md" allowed_paths_zlist="${RUNNER_TEMP}/pr-review-autofix-allowed-paths.zlist" allowed_paths_context="$( @@ -416,6 +477,8 @@ jobs: Edit only the checked-out repository files listed in the authoritative JSON array. If the array is empty, leave the repository unchanged. Do not delete, rename, or reformat unrelated files, even if they look stale or failing. + In customer-facing copy, describe the customer's next useful action; do not expose internal implementation boundaries. + Preserve the target repository's AGENTS.md product language and governance constraints. Return a concise summary of changes made, or state that no safe change was made. EOF ordinary_scope_snapshot="${RUNNER_TEMP}/opencode-autofix-workspace-before.json" @@ -447,6 +510,7 @@ jobs: else rm -f "$TARGET_WORKSPACE/autofix-prompt.md" fi + cleanup_contextual_gateway } trap restore_workspace_config EXIT cd "$TARGET_WORKSPACE" @@ -569,11 +633,15 @@ jobs: - name: Merge base branch and resolve conflicts with OpenCode if: env.RESOLVE_CONFLICT == 'true' env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} + NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GITHUB_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token }} GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token }} MUTATION_CREDENTIAL_AVAILABLE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' || steps.target_app_token.outputs.available == 'true' }} - MODEL: nvidia-nim/mistralai/mistral-small-4-119b-2603 + MODEL: contextual-orchestrator/contextual-orchestrator SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" @@ -584,10 +652,67 @@ jobs: echo "::error::Conflict-resolution mutation requires PR_REVIEW_MERGE_TOKEN, OPENCODE_APPROVE_TOKEN, or the exchanged OpenCode app token; github.token remains read-only." exit 1 fi - if [ -z "${NVIDIA_API_KEY:-}" ]; then - echo "::error::NVIDIA_NIM_API_KEY is required for scheduled OpenCode autofix." + contextual_gateway_pid="" + cleanup_contextual_gateway() { + if [ -n "$contextual_gateway_pid" ]; then + kill "$contextual_gateway_pid" 2>/dev/null || true + for _ in 1 2 3 4 5; do + if ! kill -0 "$contextual_gateway_pid" 2>/dev/null; then + wait "$contextual_gateway_pid" 2>/dev/null || true + contextual_gateway_pid="" + return + fi + sleep 1 + done + kill -KILL "$contextual_gateway_pid" 2>/dev/null || true + wait "$contextual_gateway_pid" 2>/dev/null || true + contextual_gateway_pid="" + fi + } + trap cleanup_contextual_gateway EXIT + gateway_license="$GITHUB_WORKSPACE/trusted-contextual-orchestrator/LICENSE" + if [ "$(sed -n '1p' "$gateway_license")" != "MIT License" ] || + [ "$(git hash-object --no-filters "$gateway_license")" != "591bbf197b355e60604618c8a8a50bc5a839b204" ]; then + echo "::error::Pinned contextual-orchestrator license identity did not match the reviewed MIT source." + exit 1 + fi + contextual_gateway_token="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')" + contextual_gateway_base_url="http://127.0.0.1:18080/v1" + echo "::add-mask::$contextual_gateway_token" + env -i \ + PATH="$PATH" HOME="$HOME" \ + PYTHONPATH="$GITHUB_WORKSPACE/trusted-contextual-orchestrator" \ + BYTEZ_API_KEY="${BYTEZ_API_KEY:-}" \ + NVIDIA_NIM_API_KEY="${NVIDIA_NIM_API_KEY:-}" \ + NVIDIA_NIM_API_KEY_SUB="${NVIDIA_NIM_API_KEY_SUB:-}" \ + OPENROUTER_API_KEY="${OPENROUTER_API_KEY:-}" \ + OPENAI_API_KEY="${OPENAI_API_KEY:-}" \ + CONTEXTUAL_ORCHESTRATOR_TOKEN="$contextual_gateway_token" \ + GITHUB_ENV=/dev/null GITHUB_OUTPUT=/dev/null GITHUB_PATH=/dev/null \ + GITHUB_STEP_SUMMARY=/dev/null GITHUB_STATE=/dev/null BASH_ENV=/dev/null \ + python3 -m contextual_orchestrator.review_gateway \ + --host 127.0.0.1 --port 18080 \ + >"${RUNNER_TEMP}/contextual-orchestrator-autofix-conflict.log" 2>&1 & + contextual_gateway_pid=$! + models_file="$(mktemp "${RUNNER_TEMP}/contextual-orchestrator-models.XXXXXX")" + gateway_ready=false + for _ in {1..60}; do + if curl -fsS --max-time 3 "${contextual_gateway_base_url%/v1}/healthz" >/dev/null && + curl -fsS --max-time 3 -H "Authorization: Bearer ${contextual_gateway_token}" \ + -o "$models_file" "${contextual_gateway_base_url}/models" && + python3 -c 'import json,sys; data=json.load(open(sys.argv[1], encoding="utf-8")).get("data", []); raise SystemExit(0 if any(str(item.get("id") or "").strip() for item in data if isinstance(item, dict)) else 1)' "$models_file"; then + gateway_ready=true + break + fi + sleep 1 + done + if [ "$gateway_ready" != "true" ]; then + echo "::error::Pinned contextual-orchestrator gateway did not expose a usable auto-discovered model catalog." exit 1 fi + export CONTEXTUAL_ORCHESTRATOR_TOKEN="$contextual_gateway_token" + export CONTEXTUAL_ORCHESTRATOR_BASE_URL="$contextual_gateway_base_url" + unset BYTEZ_API_KEY NVIDIA_NIM_API_KEY NVIDIA_NIM_API_KEY_SUB OPENROUTER_API_KEY OPENAI_API_KEY cd "$TARGET_WORKSPACE" # Merge the base branch into the detached head. A clean merge stays @@ -651,6 +776,7 @@ jobs: else rm -f "$TARGET_WORKSPACE/opencode.jsonc" fi + cleanup_contextual_gateway } trap restore_workspace_config EXIT env -u GITHUB_TOKEN -u GH_TOKEN -u ACTIONS_ID_TOKEN_REQUEST_TOKEN -u ACTIONS_ID_TOKEN_REQUEST_URL \ diff --git a/.github/workflows/pr-review-fix-scheduler.yml b/.github/workflows/pr-review-fix-scheduler.yml index cc9d3e60e..a92e8b0cd 100644 --- a/.github/workflows/pr-review-fix-scheduler.yml +++ b/.github/workflows/pr-review-fix-scheduler.yml @@ -1,6 +1,8 @@ name: PR Review Fix Scheduler on: + # Same-repository callers resolve this file from their own immutable commit; + # only the explicitly declared optional secrets below cross that boundary. workflow_call: inputs: dry_run: diff --git a/CHANGELOG.md b/CHANGELOG.md index 04f023e4a..330172a03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Route write-capable PR autofix model traffic through a SHA-pinned, + loopback-only contextual-orchestrator sidecar with auto-discovered providers. + Provider credentials remain scoped to the sidecar and are removed before + OpenCode executes; the existing review, OIDC, GitHub mutation, and protected + merge credentials are unchanged. Document that LineageWeave's existing + repository-local commercialization loop, not a duplicate central writer, + owns the zero-open-PR product-gap continuation. - Dependency updates now keep coverage evidence when the lock file passes validation. If validation reports a problem, refresh the lock file and run the review again before merging. diff --git a/docs/adr/0002-product-technical-gap-baseline.md b/docs/adr/0002-product-technical-gap-baseline.md index 4caf92257..aaedd1a27 100644 --- a/docs/adr/0002-product-technical-gap-baseline.md +++ b/docs/adr/0002-product-technical-gap-baseline.md @@ -19,3 +19,15 @@ ContextualWisdomLab/disksage#247, before any target-repository security analysis ran. The workflow, smoke contract, model-pool configuration, and regression tests now share `gpt-5.4`; the change does not weaken provider failure or vulnerability fail-closed behavior. + +## Amendment: autofix model ownership boundary (2026-08-26) + +The write-capable PR autofix worker enables only the OpenAI-compatible +contextual-orchestrator gateway. The central workflow pins the gateway source, +verifies its reviewed license identity, binds it to loopback, and fails closed +unless authenticated model discovery returns a usable catalog. Provider keys +are passed only to the isolated sidecar and are removed before OpenCode runs; +the established review, OIDC, branch-write, independent-approval, and protected +merge credential contracts are unchanged. Product-specific hourly callers do +not duplicate a repository-local product-development writer when one already +owns the zero-open-PR continuation. diff --git a/docs/automation/hourly-review-repair.md b/docs/automation/hourly-review-repair.md index 722724958..31fcccef6 100644 --- a/docs/automation/hourly-review-repair.md +++ b/docs/automation/hourly-review-repair.md @@ -11,12 +11,13 @@ engine**. module. It has no product-specific timer and can be called by naruon, contextual-orchestrator, Inkspan, or another CWL service with an explicit repository and base branch. -- `pr-review-autofix.yml` is the bounded write-capable worker. It uses OpenCode - with NVIDIA NIM and does not approve or merge pull requests. +- `pr-review-autofix.yml` is the bounded write-capable worker. OpenCode reaches + every model through a pinned, loopback-only contextual-orchestrator gateway + and does not approve or merge pull requests. -Orgmetra's caller remains provider-neutral. The intended model boundary is the -contextual-orchestrator gateway: provider keys stay in its KV registry and -automatic model discovery selects upstream models. A caller schedule is not +All callers remain provider-neutral. Provider credentials are visible only to +the isolated contextual-orchestrator sidecar; they are removed before OpenCode +starts, and automatic model discovery selects upstream models. A caller schedule is not evidence that gateway credentials, discovery, or a live OpenCode tool loop are available; those facts require exact worker-run evidence. @@ -44,8 +45,8 @@ not overlap its successor. At most one repair dispatch is created per run. The caller passes only the established `PR_REVIEW_MERGE_TOKEN` and `OPENCODE_APPROVE_TOKEN` scheduler credentials. It does not receive or forward -`NVIDIA_NIM_API_KEY`; the model credential is scoped exclusively to the two -OpenCode execution steps in the separately reviewed autofix worker. +provider credentials; those credentials are scoped exclusively to the two +gateway-owning execution steps in the separately reviewed autofix worker. ## Orgmetra execution contract @@ -199,7 +200,9 @@ organization-level queue inspection and bounded repair dispatch. When a scheduled run fails, classify the result before rerunning: - no actionable file-scoped feedback: expected no-op; -- missing `NVIDIA_NIM_API_KEY`: central secret configuration failure; +- unavailable pinned contextual-orchestrator source, invalid reviewed-license + identity, or an empty auto-discovered model catalog: fail-closed gateway + configuration failure; - head changed: safe optimistic-concurrency refusal; inspect the new head rather than retrying predecessor evidence; - out-of-scope or ignored-path change: treat as a security failure and preserve @@ -225,7 +228,7 @@ Permanent tests prove: - the dispatch budget and same-head retry floor remain one; - caller and reusable-workflow secrets are explicit and never use `secrets: inherit`; -- immutable source, NVIDIA-only model authentication, child-process credential +- immutable source, contextual-orchestrator-only model authentication, child-process credential stripping, live-head guards, and independent reviewer identity remain intact; - ordinary and conflict repair share the complete ignored-inclusive snapshot and NUL-delimited allowlist boundary; diff --git a/docs/doctoring/hourly-nvidia-nim-autofix.md b/docs/doctoring/hourly-nvidia-nim-autofix.md index 6b05c6bd6..2c3750cb3 100644 --- a/docs/doctoring/hourly-nvidia-nim-autofix.md +++ b/docs/doctoring/hourly-nvidia-nim-autofix.md @@ -1,11 +1,12 @@ -# Hourly NVIDIA NIM Review-Autofix Boundary +# Hourly Contextual-Orchestrator Review-Autofix Boundary ## Decision Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include; a lone `--require-hashes` line is not lock evidence. -The write-capable scheduled pull-request autofix agent uses OpenCode with the -NVIDIA NIM API and the organization Actions secret `NVIDIA_NIM_API_KEY`. The +The write-capable scheduled pull-request autofix agent uses OpenCode only +through contextual-orchestrator. The gateway auto-discovers the organization +provider credentials, including `NVIDIA_NIM_API_KEY`, while the independent read-only review agent remains unchanged and continues to use its existing credential and model-pool contract. @@ -58,23 +59,22 @@ The client payload remains untrusted metadata. It identifies a target only after the worker re-reads live pull-request state and verifies the exact repository, open state, same-repository branch, base ref and SHA, and head ref and SHA. -## Provider contract +## Orchestration contract -The pinned OpenCode runtime enables only `nvidia-nim` through the -OpenAI-compatible adapter and NVIDIA hosted endpoint: +The worker checks out contextual-orchestrator at commit +`838b3de160c341a6f36bf588ae9fcc09989c040c`, verifies the reviewed MIT license +blob `591bbf197b355e60604618c8a8a50bc5a839b204`, and starts its review gateway on +loopback. OpenCode enables only this provider-neutral endpoint: ```text -https://integrate.api.nvidia.com/v1 +http://127.0.0.1:18080/v1 ``` -The primary repair model is `mistralai/mistral-small-4-119b-2603`. The -`ci-autofix` agent and its model configuration both request high reasoning -through OpenCode's provider-option contract (`reasoningEffort: "high"`). NVIDIA's -Mistral Small 4 NIM API documents the corresponding request behavior as -`reasoning_effort: "high"`, which enables the model's reasoning mode. The small -model used for bounded helper work remains `nvidia/nemotron-3-nano-30b-a3b` and -is not a fallback provider. GitHub Models configuration, identifiers, base URLs, -and model-auth fallbacks are absent from the scheduled autofix execution path. +The worker validates `/healthz` and the authenticated `/v1/models` catalog +before model execution. An unavailable or empty catalog fails closed; OpenCode +never falls back to a direct provider URL. The `ci-autofix` agent requests high +reasoning through OpenCode's provider-option contract, while model discovery, +provider selection, and cost lineage remain contextual-orchestrator concerns. The high-reasoning setting is deliberate for write-capable review repair. This workflow optimizes correctness, evidence quality, and controllability rather than @@ -84,17 +84,20 @@ writer role and remains subject to exact-head regression evidence. ## Credential boundary -The organization secret is bound as: +The organization provider secrets are bound only to the gateway-owning steps: ```yaml -NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} +NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} +NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }} +BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} +OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} +OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ``` -It is present only on the two steps that execute OpenCode: ordinary -review-feedback repair and merge-conflict repair. Metadata collection, -checkout, context preparation, validation, commit, and push do not receive the -NVIDIA credential. A missing key is a fatal configuration error rather than a -signal to choose another provider. +They are passed to the sidecar through an isolated `env -i` launch and unset +before OpenCode starts. OpenCode receives only the masked, process-local gateway +bearer token and loopback base URL. Metadata collection, checkout, context +preparation, validation, commit, and push do not receive provider credentials. The ordinary model execution step does not bind a GitHub write token. Its later commit-and-push step may mutate only with `PR_REVIEW_MERGE_TOKEN`, @@ -113,11 +116,16 @@ env -u GITHUB_TOKEN -u GH_TOKEN \ -u ACTIONS_ID_TOKEN_REQUEST_TOKEN -u ACTIONS_ID_TOKEN_REQUEST_URL ``` -The child receives the NVIDIA model credential and non-secret execution -controls, but cannot call GitHub APIs or mint an Actions OIDC token. GitHub -credentials remain available only to reviewed shell logic before or after the -child process. The key is never written to repository files, generated prompts, -command arguments, or ordinary logs. +The child receives only the gateway bearer token, loopback base URL, and +non-secret execution controls; it cannot call GitHub APIs or mint an Actions +OIDC token. GitHub credentials remain available only to reviewed shell logic +before or after the child process. Provider keys are never passed to OpenCode +or written to repository files, generated prompts, command arguments, or +ordinary logs. + +Both ordinary and conflict-repair paths allow sixty bounded readiness polls so +cold provider discovery can complete; an empty catalog still fails closed +before OpenCode starts. ## OpenCode repair sandbox diff --git a/docs/doctoring/lineageweave-hourly-review-caller.md b/docs/doctoring/lineageweave-hourly-review-caller.md new file mode 100644 index 000000000..fdaf1b51f --- /dev/null +++ b/docs/doctoring/lineageweave-hourly-review-caller.md @@ -0,0 +1,30 @@ +# LineageWeave hourly review-repair caller + +The central repository owns the bounded hourly review-repair caller for +`ContextualWisdomLab/LineageWeave`. It invokes the reusable scheduler with the +explicit all-base selector because LineageWeave uses stacked pull requests, +inspects at most 50 open pull requests, and dispatches at most one repair per +heartbeat. Merge automation remains protected-`main`-only in the separate +central merge scheduler. + +LineageWeave uses minute `4`, which is reserved for this product in the shared +heartbeat registry. Minute `56` belongs to Inkspan and must not be reused here. +The caller preserves an in-flight repair and waits two hours before retrying +the same exact head. + +The reusable scheduler remains fail-closed: the organization variable +`OPENCODE_REPOSITORY_DISPATCH_TARGETS` must contain the exact target +`ContextualWisdomLab/LineageWeave`, and the forwarded scheduler credentials +must be available. A missing allowlist entry or mutation credential is an +operational configuration action, not a reason to weaken the workflow. + +The repair worker reviews and proposes source changes only. Required checks, +independent approval, unresolved-thread policy, protected merge, release, and +rollback remain governed by the target repository and central merge scheduler. + +When the open-PR queue is empty, product-gap development is not duplicated in +this caller. LineageWeave's active repository-local +`.github/workflows/hourly-commercialization-loop.yml` owns that hourly writer +boundary. The central organization commercial-readiness coordinator recognizes +the dedicated schedule as a writer lease, so it cannot create a second +concurrent gap-development writer for the same repository. diff --git a/scripts/ci/pr_review_fix_scheduler.py b/scripts/ci/pr_review_fix_scheduler.py index 2c9745d09..153b2ff2a 100755 --- a/scripts/ci/pr_review_fix_scheduler.py +++ b/scripts/ci/pr_review_fix_scheduler.py @@ -104,6 +104,11 @@ def same_repository_head(repo: str, pr: dict[str, Any]) -> bool: return ((pr.get("headRepository") or {}).get("nameWithOwner") or "") == repo +def matches_base_branch(pr: dict[str, Any], base_branch: str) -> bool: + """Match one base branch, or every base when the caller explicitly uses ``*``.""" + return base_branch == "*" or pr.get("baseRefName") == base_branch + + def latest_current_head_opencode_review(pr: dict[str, Any]) -> dict[str, Any] | None: """Return the newest OpenCode review for the current head, if present.""" for review in reversed((pr.get("reviews") or {}).get("nodes") or []): @@ -293,7 +298,7 @@ def inspect_pr( number = int(pr["number"]) if pr.get("isDraft"): return "skip", ("draft PR",) - if pr.get("baseRefName") != args.base_branch: + if not matches_base_branch(pr, args.base_branch): return "skip", ( f"base branch is {pr.get('baseRefName')}; expected {args.base_branch}", ) @@ -363,7 +368,7 @@ def process_queue(args: argparse.Namespace) -> int: for pr in prs: if pr.get("isDraft"): continue - if pr.get("baseRefName") != args.base_branch: + if not matches_base_branch(pr, args.base_branch): continue if not same_repository_head(args.repo, pr): continue diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py new file mode 100644 index 000000000..8e5155294 --- /dev/null +++ b/tests/test_lineageweave_hourly_review_caller.py @@ -0,0 +1,44 @@ +"""Contracts for the LineageWeave stacked-PR hourly repair caller.""" + +from pathlib import Path + +CALLER = Path(".github/workflows/lineageweave-hourly-review-repair.yml") +REUSABLE_SCHEDULER = Path(".github/workflows/pr-review-fix-scheduler.yml") +DOCTORING = Path("docs/doctoring/lineageweave-hourly-review-caller.md") + + +def test_lineageweave_uses_reserved_heartbeat_and_target() -> None: + """The caller schedules the real LineageWeave repair boundary.""" + caller = CALLER.read_text(encoding="utf-8") + + assert REUSABLE_SCHEDULER.is_file() + assert 'cron: "4 * * * *"' in caller + assert "group: lineageweave-hourly-review-repair" in caller + assert "cancel-in-progress: false" in caller + assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller + workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) + assert "\npermissions:\n contents: read\n" in workflow_scope + assert ( + "\n permissions:\n contents: read\n id-token: write\n" + in jobs_scope + ) + assert "target_repository: ContextualWisdomLab/LineageWeave" in caller + assert 'base_branch: "*"' in caller + assert 'max_prs: "50"' in caller + assert 'max_dispatches: "1"' in caller + assert 'retry_hours: "2"' in caller + assert 'PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}' in caller + assert 'OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}' in caller + assert "secrets: inherit" not in caller + assert "CLOUDFLARE_API_TOKEN" not in caller + + +def test_lineageweave_doctoring_preserves_allowlist_and_protected_merge_boundary() -> None: + """The operational record tells maintainers what must be configured next.""" + doctoring = DOCTORING.read_text(encoding="utf-8") + + assert "minute `4`" in doctoring + assert "OPENCODE_REPOSITORY_DISPATCH_TARGETS" in doctoring + assert "ContextualWisdomLab/LineageWeave" in doctoring + assert "independent approval" in doctoring + assert "protected merge" in doctoring diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 7e91ab472..7a704b13c 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -35,21 +35,34 @@ def test_review_fix_caller_runs_once_each_hour() -> None: assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller -def test_scheduled_autofix_uses_only_nvidia_nim() -> None: - """Require the write-capable OpenCode autofix agent to use NVIDIA NIM only.""" +def test_autofix_prompt_keeps_customer_copy_actionable() -> None: + """Keep implementation boundaries out of customer-facing repair copy.""" + workflow = _workflow_text(AUTOFIX_WORKFLOW) + assert ( + "In customer-facing copy, describe the customer's next useful action; " + "do not expose internal implementation boundaries." + ) in workflow + assert ( + "Preserve the target repository's AGENTS.md product language and " + "governance constraints." + ) in workflow + + +def test_scheduled_autofix_uses_only_contextual_orchestrator() -> None: + """Require all write-capable OpenCode model traffic to cross the gateway.""" workflow = _workflow_text(AUTOFIX_WORKFLOW) required_fragments = ( - '"model": "nvidia-nim/mistralai/mistral-small-4-119b-2603"', - '"small_model": "nvidia-nim/nvidia/nemotron-3-nano-30b-a3b"', - '"enabled_providers": ["nvidia-nim"]', - '"nvidia-nim": {', - '"mistralai/mistral-small-4-119b-2603": {', + '"model": "contextual-orchestrator/contextual-orchestrator"', + '"small_model": "contextual-orchestrator/contextual-orchestrator"', + '"enabled_providers": ["contextual-orchestrator"]', + '"contextual-orchestrator": {', '"reasoningEffort": "high"', '"npm": "@ai-sdk/openai-compatible"', - '"baseURL": "https://integrate.api.nvidia.com/v1"', - '"apiKey": "{env:NVIDIA_API_KEY}"', - 'NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}', - 'MODEL: nvidia-nim/mistralai/mistral-small-4-119b-2603', + '"baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}"', + '"apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}"', + 'MODEL: contextual-orchestrator/contextual-orchestrator', + "ContextualWisdomLab/contextual-orchestrator", + "838b3de160c341a6f36bf588ae9fcc09989c040c", ) for fragment in required_fragments: assert fragment in workflow, fragment @@ -62,6 +75,9 @@ def test_scheduled_autofix_uses_only_nvidia_nim() -> None: '"apiKey": "{env:STRIX_GITHUB_MODELS_TOKEN}"', '"baseURL": "https://models.github.ai/inference"', 'COPILOT_GITHUB_TOKEN', + 'https://integrate.api.nvidia.com/v1', + '"enabled_providers": ["nvidia-nim"]', + '"apiKey": "{env:NVIDIA_API_KEY}"', ) for fragment in forbidden_fragments: assert fragment not in workflow, fragment @@ -98,20 +114,33 @@ def test_opencode_agent_denies_non_file_interactions() -> None: assert workflow.count(f'"{permission_name}": "deny"') == 2 -def test_nvidia_nim_secret_is_scoped_to_agent_execution_steps() -> None: - """Prevent the NVIDIA credential from leaking beyond the two OpenCode runs.""" +def test_provider_secrets_are_scoped_to_gateway_execution_steps() -> None: + """Keep provider credentials inside the two gateway-owning shell steps.""" workflow = _workflow_text(AUTOFIX_WORKFLOW) - binding = 'NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}' + bindings = ( + 'BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }}', + 'NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}', + 'NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }}', + 'OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}', + 'OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}', + ) ordinary_start = workflow.index(" - name: Run OpenCode review autofix") ordinary_end = workflow.index(" - name: Validate changed files", ordinary_start) conflict_start = workflow.index( " - name: Merge base branch and resolve conflicts with OpenCode" ) - assert workflow.count(binding) == 2 - assert binding in workflow[ordinary_start:ordinary_end] - assert binding in workflow[conflict_start:] - assert binding not in workflow[:ordinary_start] - assert binding not in workflow[ordinary_end:conflict_start] + for binding in bindings: + assert workflow.count(binding) == 2 + assert binding in workflow[ordinary_start:ordinary_end] + assert binding in workflow[conflict_start:] + assert binding not in workflow[:ordinary_start] + assert binding not in workflow[ordinary_end:conflict_start] + unset = "unset BYTEZ_API_KEY NVIDIA_NIM_API_KEY NVIDIA_NIM_API_KEY_SUB OPENROUTER_API_KEY OPENAI_API_KEY" + assert workflow.count(unset) == 2 + + doctoring = _workflow_text(DOCTORING_RECORD) + assert "The child receives the NVIDIA model credential" not in doctoring + assert "Provider keys are never passed to OpenCode" in doctoring def test_model_subprocesses_receive_no_github_or_oidc_write_credentials() -> None: @@ -135,16 +164,10 @@ def test_model_subprocesses_receive_no_github_or_oidc_write_credentials() -> Non assert workflow.count(sanitized_invocation) == 2 -def test_missing_nvidia_nim_secret_fails_closed_before_model_execution() -> None: - """Reject an empty model credential instead of falling back to another provider.""" +def test_unavailable_gateway_fails_closed_before_model_execution() -> None: + """Reject an empty auto-discovered catalog instead of calling a provider directly.""" workflow = _workflow_text(AUTOFIX_WORKFLOW) - guard = ( - 'if [ -z "${NVIDIA_API_KEY:-}" ]; then\n' - ' echo "::error::NVIDIA_NIM_API_KEY is required for scheduled ' - 'OpenCode autofix."\n' - " exit 1\n" - " fi" - ) + guard = 'if [ "$gateway_ready" != "true" ]; then' ordinary_start = workflow.index(" - name: Run OpenCode review autofix") ordinary_end = workflow.index(" - name: Validate changed files", ordinary_start) conflict_start = workflow.index( @@ -155,6 +178,26 @@ def test_missing_nvidia_nim_secret_fails_closed_before_model_execution() -> None assert guard in workflow[conflict_start:] +def test_gateway_readiness_allows_cold_provider_discovery() -> None: + """Give both gateway paths a bounded cold-start discovery window.""" + workflow = _workflow_text(AUTOFIX_WORKFLOW) + assert workflow.count("for _ in {1..60}; do") == 2 + assert "for _ in 1 2 3 4 5 6 7 8 9 10; do" not in workflow + + +def test_gateway_is_loopback_pinned_and_receives_no_github_credentials() -> None: + """Bind the reviewed gateway source to loopback and an isolated environment.""" + workflow = _workflow_text(AUTOFIX_WORKFLOW) + assert workflow.count("--host 127.0.0.1 --port 18080") == 2 + assert workflow.count("env -i \\") == 2 + assert workflow.count("591bbf197b355e60604618c8a8a50bc5a839b204") == 2 + for block in workflow.split("env -i \\")[1:]: + launch = block.split("python3 -m contextual_orchestrator.review_gateway", 1)[0] + assert "GITHUB_TOKEN" not in launch + assert "GH_TOKEN" not in launch + assert "ACTIONS_ID_TOKEN" not in launch + + def test_independent_review_agent_workflow_matches_reviewed_blob() -> None: """Pin the reviewed read-only reviewer workflow byte-for-byte.""" result = subprocess.run( diff --git a/tests/test_pr_review_autofix_writer_security_contract.py b/tests/test_pr_review_autofix_writer_security_contract.py index 58ea05877..7d9edad1a 100644 --- a/tests/test_pr_review_autofix_writer_security_contract.py +++ b/tests/test_pr_review_autofix_writer_security_contract.py @@ -6,7 +6,7 @@ _AUTOFIX_WORKFLOW = Path(".github/workflows/pr-review-autofix.yml") -_TARGET_MODEL = "nvidia-nim/mistralai/mistral-small-4-119b-2603" +_TARGET_MODEL = "contextual-orchestrator/contextual-orchestrator" def _workflow_text() -> str: @@ -30,15 +30,16 @@ def _step_header(workflow: str, step_name: str) -> str: return step[:run_start] -def test_writer_uses_supported_nvidia_mistral_small_with_high_reasoning() -> None: - """Pin the write-capable model and its deliberate high-reasoning budget.""" +def test_writer_uses_contextual_orchestrator_with_high_reasoning() -> None: + """Pin the writer to the provider-neutral gateway and high reasoning.""" workflow = _workflow_text() assert f'"model": "{_TARGET_MODEL}"' in workflow - assert '"mistralai/mistral-small-4-119b-2603": {' in workflow + assert '"contextual-orchestrator": {' in workflow assert workflow.count(f"MODEL: {_TARGET_MODEL}") == 2 assert '"reasoningEffort": "high"' in workflow - assert "nvidia-nim/mistralai/mistral-nemotron" not in workflow + assert '"enabled_providers": ["contextual-orchestrator"]' in workflow + assert "https://integrate.api.nvidia.com/v1" not in workflow assert "COPILOT_GITHUB_TOKEN" not in workflow diff --git a/tests/test_pr_review_fix_scheduler.py b/tests/test_pr_review_fix_scheduler.py index 74366f686..ea1229b59 100644 --- a/tests/test_pr_review_fix_scheduler.py +++ b/tests/test_pr_review_fix_scheduler.py @@ -163,6 +163,22 @@ def test_process_queue_dispatches_same_repo_current_head(monkeypatch, capsys): assert payload["autofix_dispatches"] == 1 +def test_process_queue_can_repair_stacked_prs_when_caller_selects_all_bases(monkeypatch, capsys): + """An explicit wildcard includes reviewed stacked heads without changing other callers.""" + pr = make_pr(baseRefName="feature-base") + calls = [] + + monkeypatch.setattr(fix, "fetch_open_prs", lambda repo, max_prs: [pr]) + monkeypatch.setattr(fix, "needs_autofix", lambda pr: (True, ("current-head OpenCode requested changes",))) + monkeypatch.setattr(fix, "issue_comments", lambda repo, number: []) + monkeypatch.setattr(fix, "dispatch_autofix", lambda *args, **kwargs: calls.append((args, kwargs))) + monkeypatch.setattr(fix, "create_fix_marker", lambda repo, pr, dry_run: None) + + assert fix.main(["--repo", "owner/repo", "--base-branch", "*", "--dry-run"]) == 0 + assert len(calls) == 1 + assert json.loads(capsys.readouterr().out.strip().splitlines()[-1])["autofix_dispatches"] == 1 + + def test_autofix_context_filters_outdated_threads_and_renders_checks(): """The context helper filters stale threads and renders compact checks.""" assert context.repo_parts("owner/repo") == ("owner", "repo")