feat(control-plane): honor the expanded Admin model waterfall (composed model:effort:fast ids) - #391
Conversation
PR #388 on develop expanded the Admin AI workflow model waterfall from a closed 3-id list to unlimited composed option ids (`<model>[:<effort>][:fast]`) over a 33-model Claude+OpenAI catalog. Until now every control-plane loader failed closed to ["default"] for any such config, silently ignoring the admin's preference. All three Admin-waterfall loaders (resolver model_config, the rebase workflow's copy, and the all-branch build doctor) now validate the widened-but-still-closed grammar: - jq accepts a unique 1..256 string array whose entries match the closed charset ^[a-z0-9][a-z0-9.:-]{0,63}$ — an entry can never lead with `-` or contain a space, so no CLI flag can be smuggled. - Each entry is re-validated and parsed in bash into base model, effort (none|minimal|low|medium|high|xhigh|max|ultra), and fast segments; duplicate or unknown segments and empty segments fail the whole mapping closed to [default] with the existing warnings. - Claude Code runs Claude-capable entries only: `default` plus bases re-matched against ^claude-[a-z0-9-]{1,48}$ (rebuilt from the match, never interpolated raw). Other providers' entries (OpenAI ids) are skipped with a log line — they configure those providers' own consumers. `default` is still appended defensively, variants of one base collapse into its single CLI slot, and an all-skipped list falls back to [default]. - The primary entry's effort now steers the session: model_args gains a mandatory `--effort <low|medium|high|xhigh|max>` (default max, preserving today's behavior), and the eight fixed `--effort max` lines in claude_args blocks and exact-session continuations are gone. The rebase-conflict-round action falls back to `--effort max` only when no model-args input is supplied. Fast mode has no headless Claude Code flag; it is logged as recorded-but-not-applied. - All four downstream MODEL_ARGS regex gates (three continuation gates in resolve-pr-conflicts.yml plus the action's) enforce the same widened-but-closed grammar including the --effort tail. - Graphify PREFERRED_MODEL cases (resolver, rebase, promotion script) accept any charset-valid claude-* primary instead of two literals. - Both contract self-tests now pin the new invariants (charset, effort segment set, Claude base pattern, defensive default, effort in model_args) and pass. Verified: 36-case behavioral harness executing the extracted loader blocks verbatim (legacy trio unchanged; the real PR #388 composed config maps to `--model claude-opus-5 --fallback-model default,claude-haiku-4-5 --effort high`; injection, malformed-segment, trailing-colon, oversized, wrong-key, and endpoint-down inputs all fail closed; every emitted model_args passes the downstream gate), a live run against the production endpoint (current saved order maps identically plus explicit `--effort max`), YAML parse checks, and both routing/control-plane contract self-tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lopu repository reviewLopu reviewed this PR against github-actions as Thingtime's principal PR and repository manager. Using Claude Opus 5. Lopu found no justified local change to publish from this review pass. Lopu review — round 4, at the new merged headReviewed Woken by a conversation comment What I compared
FindingsNo defects. No changes made.Checks: all green. 41 checks — 21 pass, 20 skipped, none failing, CodeQL: zero open alerts on this head (trusted snapshot is The merge is clean, verified mechanically rather than by eye. The The two ways this merge could have silently broken the PR — both checked,
No new or duplicated Validation runAll executed in the worktree at Differential harness, 3 loaders × 34 cases, executed not read. The three
Transport-cap boundary is exact in all three copies. Constructed chains
No off-by-one in either direction, and the loaders stay consistent with the Mutation-tested the pin the ack just pulled — each applied alone, then
Repository contract suite: 13/13 pass, including both self-tests the ack Answering the open questionThe ack asks whether the Executed evidence:
All three are log-only. No output is affected: on every collapse path I made no change for this, deliberately. It was explicitly ruled out of VerdictReady to merge. No defect found on this head, no worktree change made, no |
|
Residual conflicted files:
|
|
Residual conflicted files:
|
|
Residual conflicted files:
|
|
Residual conflicted files:
|
|
Residual conflicted files:
|
|
Residual conflicted files:
|
|
Residual conflicted files:
|
|
🤝 Merged Conflicted files:
Please review the merge commit before relying on it. |
…solved conflicts) Conflicted paths: .github/actions/rebase-conflict-round/action.yml, .github/scripts/rebase-stack/refresh-promotion-graphify.sh Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32899512380 Co-Authored-By: Lopu <github-actions[bot]@users.noreply.github.com>
Structural `graphify update` followed by LLM semantic `graphify extract` (graphify 0.9.4, claude-cli backend); unchanged content served from the tracked semantic cache. Refreshed by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32899512380
|
🤝 Merged No AI resolution was needed by merge time; the branch was updated with a plain merge commit.
Please review the merge commit before relying on it. |
# Conflicts: # graphify-out/GRAPH_REPORT.md # graphify-out/graph.json # graphify-out/manifest.json
Structural `graphify update` followed by LLM semantic `graphify extract` (graphify 0.9.4, claude-cli backend); unchanged content served from the tracked semantic cache. Refreshed by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32945995253
Lopu review — composed model waterfall grammarReviewed ✅ The failing check is a manual cancellation, not a defect
It sat queued 27m16s in the repo-wide 🔧 Fixed: the three loader copies disagreed on one inputThe PR states duplicate segments fail the mapping closed. The resolver does that; the
One admin dial drives all three lanes, so a malformed id would have produced a silent 🔒 I tried to break the widened grammar and could notEvery one of these fails closed to I specifically checked the one bypass I thought might exist: jq's Oniguruma 🔍 Removing the eight fixed
|
|
🤝 Merged No AI resolution was needed by merge time; the branch was updated with a plain merge commit.
Please review the merge commit before relying on it. |
# Conflicts: # graphify-out/GRAPH_REPORT.md # graphify-out/graph.json # graphify-out/manifest.json
Structural `graphify update` followed by LLM semantic `graphify extract` (graphify 0.9.4, claude-cli backend); unchanged content served from the tracked semantic cache. Refreshed by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32980929603
|
🤝 Merged No AI resolution was needed by merge time; the branch was updated with a plain merge commit.
Please review the merge commit before relying on it. |
# Conflicts: # graphify-out/GRAPH_REPORT.md # graphify-out/graph.json # graphify-out/manifest.json
Structural `graphify update` followed by LLM semantic `graphify extract` (graphify 0.9.4, claude-cli backend); unchanged content served from the tracked semantic cache. Refreshed by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/33017060489
Lopu review — round 2, at the new baseRe-reviewed ✅ The grammar still holds, and last round's fix survived the mergesI re-extracted all three loaders' I also stopped assuming the CLI contract and checked it: 🔧 Fixed: one of the three loaders had no contract coverage at all
That's precisely the gap the duplicate- Split the grammar pins into Mutation-tested, because a green assertion proves nothing until it can fail —
|
…n-model-waterfall-grammar # Conflicts: # graphify-out/GRAPH_REPORT.md # graphify-out/cache/semantic/337100725a28b633408a6858e9a54adfb464b90aed3591875ed1d01fb071f697.json # graphify-out/cache/semantic/7c2d0a03f92fdb76691c98c37c0062f234440478de562ac867a717c945cbfeeb.json # graphify-out/cache/semantic/93258826c8c86af77bb10853c4732c4110af620b658ed9ab5ed685e37d153b60.json # graphify-out/cache/semantic/ad6637f804333a1b6d93bff06b1533daf4b1d672a4511c2d9811861b100e3c47.json # graphify-out/cache/semantic/aecbd9a9111cd08ad8974a7e83ee681caf0efa5162bdb1b7c25e8ee0f3993515.json # graphify-out/manifest.json
…skip log Review round-2 policy calls: an oversized (yet valid) model chain now fails closed to [default] AT the loader, instead of tripping the promotion publish step's 2048-character cap after resolution work is done; the provider-skip log line no longer claims a skipped entry belongs to another provider (a dotted claude typo is skipped too, and the old wording was false for it). Guard composes its fallback from variables so the no-hardcoded-model contract pin holds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round-2 follow-ups applied in 9bc7f35:
Both contract self-tests and the 3-loader harness pass on the head. Thanks for the assertAdminWaterfallGrammar split + mutation testing — the repeated-fast divergence was mine, good catch. |
Lopu review — round 3, on the round-2 follow-upsRe-reviewed ✅ The 2048 guard is exact at the boundaryAgreed on fail-closed over truncation. I checked the threshold rather than
No off-by-one in either direction. The part I most expected to be wrong was output completeness — rewriting 🔧 Fixed: the new guard had no contract pinSame gap class as last round, in the same three files. The guard now lives in Added Mutation-tested, each applied alone then reverted:
I also drafted and then dropped an eighth assertion (a
👍 The other two callsDotted Claude id — the reworded log line is accurate for every entry that
|
|
Round-3 ack: pulled e8bfff8 (assertAdminTransportCap + cross-file cap pin) — both contract self-tests pass locally on the merged head. On the source-label nit: agreed it's cosmetic and half-fixing would preserve the inconsistency, so leaving both paths as they are; happy for a tiny follow-up PR to align |
…n-model-waterfall-grammar # Conflicts: # graphify-out/cache/semantic-cas/v1/3aee611299d7dbf873292a7bde825412cebd80d72aefe814095a5470230e9d4c/1b8a32a8e38e4b51ac8179c84abf8d5232bdef2b90a5206374b78ecbd5da7d59.json # graphify-out/cache/semantic-cas/v1/788e65310b84ed8e0872d8a32971f3dc8ba7fdb6f6a42a1050339f6ec64e2b2a/678bfef7062180b47a56fb1d3b143d4cc087150ec0fa875615639d305f73c92e.json # graphify-out/snapshots/v1/fb533b7ffb7dfb984073eaa5e9122eedfaa8e2a86b09dcded475c48d188fce1b/a46a66a779cff71e12336a6de500883fc583b76e0a03de5967aa27d30f7f9289/graph.json
Lopu review — round 4, at the new merged headAck received, and confirmed from my side: No defects. No changes made. All 41 checks green (21 pass, 20 skipped), The merge didn't quietly eat either sideChecked mechanically, not by eye: the head→base diff is exactly the The two ways this particular merge could have broken the PR, both clean:
No new or duplicated Validation3 loaders × 34 cases, executed — Cap boundary re-confirmed exact on this head, in all three copies: a Mutations, each alone then reverted: deleting the Contract suite 13/13, four YAML files parse, On the follow-up you offered to trackWorth tracking — and I measured it so it can be written once instead of twice.
All three are log-only — on every collapse path Ready to merge from my side. — Lopu |
What
PR #388 (develop) expanded the Admin AI workflow model waterfall from a closed 3-id list to unlimited composed option ids —
<model>[:<effort>][:fast]over a 33-model Claude+OpenAI catalog. Until now, every control-plane loader on this branch failed closed to["default"]for any such config, so the admin's expanded preference was safely ignored. This PR teaches the control plane the widened grammar without weakening its injection-safety posture.Grammar (widened but still closed)
model_config, the rebase workflow's copy, the all-branch build doctor): unique 1..256 string array, every entry matching^[a-z0-9][a-z0-9.:-]{0,63}$. An entry can never lead with-or contain a space, so a compromised response still cannot smuggle a CLI flag. Malformed JSON / wrong key / unavailable endpoint fail closed to["default"]with the existing::warning::lines, unchanged.none|minimal|low|medium|high|xhigh|max|ultra) /fastsegments. Duplicate segments, unknown segments, or empty segments (model::x, trailing:) fail the whole mapping closed to[default].defaultplus bases re-matched against^claude-[a-z0-9-]{1,48}$— the chain is rebuilt from the regex match, never from raw API strings. Other providers' entries (OpenAI ids) are skipped with a log line: they configure those providers' own consumers (e.g. the Codex review backend's repo vars).defaultis still appended defensively; variants of one base collapse into its single CLI slot; a list with nothing Claude-capable falls back to[default].Effort now applies (fast is logged)
Claude Code headless supports
--effort(verified against the CLI, and this control plane already passed--effort maxeverywhere). So:model_argsnow carries a mandatory--effort <low|medium|high|xhigh|max>— the primary entry's explicit effort, defaulting tomax(today's behavior) when the primary has none. Non-CLI tiers on a Claude entry keepmaxwith a note.--effort maxlines (claude_args blocks + exact-session continuations inresolve-pr-conflicts.yml,all-branch.yml, andrebase-conflict-round/action.yml) are removed; the action falls back to--effort maxonly when itsmodel-argsinput is empty.MODEL_ARGSregex gates enforce the same widened-but-closed grammar including the--efforttail:^--model (default|claude-[a-z0-9-]{1,48})( --fallback-model …)? --effort (low|medium|high|xhigh|max)$.fasthas no headless Claude Code flag → logged as recorded-but-not-applied, per the product-side docs.Graphify primaries
The three
PREFERRED_MODELcases (resolver job, rebase workflow,refresh-promotion-graphify.sh) accept any charset-validclaude-*primary instead of the two hard-coded literals, so a future Claude model flows through with no control-plane edit.Contracts
workflow-control-plane-contract.mjsandresolve-pr-conflicts-routing-contract.mjsreplace theALLOWED_MODELSincludes-loop with pins on the new invariants (charset, effort segment set, Claude base pattern, defensive. + ["default"],--effort $claude_effortin model args). The existing negative pins (no hard-coded model literals, no hard-coded--model, turn budgets) still pass — this change adds no model-name literals anywhere.Verification
run:blocks extracted verbatim from the edited YAML, with a stubbed curl: legacy trio maps unchanged (--model claude-opus-5 --fallback-model claude-fable-5,default --effort max); the real PR feat(admin): unlimited AI model waterfall with full Claude + OpenAI catalog, per-entry effort and fast mode #388 composed config maps to--model claude-opus-5 --fallback-model default,claude-haiku-4-5 --effort high(OpenAI entries skipped, primary effort honored, fast noted); flag injection (--allowedTools=Bashentry), unknown segments, trailing colon, oversized (257) arrays, wrong key, and endpoint-down all fail closed to--model default --effort max; variant dedupe collapsesclaude-opus-5:xhigh+claude-opus-5:maxinto one slot keepingxhigh; a futureclaude-omega-6:lowflows through; every emittedmodel_argspasses the downstream gate regex.--effort max.node .github/scripts/workflow-control-plane-contract.mjs --self-test✓ andnode .github/scripts/resolve-pr-conflicts-routing-contract.mjs --self-test✓; all four edited YAML files parse.Rollout notes
🤖 Generated with Claude Code