Skip to content

feat(pr-pipeline): route lane models by role, falling back to opus - #8722

Merged
kyle-sexton merged 6 commits into
mainfrom
feat/pr-pipeline-model-routes
Oct 21, 2026
Merged

kyle-sexton merged 6 commits into
mainfrom
feat/pr-pipeline-model-routes

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

No related issue: Phase 5a of the lane-funding write-lane plan (model routing that fix-ci needs before it moves to run-claude-cli).

Broker pin: melodic-software/azure-iac#69

Hold: the merge waits for Phase 5b (fix-ci onto run-claude-cli), so both land back to back. On its own, this change moves fix-ci to opus at high effort on the subscription.

Summary

Lanes get their model from a role instead of a fixed model id. A profile names a role, a workload and an effort-role. resolve-config resolves them through plugins/multi-agent/reference/defaults.yaml on the base. A route never resolves to an empty model, default, inherit, a model id or the frontier tier.

  • New code-fix profile: role worker, workload code, effort-role verifier, max-turns 75. models.routes.fix-ci: code-fix. The route stays a plain profile name, which the token broker's pinned reader requires.
  • inherit, an absent or unreadable role map, and any role that resolves outside haiku, sonnet or opus all fall back to opus.
  • New applied-by-patch output. It is written only when the selected activity sets applied-by.patch.

Fix

  • .github/actions/resolve-config/resolve.mjs: role-profile resolution (roleMapOf, roleModels, ROUTED_ALIASES, FALLBACK_MODEL). It adds no import.
  • .github/actions/resolve-config/resolve-config.mjs, action.yml: the applied-by-patch output.
  • docs/conventions/pr-pipeline.yaml: the code-fix profile and the fix-ci route.
  • docs/conventions/pr-pipeline/pr-pipeline.schema.json: role profiles take no model or effort.
  • .github/actions/resolve-config/models.test.mjs: 26 new tests.
  • A .changes/pr-pipeline fragment.

Verification

  • node --test --test-concurrency=1 '.github/actions/resolve-config/**/*.test.mjs': 483 of 483 pass.
  • A broker comparison of the pinned reader 94f6031 against this head shows 0 grant differences over 13 lane slots.
  • uv run --with pyyaml bash scripts/check-affected.sh --tests: exit 0.
  • fix-ci resolves to --model opus --max-turns 75 --effort high, or to --model opus --max-turns 75 when the base has no role map. Every other slot and run route is unchanged.
  • A fresh-context verifier passed 9 of 9 criteria.

Related

  • Lands with Phase 5b, which moves fix-ci to run-claude-cli with the automations API key first and the subscription only on a credit stop.

🤖 Generated with Claude Code

https://claude.ai/code/session_015WJUza2M1rzSUKQrtdWrXw

kyle-sexton and others added 3 commits October 20, 2026 15:19
A models profile may now name a multi-agent role (role, workload,
effort-role) in place of a model. resolve-config reads the base's
plugins/multi-agent/reference/defaults.yaml and resolves the role's
model and effort from it. inherit, a missing role map or role, and any
value that is not an alias (default, a model ID) run on opus. The schema
refuses model and effort keys on a role profile, so a role route can
never carry default or a model ID.

fix-ci routes to code-fix: a worker on code at the verifier's effort,
75 turns. resolve.mjs gains no import, and the role sits on a profile,
not the route, so the broker's pinned loadConfig still accepts the
config. New resolve-config output applied-by-patch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015WJUza2M1rzSUKQrtdWrXw
A role whose model resolves to fable or best now runs on opus, like any
other value outside the routed aliases, so CI never picks Fable on its
own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015WJUza2M1rzSUKQrtdWrXw
A role map that parses but cannot convert (a YAML alias, which the
reader refuses) no longer throws out of resolve and fails every slot of
the lane; role routes fall back to opus as with no role map. The live
route test now requires one of haiku, sonnet or opus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015WJUza2M1rzSUKQrtdWrXw
…l-routes

# Conflicts:
#	docs/conventions/pr-pipeline.yaml
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 21, 2026 18:15
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T18:16:42.286632Z bbfbee3 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 3ffd62cb9a5ab3303b142283bdc0b3ab5eb23116: 0 finding(s), 0 as inline comments.

