From b51347a5f8b30d6f54749eafe1c5932dab377743 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 05:23:39 +0000 Subject: [PATCH 1/4] Initial plan From c1661f530aa9254d83684fec86baad58aec397d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 05:31:34 +0000 Subject: [PATCH 2/4] repo-architect: switch preferred model to claude-sonnet-4.6 with gemini fallback Co-authored-by: Steake <530040+Steake@users.noreply.github.com> --- .github/workflows/repo-architect.yml | 4 +- repo_architect.py | 16 ++++---- tests/test_repo_architect.py | 58 +++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/.github/workflows/repo-architect.yml b/.github/workflows/repo-architect.yml index 818d447..3e82974 100644 --- a/.github/workflows/repo-architect.yml +++ b/.github/workflows/repo-architect.yml @@ -83,8 +83,8 @@ jobs: GITHUB_REPO: ${{ github.repository }} GITHUB_BASE_BRANCH: ${{ github.event.repository.default_branch }} REPO_ARCHITECT_BRANCH_SUFFIX: ${{ github.run_id }}-${{ github.run_attempt }} - REPO_ARCHITECT_PREFERRED_MODEL: openai/gpt-5.4 - REPO_ARCHITECT_FALLBACK_MODEL: openai/gpt-4.1 + REPO_ARCHITECT_PREFERRED_MODEL: anthropic/claude-sonnet-4.6 + REPO_ARCHITECT_FALLBACK_MODEL: google/gemini-3-pro run: | MODE="${{ github.event.inputs.mode }}" MODEL="${{ github.event.inputs.github_model }}" diff --git a/repo_architect.py b/repo_architect.py index 94c96e4..f812538 100644 --- a/repo_architect.py +++ b/repo_architect.py @@ -60,8 +60,8 @@ } # Model selection defaults -DEFAULT_PREFERRED_MODEL = "openai/gpt-5.4" -DEFAULT_FALLBACK_MODEL = "openai/gpt-4.1" +DEFAULT_PREFERRED_MODEL = "anthropic/claude-sonnet-4.6" +DEFAULT_FALLBACK_MODEL = "google/gemini-3-pro" # Substrings in HTTP error bodies that indicate the model itself is unavailable (not a transient error) _MODEL_UNAVAILABLE_SIGNALS = frozenset({ "unknown_model", "model_not_found", "unsupported_model", "unsupported model", @@ -697,7 +697,7 @@ def build_analysis(root: pathlib.Path) -> Dict[str, Any]: # ----------------------------- def enrich_with_github_models(config: Config, analysis: Dict[str, Any]) -> Dict[str, Any]: - preferred = config.preferred_model or config.github_model + preferred = config.github_model or config.preferred_model fallback = config.fallback_model meta: Dict[str, Any] = { "enabled": False, @@ -975,7 +975,7 @@ def build_parse_errors_plan(config: Config, analysis: Dict[str, Any]) -> Optiona errors = analysis.get("parse_error_files", []) if not errors: return None - preferred = config.preferred_model or config.github_model + preferred = config.github_model or config.preferred_model if not config.github_token or not preferred: return None fallback = config.fallback_model @@ -1042,7 +1042,7 @@ def build_import_cycles_plan(config: Config, analysis: Dict[str, Any]) -> Option cycles = analysis.get("cycles", []) if not cycles: return None - preferred = config.preferred_model or config.github_model + preferred = config.github_model or config.preferred_model if not config.github_token or not preferred: return None fallback = config.fallback_model @@ -1128,7 +1128,7 @@ def build_entrypoint_consolidation_plan(config: Config, analysis: Dict[str, Any] backend_eps = clusters.get("backend_servers", []) if len(backend_eps) < _ENTRYPOINT_CONSOLIDATION_THRESHOLD: return None - preferred = config.preferred_model or config.github_model + preferred = config.github_model or config.preferred_model if not config.github_token or not preferred: return None fallback = config.fallback_model @@ -1488,8 +1488,8 @@ def workflow_yaml(secret_env_names: Sequence[str], cron: str, github_model: Opti GITHUB_REPO: ${{{{ github.repository }}}} GITHUB_BASE_BRANCH: ${{{{ github.event.repository.default_branch }}}} REPO_ARCHITECT_BRANCH_SUFFIX: ${{{{ github.run_id }}}}-${{{{ github.run_attempt }}}} - REPO_ARCHITECT_PREFERRED_MODEL: openai/gpt-5.4 - REPO_ARCHITECT_FALLBACK_MODEL: openai/gpt-4.1 + REPO_ARCHITECT_PREFERRED_MODEL: anthropic/claude-sonnet-4.6 + REPO_ARCHITECT_FALLBACK_MODEL: google/gemini-3-pro {extra_env} run: | MODE="${{{{ github.event.inputs.mode }}}}" MODEL="${{{{ github.event.inputs.github_model }}}}" diff --git a/tests/test_repo_architect.py b/tests/test_repo_architect.py index a84e623..fca5254 100644 --- a/tests/test_repo_architect.py +++ b/tests/test_repo_architect.py @@ -250,7 +250,61 @@ def side_effect(token: str, model: str, messages: list) -> dict: # --------------------------------------------------------------------------- -# 3. Syntax validation of generated Python (ast.parse gate) +# 3. Model configuration behaviour +# --------------------------------------------------------------------------- + +class TestModelConfiguration(unittest.TestCase): + def test_build_config_uses_env_models_when_github_model_blank(self) -> None: + env = dict(os.environ) + env.pop("GITHUB_MODEL", None) + env["REPO_ARCHITECT_PREFERRED_MODEL"] = "anthropic/claude-sonnet-4.6" + env["REPO_ARCHITECT_FALLBACK_MODEL"] = "google/gemini-3-pro" + with patch.object(ra, "discover_git_root", return_value=pathlib.Path("/tmp/repo")): + with patch.dict(os.environ, env, clear=True): + config = ra.build_config(ra.parse_args([])) + self.assertIsNone(config.github_model) + self.assertEqual(config.preferred_model, "anthropic/claude-sonnet-4.6") + self.assertEqual(config.fallback_model, "google/gemini-3-pro") + + def test_github_model_override_takes_precedence_over_preferred(self) -> None: + analysis = { + "architecture_score": 0.8, + "cycles": [], + "parse_error_files": [], + "entrypoint_paths": [], + "roadmap": [], + } + response = {"choices": [{"message": {"content": "ok"}}], "model": "openai/manual-override"} + with tempfile.TemporaryDirectory() as tmp: + root = _make_git_root(tmp) + config = _make_config( + root, + github_token="tok", + github_model="openai/manual-override", + preferred_model="anthropic/claude-sonnet-4.6", + fallback_model="google/gemini-3-pro", + ) + with patch.object( + ra, + "call_models_with_fallback_or_none", + return_value=(response, "openai/manual-override", None, False), + ) as mocked_call: + meta = ra.enrich_with_github_models(config, analysis) + self.assertEqual(mocked_call.call_args.args[1], "openai/manual-override") + self.assertEqual(mocked_call.call_args.args[2], "google/gemini-3-pro") + self.assertEqual(meta["requested_model"], "openai/manual-override") + self.assertEqual(meta["actual_model"], "openai/manual-override") + + def test_workflow_yaml_uses_new_model_defaults_and_blank_override_logic(self) -> None: + workflow = ra.workflow_yaml([], "17 * * * *", None) + self.assertIn("REPO_ARCHITECT_PREFERRED_MODEL: anthropic/claude-sonnet-4.6", workflow) + self.assertIn("REPO_ARCHITECT_FALLBACK_MODEL: google/gemini-3-pro", workflow) + self.assertIn('if [ -n "$MODEL" ]; then EXTRA_ARGS="$EXTRA_ARGS --github-model $MODEL"; fi', workflow) + self.assertIn("models: read", workflow) + + +# --------------------------------------------------------------------------- +# 4. Syntax validation of generated Python (ast.parse gate) # --------------------------------------------------------------------------- class TestSyntaxValidationGate(unittest.TestCase): @@ -313,7 +367,7 @@ def test_build_parse_errors_plan_accepts_valid_fix(self) -> None: # --------------------------------------------------------------------------- -# 4. Campaign aggregation behaviour +# 5. Campaign aggregation behaviour # --------------------------------------------------------------------------- class TestCampaignAggregation(unittest.TestCase): From a321856b375d1623ee9f060f97545026caad3c10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 05:52:29 +0000 Subject: [PATCH 3/4] repo-architect: resolve models via catalog with ordered fallback Co-authored-by: Steake <530040+Steake@users.noreply.github.com> --- .github/workflows/repo-architect.yml | 74 +++++++++++++++++++++++++++- repo_architect.py | 74 +++++++++++++++++++++++++++- tests/test_repo_architect.py | 12 +++-- 3 files changed, 153 insertions(+), 7 deletions(-) diff --git a/.github/workflows/repo-architect.yml b/.github/workflows/repo-architect.yml index 3e82974..e5747c4 100644 --- a/.github/workflows/repo-architect.yml +++ b/.github/workflows/repo-architect.yml @@ -77,14 +77,84 @@ jobs: run: | mkdir -p .agent docs/repo_architect + - name: Resolve GitHub Models configuration + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + python - <<'PY' + import json + import os + import urllib.request + + order = [ + "anthropic/claude-sonnet-4.6", + "anthropic/claude-sonnet-4.5", + "openai/gpt-4.1", + ] + secondary = "google/gemini-3-pro" + available = set() + try: + req = urllib.request.Request( + "https://models.github.ai/catalog/models", + headers={ + "Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}", + "Accept": "application/json", + "User-Agent": "repo-architect-workflow", + }, + ) + with urllib.request.urlopen(req, timeout=30) as resp: + payload = json.loads(resp.read().decode("utf-8")) + models = payload.get("data", payload) if isinstance(payload, dict) else payload + if isinstance(models, list): + for item in models: + if isinstance(item, dict): + model_id = item.get("id") or item.get("name") or item.get("model") + if isinstance(model_id, str) and model_id: + available.add(model_id) + except Exception as exc: + print(f"warning: GitHub Models catalog lookup failed; using defaults ({exc})") + + def first_available(candidates): + for candidate in candidates: + if candidate in available: + return candidate + return None + + if available: + preferred = first_available(order) + if preferred is None: + preferred = secondary if secondary in available else order[0] + else: + preferred = order[0] + + if secondary in available and secondary != preferred: + fallback = secondary + else: + fallback = ( + first_available([c for c in order if c != preferred]) + or (secondary if secondary in available else order[-1]) + ) + + if not isinstance(preferred, str) or not preferred: + preferred = order[0] + if not isinstance(fallback, str) or not fallback: + fallback = secondary if secondary != preferred else order[-1] + + env_file = os.environ.get("GITHUB_ENV") + if not env_file: + raise RuntimeError("GITHUB_ENV is not set; this internal workflow step must run inside GitHub Actions with environment-file support.") + with open(env_file, "a", encoding="utf-8") as fh: + fh.write(f"REPO_ARCHITECT_PREFERRED_MODEL={preferred}\n") + fh.write(f"REPO_ARCHITECT_FALLBACK_MODEL={fallback}\n") + print(f"selected preferred={preferred} fallback={fallback}") + PY + - name: Run repo architect env: GITHUB_TOKEN: ${{ github.token }} GITHUB_REPO: ${{ github.repository }} GITHUB_BASE_BRANCH: ${{ github.event.repository.default_branch }} REPO_ARCHITECT_BRANCH_SUFFIX: ${{ github.run_id }}-${{ github.run_attempt }} - REPO_ARCHITECT_PREFERRED_MODEL: anthropic/claude-sonnet-4.6 - REPO_ARCHITECT_FALLBACK_MODEL: google/gemini-3-pro run: | MODE="${{ github.event.inputs.mode }}" MODEL="${{ github.event.inputs.github_model }}" diff --git a/repo_architect.py b/repo_architect.py index f812538..03cf5ab 100644 --- a/repo_architect.py +++ b/repo_architect.py @@ -1482,14 +1482,84 @@ def workflow_yaml(secret_env_names: Sequence[str], cron: str, github_model: Opti run: | mkdir -p .agent docs/repo_architect + - name: Resolve GitHub Models configuration + env: + GITHUB_TOKEN: ${{{{ github.token }}}} + run: | + python - <<'PY' + import json + import os + import urllib.request + + order = [ + "anthropic/claude-sonnet-4.6", + "anthropic/claude-sonnet-4.5", + "openai/gpt-4.1", + ] + secondary = "google/gemini-3-pro" + available = set() + try: + req = urllib.request.Request( + "https://models.github.ai/catalog/models", + headers={{ + "Authorization": f"Bearer {{os.environ['GITHUB_TOKEN']}}", + "Accept": "application/json", + "User-Agent": "repo-architect-workflow", + }}, + ) + with urllib.request.urlopen(req, timeout=30) as resp: + payload = json.loads(resp.read().decode("utf-8")) + models = payload.get("data", payload) if isinstance(payload, dict) else payload + if isinstance(models, list): + for item in models: + if isinstance(item, dict): + model_id = item.get("id") or item.get("name") or item.get("model") + if isinstance(model_id, str) and model_id: + available.add(model_id) + except Exception as exc: + print(f"warning: GitHub Models catalog lookup failed; using defaults ({{exc}})") + + def first_available(candidates): + for candidate in candidates: + if candidate in available: + return candidate + return None + + if available: + preferred = first_available(order) + if preferred is None: + preferred = secondary if secondary in available else order[0] + else: + preferred = order[0] + + if secondary in available and secondary != preferred: + fallback = secondary + else: + fallback = ( + first_available([c for c in order if c != preferred]) + or (secondary if secondary in available else order[-1]) + ) + + if not isinstance(preferred, str) or not preferred: + preferred = order[0] + if not isinstance(fallback, str) or not fallback: + fallback = secondary if secondary != preferred else order[-1] + + env_file = os.environ.get("GITHUB_ENV") + if not env_file: + raise RuntimeError("GITHUB_ENV is not set; this internal workflow step must run inside GitHub Actions with environment-file support.") + with open(env_file, "a", encoding="utf-8") as fh: + fh.write(f"REPO_ARCHITECT_PREFERRED_MODEL={{preferred}}\\n") + fh.write(f"REPO_ARCHITECT_FALLBACK_MODEL={{fallback}}\\n") + print(f"selected preferred={{preferred}} fallback={{fallback}}") + PY + - name: Run repo architect env: GITHUB_TOKEN: ${{{{ github.token }}}} GITHUB_REPO: ${{{{ github.repository }}}} GITHUB_BASE_BRANCH: ${{{{ github.event.repository.default_branch }}}} REPO_ARCHITECT_BRANCH_SUFFIX: ${{{{ github.run_id }}}}-${{{{ github.run_attempt }}}} - REPO_ARCHITECT_PREFERRED_MODEL: anthropic/claude-sonnet-4.6 - REPO_ARCHITECT_FALLBACK_MODEL: google/gemini-3-pro {extra_env} run: | MODE="${{{{ github.event.inputs.mode }}}}" MODEL="${{{{ github.event.inputs.github_model }}}}" diff --git a/tests/test_repo_architect.py b/tests/test_repo_architect.py index fca5254..d67e024 100644 --- a/tests/test_repo_architect.py +++ b/tests/test_repo_architect.py @@ -295,10 +295,16 @@ def test_github_model_override_takes_precedence_over_preferred(self) -> None: self.assertEqual(meta["requested_model"], "openai/manual-override") self.assertEqual(meta["actual_model"], "openai/manual-override") - def test_workflow_yaml_uses_new_model_defaults_and_blank_override_logic(self) -> None: + def test_workflow_yaml_resolves_models_via_catalog_and_keeps_blank_override_logic(self) -> None: workflow = ra.workflow_yaml([], "17 * * * *", None) - self.assertIn("REPO_ARCHITECT_PREFERRED_MODEL: anthropic/claude-sonnet-4.6", workflow) - self.assertIn("REPO_ARCHITECT_FALLBACK_MODEL: google/gemini-3-pro", workflow) + self.assertIn("Resolve GitHub Models configuration", workflow) + self.assertIn("https://models.github.ai/catalog/models", workflow) + self.assertIn('"anthropic/claude-sonnet-4.6"', workflow) + self.assertIn('"anthropic/claude-sonnet-4.5"', workflow) + self.assertIn('"openai/gpt-4.1"', workflow) + self.assertIn('secondary = "google/gemini-3-pro"', workflow) + self.assertIn("REPO_ARCHITECT_PREFERRED_MODEL={preferred}", workflow) + self.assertIn("REPO_ARCHITECT_FALLBACK_MODEL={fallback}", workflow) self.assertIn('if [ -n "$MODEL" ]; then EXTRA_ARGS="$EXTRA_ARGS --github-model $MODEL"; fi', workflow) self.assertIn("models: read", workflow) From bd6f8a89e1f7586a567ec19621f68bab4af18a64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:09:43 +0000 Subject: [PATCH 4/4] repo-architect: avoid unavailable model selection after catalog lookup Co-authored-by: Steake <530040+Steake@users.noreply.github.com> --- .github/workflows/repo-architect.yml | 32 +++++++++++++++++++--------- repo_architect.py | 32 +++++++++++++++++++--------- tests/test_repo_architect.py | 5 +++++ 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/.github/workflows/repo-architect.yml b/.github/workflows/repo-architect.yml index e5747c4..2d2bba8 100644 --- a/.github/workflows/repo-architect.yml +++ b/.github/workflows/repo-architect.yml @@ -93,6 +93,7 @@ jobs: ] secondary = "google/gemini-3-pro" available = set() + catalog_ok = False try: req = urllib.request.Request( "https://models.github.ai/catalog/models", @@ -106,6 +107,7 @@ jobs: payload = json.loads(resp.read().decode("utf-8")) models = payload.get("data", payload) if isinstance(payload, dict) else payload if isinstance(models, list): + catalog_ok = True for item in models: if isinstance(item, dict): model_id = item.get("id") or item.get("name") or item.get("model") @@ -120,20 +122,30 @@ jobs: return candidate return None - if available: - preferred = first_available(order) - if preferred is None: - preferred = secondary if secondary in available else order[0] + def deterministic_available(exclude=None): + candidates = sorted(m for m in available if m != exclude) + return candidates[0] if candidates else None + + if catalog_ok and available: + preferred = ( + first_available(order) + or (secondary if secondary in available else None) + or deterministic_available() + ) else: preferred = order[0] - if secondary in available and secondary != preferred: - fallback = secondary + if catalog_ok and available: + if secondary in available and secondary != preferred: + fallback = secondary + else: + fallback = ( + first_available([c for c in order if c != preferred]) + or deterministic_available(exclude=preferred) + or preferred + ) else: - fallback = ( - first_available([c for c in order if c != preferred]) - or (secondary if secondary in available else order[-1]) - ) + fallback = secondary if not isinstance(preferred, str) or not preferred: preferred = order[0] diff --git a/repo_architect.py b/repo_architect.py index 03cf5ab..e64cc5f 100644 --- a/repo_architect.py +++ b/repo_architect.py @@ -1498,6 +1498,7 @@ def workflow_yaml(secret_env_names: Sequence[str], cron: str, github_model: Opti ] secondary = "google/gemini-3-pro" available = set() + catalog_ok = False try: req = urllib.request.Request( "https://models.github.ai/catalog/models", @@ -1511,6 +1512,7 @@ def workflow_yaml(secret_env_names: Sequence[str], cron: str, github_model: Opti payload = json.loads(resp.read().decode("utf-8")) models = payload.get("data", payload) if isinstance(payload, dict) else payload if isinstance(models, list): + catalog_ok = True for item in models: if isinstance(item, dict): model_id = item.get("id") or item.get("name") or item.get("model") @@ -1525,20 +1527,30 @@ def first_available(candidates): return candidate return None - if available: - preferred = first_available(order) - if preferred is None: - preferred = secondary if secondary in available else order[0] + def deterministic_available(exclude=None): + candidates = sorted(m for m in available if m != exclude) + return candidates[0] if candidates else None + + if catalog_ok and available: + preferred = ( + first_available(order) + or (secondary if secondary in available else None) + or deterministic_available() + ) else: preferred = order[0] - if secondary in available and secondary != preferred: - fallback = secondary + if catalog_ok and available: + if secondary in available and secondary != preferred: + fallback = secondary + else: + fallback = ( + first_available([c for c in order if c != preferred]) + or deterministic_available(exclude=preferred) + or preferred + ) else: - fallback = ( - first_available([c for c in order if c != preferred]) - or (secondary if secondary in available else order[-1]) - ) + fallback = secondary if not isinstance(preferred, str) or not preferred: preferred = order[0] diff --git a/tests/test_repo_architect.py b/tests/test_repo_architect.py index d67e024..5547b6c 100644 --- a/tests/test_repo_architect.py +++ b/tests/test_repo_architect.py @@ -299,10 +299,15 @@ def test_workflow_yaml_resolves_models_via_catalog_and_keeps_blank_override_logi workflow = ra.workflow_yaml([], "17 * * * *", None) self.assertIn("Resolve GitHub Models configuration", workflow) self.assertIn("https://models.github.ai/catalog/models", workflow) + self.assertIn("catalog_ok = False", workflow) self.assertIn('"anthropic/claude-sonnet-4.6"', workflow) self.assertIn('"anthropic/claude-sonnet-4.5"', workflow) self.assertIn('"openai/gpt-4.1"', workflow) self.assertIn('secondary = "google/gemini-3-pro"', workflow) + self.assertIn("def deterministic_available(exclude=None):", workflow) + self.assertIn("or deterministic_available()", workflow) + self.assertIn("or deterministic_available(exclude=preferred)", workflow) + self.assertIn("or preferred", workflow) self.assertIn("REPO_ARCHITECT_PREFERRED_MODEL={preferred}", workflow) self.assertIn("REPO_ARCHITECT_FALLBACK_MODEL={fallback}", workflow) self.assertIn('if [ -n "$MODEL" ]; then EXTRA_ARGS="$EXTRA_ARGS --github-model $MODEL"; fi', workflow)