Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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: 2 additions & 0 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",
"review_interval_days": 60,
"review_by": "2026-06-13",
"models": [
{
"model_id": "gpt-5.4",
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 changes).
Comment thread
stranske marked this conversation as resolved.
Outdated
* [`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
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