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
3 changes: 3 additions & 0 deletions .github/sync-manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,9 @@ scripts:
- source: tools/embedding_provider.py
description: "Embedding provider registry used by synced semantic matching helpers"

- source: tools/check_model_registry_freshness.py
description: "Model-registry freshness gate - offline check that flags stale/blocked/dominated model pins so old models do not get stuck as primary"

# Research-backplane run-contract/v1 validator (run locally + in the conformance gate)
- source: scripts/validate_run_contract.py
description: "Validates a repo's emitted run-contract/v1 run envelope (producer/bridge) or ingested satellite object (consumer) against the canonical Workflows schemas + opt-in participant registry. Offline/deterministic; opt-in skip for non-participants. Synced so participants can validate locally and the reusable conformance gate can invoke it."
Expand Down
133 changes: 133 additions & 0 deletions .github/workflows/maint-77-model-registry-freshness.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
name: Maint 77 Model Registry Freshness
Comment thread
stranske marked this conversation as resolved.

# Detects when the canonical LLM model configuration has gone stale so old models
# do not get stuck as the primary ones indefinitely. OFFLINE + deterministic:
# it only reads config/model_registry.json + config/llm_slots.json (no provider
# API calls, no secrets). On staleness it opens/refreshes a single tracking issue;
# a human reviews + refreshes the registry, which then propagates to consumers via
# the existing maint-68 sync (model_registry.json is copy-synced).
#
# This does NOT change model selection and does NOT touch the sync workflow.

on:
schedule:
# Weekly, Monday 05:20 UTC (after the 05:00 maint-68 sync window).
- cron: '20 5 * * 1'
workflow_dispatch: {}
pull_request:
paths:
- config/model_registry.json
- config/llm_slots.json
- tools/check_model_registry_freshness.py

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
freshness:
runs-on: ubuntu-latest
outputs:
rc: ${{ steps.gate.outputs.rc }}
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
with:
persist-credentials: false

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.14'

