Skip to content

fix(actions): Lopu repairs failed PR checks - #425

Closed
lopugit wants to merge 1 commit into
github-actionsfrom
lopu/workflow-check-fix-33028418796
Closed

fix(actions): Lopu repairs failed PR checks#425
lopugit wants to merge 1 commit into
github-actionsfrom
lopu/workflow-check-fix-33028418796

Conversation

@lopugit

@lopugit lopugit commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Lopu controller check repair

Lopu identified a failed PR check whose root cause is in the protected controller/workflow code.

Lopu control-plane fix — widen the AI model waterfall grammar

Diagnosis

PR #388 (feat(admin): unlimited AI model waterfall with full Claude + OpenAI catalog, per-entry effort and fast mode) turns the Admin "AI workflow model
order" into an open catalog whose entries are composed ids
(<model>[:<effort>][:fast], e.g. claude-opus-5:high:fast, gpt-5.6-sol:ultra)
with no length cap.

The protected control plane still validated that setting against the historical
closed trio and a 1..3 length bound:

| select(length >= 1 and length <= 3)
| select(all(.[]; . == "default" or . == "claude-fable-5" or . == "claude-opus-5"))

Because that is a single whole-array select, one new-style entry made the
whole filter fail, so the loader fell back to ["default"] — and the bash
mapping loop then hard-failed closed on the first unrecognised id as well.

Net effect once #388 ships: the moment an administrator uses the new feature at
all, the conflict resolver, the rebase/stack-cascade engine, and the all-branch
build doctor silently discard their entire model preference — including the
plain Claude entries the control plane can perfectly well run — and drop to the
Claude Code default. The PR's own live-browser test order
(['claude-opus-5:high:fast','default','gpt-5.6-sol:ultra','gpt-5.3-codex:xhigh','claude-haiku-4-5'])
reproduced exactly this: --model default, admin preference lost.

This is a control-plane defect, not a PR-branch defect: nothing in #388's
worktree can fix it, and #388's description explicitly defers it here.

Change

Three loaders (resolve-pr-conflicts.yml, rebase-pr-stacks.yml,
all-branch.yml) now share one design:

  1. Shape validation, not name validation. The jq stage bounds the array
    (1..512 — the catalog admits 226 unique composed ids today, so a legitimate
    order can never trip it; --max-filesize 65536 remains the real bound),
    requires uniqueness, and gates every entry on
    ^[a-z0-9][a-z0-9.-]{0,47}(:[a-z]{1,8}){0,2}$. No space, quote, comma, or
    leading dash survives that charset, which is precisely what makes a value
    safe to place next to a CLI flag.
  2. Provider eligibility comes from the response's own catalog. The loader
    reads .models[].provider from the same payload and keeps only
    anthropic/default bases. A pre-feat(admin): unlimited AI model waterfall with full Claude + OpenAI catalog, per-entry effort and fast mode #388 deployment publishes no provider
    field and was Claude-only, so every id it lists stays eligible — the change
    is backward compatible with production today.
  3. Variant segments are stripped (base="${id%%:*}") because Claude Code
    consumes a bare model id, then collapsed order-preservingly so
    claude-opus-5:high and claude-opus-5:max yield one CLI entry.
  4. Skip, don't fail closed. An entry this backend cannot run (an OpenAI
    model, or one absent from the catalog) is skipped with a ::notice::; the
    rest of the administrator's order survives. Only an empty result falls back
    to ["default"].
  5. default is re-appended if dedup/truncation would drop it, and the emitted
    chain is capped at 16 with an explicit ::warning:: (no silent cap) so
    MODEL_ARGS stays inside the downstream 2048-character audit budget even
    for a hostile catalog response.

rebase-pr-stacks.yml additionally gains the literal-rebuild path the other two
already had — it previously interpolated jq -r '.[0]' straight into
--model $primary and leaned entirely on the closed name allowlist for safety.

No model name is pinned anywhere. That was the point of the pre-existing
contains no legacy model / contains no hardcoded --model selection
assertions, and enumerating the eight current Claude models would have
reintroduced exactly the drifting second catalog they guard against. Both
contract scripts were updated to assert the new invariant, and the model-name
check is now a negative assertion (pins no model id) — strictly stronger
than the positive allowlist it replaces.

Validation

  • python3 -c "yaml.safe_load(...)" on all three workflows — parse clean.

  • node .github/scripts/workflow-control-plane-contract.mjs --self-test — OK.

  • node .github/scripts/resolve-pr-conflicts-routing-contract.mjs --self-test — OK.

  • Behavioural test of the real step scripts, extracted from the YAML with
    only curl swapped for a fixture copy, run for all three loaders:

    fixture result
    ["default"] --model default (unchanged)
    legacy trio --model claude-opus-5 --fallback-model claude-fable-5,default (unchanged)
    feat(admin): unlimited AI model waterfall with full Claude + OpenAI catalog, per-entry effort and fast mode #388 live order --model claude-opus-5 --fallback-model default,claude-haiku-4-5 + ::notice::Skipped 2was --model default
    OpenAI-only above default --model default + skip notice
    claude-opus-5:high + claude-opus-5:max collapses to one claude-opus-5
    unknown id mixed in unknown dropped, rest preserved
    pre-feat(admin): unlimited AI model waterfall with full Claude + OpenAI catalog, per-entry effort and fast mode #388 response (no provider) --model claude-opus-5 --fallback-model default
    "claude-opus-5; rm -rf /" rejected, fails closed to default
    "--dangerously-skip-permissions" rejected, fails closed to default
    fetch failure default
    20 runnable entries, default last capped at 16 + ::warning::, default preserved, MODEL_ARGS 774 chars

Not changed

Effort and fast-mode segments are parsed and stripped, not forwarded: the
Claude Code CLI handoff in this plane takes --model only, and its --effort
is set per-invocation by the workflow. Plumbing per-entry effort through to the
CLI is a separate change with its own argument-grammar surface.

Source Lopu workflow run

@lopugit

lopugit commented Aug 27, 2026

Copy link
Copy Markdown
Owner Author

Closing as fully superseded by merged PR #391. #391 contains the complete controller grammar (model, effort, fast mode, composed model:effort:fast forms, transport bounds, and mutation-tested contracts), while this branch is the earlier partial implementation. No unique fix remains to merge.

@lopugit lopugit closed this Aug 27, 2026
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