Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 105 additions & 46 deletions .github/workflows/hourly-nim-product-development.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Hourly NVIDIA NIM Product Development
name: Hourly Contextual Orchestrator Product Development

on:
workflow_dispatch:
Expand All @@ -25,10 +25,10 @@ env:
# for the newer Linux x64 asset is committed.
OPENCODE_VERSION: "1.17.13"
OPENCODE_SHA256: 157afa289d1a8d9372de0ce19ac726119b937a1f6b201808d46f06e4e59bb348
OPENCODE_MODELS: >-
nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5
nvidia-nim/nvidia/nemotron-3-super-120b-a12b
nvidia-nim/deepseek-ai/deepseek-v4-pro
CONTEXTUAL_ORCHESTRATOR_COMMIT: e226e1197bdfc890c9d8e5b9b648c78857d7e465
CONTEXTUAL_ORCHESTRATOR_SHA256: 964b22ff577e3862b761af847ccad65489bb3f8fc750c8f84fcf8628df096673
CONTEXTUAL_ORCHESTRATOR_PORT: "18000"
OPENCODE_MODEL: contextual-orchestrator/contextual-orchestrator
OPENCODE_RUN_TIMEOUT_SECONDS: "900"
OPENCODE_KILL_GRACE_SECONDS: "30"
MAX_CHANGED_FILES: "40"
Expand All @@ -43,6 +43,7 @@ jobs:
timeout-minutes: 55
permissions:
contents: read
issues: read
pull-requests: read
outputs:
has_changes: ${{ steps.proposal.outputs.has_changes }}
Expand All @@ -55,12 +56,12 @@ jobs:
env:
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}
steps:
- name: Enforce zero-open-PR single-flight gate
- name: Enforce zero-open-PR-and-issue single-flight gate
id: gate
shell: bash
env:
GH_TOKEN: ${{ github.token }}
NIM_CONFIGURED: ${{ secrets.NVIDIA_NIM_API_KEY != '' }}
ORCHESTRATOR_KEYS_CONFIGURED: ${{ secrets.BYTEZ_API_KEY != '' && secrets.NVIDIA_NIM_API_KEY != '' && secrets.NVIDIA_NIM_API_KEY_SUB != '' && secrets.OPENROUTER_API_KEY != '' && secrets.OPENAI_API_KEY != '' }}
APP_ID_CONFIGURED: ${{ vars.TEPP_MAINTAINER_APP_CLIENT_ID != '' }}
APP_KEY_CONFIGURED: ${{ secrets.TEPP_MAINTAINER_APP_PRIVATE_KEY != '' }}
run: |
Expand All @@ -75,9 +76,19 @@ jobs:
echo 'reason=open_pull_request' >>"$GITHUB_OUTPUT"
exit 0
fi
if [ "$NIM_CONFIGURED" != true ] && [ "$DRY_RUN" != true ]; then
if ! open_issues="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --limit 1 --json number,url)"; then
echo 'dispatch=false' >>"$GITHUB_OUTPUT"
echo 'reason=nim_api_key_unavailable' >>"$GITHUB_OUTPUT"
echo 'reason=issue_inventory_unavailable' >>"$GITHUB_OUTPUT"
exit 0
fi
if [ "$(jq 'length' <<<"$open_issues")" -gt 0 ]; then
echo 'dispatch=false' >>"$GITHUB_OUTPUT"
echo 'reason=open_issue' >>"$GITHUB_OUTPUT"
exit 0
fi
if [ "$ORCHESTRATOR_KEYS_CONFIGURED" != true ] && [ "$DRY_RUN" != true ]; then
echo 'dispatch=false' >>"$GITHUB_OUTPUT"
echo 'reason=contextual_orchestrator_credentials_unavailable' >>"$GITHUB_OUTPUT"
exit 0
fi
if { [ "$APP_ID_CONFIGURED" != true ] || [ "$APP_KEY_CONFIGURED" != true ]; } \
Expand Down Expand Up @@ -174,7 +185,22 @@ jobs:
install -D -m 0755 "$RUNNER_TEMP/opencode" "$HOME/.opencode/bin/opencode"
echo "$HOME/.opencode/bin" >>"$GITHUB_PATH"