# On PRs, fail the job on staleness so registry/slot changes are gated.
# On schedule/dispatch, never fail the run — open a tracking issue instead.
- name: Run freshness gate
id: gate
run: |
set +e
python3 tools/check_model_registry_freshness.py --json > freshness.json
rc=$?
echo "rc=$rc" >> "$GITHUB_OUTPUT"
cat freshness.json
{
echo '### Model registry freshness'
echo '```json'
cat freshness.json
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
if [ "$rc" = "2" ]; then
echo "::error::Freshness gate configuration error."
exit 2
fi

- name: Fail PRs on staleness
if: github.event_name == 'pull_request' && steps.gate.outputs.rc == '1'
run: |
echo "::error::Model registry/slots are stale — see job summary."
exit 1

tracking-issue:
if: github.event_name != 'pull_request' && needs.freshness.outputs.rc == '1'
needs: freshness
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.14'

- name: Recreate freshness report
run: |
set +e
python3 tools/check_model_registry_freshness.py --json > freshness.json
rc=$?
cat freshness.json
if [ "$rc" = "2" ]; then
echo "::error::Freshness gate configuration error."
exit 2
fi
if [ "$rc" != "1" ]; then
echo "::notice::Freshness finding cleared before issue update."
exit 0
fi

- name: Open or refresh tracking issue (scheduled/dispatch only)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TITLE="🧭 LLM model registry needs review (freshness gate)"
BODY_FILE=issue_body.md
{
echo "The model-registry freshness gate (\`tools/check_model_registry_freshness.py\`, maint-77) flagged stale or dominated model configuration."
echo ""
echo "Refresh \`config/model_registry.json\` (and \`config/llm_slots.json\` if pins are dominated/blocked), bump \`review_by\`, and merge — the update propagates to consumers via maint-68 (model_registry.json is copy-synced)."
echo ""
echo "**Findings:**"
echo '```json'
cat freshness.json
echo '```'
echo ""
echo "_Run:_ ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
} > "$BODY_FILE"
EXISTING=$(gh issue list --search "$TITLE in:title" --state open --json number --jq '.[0].number' 2>/dev/null)
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --body-file "$BODY_FILE"
echo "::warning::Refreshed existing freshness issue #$EXISTING"
else
gh issue create --title "$TITLE" --body-file "$BODY_FILE" --label "automated"
echo "::warning::Opened model-registry freshness issue"
fi
2 changes: 1 addition & 1 deletion config/llm_slots.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{
"name": "slot2",
"provider": "anthropic",
"model": "claude-sonnet-4-6"
"quality_tier": "T5"
},
{
"name": "slot3",
Expand Down
6 changes: 4 additions & 2 deletions config/model_registry.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"version": "1.0.0",
"last_updated": "2026-04-14",
"last_updated": "2026-06-29",
"review_interval_days": 60,
"review_by": "2026-08-28",
"models": [
{
"model_id": "gpt-5.4",
Expand Down Expand Up @@ -103,7 +105,7 @@
},
{
"model_id": "codex-mini-latest",
"provider": "openai",
"provider": "github-models",
"api": "chat",
"quality": { "T1": 0.75, "T2": 0.80, "T3": 0.78, "T4": 0.72, "T5": 0.65 },
"cost_score": 0.80,
Expand Down
1 change: 1 addition & 0 deletions docs/ci/WORKFLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ Scheduled health jobs keep the automation ecosystem aligned:
* [`maint-71-merge-sync-prs.yml`](../../.github/workflows/maint-71-merge-sync-prs.yml) automates merging sync PRs in consumer repos - checks status, merges passing PRs, cleans up stale PRs, and deletes leftover `sync/workflows-*` branches tied to closed or merged sync PRs (manual dispatch).
* [`maint-72-fix-pr-body-conflicts.yml`](../../.github/workflows/maint-72-fix-pr-body-conflicts.yml) removes pr_body.md from main branch and adds to .gitignore across consumer repos - prevents merge conflicts from PR description files (manual dispatch, weekly schedule).
* [`maint-74-ledger-base-sync.yml`](../../.github/workflows/maint-74-ledger-base-sync.yml) aligns `.agents` ledger base entries to the repository default branch on a weekly schedule or manual dispatch.
* [`maint-77-model-registry-freshness.yml`](../../.github/workflows/maint-77-model-registry-freshness.yml) checks the canonical LLM model registry and slot pins for stale review dates, blocked or unknown pins, and provider-dominated pinned models without contacting provider APIs (scheduled weekly, manual dispatch, PR gate for registry/slot/checker changes).
* [`maint-80-langsmith-metrics-dashboard.yml`](../../.github/workflows/maint-80-langsmith-metrics-dashboard.yml) generates weekly LangSmith trace coverage dashboard - downloads metrics from autopilot artifacts, computes coverage, creates issue report (scheduled Monday 9AM UTC, manual dispatch).
* [`maint-81-langsmith-fleet-conformance.yml`](../../.github/workflows/maint-81-langsmith-fleet-conformance.yml) validates fleet artifact coverage against `config/langsmith_fleet_registry.json` and reports missing/stale/invalid records (scheduled Monday 9:30AM UTC, manual dispatch with optional enforcement).
* [`maint-82-sync-dependency-campaign.yml`](../../.github/workflows/maint-82-sync-dependency-campaign.yml) refreshes a GitHub-visible sync/dependency campaign issue so local Codex only claims queued bot-review work when remote discovery finds active review threads.
Expand Down
1 change: 1 addition & 0 deletions docs/ci/WORKFLOW_SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,7 @@ Keep this table handy when you are triaging automation: it confirms which workfl
| **Merge Sync PRs** (`maint-71-merge-sync-prs.yml`, maintenance bucket) | `workflow_dispatch`, `workflow_call` | Automates merging sync PRs across consumer repos. Checks CI status, merges passing PRs, cleans up stale sync PRs. Reads consumer repo list from maint-68-sync-consumer-repos.yml. | ⚪ Manual/callable | [Merge sync runs](https://github.com/stranske/Workflows/actions/workflows/maint-71-merge-sync-prs.yml) |
| **Maint 72 Fix PR Body Conflicts** (`maint-72-fix-pr-body-conflicts.yml`, maintenance bucket) | `workflow_dispatch`, `schedule` (weekly) | Removes `pr_body.md` from main and adds to `.gitignore` in consumer repos to prevent merge conflicts. | ⚪ Manual/scheduled | [PR body fix runs](https://github.com/stranske/Workflows/actions/workflows/maint-72-fix-pr-body-conflicts.yml) |
| **Maint 74 Ledger Base Sync** (`maint-74-ledger-base-sync.yml`, maintenance bucket) | `workflow_dispatch`, `schedule` (Mondays 06:00 UTC) | Align `.agents` ledger base entries to the repository default branch. | ⚪ Manual/scheduled | [Ledger base sync runs](https://github.com/stranske/Workflows/actions/workflows/maint-74-ledger-base-sync.yml) |
| **Maint 77 Model Registry Freshness** (`maint-77-model-registry-freshness.yml`, maintenance bucket) | `schedule` (Mondays 05:20 UTC), `workflow_dispatch`, `pull_request` (registry/slot/checker paths) | Runs an offline model-registry freshness gate over `config/model_registry.json` and `config/llm_slots.json`; PRs fail on stale or dominated pins while scheduled/manual runs refresh a tracking issue. | ⚪ Scheduled/manual + PR gate | [Model registry freshness runs](https://github.com/stranske/Workflows/actions/workflows/maint-77-model-registry-freshness.yml) |
| **LangSmith Metrics Dashboard** (`maint-80-langsmith-metrics-dashboard.yml`, maintenance bucket) | `workflow_dispatch`, `schedule` (Mondays 09:00 UTC) | Generates weekly LangSmith trace coverage dashboard by downloading metrics from autopilot artifacts, computing coverage, and creating issue reports. | ⚪ Manual/scheduled | [LangSmith metrics runs](https://github.com/stranske/Workflows/actions/workflows/maint-80-langsmith-metrics-dashboard.yml) |
| **LangSmith Fleet Conformance** (`maint-81-langsmith-fleet-conformance.yml`, maintenance bucket) | `workflow_dispatch`, `schedule` (Mondays 09:30 UTC) | Validates LangSmith fleet artifact coverage against `config/langsmith_fleet_registry.json`, emits markdown/JSON reports, and can optionally enforce non-valid rows. | ⚪ Manual/scheduled | [LangSmith fleet conformance runs](https://github.com/stranske/Workflows/actions/workflows/maint-81-langsmith-fleet-conformance.yml) |
| **Sync/Dependency Campaign** (`maint-82-sync-dependency-campaign.yml`, maintenance bucket) | `schedule`, `workflow_dispatch`, `repository_dispatch` | Refreshes a GitHub-visible campaign issue for sync-generated and dependency-bot PRs with active bot review threads so local Codex only claims queued work when remote discovery finds it. | ⚪ Scheduled/manual | [Sync/Dependency campaign runs](https://github.com/stranske/Workflows/actions/workflows/maint-82-sync-dependency-campaign.yml) |
Expand Down
152 changes: 152 additions & 0 deletions tests/test_check_model_registry_freshness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Tests for the model-registry freshness gate (tools/check_model_registry_freshness.py)."""

from __future__ import annotations

import datetime as dt
import json
from pathlib import Path

from tools import check_model_registry_freshness as gate

TODAY = dt.date(2026, 6, 28)


def _registry(**over):
base = {
"version": "1.0.0",
"review_by": "2026-12-31",
"models": [
{"model_id": "gpt-5.4", "provider": "openai", "quality": {"T5": 0.97}},
{"model_id": "gpt-5.1", "provider": "openai", "quality": {"T5": 0.93}},
{"model_id": "claude-sonnet-4-6", "provider": "anthropic", "quality": {"T5": 0.95}},
{
"model_id": "old-blocked",
"provider": "openai",
"quality": {"T5": 0.30},
"blocked": True,
},
],
}
base.update(over)
return base


def _slots(*pairs):
return {
"slots": [
{"name": f"slot{i+1}", "provider": p, "model": m} for i, (p, m) in enumerate(pairs)
]
}


def _kinds(findings):
return sorted(f["kind"] for f in findings)


def test_fresh_registry_has_no_findings():
findings = gate.evaluate(_registry(), _slots(("openai", "gpt-5.4")), today=TODAY)
assert findings == []


def test_review_overdue_explicit_review_by():
reg = _registry(review_by="2026-05-01")
findings = gate.evaluate(reg, _slots(("openai", "gpt-5.4")), today=TODAY)
assert _kinds(findings) == ["review_overdue"]
assert "overdue" in findings[0]["detail"]


def test_review_overdue_derived_from_last_updated():
reg = _registry()
reg.pop("review_by")
reg["last_updated"] = "2026-04-14" # +60d -> 2026-06-13 < today
findings = gate.evaluate(reg, _slots(("openai", "gpt-5.4")), today=TODAY, max_age_days=60)
assert "review_overdue" in _kinds(findings)


def test_review_not_overdue_within_window():
reg = _registry()
reg.pop("review_by")
reg["last_updated"] = "2026-06-20" # +60d in the future
findings = gate.evaluate(reg, _slots(("openai", "gpt-5.4")), today=TODAY, max_age_days=60)
assert "review_overdue" not in _kinds(findings)


def test_blocked_pin():
findings = gate.evaluate(_registry(), _slots(("openai", "old-blocked")), today=TODAY)
assert _kinds(findings) == ["blocked_pin"]


def test_unknown_pin():
findings = gate.evaluate(_registry(), _slots(("openai", "gpt-9-imaginary")), today=TODAY)
assert _kinds(findings) == ["unknown_pin"]


def test_dominated_pin_flags_better_same_provider_model():
# Pin sonnet-4-6 (0.95) while registry also has a higher claude model.
reg = _registry()
reg["models"].append(
{"model_id": "claude-opus-4-6", "provider": "anthropic", "quality": {"T5": 0.98}}
)
findings = gate.evaluate(reg, _slots(("anthropic", "claude-sonnet-4-6")), today=TODAY)
assert _kinds(findings) == ["dominated_pin"]
assert "claude-opus-4-6" in findings[0]["detail"]


def test_dominated_pin_uses_slot_quality_tier():
reg = _registry()
reg["models"] = [
{
"model_id": "primary-for-t4",
"provider": "openai",
"quality": {"T4": 0.95, "T5": 0.80},
},
{
"model_id": "better-t5-only",
"provider": "openai",
"quality": {"T4": 0.90, "T5": 0.99},
},
]
slots = {
"slots": [
{
"name": "slot1",
"provider": "openai",
"model": "primary-for-t4",
"quality_tier": "T4",
}
]
}
findings = gate.evaluate(reg, slots, today=TODAY)
assert findings == []


def test_tier_derived_slot_without_model_is_not_flagged():
# A slot with no pinned model derives from the registry at runtime -> non-ossifying.
slots = {"slots": [{"name": "slot1", "provider": "anthropic", "quality_tier": "T5"}]}
findings = gate.evaluate(_registry(), slots, today=TODAY)
assert findings == []


def test_real_repo_files_parse_and_run(tmp_path):
# The shipped config must at least load and evaluate without raising.
root = Path(__file__).resolve().parent.parent
reg = json.loads((root / "config" / "model_registry.json").read_text())
slots = json.loads((root / "config" / "llm_slots.json").read_text())
findings = gate.evaluate(reg, slots, today=TODAY)
assert isinstance(findings, list)


def test_main_exit_codes(tmp_path):
reg = tmp_path / "reg.json"
slots = tmp_path / "slots.json"
reg.write_text(json.dumps(_registry(review_by="2026-05-01")))
slots.write_text(json.dumps(_slots(("openai", "gpt-5.4"))))
rc = gate.main(
["--registry", str(reg), "--slots", str(slots), "--today", "2026-06-28", "--json"]
)
assert rc == 1 # overdue
reg.write_text(json.dumps(_registry(review_by="2026-12-31")))
rc = gate.main(["--registry", str(reg), "--slots", str(slots), "--today", "2026-06-28"])
assert rc == 0
rc = gate.main(["--registry", str(reg), "--slots", str(slots), "--today", "not-a-date"])
assert rc == 2
2 changes: 1 addition & 1 deletion tests/tools/test_langchain_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def test_build_chat_client_anthropic_fallback(monkeypatch: pytest.MonkeyPatch) -
assert resolved.provider == langchain_client.PROVIDER_ANTHROPIC
assert isinstance(resolved.client, FakeChatAnthropic)
assert resolved.client.kwargs["anthropic_api_key"] == "claude-token"
assert resolved.model == "claude-sonnet-4-6"
assert resolved.model == "claude-opus-4-6"


def test_build_chat_client_anthropic_without_openai_package(
Expand Down
1 change: 1 addition & 0 deletions tests/workflows/test_workflow_naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ def test_workflow_display_names_are_unique():
"maint-71-auto-fix-integration.yml": "Auto-Fix Integration Test Failures",
"maint-71-merge-sync-prs.yml": "Merge Sync PRs",
"maint-74-ledger-base-sync.yml": "Ledger Base Sync",
"maint-77-model-registry-freshness.yml": "Maint 77 Model Registry Freshness",
"maint-80-langsmith-metrics-dashboard.yml": "LangSmith Metrics Dashboard",
"maint-81-langsmith-fleet-conformance.yml": "LangSmith Fleet Conformance",
"maint-82-sync-dependency-campaign.yml": "Sync/Dependency Campaign",
Expand Down
Loading
Loading