Repository navigation
feat: route hourly product agents through contextual orchestrator #165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
95dca3a
a7ce5ad
37f85fe
02ace78
619bdbc
5a98c8a
da2bc4c
d63621a
9558591
087ed2d
44903e3
b4914aa
3a0fbfc
34083c3
96a90aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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: | ||
|
|
@@ -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" | ||
|
|
@@ -43,6 +43,7 @@ jobs: | |
| timeout-minutes: 55 | ||
| permissions: | ||
| contents: read | ||
| issues: read | ||
| pull-requests: read | ||
| outputs: | ||
| has_changes: ${{ steps.proposal.outputs.has_changes }} | ||
|
|
@@ -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: | | ||
|
|
@@ -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 ]; } \ | ||
|
|
@@ -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: | | ||
|
|
@@ -185,7 +211,7 @@ jobs: | |
| "autoupdate": false, | ||
| "lsp": false, | ||
| "mcp": {}, | ||
| "enabled_providers": ["nvidia-nim"], | ||
| "enabled_providers": ["contextual-orchestrator"], | ||
| "permission": { | ||
| "*": "allow", | ||
| "external_directory": "deny", | ||
|
|
@@ -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", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Gateway survives across steps by design The gateway is launched with 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 | ||
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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; } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
|
|
||
| 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. |
There was a problem hiding this comment.
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 withopen_issue.gh issue listexcludes 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.Was this helpful? React with 👍 or 👎 to provide feedback.