- name: Configure OpenCode for NVIDIA NIM only
- name: Install checksum-pinned contextual-orchestrator
if: steps.gate.outputs.dispatch == 'true' && env.DRY_RUN != 'true'
shell: bash
run: |
set -euo pipefail
archive="$RUNNER_TEMP/contextual-orchestrator.tar.gz"
package_root="$RUNNER_TEMP/contextual-orchestrator"
install -d -m 0700 "$package_root"
curl --fail --silent --show-error --location --proto '=https' \
--connect-timeout 10 --max-time 120 --output "$archive" \
"https://github.com/ContextualWisdomLab/contextual-orchestrator/archive/${CONTEXTUAL_ORCHESTRATOR_COMMIT}.tar.gz"
printf '%s %s\n' "$CONTEXTUAL_ORCHESTRATOR_SHA256" "$archive" | sha256sum -c -
tar -xzf "$archive" --strip-components=1 -C "$package_root"
test -f "$package_root/contextual_orchestrator/__main__.py"

- name: Configure OpenCode for contextual-orchestrator
if: steps.gate.outputs.dispatch == 'true' && env.DRY_RUN != 'true'
shell: bash
run: |
Expand All @@ -185,7 +211,7 @@ jobs:
"autoupdate": false,
"lsp": false,
"mcp": {},
"enabled_providers": ["nvidia-nim"],
"enabled_providers": ["contextual-orchestrator"],
"permission": {
"*": "allow",
"external_directory": "deny",
Expand All @@ -209,63 +235,90 @@ 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_NIM_API_KEY}"
"baseURL": "http://127.0.0.1:18000/v1",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Open-issue gate makes the scheduler dispatch only when both queues are empty

The gate now fails closed on any open issue (hourly-nim-product-development.yml) in addition to any open PR, and the publisher re-checks issues before publication (:618-622). This is the stated intent (ADR 0017), but note the practical consequence: on a repo that normally carries open issues, the hourly proposal job will essentially always no-op with open_issue. gh issue list excludes PRs, so the PR and issue checks are independent as intended. Flagging so reviewers confirm the operational expectation that a non-empty issue backlog fully disables autonomous proposals.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: OpenCode baseURL hardcodes port while workflow parameterizes it

The OpenCode provider config hardcodes http://127.0.0.1:18000/v1 (hourly-nim-product-development.yml) while the gateway is started with --port "$CONTEXTUAL_ORCHESTRATOR_PORT" and the health/model probes use ${CONTEXTUAL_ORCHESTRATOR_PORT}. Today the env var is 18000 so they agree, but changing CONTEXTUAL_ORCHESTRATOR_PORT would silently break OpenCode's connection because the config would still point at 18000. Not a bug now, but a fragile coupling worth noting.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

"apiKey": "{env:OPENCODE_GATEWAY_TOKEN}"
},
"models": {
"nvidia/llama-3.3-nemotron-super-49b-v1.5": {"tool_call": true},
"nvidia/nemotron-3-super-120b-a12b": {"tool_call": true},
"deepseek-ai/deepseek-v4-pro": {"tool_call": true}
"contextual-orchestrator": {"tool_call": true}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 OpenCode single virtual model name depends on unverifiable orchestrator routing contract

OpenCode is configured with a single model contextual-orchestrator (hourly-nim-product-development.yml) and invoked with --model contextual-orchestrator/contextual-orchestrator (line 310). The gateway's discovered /v1/models list (validated at line 283) contains the actual provider model IDs, not necessarily a virtual model literally named contextual-orchestrator. Whether the gateway accepts a chat/completions request with model contextual-orchestrator and internally routes to a discovered model is an assumption about the pinned ContextualWisdomLab/contextual-orchestrator commit that cannot be verified from this repo (the package is downloaded at runtime). If the gateway rejects unknown model names, the agent run fails at request time even though healthz and model-list probes pass. Worth confirming against the orchestrator's routing behavior.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
}
}
}
CONFIG
chmod 0400 "$RUNNER_TEMP/opencode.json"

- name: Run bounded NVIDIA NIM fallback
- name: Start contextual-orchestrator with all-provider discovery
if: steps.gate.outputs.dispatch == 'true' && env.DRY_RUN != 'true'
shell: bash
env:
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 }}
run: |
set -euo pipefail
umask 077
token_file="$RUNNER_TEMP/contextual-orchestrator-token"
printf '%s' "$(openssl rand -hex 32)" >"$token_file"
report_file="$RUNNER_TEMP/contextual-orchestrator-discovery.json"
log_file="$RUNNER_TEMP/contextual-orchestrator.log"
gateway_token="$(cat "$token_file")"
export CONTEXTUAL_ORCHESTRATOR_INFERENCE_TOKEN="$gateway_token"
export PYTHONPATH="$RUNNER_TEMP/contextual-orchestrator"
python3 scripts/run_contextual_orchestrator.py \
--host 127.0.0.1 --port "$CONTEXTUAL_ORCHESTRATOR_PORT" \
--report "$report_file" >"$log_file" 2>&1 &
echo $! >"$RUNNER_TEMP/contextual-orchestrator.pid"
ready=false
for _ in $(seq 1 120); do
if curl --fail --silent --show-error "http://127.0.0.1:${CONTEXTUAL_ORCHESTRATOR_PORT}/healthz" >/dev/null; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Liveness probe assumes /healthz is exempt from bearer auth