Security review: PR #8722 at 3ffd62c

Findings: 0.

No security issues found. Role profiles resolve their model and effort from plugins/multi-agent/reference/defaults.yaml. They add no new trust boundary.

  • Source of the role map. resolve-config.mjs reads it through readUnderBase, so it comes from the trusted base checkout and never from the PR head. readUnderBase resolves the real path and rejects anything outside base-path. A PR cannot change which model or effort its own lane runs on.
  • Parsing. roleMapOf parses with maxAliasCount: 0, merge: false and uniqueKeys: true. A malformed or alias-bearing file counts as absent and falls back to opus. roleOf uses Object.hasOwn, so keys like __proto__ cannot reach the prototype. The schema enums restrict role, workload and effort-role.
  • Output injection. The model is checked against {haiku, opus, sonnet} and the effort against {low, medium, high, xhigh, max}. Anything else becomes opus or no effort, so only whitelisted tokens reach claude-args and GITHUB_OUTPUT.
  • Frontier tier. fable and best map to opus, so the change cannot route an automated run onto a more expensive tier.
  • applied-by-patch output. It echoes the applied-by.patch value from the base config. That value is checked as a script activity inside the lane, already enforced by resolve.mjs.
  • Schema. The oneOf stops a role profile from also carrying model or effort, so it cannot name default or a model ID.
  • Behaviour change. The fix-ci route moves from the standard profile to code-fix. This only picks the model, effort and turn budget (still 75), and grants no new permissions or tools.
  • Workflow checks. I did not review the workflow-hardening checks (triggers, permissions, pins), which are zizmor's lane.

Manifest items: the diff touches no .claude-plugin/plugin.json or marketplace.json, so there are no manifest <field> lines.

Instruction-surface deletions: none. The README and YAML comment edits only add text or reword it without softening any rule. I did not read the instruction exception register, because no standing instruction was deleted or narrowed.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed 3ffd62cb9a5ab3303b142283bdc0b3ab5eb23116: 0 finding(s), 0 as inline comments.

Code review: #8722 at 3ffd62c

Findings: 0. No inline comments were posted. I did not run the test suite, so the PR's "483 of 483 pass" and "0 grant differences" are author-claimed and unverified by me.

Closing issue: the PR says "No related issue", so there are no acceptance criteria to check.

Rules and manifests: no .claude/rules/ file with a paths: glob matches the changed files. The diff touches no plugin.json or marketplace.json, so there are no manifest <field> lines. The .changes fragment's bump: minor fits a new output and a new profile kind.

What I checked:

  • Every other reader of models.profiles goes through routeModels. checkModels only checks profile names, and the --model/--effort builder reads the resolved model and effort. No path reads a literal profile.model from a role profile.
  • The role and workload enums match plugins/multi-agent/reference/defaults.yaml.
  • The role-map reader refuses YAML aliases (maxAliasCount: 0), as the existing config reader does.
  • The opus fallback covers inherit, default, a model ID, fable, best, an unknown role and an unparsable map.
  • The schema oneOf rejects model beside role, and workload or effort-role without role.
  • applied-by-patch is declared in action.yml, written only when applied-by.patch is set, and tested.

Latent risk (not a finding): the sparse-checkout callers (pr-review-hosted.yml, pr-review-security-hosted.yml, intake-triage.yml) don't include plugins/multi-agent/. They route only to model profiles today, so nothing breaks. A future route from them to a role profile would silently fall back to opus with no --effort. callers.test.mjs BASE_FILES would not catch it. The activity workflows that resolve fix-ci (pr-run-activity-write.yml and pr-run-activity-read.yml) check out the base without a sparse list, so fix-ci is unaffected.

Merge hold: the PR body says the merge waits for Phase 5b (fix-ci onto run-claude-cli). That is a merge-ordering hold for the owner, not a code defect.

@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit e9823c5 Oct 21, 2026
27 checks passed
@kyle-sexton
kyle-sexton deleted the feat/pr-pipeline-model-routes branch October 21, 2026 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant