From 13b02567b1a16517531f6babdfbf40c22c756d6f Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 8 Jul 2026 20:25:47 +0200 Subject: [PATCH] fix: inherit provider thinking and use rapid orchestration ci --- .github/workflows/mockup-sprint-pentest.yml | 31 +------- .../settings/panels/SettingsModelsPanel.tsx | 56 +++++++++++++- .../src/v2/components/ui/AvantgardeSelect.tsx | 26 +++++-- .../developer-orchestration-debugging.mdx | 6 +- .../content/docs/user-dashboard-settings.mdx | 8 +- .../docs/user-providers-and-models.mdx | 2 + docs-web/developer/orchestration-debugging.md | 6 +- docs-web/user/dashboard/settings.md | 4 +- docs-web/user/providers-and-models.md | 2 + .../rapid-orchestration-debugging.md | 8 +- docs/settings/configuration-and-storage.md | 1 + docs/settings/provider-routing.md | 2 + .../base-provider-configuration.md | 4 + docs/settings/subcategories/route-mapping.md | 4 +- src/services/settings-resolution-service.ts | 5 -- tests/backend/ci/workflow-health.test.ts | 21 ++---- .../settings-resolution-service.test.ts | 73 +++++++++++++++++++ tests/dashboard/settings-page.test.tsx | 72 ++++++++++++++++++ tests/dashboard/v2/avantgarde-select.test.tsx | 39 ++++++++++ 19 files changed, 293 insertions(+), 77 deletions(-) diff --git a/.github/workflows/mockup-sprint-pentest.yml b/.github/workflows/mockup-sprint-pentest.yml index c3e9f92629..e404d1e961 100644 --- a/.github/workflows/mockup-sprint-pentest.yml +++ b/.github/workflows/mockup-sprint-pentest.yml @@ -4,7 +4,7 @@ on: push: branches: - main - # Dev pushes run the full mockup pentest catalog before integration. + # Dev pushes run the rapid orchestration lane before integration. - dev workflow_dispatch: @@ -40,30 +40,5 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts - - name: Build - run: pnpm run build - - - name: Verify Docker availability - run: | - if ! docker version; then - echo "::error::Docker is required for the mockup sprint pentest Docker lane." - exit 1 - fi - - - name: Run mockup sprint pentest - run: | - if [ "${GITHUB_REF_NAME}" = "dev" ]; then - node scripts/e2e/run-mockup-sprint-pentest.mjs --scenario pentest --timeout-ms 3600000 - else - pnpm run test:e2e:mockup-sprint-pentest - fi - - - name: Upload mockup sprint pentest artifacts - if: ${{ failure() || hashFiles('.cache/e2e-mockup-sprint-pentest/**') != '' }} - uses: actions/upload-artifact@v4 - with: - name: mockup-sprint-pentest-artifacts - path: .cache/e2e-mockup-sprint-pentest/ - if-no-files-found: ignore - include-hidden-files: true - retention-days: 5 + - name: Run rapid orchestration validation + run: pnpm run test:orchestration:rapid diff --git a/dashboard/src/v2/components/settings/panels/SettingsModelsPanel.tsx b/dashboard/src/v2/components/settings/panels/SettingsModelsPanel.tsx index 029750d318..dd79eb2023 100644 --- a/dashboard/src/v2/components/settings/panels/SettingsModelsPanel.tsx +++ b/dashboard/src/v2/components/settings/panels/SettingsModelsPanel.tsx @@ -277,6 +277,43 @@ export const SettingsModelsPanel: FunctionComponent<{ state: SettingsPageState } })); }; + const clearRouteProviderOverrideField = ( + routeId: InvocationRoutingId, + providerConfigId: ProviderConfigId, + field: keyof ProjectSettings["aiProvider"]["invocationRouting"][InvocationRoutingId]["providers"][ProviderConfigId], + ): void => { + updateEditableSettings((current) => { + const route = current.aiProvider.invocationRouting[routeId]; + const currentOverride = route.providers[providerConfigId]; + if (!currentOverride || !(field in currentOverride)) { + return current; + } + + const nextOverride = { ...currentOverride }; + delete nextOverride[field]; + const nextProviders = { ...route.providers }; + if (Object.keys(nextOverride).length > 0) { + nextProviders[providerConfigId] = nextOverride; + } else { + delete nextProviders[providerConfigId]; + } + + return { + ...current, + aiProvider: { + ...current.aiProvider, + invocationRouting: { + ...current.aiProvider.invocationRouting, + [routeId]: { + ...route, + providers: nextProviders, + }, + }, + }, + }; + }); + }; + const clearRouteProviderOverride = ( routeId: InvocationRoutingId, providerConfigId: ProviderConfigId, @@ -863,6 +900,8 @@ export const SettingsModelsPanel: FunctionComponent<{ state: SettingsPageState } const effectiveModel = getEffectiveProviderDisplayModel(providerConfigId, provider, override.model || provider.model); const inheritedModel = getEffectiveProviderDisplayModel(providerConfigId, provider); const effectiveThinking = getThinkingSelectValue(provider, override.thinkingMode); + const inheritedThinkingLabel = getProviderThinkingModeLabel(provider.provider, provider.thinkingMode); + const hasThinkingOverride = typeof override.thinkingMode === "string"; const effectiveWeight = override.weight ?? provider.weight; const supportsModel = providerSupportsModelSelection(provider.provider); return ( @@ -956,12 +995,21 @@ export const SettingsModelsPanel: FunctionComponent<{ state: SettingsPageState } ) : null} {providerSupportsThinkingMode(provider.provider) ? ( - + updateRouteProviderOverride(activeRouteDefinition.id, providerConfigId, { thinkingMode: value as ThinkingMode })} - options={getProviderThinkingModeOptions(provider.provider)} + onChange={(value) => { + if (value === INHERIT_VALUE) { + clearRouteProviderOverrideField(activeRouteDefinition.id, providerConfigId, "thinkingMode"); + return; + } + updateRouteProviderOverride(activeRouteDefinition.id, providerConfigId, { thinkingMode: value as ThinkingMode }); + }} + options={[ + { value: INHERIT_VALUE, label: `Inherit base thinking (${inheritedThinkingLabel})` }, + ...getProviderThinkingModeOptions(provider.provider), + ]} /> ) : null} diff --git a/dashboard/src/v2/components/ui/AvantgardeSelect.tsx b/dashboard/src/v2/components/ui/AvantgardeSelect.tsx index 7fc91a4ca4..df2d418834 100644 --- a/dashboard/src/v2/components/ui/AvantgardeSelect.tsx +++ b/dashboard/src/v2/components/ui/AvantgardeSelect.tsx @@ -128,22 +128,32 @@ export const AvantgardeSelect: FunctionComponent = ({ ? boundary.getBoundingClientRect() : { top: 0, left: 0, right: window.innerWidth, bottom: window.innerHeight }; + const viewportBounds = { + top: Math.max(bounds.top, 0), + bottom: Math.min(bounds.bottom, window.innerHeight), + left: Math.max(bounds.left, 0), + right: Math.min(bounds.right, window.innerWidth), + }; + // --- Vertical direction --- - const spaceBelow = bounds.bottom - triggerRect.bottom - GAP - EDGE_MARGIN; - const spaceAbove = triggerRect.top - bounds.top - GAP - EDGE_MARGIN; + const spaceBelow = viewportBounds.bottom - triggerRect.bottom - GAP - EDGE_MARGIN; + const spaceAbove = triggerRect.top - viewportBounds.top - GAP - EDGE_MARGIN; const direction: "down" | "up" = spaceBelow >= PANEL_MAX_H || spaceBelow >= spaceAbove ? "down" : "up"; - const top = + const rawTop = direction === "down" ? triggerRect.bottom + GAP - : triggerRect.top - GAP; + : triggerRect.top - GAP - PANEL_MAX_H; + const minTop = viewportBounds.top + EDGE_MARGIN; + const maxTop = Math.max(minTop, viewportBounds.bottom - PANEL_MAX_H - EDGE_MARGIN); + const top = Math.min(Math.max(rawTop, minTop), maxTop); // --- Horizontal: keep panel within bounds --- let left = triggerRect.left; const panelRight = left + panelWidth; - const boundsRight = bounds.right; - const boundsLeft = bounds.left; + const boundsRight = viewportBounds.right; + const boundsLeft = viewportBounds.left; if (panelRight > boundsRight - EDGE_MARGIN) { // Align right edge of panel with right edge of trigger (or boundary) @@ -189,8 +199,8 @@ export const AvantgardeSelect: FunctionComponent = ({ const panel = panelRef.current; let ctx = gsap.context(() => { const isUp = position.direction === "up"; - const initialY = isUp ? "calc(-100% + 10px)" : "-10px"; - const targetY = isUp ? "-100%" : "0px"; + const initialY = isUp ? "10px" : "-10px"; + const targetY = "0px"; // Check if gsap is mocked or unavailable in test environment if (typeof gsap.fromTo !== 'function' || typeof gsap.to !== 'function') { diff --git a/docs-web/content/docs/developer-orchestration-debugging.mdx b/docs-web/content/docs/developer-orchestration-debugging.mdx index 0f7022040c..95816e5da5 100644 --- a/docs-web/content/docs/developer-orchestration-debugging.mdx +++ b/docs-web/content/docs/developer-orchestration-debugging.mdx @@ -1,6 +1,6 @@ # Rapid orchestration debugging suite -Use this suite when a sprint stalls, local merges fail, worker-owned attention items churn, or memory usage needs extended observation after a fix. +Use this suite when a sprint stalls, local merges fail, worker-owned attention items churn, or memory usage needs extended observation after a fix. CI uses the rapid lane by default; full mockup pentest lanes are manual escalation tools for targeted investigations. ## Commands @@ -8,7 +8,7 @@ Use this suite when a sprint stalls, local merges fail, worker-owned attention i | --- | --- | --- | | Fast regressions | `pnpm run test:orchestration:rapid` | Watch-loop, feature merge, and local final-merge regressions without Docker or provider CLIs. | | Mockup merge E2E | `pnpm run test:orchestration:merge-e2e` | Compiled runtime plus `mockup-cli` through a deterministic local merge-conflict DAG. | -| Full mockup pentest | `pnpm run test:orchestration:full` | All deterministic mockup scenarios: smoke, CI repair, merge conflict, parallel DAG, dirty checkout, multi-project overrides. | +| Full mockup pentest | `pnpm run test:orchestration:full` | Manual escalation for all deterministic mockup scenarios: smoke, CI repair, merge conflict, parallel DAG, dirty checkout, multi-project overrides. | | Large DAG stress | `pnpm run test:orchestration:large-dag` | Heavy 129-task mockup DAG with wide fan-out and layered joins. | | Full heavy pentest | `pnpm run test:orchestration:pentest` | Default mockup catalog plus heavy stress scenarios. | | Backend broadening | `pnpm run test:backend` | Full backend suite after focused fixes. | @@ -20,7 +20,7 @@ Start with `pnpm run test:orchestration:rapid`. It covers provider routing, watc Run `pnpm run test:orchestration:merge-e2e` after a unit-level merge fix passes. It builds the compiled runtime and exercises the `merge-conflict-dag` mockup scenario through the local project runtime. -Run `pnpm run test:orchestration:full` before calling a scheduler, provider, CI, QA, or multi-project orchestration issue fixed. It executes every deterministic mockup scenario and writes artifacts under `.cache/e2e-mockup-sprint-pentest//`. +Run `pnpm run test:orchestration:full` manually when a scheduler, provider, CI, QA, or multi-project orchestration issue needs broader compiled-runtime evidence. It executes every deterministic mockup scenario and writes artifacts under `.cache/e2e-mockup-sprint-pentest//`. Run `pnpm run test:orchestration:large-dag` for a heavy 129-task DAG with 96 leaf tasks, 24 batch joins, 6 group joins, one final manifest, and one validation task. Use `pnpm run test:orchestration:pentest` for the default catalog plus heavy stress scenarios. diff --git a/docs-web/content/docs/user-dashboard-settings.mdx b/docs-web/content/docs/user-dashboard-settings.mdx index 8054873b8d..25ac5fda06 100644 --- a/docs-web/content/docs/user-dashboard-settings.mdx +++ b/docs-web/content/docs/user-dashboard-settings.mdx @@ -304,12 +304,16 @@ Related docs: Defines each named provider instance's default eligibility, model, thinking depth, weight, and concurrency. +Project and sprint scopes store base provider cards as sparse field overrides. Editing a named instance's base model or thinking depth keeps inherited fields such as eligibility, weight, and max concurrency from the parent scope unless those fields are explicitly changed. + **What it controls:** Provider cards set default route participation, model, thinking mode, weighted routing weight, and max concurrent tasks. **Recommended defaults:** Keep only healthy instances eligible and use weights to express preference rather than hard pinning every route. **Risks and gotchas:** Incompatible model choices or high concurrency can cause repeated provider failures or quota pressure. +Base provider cards inherit field-by-field, but invocation route provider maps remain replace-on-write for that route's pool and overrides. + Related docs: - [Provider Routing](/docs/user-providers-and-models) @@ -322,11 +326,11 @@ Related docs: Routes each invocation type to inherited, manual, weighted, or agent-selected provider pools. -**What it controls:** Each route chooses a profile, strategy, primary instance, allowed weighted pool, and per-provider overrides. +**What it controls:** Each route chooses a profile, strategy, primary instance, allowed weighted pool, and per-provider overrides. Thinking overrides can be reset to inherit the provider instance's base thinking setting. **Recommended defaults:** Use inherited defaults first, then override high-risk routes such as planning, QA, CI repair, and remediation. -**Risks and gotchas:** Weighted pools with unavailable providers can spread failures across multiple task types. +**Risks and gotchas:** Weighted pools with unavailable providers can spread failures across multiple task types. Stale route thinking overrides can keep using an older thinking budget until reset to inherit. Related docs: diff --git a/docs-web/content/docs/user-providers-and-models.mdx b/docs-web/content/docs/user-providers-and-models.mdx index 5fa847d7c7..d807f0d664 100644 --- a/docs-web/content/docs/user-providers-and-models.mdx +++ b/docs-web/content/docs/user-providers-and-models.mdx @@ -132,6 +132,8 @@ CLI providers expose provider-specific **thinking** or **reasoning** selections. Older saved values `SMALL`, `MEDIUM`, and `HIGH` continue to load and are migrated to the closest supported value for the selected provider. +Route-specific thinking overrides are optional. Selecting **Inherit base thinking** on a route removes that route's `thinkingMode` override, so later provider-level thinking budget changes apply to the route. + ## Provider weights and strategies In **Settings → AI providers** each provider has a `weight` (0–100). Weights are used by the routing strategy: diff --git a/docs-web/developer/orchestration-debugging.md b/docs-web/developer/orchestration-debugging.md index 0f7022040c..95816e5da5 100644 --- a/docs-web/developer/orchestration-debugging.md +++ b/docs-web/developer/orchestration-debugging.md @@ -1,6 +1,6 @@ # Rapid orchestration debugging suite -Use this suite when a sprint stalls, local merges fail, worker-owned attention items churn, or memory usage needs extended observation after a fix. +Use this suite when a sprint stalls, local merges fail, worker-owned attention items churn, or memory usage needs extended observation after a fix. CI uses the rapid lane by default; full mockup pentest lanes are manual escalation tools for targeted investigations. ## Commands @@ -8,7 +8,7 @@ Use this suite when a sprint stalls, local merges fail, worker-owned attention i | --- | --- | --- | | Fast regressions | `pnpm run test:orchestration:rapid` | Watch-loop, feature merge, and local final-merge regressions without Docker or provider CLIs. | | Mockup merge E2E | `pnpm run test:orchestration:merge-e2e` | Compiled runtime plus `mockup-cli` through a deterministic local merge-conflict DAG. | -| Full mockup pentest | `pnpm run test:orchestration:full` | All deterministic mockup scenarios: smoke, CI repair, merge conflict, parallel DAG, dirty checkout, multi-project overrides. | +| Full mockup pentest | `pnpm run test:orchestration:full` | Manual escalation for all deterministic mockup scenarios: smoke, CI repair, merge conflict, parallel DAG, dirty checkout, multi-project overrides. | | Large DAG stress | `pnpm run test:orchestration:large-dag` | Heavy 129-task mockup DAG with wide fan-out and layered joins. | | Full heavy pentest | `pnpm run test:orchestration:pentest` | Default mockup catalog plus heavy stress scenarios. | | Backend broadening | `pnpm run test:backend` | Full backend suite after focused fixes. | @@ -20,7 +20,7 @@ Start with `pnpm run test:orchestration:rapid`. It covers provider routing, watc Run `pnpm run test:orchestration:merge-e2e` after a unit-level merge fix passes. It builds the compiled runtime and exercises the `merge-conflict-dag` mockup scenario through the local project runtime. -Run `pnpm run test:orchestration:full` before calling a scheduler, provider, CI, QA, or multi-project orchestration issue fixed. It executes every deterministic mockup scenario and writes artifacts under `.cache/e2e-mockup-sprint-pentest//`. +Run `pnpm run test:orchestration:full` manually when a scheduler, provider, CI, QA, or multi-project orchestration issue needs broader compiled-runtime evidence. It executes every deterministic mockup scenario and writes artifacts under `.cache/e2e-mockup-sprint-pentest//`. Run `pnpm run test:orchestration:large-dag` for a heavy 129-task DAG with 96 leaf tasks, 24 batch joins, 6 group joins, one final manifest, and one validation task. Use `pnpm run test:orchestration:pentest` for the default catalog plus heavy stress scenarios. diff --git a/docs-web/user/dashboard/settings.md b/docs-web/user/dashboard/settings.md index 6199572c2b..ecc24e7014 100644 --- a/docs-web/user/dashboard/settings.md +++ b/docs-web/user/dashboard/settings.md @@ -322,11 +322,11 @@ Related docs: Routes each invocation type to inherited, manual, weighted, or agent-selected provider pools. -**What it controls:** Each route chooses a profile, strategy, primary instance, allowed weighted pool, and per-provider overrides. +**What it controls:** Each route chooses a profile, strategy, primary instance, allowed weighted pool, and per-provider overrides. Thinking overrides can be reset to inherit the provider instance's base thinking setting. **Recommended defaults:** Use inherited defaults first, then override high-risk routes such as planning, QA, CI repair, and remediation. -**Risks and gotchas:** Weighted pools with unavailable providers can spread failures across multiple task types. +**Risks and gotchas:** Weighted pools with unavailable providers can spread failures across multiple task types. Stale route thinking overrides can keep using an older thinking budget until reset to inherit. Related docs: diff --git a/docs-web/user/providers-and-models.md b/docs-web/user/providers-and-models.md index 665ceba23d..e47385a2c3 100644 --- a/docs-web/user/providers-and-models.md +++ b/docs-web/user/providers-and-models.md @@ -132,6 +132,8 @@ CLI providers expose provider-specific **thinking** or **reasoning** selections. Older saved values `SMALL`, `MEDIUM`, and `HIGH` continue to load and are migrated to the closest supported value for the selected provider. +Route-specific thinking overrides are optional. Selecting **Inherit base thinking** on a route removes that route's `thinkingMode` override, so later provider-level thinking budget changes apply to the route. + ## Provider weights and strategies In **Settings → AI providers** each provider has a `weight` (0–100). Weights are used by the routing strategy: diff --git a/docs/development/rapid-orchestration-debugging.md b/docs/development/rapid-orchestration-debugging.md index 9dcba6773e..115f1f3513 100644 --- a/docs/development/rapid-orchestration-debugging.md +++ b/docs/development/rapid-orchestration-debugging.md @@ -2,7 +2,7 @@ This suite is the escalation ladder for sprint orchestration failures. Use it when a sprint stalls, local merges fail, worker-owned attention items churn, or memory usage needs extended observation after a fix. -The suite is intentionally split into fast deterministic lanes and slower compiled-runtime lanes. Start with the smallest lane that can reproduce the issue, then broaden only after it passes. +The suite is intentionally split into fast deterministic lanes and slower compiled-runtime lanes. CI uses the rapid lane by default; full mockup pentest lanes are manual escalation tools for targeted investigations. Start with the smallest lane that can reproduce the issue, then broaden only after it passes. ## Lane Summary @@ -11,7 +11,7 @@ The suite is intentionally split into fast deterministic lanes and slower compil | Fast regressions | `pnpm run test:orchestration:rapid` | Watch-loop, feature merge, and local final-merge regressions without Docker or provider CLIs. | Seconds to a few minutes | | Mockup merge E2E | `pnpm run test:orchestration:merge-e2e` | Compiled runtime plus `mockup-cli` through a deterministic local merge-conflict DAG. | Up to 15 minutes | | Completion conflict E2E | `pnpm run test:orchestration:completion-conflict` | Compiled runtime final LOCAL merge conflict repair after default-branch mutation during orchestration. | Up to 20 minutes | -| Full mockup pentest | `pnpm run test:orchestration:full` | All deterministic mockup scenarios: smoke, CI repair, merge conflict, parallel DAG, multi-project overrides. | Longer-running | +| Full mockup pentest | `pnpm run test:orchestration:full` | Manual escalation for all deterministic mockup scenarios: smoke, CI repair, merge conflict, parallel DAG, multi-project overrides. | Longer-running | | Large DAG stress | `pnpm run test:orchestration:large-dag` | Heavy 129-task mockup DAG with wide fan-out and layered joins. | Long-running | | Full heavy pentest | `pnpm run test:orchestration:pentest` | Default mockup catalog plus heavy stress scenarios. | Long-running | | Backend broadening | `pnpm run test:backend` | Full backend suite after focused fixes. | Medium | @@ -108,7 +108,7 @@ pnpm run test:orchestration:full This builds the runtime and runs every `mockup-cli` scenario through `scripts/e2e/run-mockup-sprint-pentest.mjs`. -Run this lane before calling a merge/orchestration incident fixed. It covers: +Run this lane manually when a merge/orchestration incident needs broader compiled-runtime evidence beyond the rapid lane or a targeted E2E lane. It covers: - `smoke-completion`: dependency-chain completion and final local repository assertions. - `ci-repair`: deterministic failing validation repaired by a worker. @@ -303,7 +303,7 @@ Treat an orchestration fix as ready only when: - `pnpm run test:orchestration:rapid` passes. - `pnpm run test:orchestration:merge-e2e` passes for merge-related fixes. -- `pnpm run test:orchestration:full` passes for scheduler, provider, CI, QA, or multi-project changes. +- `pnpm run test:orchestration:full` is run only when the change specifically needs full mockup catalog coverage. - The approved local test project reaches terminal `completed` after the relevant dirty-checkout or conflict scenario. - The local default branch contains the expected final merge commit. - No stale open attention remains for a completed run. diff --git a/docs/settings/configuration-and-storage.md b/docs/settings/configuration-and-storage.md index 78f96d6af4..d188cf2b48 100644 --- a/docs/settings/configuration-and-storage.md +++ b/docs/settings/configuration-and-storage.md @@ -274,6 +274,7 @@ Dashboard behavior: - `providers` map keyed by provider config id - each provider config stores `provider`, `name`, `enabled`, `model`, `weight`, `thinkingMode`, and `maxConcurrentTasks` - provider config entries are base defaults for route inheritance; manual, weighted, or agent-based selection is controlled by each invocation route rather than by the base provider configuration panel + - project and sprint provider config entries are sparse, field-level overrides for the same provider config id; changing `model` or `thinkingMode` does not reset inherited fields such as `enabled`, `weight`, or `maxConcurrentTasks` - multiple entries may share the same underlying provider type, so weighted/manual routing can target separate Codex, Gemini, Claude, or Jules instances independently - Jules remains routable with `enabled` and `weight`, but the current Jules REST API does not expose model-selection or thinking controls. - Dashboard settings editors therefore hide `model` and `thinkingMode` for Jules and show an informational note instead. diff --git a/docs/settings/provider-routing.md b/docs/settings/provider-routing.md index 462c214d5a..3ecde44b1e 100644 --- a/docs/settings/provider-routing.md +++ b/docs/settings/provider-routing.md @@ -87,6 +87,8 @@ Thinking/reasoning settings are provider-keyed rather than global. Base provider Legacy persisted values `SMALL`, `MEDIUM`, and `HIGH` are accepted during load and validation for CLI providers and are normalized to provider-appropriate values. For example, Codex `HIGH` becomes `high`, while Antigravity `MEDIUM` becomes `high` because Antigravity exposes only low/high reasoning selections. +Route-specific thinking overrides are optional. When the AI Models route card is set to **Inherit base thinking**, Code UX removes the route's `thinkingMode` override and the invocation uses the provider instance's current base thinking value. This keeps provider-level thinking budget changes from being shadowed by stale route overrides. + Runtime delivery matches each CLI's reliable headless surface. Codex receives `model_reasoning_effort` via CLI config overrides, Claude Code receives `--effort`, Qwen Code receives generated runtime config `model.reasoningEffort`, and OpenCode receives `--variant`. Gemini and Antigravity do not expose a reliable per-run headless flag in the supported CLI path, so Code UX adds provider-specific prompt guidance for those providers only. Legacy saved values of `ORCHESTRATOR` are normalized to `AGENT` when settings are loaded. The old rule-based provider picker is no longer exposed. diff --git a/docs/settings/subcategories/base-provider-configuration.md b/docs/settings/subcategories/base-provider-configuration.md index 142a87290e..efdc859055 100644 --- a/docs/settings/subcategories/base-provider-configuration.md +++ b/docs/settings/subcategories/base-provider-configuration.md @@ -6,6 +6,8 @@ Defines each named provider instance's default eligibility, model, thinking dept Provider cards set default route participation, model, thinking mode, weighted routing weight, and max concurrent tasks. +Project and sprint scopes store these provider cards as sparse overrides. When a project changes only a named instance's base model or thinking depth, the other fields for that same provider-config ID, such as `enabled`, `weight`, and `maxConcurrentTasks`, continue to inherit from the parent scope. + ## Recommended Defaults Keep only healthy instances eligible and use weights to express preference rather than hard pinning every route. @@ -14,6 +16,8 @@ Keep only healthy instances eligible and use weights to express preference rathe Incompatible model choices or high concurrency can cause repeated provider failures or quota pressure. +Base provider inheritance is field-by-field, but invocation route provider maps are narrower: a route-level `providers` map replaces the inherited provider map for that route so explicit pools do not silently admit parent-scope providers. + ## Dashboard Link Open this subcategory from the dashboard docs route at `/docs/user/dashboard/settings#base-provider-configuration`. The Settings card header links to the matching published docs anchor. diff --git a/docs/settings/subcategories/route-mapping.md b/docs/settings/subcategories/route-mapping.md index 42b55909a6..26435511e3 100644 --- a/docs/settings/subcategories/route-mapping.md +++ b/docs/settings/subcategories/route-mapping.md @@ -4,7 +4,7 @@ Routes each invocation type to inherited, manual, weighted, or agent-selected pr ## What It Controls -Each route chooses a profile, strategy, primary instance, allowed weighted pool, and per-provider overrides. +Each route chooses a profile, strategy, primary instance, allowed weighted pool, and per-provider overrides. Thinking overrides can be cleared back to **Inherit base thinking**, which removes only the route-level `thinkingMode` field and lets the provider instance's base setting apply. ## Recommended Defaults @@ -12,7 +12,7 @@ Use inherited defaults first, then override high-risk routes such as planning, Q ## Risks And Gotchas -Weighted pools with unavailable providers can spread failures across multiple task types. +Weighted pools with unavailable providers can spread failures across multiple task types. Stale route thinking overrides can also hide a provider-level thinking budget change until the route is reset to inherit. ## Dashboard Link diff --git a/src/services/settings-resolution-service.ts b/src/services/settings-resolution-service.ts index 36093a3eff..eabcc6e2b5 100644 --- a/src/services/settings-resolution-service.ts +++ b/src/services/settings-resolution-service.ts @@ -323,11 +323,6 @@ function mergeSettingsPatch(base: T, patch: unknown): T { const mergedAiProvider = toRecord(merged.aiProvider); const mergedInvocationRouting = toRecord(mergedAiProvider.invocationRouting); - if (Object.prototype.hasOwnProperty.call(patchAiProvider, "providers")) { - mergedAiProvider.providers = cloneUnknown(patchAiProvider.providers); - merged.aiProvider = mergedAiProvider; - } - for (const [routeId, rawRoutePatch] of Object.entries(patchInvocationRouting)) { const routePatch = toRecord(rawRoutePatch); if (!Object.prototype.hasOwnProperty.call(routePatch, "providers")) { diff --git a/tests/backend/ci/workflow-health.test.ts b/tests/backend/ci/workflow-health.test.ts index 6f924ab3a7..4dbcff3f93 100644 --- a/tests/backend/ci/workflow-health.test.ts +++ b/tests/backend/ci/workflow-health.test.ts @@ -252,13 +252,13 @@ describe("GitHub workflow health", () => { expect(config).toContain("...devices['Pixel 5']"); }); - it("keeps mockup sprint pentest on a Docker-backed no-secret Linux CI lane", async () => { + it("keeps mockup sprint pentest on a no-secret rapid Linux CI lane", async () => { const workflow = await readRepoFile(WORKFLOWS.mockupSprintPentest); const job = getJobBlock(workflow, "mockup-sprint-pentest"); expect(workflow).toContain("Mockup Sprint Pentest (temporary dev validation)"); expectConcurrencyCancellation(workflow, "Mockup sprint pentest"); - expect(workflow).toMatch(/push:\n branches:\n - main\n # Dev pushes run the full mockup pentest catalog/); + expect(workflow).toMatch(/push:\n branches:\n - main\n # Dev pushes run the rapid orchestration lane before integration/); expect(workflow).toContain("- dev"); expect(workflow).toContain("workflow_dispatch:"); @@ -269,20 +269,9 @@ describe("GitHub workflow health", () => { expect(job).toContain("uses: actions/setup-node@v5"); expect(job).toContain("node-version: 22"); expect(job).toContain("run: pnpm install --frozen-lockfile"); - expect(job).toContain("run: pnpm run build"); - expect(job).toContain("docker version"); - expect(job).toContain("Docker is required for the mockup sprint pentest Docker lane."); - expect(job).toContain('if [ "${GITHUB_REF_NAME}" = "dev" ]; then'); - expect(job).toContain("node scripts/e2e/run-mockup-sprint-pentest.mjs --scenario pentest --timeout-ms 3600000"); - expect(job).toContain("pnpm run test:e2e:mockup-sprint-pentest"); - expectCommandBefore(job, "run: pnpm run build", "- name: Verify Docker availability"); - expectCommandBefore(job, "docker version", 'if [ "${GITHUB_REF_NAME}" = "dev" ]; then'); - - expect(job).toMatch(/if: \$\{\{ failure\(\) \|\| hashFiles\('\.cache\/e2e-mockup-sprint-pentest\/\*\*'\) != '' \}\}/); - expect(job).toContain("uses: actions/upload-artifact@v4"); - expect(job).toContain("path: .cache/e2e-mockup-sprint-pentest/"); - expect(job).toContain("include-hidden-files: true"); - expect(job).toContain("retention-days: 5"); + expect(job).toContain("run: pnpm run test:orchestration:rapid"); + expect(job).not.toContain("node scripts/e2e/run-mockup-sprint-pentest.mjs --scenario pentest --timeout-ms 3600000"); + expect(job).not.toContain("pnpm run test:e2e:mockup-sprint-pentest"); expect(job).not.toContain("OPENROUTER_API_KEY"); expect(job).not.toContain("GITHUB_TOKEN"); }); diff --git a/tests/backend/services/settings-resolution-service.test.ts b/tests/backend/services/settings-resolution-service.test.ts index 75aac00c72..156c6b4615 100644 --- a/tests/backend/services/settings-resolution-service.test.ts +++ b/tests/backend/services/settings-resolution-service.test.ts @@ -279,6 +279,79 @@ describe("Settings Resolution Service", () => { expect(unlimited.settings.aiProvider.providers.jules.maxConcurrentTasks).toBe(15); }); + it("preserves inherited provider instance fields for sparse project base-provider overrides", () => { + const baseProject = buildDefaultProjectSettings(); + const localProviderId = "claude-code-local"; + baseProject.aiProvider.providers[localProviderId] = { + provider: "claude-code", + name: "Claude Local", + enabled: true, + model: "default", + weight: 50, + thinkingMode: "high", + maxConcurrentTasks: 7, + }; + const systemSettings: SystemSettings = { + runtime: { dashboardPort: 4444, consoleLogLevel: "info", debugLogFileLevel: "error", consoleLogMode: "standard" }, + integrations: { + providers: { + [localProviderId]: { + provider: "claude-code", + name: "Claude Local", + apiKey: "", + mountAuth: false, + authPath: "~/.claude", + providerConfigMode: "copyHost", + providerConfigPath: "~/.claude.json", + authType: "apiKey", + }, + }, + githubToken: "", + } as unknown as SystemSettings["integrations"], + defaults: baseProject, + mcpTools: [], + }; + + const resolved = resolveDashboardSettings({ + systemSettings, + projectOverride: { + aiProvider: { + providers: { + [localProviderId]: { + model: "opus", + thinkingMode: "xhigh", + }, + }, + }, + } as unknown as ProjectSettingsOverride, + }); + + expect(resolved.settings.aiProvider.providers[localProviderId]).toMatchObject({ + enabled: true, + model: "opus", + weight: 50, + thinkingMode: "xhigh", + maxConcurrentTasks: 7, + }); + expect(resolved.sources[`aiProvider.providers.${localProviderId}.enabled`]).toBe("system"); + + const explicitDisabled = resolveDashboardSettings({ + systemSettings, + projectOverride: { + aiProvider: { + providers: { + [localProviderId]: { + enabled: false, + }, + }, + }, + } as unknown as ProjectSettingsOverride, + }); + + expect(explicitDisabled.settings.aiProvider.providers[localProviderId].enabled).toBe(false); + expect(explicitDisabled.sources[`aiProvider.providers.${localProviderId}.enabled`]).toBe("project"); + }); + it("merges custom MCP servers and tool toggles across system and project scope", () => { const baseProject = buildDefaultProjectSettings(); const systemSettings: SystemSettings = { diff --git a/tests/dashboard/settings-page.test.tsx b/tests/dashboard/settings-page.test.tsx index 958ad6ecac..fa3a78d249 100644 --- a/tests/dashboard/settings-page.test.tsx +++ b/tests/dashboard/settings-page.test.tsx @@ -229,4 +229,76 @@ describe("ProjectSettingsEditor", () => { expect(await screen.findByRole("option", { name: "Extra High" })).toBeInTheDocument(); expect(screen.queryByRole("option", { name: "Max" })).not.toBeInTheDocument(); }); + + it("can clear stale route thinking overrides back to inherited provider thinking", async () => { + const settings = cloneProjectSettings(DEFAULT_DASHBOARD_SETTINGS) as ProjectSettings; + settings.aiProvider.provider = "codex"; + settings.workers.virtualWorkerProvider = "codex"; + settings.aiProvider.providers.codex.enabled = true; + settings.aiProvider.providers.codex.thinkingMode = "xhigh"; + settings.aiProvider.invocationRouting.task_coding.provider = "codex"; + settings.aiProvider.invocationRouting.task_coding.providers.codex = { + enabled: true, + model: "gpt-5.6-sol", + thinkingMode: "high", + }; + const systemSettings = { + runtime: {} as SystemSettings["runtime"], + integrations: { providers: {}, githubToken: "" } as SystemSettings["integrations"], + defaults: settings, + mcpTools: [], + customMcpServers: [], + modelPricing: { overrides: {} }, + } as SystemSettings; + const updateEditableSettings = vi.fn(); + const state = { + activeScope: "system", + editableSettings: settings, + projectSources: {}, + systemSettings, + externalHints: { + env: {}, + settingsJson: {}, + resolved: { + julesApiKey: "", + geminiApiKey: "", + codexApiKey: "", + claudeCodeApiKey: "", + qwenCodeApiKey: "", + openCodeApiKey: "", + antigravityApiKey: "", + githubToken: "", + }, + providerAvailability: {}, + }, + activeInvocationRoute: "task_coding", + setActiveInvocationRoute: vi.fn(), + invocationRouteDefinitions: [ + { id: "task_coding", label: "Task coding", description: "Task coding route." }, + ], + routingProfileOptions: [ + { value: "GLOBAL", label: "Global defaults" }, + { value: "WORKER", label: "Worker defaults" }, + ], + updateEditableSettings, + updateSystem: vi.fn(), + } as unknown as SettingsPageState; + + render(); + + fireEvent.click(screen.getByRole("button", { name: "Expand Codex Primary overrides" })); + const routeThinking = screen.getByRole("button", { name: "Codex Primary thinking override for Task coding" }); + expect(routeThinking).toHaveTextContent("High"); + + fireEvent.click(routeThinking); + fireEvent.click(await screen.findByRole("option", { name: "Inherit base thinking (Extra High)" })); + + expect(updateEditableSettings).toHaveBeenCalled(); + const recipe = updateEditableSettings.mock.calls.at(-1)?.[0] as (current: ProjectSettings) => ProjectSettings; + const next = recipe(settings); + expect(next.aiProvider.invocationRouting.task_coding.providers.codex).toEqual({ + enabled: true, + model: "gpt-5.6-sol", + }); + }); }); diff --git a/tests/dashboard/v2/avantgarde-select.test.tsx b/tests/dashboard/v2/avantgarde-select.test.tsx index e0a5aaa7d8..e670edf858 100644 --- a/tests/dashboard/v2/avantgarde-select.test.tsx +++ b/tests/dashboard/v2/avantgarde-select.test.tsx @@ -167,6 +167,45 @@ describe("AvantgardeSelect", () => { expect(screen.getByRole("listbox")).toBeDefined(); }); + it("clamps the portal panel inside the viewport when the trigger is near the bottom", () => { + const innerHeight = vi.spyOn(window, "innerHeight", "get").mockReturnValue(768); + const rectSpy = vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function () { + if (this instanceof HTMLButtonElement) { + return { + top: 900, + bottom: 930, + left: 50, + right: 250, + width: 200, + height: 30, + x: 50, + y: 900, + toJSON: () => ({}), + } as DOMRect; + } + return { + top: 0, + bottom: 768, + left: 0, + right: 1024, + width: 1024, + height: 768, + x: 0, + y: 0, + toJSON: () => ({}), + } as DOMRect; + }); + + render( {}} options={[{ value: "1", label: "Opt" }]} />); + fireEvent.click(screen.getByText("Opt")); + + const panel = screen.getByRole("listbox").parentElement as HTMLElement; + expect(Number.parseFloat(panel.style.top)).toBeLessThanOrEqual(488); + + rectSpy.mockRestore(); + innerHeight.mockRestore(); + }); + it("handles empty options", () => { render( {}} options={[]} />); fireEvent.click(screen.getByText("Select\u2026"));