The startup step polls http://127.0.0.1:.../healthz with curl --fail and no Authorization header (hourly-nim-product-development.yml), while the gateway is constructed with SecurityConfig(auth_token=gateway_token) (run_contextual_orchestrator.py). If the gateway enforces bearer auth on /healthz, the probe will receive 401, ready never becomes true, and the step fails after ~120s on every run. The authenticated /v1/models probe (:279-282) does send the token. Confirm the pinned gateway leaves /healthz unauthenticated.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

ready=true
break
fi
if ! kill -0 "$(cat "$RUNNER_TEMP/contextual-orchestrator.pid")" 2>/dev/null; then
cat "$log_file"
exit 1
fi
sleep 1
done
[ "$ready" = true ]
curl --fail --silent --show-error \
-H "Authorization: Bearer $(cat "$token_file")" \
"http://127.0.0.1:${CONTEXTUAL_ORCHESTRATOR_PORT}/v1/models" \
>"$RUNNER_TEMP/contextual-orchestrator-models.json"
jq -e '.data | length > 0' "$RUNNER_TEMP/contextual-orchestrator-models.json" >/dev/null
jq -e '.discovered_count > 0 and (.selected_models | length > 0)' "$report_file" >/dev/null
echo "CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE=$token_file" >>"$GITHUB_ENV"
Comment on lines +273 to +296

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Gateway survives across steps by design

The gateway is launched with & in the start step and used by the later agent step through the token-file path in GITHUB_ENV. This depends on the background process persisting across steps, which hosted runners allow for the job lifetime; the process is reclaimed at job end. No explicit teardown exists, which is acceptable on ephemeral runners.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


- name: Run bounded contextual-orchestrator agent
if: steps.gate.outputs.dispatch == 'true' && env.DRY_RUN != 'true'
shell: bash
env:
OPENCODE_CONFIG: ${{ runner.temp }}/opencode.json
OPENCODE_DISABLE_AUTOUPDATE: 'true'
XDG_CONFIG_HOME: ${{ runner.temp }}/opencode-config
XDG_CACHE_HOME: ${{ runner.temp }}/opencode-cache
run: |
set -euo pipefail
prompt="$(cat "$RUNNER_TEMP/agent-prompt.md")"
status=1
read -r -a models <<<"$OPENCODE_MODELS"
model_count=${#models[@]}
for ((model_index = 0; model_index < model_count; model_index++)); do
model="${models[$model_index]}"
if timeout --kill-after="${OPENCODE_KILL_GRACE_SECONDS}s" \
"${OPENCODE_RUN_TIMEOUT_SECONDS}s" \
env -u GH_TOKEN -u GITHUB_TOKEN -u REPOSITORY_TOKEN \
-u ACTIONS_ID_TOKEN_REQUEST_TOKEN -u ACTIONS_ID_TOKEN_REQUEST_URL \
-u ACTIONS_RUNTIME_TOKEN -u ACTIONS_RUNTIME_URL \
-u ACTIONS_RESULTS_URL -u ACTIONS_CACHE_URL \
-u GITHUB_ENV -u GITHUB_OUTPUT -u GITHUB_PATH \
-u GITHUB_STATE -u GITHUB_STEP_SUMMARY \
opencode run "$prompt" --agent build --model "$model"; then
status=0
break
fi
if [ "$model_index" -eq $((model_count - 1)) ]; then
break
fi
git reset --hard HEAD
git clean -fdx
env -u NVIDIA_NIM_API_KEY \
timeout --kill-after=15s 180s \
python3 -m pip install --disable-pip-version-check --no-deps --require-hashes -r requirements-quality.txt
done
[ "$status" -eq 0 ] || { echo '::error::All NIM candidates failed'; exit 1; }
gateway_token="$(cat "$CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE")"
export OPENCODE_GATEWAY_TOKEN="$gateway_token"
timeout --kill-after="${OPENCODE_KILL_GRACE_SECONDS}s" \
"${OPENCODE_RUN_TIMEOUT_SECONDS}s" \
env -u GH_TOKEN -u GITHUB_TOKEN -u REPOSITORY_TOKEN \
-u ACTIONS_ID_TOKEN_REQUEST_TOKEN -u ACTIONS_ID_TOKEN_REQUEST_URL \
-u ACTIONS_RUNTIME_TOKEN -u ACTIONS_RUNTIME_URL \
-u ACTIONS_RESULTS_URL -u ACTIONS_CACHE_URL \
-u GITHUB_ENV -u GITHUB_OUTPUT -u GITHUB_PATH \
-u GITHUB_STATE -u GITHUB_STEP_SUMMARY \
-u BYTEZ_API_KEY -u NVIDIA_NIM_API_KEY -u NVIDIA_NIM_API_KEY_SUB \
-u OPENROUTER_API_KEY -u OPENAI_API_KEY \
opencode run "$prompt" --agent build --model "$OPENCODE_MODEL"

- name: Verify, bound, and export the uncredentialed proposal
id: proposal
Expand Down Expand Up @@ -545,6 +598,7 @@ jobs:
owner: ContextualWisdomLab
repositories: ${{ github.event.repository.name }}
permission-contents: write
permission-issues: read
permission-metadata: read
permission-pull-requests: write

Expand All @@ -561,6 +615,11 @@ jobs:
exit 1
fi
[ "$(jq 'length' <<<"$open_prs")" -eq 0 ] || { echo '::error::open_pull_request_after_generation'; exit 1; }
if ! open_issues="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --limit 1 --json number,url)"; then
echo '::error::issue_inventory_unavailable_after_generation'
exit 1
fi
[ "$(jq 'length' <<<"$open_issues")" -eq 0 ] || { echo '::error::open_issue_after_generation'; exit 1; }
live_base="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${DEFAULT_BRANCH}" --jq '.object.sha')"
[ "$live_base" = "$expected_base" ] || { echo '::error::base_branch_advanced'; exit 1; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Publisher revalidation references an unset variable, making publication fail-closed

In the publisher's "Revalidate queue and default-branch head" step, line 624 compares $live_base against $expected_base (lowercase), but the environment variable is exported as EXPECTED_BASE (uppercase) at line 608 and used correctly on line 612. Under set -euo pipefail, referencing the unset $expected_base triggers an unbound variable error, so this step always fails before it can create a PR. This is a pre-existing latent bug (the line is unchanged context, not part of this PR's added lines), but this PR adds the open-issue revalidation block (hourly-nim-product-development.yml) directly above it, so a reviewer touching this step should fix the casing while here. Note it likely went unnoticed because the workflow has not run to publication (credentials unconfigured).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

### Added

- Hourly contextual-orchestrator discovery records all provider models but routes OpenCode only through general-chat candidates, excluding embedding, image, reranker, transcription, moderation, safety, and other endpoint-only identifiers before price selection.
- Live `docs/product-technical-gap-baseline.md` mapping operator-visible gaps to
protected-main maturity, exact current PR/issue state, stacked delivery order,
and closure evidence; placeholder-only issues #161 and #162 were closed as
Expand Down Expand Up @@ -50,6 +51,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang
- `persistence_postgres` live SQL port: `SqlSession` transport, migration batch applicator, document/audit SQL contracts, `LiveDocumentRepository`, and fail-closed `DATABASE_URL`/`LiveSqlxConfig` gate for SQLx pool wiring (optional `live-sqlx` driver attaches `PgPool`).
- `membership_core` Kish effective sample size, design effect, and group-normalized ESS helpers for multiple-membership estimation inputs.
- Credential-separated hourly NVIDIA NIM/OpenCode product-development workflow (issue #2): proposal, independent verification, and late Maintainer-App publication with `NVIDIA_NIM_API_KEY` only for model work.
- Hourly product-development queue gate now fails closed when either an open pull request or an open issue exists, including a second queue check immediately before publication.
- Documented modular naruon consumer contract for TEPP analysis-run and export surfaces, with a committed example request payload.
- Documented contextual-orchestrator interpretation port boundary and credential separation for TEPP.
- Foundation validation/release-readiness ledger at `docs/validation/temporal-event-foundation.md` tracking capability maturity and scientific acceptance gates.
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0010-adaptive-llm-orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Statistical estimation, temporal eligibility, event-relation validity, measureme

## Security and privacy

Documents cannot change orchestration policy, allowed tools, provider credentials, access lists, or role authority. Calls use evidence-minimized payloads and raw secrets are never exposed to models. Live development/model tests use `NVIDIA_NIM_API_KEY`; `COPILOT_GITHUB_TOKEN` is prohibited. Review-agent identities and credentials remain separate.
Documents cannot change orchestration policy, allowed tools, provider credentials, access lists, or role authority. Calls use evidence-minimized payloads and raw secrets are never exposed to models. Live scientific/model tests use `NVIDIA_NIM_API_KEY`; the separate hourly proposal gateway exception is governed by ADR 0017 and uses its own all-provider bootstrap. `COPILOT_GITHUB_TOKEN` is prohibited. Review-agent identities and credentials remain separate.

## Compatibility and migration

Expand Down
80 changes: 80 additions & 0 deletions docs/adr/0017-hourly-contextual-orchestrator-gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# ADR 0017 — Hourly contextual-orchestrator gateway for autonomous proposals

**Decision status:** Accepted
**Implementation maturity:** active-PR — workflow wiring and bootstrap contract are implemented on this PR and require exact-head Checks and protected-main integration
**Date:** 2026-08-20
**Supersedes:** None; this ADR clarifies the hourly proposal credential route without changing scientific or reviewer authority
**Related ADRs:** ADR 0010 (LLM test-time compute), ADR 0011 (modular MSA), ADR 0015 (autonomous development authority)

## Context

The hourly product-development workflow previously configured OpenCode directly
against one NVIDIA NIM endpoint. That did not exercise the repository's
contextual-orchestrator integration or its model-discovery contract, and it put a
provider key in the model-agent process. The scheduler must remain a proposal
producer: it may not merge, release, deploy, approve, or alter reviewer-agent
credentials.

## Decision

The hourly proposal runner starts a pinned, ephemeral
`ContextualWisdomLab/contextual-orchestrator` loopback gateway. At bootstrap it
registers `BYTEZ_API_KEY`, `NVIDIA_NIM_API_KEY`, `NVIDIA_NIM_API_KEY_SUB`,
`OPENROUTER_API_KEY`, and `OPENAI_API_KEY` in the gateway KV, removes those
provider values from the gateway environment, and discovers models from all five
provider entries. The gateway records every discovered provider model, excludes
endpoint-only and safety-only model identifiers from the chat pool, and enables
the three lowest-cost general-chat candidates using contextual-orchestrator's
existing price-book selector. This prevents an embedding or image model from
being sent to an ordinary chat endpoint while preserving full discovery
evidence.

OpenCode calls only `http://127.0.0.1:18000/v1` with a separately generated
loopback bearer token. It receives no provider credential. Liveness and
authenticated model-list checks must pass before the proposal agent runs.

The workflow remains fail-closed when any provider credential is absent, the
PR or issue queue is unreadable, an open PR or issue exists, or the publication
App is unavailable.
The verifier and publisher receive neither provider nor model credentials.

## Alternatives considered

1. **Direct NVIDIA NIM configuration** — rejected because it bypasses the
contextual-orchestrator discovery and routing boundary.
2. **One CLI registration process per provider** — rejected because the default
in-memory KV is process-local and would silently lose earlier registrations.
3. **Persistent shared credential database in the workflow** — rejected for this
ephemeral proposal path because it expands infrastructure and retention scope.
4. **One in-process gateway bootstrap with pinned source and loopback auth** —
accepted as the smallest auditable boundary.

## Consequences

- All configured provider discovery paths are exercised and auditable without
exposing provider keys to OpenCode.
- Discovery can fail for an individual provider while another provider supplies
a candidate; zero discovered candidates fail the proposal job.
- The three-candidate selection is cost-oriented, not a scientific quality
claim. TEPP deterministic evidence and human review remain authoritative.
- Provider terms, retention, region, and confidentiality must be reviewed for
every configured provider before the schedule is enabled.
- The workflow is not a Figma or visual interaction surface; no Figma file is
applicable to this headless scheduler decision.
- Figma File ID: N/A — this headless scheduler introduces no visual interaction
contract or design artifact.

## Verification

The PR contract tests assert the hourly schedule, all five credential names,
immutable gateway source digest, loopback-only OpenCode configuration, health and
model-list probes, provider-key removal from OpenCode, no Copilot token, and
absence of merge/release commands. The bootstrap import and credential transfer
self-checks verify that the provider keys enter the KV and leave the process
environment before serving.

## Rollback

Disable the schedule or revert this PR. Removing any required provider secret
causes a stable no-op. Rollback does not touch review-agent credentials,
protected-branch rules, or scientific runtime contracts.
Loading
Loading