Compare commits

..

384 Commits

Author SHA1 Message Date
Demiurge The Single
1831c6eca8 fix(auto): enforce quota cutoff before scoring (#4483)
Thanks @megamen32! Rebased onto release/v3.8.32. On review (owner decision) the auto-routing quota cutoff was made OPT-IN behind QuotaPreflightSettings.enabled (default OFF, so current behavior is unchanged) and the ...eligibleTargets last-resort fallback was restored so a quota-blocked target survives as final fallback rather than vanishing. The 429-when-all-blocked guard stays for the enabled path. typecheck clean, 13/13 tests.
2026-06-21 09:16:24 -03:00
Diego Rodrigues de Sa e Souza
5a7c1e1731 fix(antigravity): strip Claude/OpenAI thinking fields from Cloud Code envelope (port from 9router#1926) (#4485)
The unified thinking adapter can set Claude/OpenAI-native thinking fields
(thinking, reasoning_effort, reasoning, enable_thinking, thinking_budget) at
the request body root. The Antigravity envelope spreads ...passthroughFields,
so these leaked into the Google Cloud Code envelope and Google rejected the
request with `400 Bad input: oneOf at '/' not met` (or `Unknown name
"thinking"`), breaking every reasoning/thinking model served via Antigravity
(e.g. claude-opus-4-x-thinking).

Extends the existing #1944 envelope strip (output_config/output_format) to
also drop the whole thinking family. Gemini's own generationConfig.thinkingConfig
travels inside the inner request and is unaffected, so thinking still works.

Reported-by: theseven99 (https://github.com/decolua/9router/issues/1926)

Co-authored-by: Arcfoz <62255009+Arcfoz@users.noreply.github.com>
2026-06-21 09:09:19 -03:00
KooshaPari
911e1fb3cc perf(dashboard): shrink provider assets and fix usage rollup cutoff (#4464)
Thanks @KooshaPari! Merged the core of this PR (provider-asset shrink + budget gate + usage-rollup same-day cutoff fix + test), rebuilt scoped on release/v3.8.32. On review we dropped the codeqlAlerts 0->1 baseline loosening and the off-topic changes (the codex/memory contract fixes already landed via #4474; getProviderNames removal + cognitive baseline left out). Asset gate passes, rollup 22/22.
2026-06-21 09:01:11 -03:00
KooshaPari
21d61c69d7 fix(integration): restore codex and memory pipeline contracts (#4474)
Thanks @KooshaPari! Rebased onto release/v3.8.32 (clean cherry-pick — coexists with #4467's specialty changes, 118/118 unit + 12/12 memory-pipeline integration). Codex fingerprint ordering + memory-search contract fixes land.
2026-06-21 08:58:49 -03:00
Abhishek Divekar
912239f46d feat(ui): expose targetFormat selector in custom-models form (#4475)
Thanks @adivekar-utexas! Rebuilt on release/v3.8.32 with just the UI commits (combo+command-code landed via #4472/#4473). Extracted a pure targetFormatBadgeI18nKey helper + added its unit test (Rule #18 gap) and reconciled the file-size baseline. targetFormat selector now ships.
2026-06-21 08:56:42 -03:00
Diego Rodrigues de Sa e Souza
fca66c6441 docs(changelog): reconcile v3.8.32 bullets for late cycle PRs (#4435/#4459/#4462/#4466/#4471/#4472/#4473/#4477/#4479/#4433) 2026-06-21 08:56:26 -03:00
Rahul sharma
620da16805 feat(models): add opt-in low-noise /v1/models catalog mode (#4427)
Thanks @Rahulsharma0810! Rebased onto release/v3.8.32. Opt-in MODELS_CATALOG_PREFIX_MODE (dual default = no change) ships; restored 4 incidental comments + reconciled catalog.ts file-size baseline (1478->1486) on review. 48/48.
2026-06-21 08:53:18 -03:00
NOXX - Commiter
404d9b327a perf(kiro): cut request-completion hot-path CPU + cap DB-lock event-loop block (#4459)
Thanks @artickc! Rebased onto release/v3.8.32. Added a regression guard for the terminal type-scan shortcut (the one change with correctness risk) — typed/event-line/OpenAI/[DONE] shapes all verified. CRC gate stays opt-in, busy_timeout is tuning, compact artifacts round-trip via JSON.parse. 4/4.
2026-06-21 08:50:06 -03:00
Tyler Ford
52e6a8b80f fix: model-aware supportsRedactedThinking for mixed-format providers (#4479)
Thanks @TF0rd! Rebased onto release/v3.8.32. Added the missing regression test for the model-aware redacted_thinking branch (Rule #18): minimax-m3 (claude format) gets redacted_thinking, glm-5.1 (openai) stays plain. 3/3.
2026-06-21 08:48:06 -03:00
janeza2
c3fd2b9aa1 feat(usage): support quota fetch for kimi-coding-apikey (#4435)
Thanks @janeza2! Rebased onto release/v3.8.32 and completed the end-to-end wiring on review: kimi-coding-apikey was missing from USAGE_SUPPORTED_PROVIDERS (dashboard 400) and USAGE_FETCHER_PROVIDERS (preflight) — added both + header-selection/wiring tests. Now fully routable.
2026-06-21 08:45:56 -03:00
Diego Rodrigues de Sa e Souza
66909a8b90 fix(cli): omniroute update revalidates npm cache with --prefer-online (#4376) (#4486) 2026-06-21 08:45:00 -03:00
KooshaPari
147b6a36a4 feat(cluster): add memory + bifrost opt-in profiles (#3932 follow-up) (#4433)
Thanks @KooshaPari! Rebased onto release/v3.8.32. Opt-in memory/bifrost compose profiles + BIFROST_ENABLED killswitch land (fixed the doc image to maximhq + frontmatter on review). qdrant-wiring 17/17.
2026-06-21 08:42:27 -03:00
KooshaPari
2f17be3286 perf(dashboard): lazy-load usage analytics charts (#4466)
Thanks @KooshaPari! Rebased onto release/v3.8.32. Recharts now lazy-loads (public analytics API preserved, source-guard test included).
2026-06-21 08:37:15 -03:00
Abhishek Divekar
3e980a0b2b fix(command-code): pass reasoning/thinking fields through to params (#4473)
Thanks @adivekar-utexas! Rebuilt on release/v3.8.32 with just the command-code commits (the combo half landed via #4472). reasoning/thinking passthrough + body.model rewrite now ship.
2026-06-21 08:36:25 -03:00
Abhishek Divekar
6c8c87dbb6 feat(combo): per-combo stickyRoundRobinLimit override on the combos page (#4472)
Thanks @adivekar-utexas! Rebased onto release/v3.8.32 (resolved the additive conflict with #3872's queueDepth). Per-combo stickyRoundRobinLimit override now ships.
2026-06-21 08:34:44 -03:00
CleanFix-Dev
131474d487 fix(usage): keep Kiro overage quotas routable (#4471)
Thanks @CleanDev-Fix! Rebased onto release/v3.8.32 and integrated. Kiro overage quotas now stay routable (verified the buildKiroUsageResult → isExhausted → auth filter chain). Ships in the upcoming release.
2026-06-21 08:32:41 -03:00
Diego Rodrigues de Sa e Souza
e95fab8bbe refactor(chatCore): extrai resolvers de service-tier do Codex para leaf puro (#3501) (#4477)
Integrated into release/v3.8.32
2026-06-21 08:30:20 -03:00
Diego Rodrigues de Sa e Souza
3b69960b3b fix(sse): unbiased crypto.randomInt for combo selection (follow-up to #4457) (#4462)
Integrated into release/v3.8.32
2026-06-21 08:30:17 -03:00
Diego Rodrigues de Sa e Souza
a7ebf0c2a0 fix(sse): don't bypass web-search fallback for Anthropic-compatible providers without server tools (#4481) (#4490) 2026-06-21 08:12:07 -03:00
diegosouzapw
f4ffbbb507 chore(quality): rebaseline deadExports 340->343 (v3.8.32 cycle drift) 2026-06-21 07:49:01 -03:00
diegosouzapw
1e2ef819b3 chore(release): v3.8.32 — 2026-06-21 2026-06-21 03:16:40 -03:00
Diego Rodrigues de Sa e Souza
0c47d13874 fix(dashboard): resume request-log auto-refresh by gating the ghost infinite-scroll load-more (#4269) (#4478)
* fix(dashboard): resume request-log auto-refresh by gating the ghost infinite-scroll load-more (#4269)

* chore(quality): rebaseline RequestLoggerV2.tsx file-size 1287->1316 (#4269 ghost-load-more fix)
2026-06-21 03:09:35 -03:00
Diego Rodrigues de Sa e Souza
b4dbf7b235 fix(executors): synthesize x-opencode-request for custom-named OpenCode providers (#4465) (#4476) 2026-06-21 02:43:04 -03:00
Diego Rodrigues de Sa e Souza
92afce6d09 fix(compression): RTK compresses Anthropic-shape tool_result blocks (#4468)
Integrated into release/v3.8.32
2026-06-21 02:41:08 -03:00
Diego Rodrigues de Sa e Souza
1e72643595 fix(providers): generic web-cookie validator must not shadow per-provider validators (#4467)
#4023 added validateWebCookieProvider (generic /models session ping -> AUTH_007
SESSION_EXPIRED on 401/403) and dispatched ALL web-cookie providers to it at the TOP
of validateProviderApiKey, BEFORE the SPECIALTY_VALIDATORS table. That shadowed the
rich per-provider validators (validateGrokWebProvider with #3474 IP-reputation/
Cloudflare guidance, validateChatGptWebProvider cf-mitigated, claude/gemini/copilot/
qwen/t3-web), which became dead code and broke all 41 web-cookie assertions in
provider-validation-specialty.test.ts (latent on release/v3.8.32; surfaced by a
__RUN_ALL__ unit run).

Move the generic dispatch to AFTER SPECIALTY_VALIDATORS so it is a FALLBACK only for
web-cookie providers without a dedicated validator. Rich validators run first
(provider-validation-specialty 112/112 restored); the generic + its AUTH_007
capability are preserved (web-cookie-auth007 stays 5/5, it calls the function directly).

Rebaselines validation.ts file-size 4518->4522 (+4, justified). Note: the sibling
pricing half of this restore-green already landed via #4447; this PR carries only the
stranded web-cookie validator fix.
2026-06-20 21:54:16 -03:00
Diego Rodrigues de Sa e Souza
12bfca6cec chore(quality): reconcile file-size + complexity baselines after /review-prs mine r2 (#4461)
Baseline reconcile for /review-prs mine r2 batch
2026-06-20 20:47:49 -03:00
Diego Rodrigues de Sa e Souza
8284650529 feat(routing): opt-in echo of requested alias/combo name in response model (#4450)
Integrated into release/v3.8.32
2026-06-20 20:37:37 -03:00
Diego Rodrigues de Sa e Souza
ee494d2c9a fix(antigravity): strip Anthropic-only output_config from Cloud Code envelope (port from 9router#1944) (#4431)
Integrated into release/v3.8.32
2026-06-20 20:35:41 -03:00
Diego Rodrigues de Sa e Souza
b0e9cd4f2b fix(combo): model lockout honors long upstream quota reset (#4429)
Integrated into release/v3.8.32
2026-06-20 20:33:44 -03:00
Diego Rodrigues de Sa e Souza
0d673f7c9e fix(translator): forward OpenAI audio input to Gemini/Antigravity (port from 9router#912) (#4426)
Integrated into release/v3.8.32
2026-06-20 20:27:04 -03:00
Diego Rodrigues de Sa e Souza
3025a2aa0a feat(oauth): import accounts from CLIProxyAPI auth files (port from 9router#1934) (#4458)
Integrated into release/v3.8.32
2026-06-20 20:26:53 -03:00
Diego Rodrigues de Sa e Souza
86fc1e4234 fix(sse): refactor stall detection to reduce false positives on slow but progressing streams (#4456)
Integrated into release/v3.8.32
2026-06-20 20:26:42 -03:00
Diego Rodrigues de Sa e Souza
7abace590d feat(translator): OpenAI SSE → Gemini SSE conversion for /v1beta/models route (#4453)
Integrated into release/v3.8.32
2026-06-20 20:26:19 -03:00
Diego Rodrigues de Sa e Souza
ce3909be86 fix(executors): sanitize Anthropic-shape content for Copilot /chat/completions (#4452)
Integrated into release/v3.8.32
2026-06-20 20:26:08 -03:00
Diego Rodrigues de Sa e Souza
9708feddc9 fix(translator): strip Claude output_config before MiniMax (#4448)
Integrated into release/v3.8.32
2026-06-20 20:25:50 -03:00
Diego Rodrigues de Sa e Souza
0403f29805 fix(executors): granular reasoning_effort handling for Claude models on Copilot (#4443)
Integrated into release/v3.8.32
2026-06-20 20:25:39 -03:00
Diego Rodrigues de Sa e Souza
53a3b597b2 fix(dashboard): surface manual config CTA when Claude CLI auto-detect fails (#4442)
Integrated into release/v3.8.32
2026-06-20 20:25:21 -03:00
Diego Rodrigues de Sa e Souza
3b25297110 fix(pricing): align Claude Code (cc) model pricing with Anthropic (#4440)
Integrated into release/v3.8.32
2026-06-20 20:25:11 -03:00
Diego Rodrigues de Sa e Souza
84fd18726d fix(usage): reuse Gemini CLI project ID for quota checks (#4454)
Integrated into release/v3.8.32
2026-06-20 20:25:00 -03:00
Diego Rodrigues de Sa e Souza
0225a0a02d fix(translator): sanitize Read tool args from non-Anthropic models (#4451)
Integrated into release/v3.8.32
2026-06-20 20:24:36 -03:00
Diego Rodrigues de Sa e Souza
bda88db555 fix(embeddings): forward output dimensions to Gemini (#4449)
Integrated into release/v3.8.32
2026-06-20 20:23:09 -03:00
Diego Rodrigues de Sa e Souza
9d7167094d fix(sse): use crypto-secure RNG for combo/deck load-balancing selection (CodeQL #665) (#4457)
Integrated into release/v3.8.32
2026-06-20 20:22:38 -03:00
Diego Rodrigues de Sa e Souza
7f62e50cba fix(catalog): add OpenAI gpt-4.1-mini/nano + o3-mini/o4-mini pricing rows (restore-green) (#4447)
Integrated into release/v3.8.32 (restore-green: OpenAI gpt-4.1-mini/nano + o3-mini/o4-mini pricing rows)
2026-06-20 20:17:52 -03:00
Diego Rodrigues de Sa e Souza
9052c5a783 fix(cli): non-interactive-safe prompts + singular context alias (#4439)
* fix(cli): non-interactive-safe prompts + singular `context` alias

Two CLI-polish follow-ups (the third — an async-warn refactor for #2807 — is moot:
#4373 replaced the Gemini remote-URL drop+warn with fileData pass-through, so no
async warn remains).

1) createPrompt (io.mjs) — the shared interactive helper used rl.question with no
   EOF guard, so a non-interactive stdin (pipe, CI, `< /dev/null`) left the await
   pending and Node warned about an 'unsettled top-level await' while the command
   hung. ask/askSecret now resolve on the readline `close` event (fired on EOF)
   with the default / empty string. A genuinely piped line still arrives via the
   question callback first, so `echo value | omniroute …` keeps working — only the
   no-input EOF case falls back. This fixes every command that prompts, centrally
   (mirrors the contexts `confirm()` fix from #4397).

2) `contexts` gains a singular `context` alias — the connect output and older docs
   said `omniroute context current`; the alias keeps that muscle-memory working.

Tests: cli-io.test.ts (ask default/empty + askSecret resolve on EOF, no hang) and a
cli-contexts.test.ts case asserting the `context` alias is registered. 11/11.

* test(cli): mock .alias() in the fake program for the contexts subcommand test

The existing 'registers a current subcommand' test uses a minimal fake commander
program; registerContexts now calls .alias("context"), so the fake needs an alias()
stub (returns this) or the chain throws. Add it. (Self-introduced by the alias in
this branch; my 4 new tests already pass in CI.)
2026-06-20 19:49:45 -03:00
Diego Rodrigues de Sa e Souza
95e6522720 feat(compression): unified config panel — single source for engine on/off + level (Phase 1) (#4432)
* docs(compression): design spec for the unified compression config panel

Engine-centric central panel (single source for master + per-engine on/off + level),
default pipeline derived from the engines map; Combos page owns named ordered pipelines +
active-profile selection; per-engine pages keep only detailed config. Phased: (1) core
consolidation + Model A migration, (2) named profiles + active selector, (3) per-request
x-omniroute-compression header.

* docs(compression): Phase 1 implementation plan for the unified config panel

12 TDD tasks: engine catalog, engines map + activeComboId, deriveDefaultPlan,
migration 102 + backfill, resolveCompressionPlan, runtime wiring, API, default-combo
shim, the engine-grid panel, consolidation, menu. Ends with a recap + the pending
follow-ups (Phase 2 named profiles/active selector, Phase 3 header, deferred items).

* feat(compression): engine catalog metadata (levels, single-mode, order)

* feat(compression): add engines map + activeComboId to CompressionConfig

* feat(compression): deriveDefaultPlan (engines map -> mode/pipeline)

Pure function that converts a per-engine EngineToggle map + masterEnabled
flag into a { mode, stackedPipeline } plan: off when master is off or no
engines on; single-mode when exactly one single-mode engine is enabled;
stacked (sorted by stackPriority) otherwise.

* feat(compression): persist+backfill engines map and activeComboId (migration 102)

* feat(compression): resolveCompressionPlan precedence resolver (header>override>active>default)

* feat(compression): selectCompressionStrategy uses resolveCompressionPlan

* feat(api): settings/compression carries engines map + activeComboId

Extends compressionSettingsUpdateSchema with engines (Record<string,{enabled:boolean,level?:string}>)
and activeComboId (string|null) so the PUT route accepts and persists these fields.
GET already returns the full getCompressionSettings() object which includes both fields.

TDD: added route round-trip tests (PUT+GET) for engines, activeComboId, null clear,
and schema rejection of invalid engines shape.

* refactor(compression): default-combo route is a read-only shim (default derived from engines)

* feat(dashboard): engine-grid compression panel (single source for on/off + level)

* refactor(dashboard): remove duplicate compression toggles; per-engine pages keep only detailed config

* feat(compression): unified panel menu order + derived-pipeline integration coverage

* fix(compression): import ENGINE_IDS via types re-export so it resolves under vitest

The bare "@omniroute/open-sse/.../engineCatalog.ts" specifier resolves under tsc/tsx
but not under vitest's MCP config: Vite externalizes a brand-new open-sse module to
Node, which can't load the .ts subpath. types.ts is already in Vite's graph, so route
ENGINE_IDS through its re-export. Fixes 3 failing MCP vitest suites (cacheTools,
dbHealthTool, essentialTools).

* fix(compression): engines map drives dispatch only when explicitly panel-saved

Code-review finding: the legacy seeded default combo (present on every install via
migration 042/043) was silently overriding a panel-configured engines map — the
default-combo block in chatCore set compressionComboApplied=true, skipping the
engines-map override, so an operator's panel toggles were ignored.

Gate the engines-map path on a new runtime-only CompressionConfig.enginesExplicit
(true when a stored engines row exists). Panel-saved installs: the engines map is
authoritative (deriveDefaultPlanFromConfig + new enginesMapDerivesStackedPipeline
guard the chatCore default-combo block). Legacy/backfilled installs: the map is
display-only and dispatch stays on the historical defaultMode/default-combo path —
zero behaviour change until the operator opts in via the panel.

Also documents why resolveCacheAwareConfig's getCacheAwareStrategy(config.defaultMode)
arg is safe (skipSystemPrompt is mode-independent).

* docs(compression): add MDX frontmatter + escape inline brace to fix fumadocs build

docs/compression/**/*.md is compiled as MDX by the Next build (fumadocs,
source.config.ts). The two planning docs lacked the required `title`
frontmatter (build error: 'title: expected string, received undefined') and the
design doc had a bare {rtk,caveman} that MDX parsed as a JSX expression. Adds
frontmatter matching the sibling docs and backticks the brace. Fixes the
dast-smoke build step.
2026-06-20 19:07:39 -03:00
Paijo
3ad2043327 feat(providers): web-cookie + no-auth provider validation with AUTH_007 (#4023) (#4023)
Integrated into release/v3.8.32 — web-cookie + no-auth provider validation (AUTH_007 SESSION_EXPIRED detection).
2026-06-20 18:27:33 -03:00
Diego Rodrigues de Sa e Souza
63095a55f6 docs(remote-mode): add a copy-paste end-to-end verification example (#4430)
Future-reference 'Quick end-to-end check' in REMOTE-MODE.md: connect -> mint scoped
token -> route a command -> switch back -> tear down, with a placeholder host. Makes
explicit (and verified live) that `contexts remove` is LOCAL-only — it drops the saved
credential but does NOT revoke the server-side token; use `tokens revoke <id>` to kill
access. Also documents that `--yes` is required for non-interactive removal and that
removing the active context falls back to default.
2026-06-20 18:26:45 -03:00
Paijo
7c56f47073 chore: rebase PR #3455 onto release/v3.8.32 (autonomous monitor cycle 1) (#3455)
Integrated into release/v3.8.32 — operational docs (usage/quota, open-sse architecture, database, monitoring).
2026-06-20 17:47:24 -03:00
Diego Rodrigues de Sa e Souza
285608c7e6 chore(quality): reconcile server.ts file-size baseline 1468->1509 (#3368 pool tools) (#4423) 2026-06-20 17:18:20 -03:00
Diego Rodrigues de Sa e Souza
d215079f66 fix(translator): inject placeholder message when Responses API input[] is empty (#4393)
A client (e.g. Fabric-AI) calling POST /v1/responses with `input: []`
used to be translated into `messages: []`, which every upstream Chat-
Completions provider rejects with `400: at least one message is required`
(surfaced to the client as a confusing 406).

Mirror the existing empty-string handling: when input[] is empty, inject
a single placeholder user message so the request is always valid. Touches
`openaiResponsesToOpenAIRequest` only (the same translator that already
covers the empty-string case at the top of the function), preserves any
caller-supplied `instructions` as a system message, and leaves every
non-empty input path byte-identical.

Adds a TDD test reproducing the empty input[] case (RED before fix, GREEN
after) and updates one pre-existing test that enshrined the broken
behavior (`assert.deepEqual(call.body.messages, [])`) to assert the new
correct shape.


Inspired-by: https://github.com/decolua/9router/pull/419

Co-authored-by: anuragg-saxenaa <anuragg.saxenaa@gmail.com>
2026-06-20 17:07:51 -03:00
Diego Rodrigues de Sa e Souza
13288e265a feat(providers): expand openai/gemini direct registries with first-class missing variants (#4394)
OmniRoute already references gpt-4.1-mini, gpt-4.1-nano, o3-mini, and o4-mini
throughout sibling subsystems (cost estimator, modelCapabilities, taskFitness,
free-model catalog, multiple aggregator registries) but the direct openai
provider entry exposes only o3/gpt-4.1. Symmetrically, the gemini entry has
gemini-2.0-flash and gemini-3.1-flash-lite-preview but not the lite/lite-preview
siblings that pair with them. Inspired by an upstream effort to refresh both
static lists; scope is intentionally minimal:

- ADD openai: gpt-4.1-mini, gpt-4.1-nano, o3-mini, o4-mini
  (o3-mini/o4-mini get REASONING_UNSUPPORTED, matching the o3 entry)
- ADD gemini: gemini-2.0-flash-lite, gemini-3-flash-lite-preview

SKIP (already present elsewhere — kept in their typed registries):
- TTS (tts-1, tts-1-hd, gpt-4o-mini-tts)            -> audioRegistry.ts
- OpenAI embeddings (text-embedding-3-small/-large/ada-002) -> embeddingRegistry.ts
- Gemini embeddings (text-embedding-00*, gemini-embedding-*) -> embeddingRegistry.ts
- gemini-3.1-flash-image-preview                   -> imageRegistry.ts

SKIP (deliberate OmniRoute curation):
- gpt-5/gpt-5-mini/gpt-5-nano/gpt-5.1/gpt-5.2 (superseded by gpt-5.4 series)
- o1, o1-mini (superseded by o3)
- gpt-4-turbo (legacy; gpt-4o is the canonical mid-tier)
- o3-pro (no references anywhere in OmniRoute)

TDD: tests/unit/provider-registry-openai-gemini-expanded-models.test.ts
asserts the new ids and pins the existing ids so a future refactor cannot
silently drop them.


Inspired-by: https://github.com/decolua/9router/pull/398

Co-authored-by: Ibrahim Ryan <ryan@nuevanext.com>
2026-06-20 17:05:11 -03:00
Diego Rodrigues de Sa e Souza
23f38630e3 fix(providers): serve api.airforce live /models catalog instead of stale seed (#4395)
api-airforce carries a real live https://api.airforce/v1/models catalog but was
left out of NAMED_OPENAI_STYLE_PROVIDERS, so the import route served its small
hardcoded seed (grok-3, grok-2-1212, claude-3.7-sonnet …) — models that no
longer exist upstream, so chat failed even though the connection test passed.

Add api-airforce to NAMED_OPENAI_STYLE_PROVIDERS (same shape as #4249/#4202/#3976
and the provider-model-sweep rows) so import does a live <baseUrl>/models fetch.
The registry seed stays as the offline fallback, so import never breaks if the
upstream is unreachable.

TDD: extends tests/unit/provider-sweep-live-discovery.test.ts (red proven: the
api-airforce case did not probe upstream and served local_catalog).
2026-06-20 17:04:09 -03:00
Diego Rodrigues de Sa e Souza
1a9d29a9bc fix(providers): register Firecrawl and Jina Reader API-key validators (#4401) (#4412) 2026-06-20 17:01:14 -03:00
Diego Rodrigues de Sa e Souza
d6c63c7a38 fix(sse): parse the chat request body once by threading it from the route guard (#4380) (#4416) 2026-06-20 16:59:11 -03:00
Diego Rodrigues de Sa e Souza
73092146e8 refactor(chatCore): extract resolveChatCoreRequestSetup (first setup-phase slice) (#4392)
Segundo incremento da decomposição do chatCore.ts (5127 LOC). Extrai a primeira
fatia PURA da fase de request-setup do handleChatCore: a metadata de roteamento de
modelo (apiFormat + targetFormat via structural-narrowing sem 'as any', + o
requestedModel com fallback) — para um leaf testável em chatCore/requestSetup.ts.

- resolveChatCoreRequestSetup(modelInfo, body, model): ChatCoreRequestSetup — puro,
  byte-idêntico ao bloco inline. Tipo ChatCoreRequestSetup é a semente do futuro
  ChatCoreContext (as fases maiores — setup/dispatch/streaming — virão depois).
- chatCore.ts: bloco inline (~21 ln) vira 5 linhas.
- 5 testes de caracterização (narrowing + fallback do requestedModel).

185/185 chatcore tests (caracterização do handleChatCore preservada), typecheck 0,
complexity OK 1896 (mudança neutra), file-size encolhe.

Timing: feito como leaf PURO seguro porque a v3.8.30 está finalizando — o
ChatCoreContext mutável (carrega body + 20 inputs no hot-path) cabe melhor em v3.8.31.

Follow-up: atribuir requestSetup.ts a um batch de mutação no nightly (como os demais
chatCore leaves); 5 testes cobrem as branches por ora.
2026-06-20 16:56:23 -03:00
Diego Rodrigues de Sa e Souza
eb48e4d953 feat(mcp): register web-session pool observability tools (#3368) (#4399)
The pool tools (omniroute_pool_status/sessions/reset/warm/health) shipped in
open-sse/mcp-server/tools/poolTools.ts but were never imported or registered in
server.ts, so they were defined-but-dead — the PR4 observability step of the
#3368 web-session roadmap was one wiring step from live.

Wire them through the standard registration loop (import + tool-count tally +
RESERVED_MCP_NAMES + Object.values(poolTools).forEach), add per-tool scopes
(read:health for status/sessions/health, write:resilience for reset/warm) to
both the inline tool defs and the canonical MCP_TOOL_SCOPES map. The forEach is
typed structurally (no new no-explicit-any) since the shape is pinned by tests.

Tests: extend mcp-tool-collections-shape with poolTools; add a dedicated guard
pinning the server.ts wiring (import/registration/reserved-name), the scope
contract (inline == MCP_TOOL_SCOPES, all in MCP_SCOPE_LIST), and live handler
behavior against the in-memory PoolRegistry.
2026-06-20 16:53:09 -03:00
Diego Rodrigues de Sa e Souza
51642dcc99 fix(translator): drop orphan tool results on the Claude->OpenAI request path (#4385) (#4413) 2026-06-20 16:51:07 -03:00
Diego Rodrigues de Sa e Souza
4287a124e1 fix(guardrails): tighten system_prompt_leak heuristic to stop agent-traffic false-positives (#4041) (#4414) 2026-06-20 16:49:40 -03:00
Diego Rodrigues de Sa e Souza
fc57530a95 fix(sse): configurable round-robin combo queue depth for faster failover (#3872) (#4390)
Round-robin combo members deep-queued under concurrency saturation: the
per-model rate-limit semaphore had an unbounded queue and only emitted
SEMAPHORE_TIMEOUT after the full queueTimeoutMs (default 30s), so failover to
the next combo member happened far too late (or the client died first).

The per-model semaphore now accepts a bounded queue depth and rejects with
SEMAPHORE_QUEUE_FULL once the queue is full — the round-robin loop already
cascades to the next member on that code, so a low depth fails over immediately.
A new `queueDepth` combo-config knob (global default / provider override /
per-combo; default 20 for backward compatibility, 0 = never queue) is plumbed
via a resolveComboQueueDepth helper and surfaced in Settings → Combo Defaults.

TDD: rateLimitSemaphore.test.ts (bounded queue + SEMAPHORE_QUEUE_FULL, RED
before the maxQueueSize cap) and combo-config.test.ts (queueDepth cascade,
helper clamps, schema range).

Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
2026-06-20 16:48:21 -03:00
Diego Rodrigues de Sa e Souza
3cd1899484 fix(cli): non-interactive confirm() + document the contexts workflow (#4397)
* chore(release): open v3.8.31 development cycle

* fix(mitm): exact host membership in MITM hosts test (CodeQL false positive) (#4386)

getMitmToolHosts returns string[], so .includes(host) is Array.prototype.includes
(exact membership). CodeQL's js/incomplete-url-substring-sanitization heuristic
misreads it as a String.includes() URL-substring sanitization check and raises a
HIGH alert. Switch to .some(h => h === host) — identical semantics, explicit intent,
no flagged pattern. Surfaced post-v3.8.30 (#4325) once the test landed on main.

Test-only change (no runtime behavior); the suite still passes and the CodeQL
re-scan on merge clears the alert.

* fix(codex): request reasoning summaries (#4359)

Adds reasoning.summary=auto + reasoning.encrypted_content include for Codex. Thanks @xz-dev.

* fix(embeddings): inject NVIDIA NIM input_type for asymmetric embed models (#4341)

NVIDIA NIM asymmetric embedding models (e.g. nvidia/nv-embedqa-e5-v5) reject
requests without an `input_type` ("query" | "passage") with 400 "'input_type'
parameter is required". The embedding registry now carries a model-level
default param for the asymmetric NVIDIA model, and the embeddings handler
injects a model's default params into the upstream body only when the client
omitted them, leaving a client-supplied value untouched.

Reported-by: hydraromania (https://github.com/decolua/9router/issues/1378)

Co-authored-by: hydraromania <252583922+hydraromania@users.noreply.github.com>

* fix(api): migrate deprecated Codex [features].codex_hooks to [features].hooks (#4342)

Codex renamed the `codex_hooks` feature flag to `hooks`; recent Codex CLI
versions ignore the old key and warn. When OmniRoute rewrites an existing
config.toml (configure/reset Codex provider) it now renames
[features].codex_hooks -> [features].hooks, preserving the value and never
clobbering an already-present `hooks`, then drops the deprecated key. The
migration is a no-op when the flag is absent and runs on both the POST and
DELETE config paths.

Reported-by: Bian-Sh (https://github.com/decolua/9router/issues/1327)

Co-authored-by: Bian-Sh <24520547+Bian-Sh@users.noreply.github.com>

* fix(translator): drop the null flush on the same-format response path (#4344)

The streaming response translator's same-format fast path returned
`[chunk]` unconditionally, so the end-of-stream null/flush signal
(chunk === null) propagated as a literal `[null]`. Downstream this surfaced
as an empty `data: null` SSE event between chunks and crashed strict clients
(e.g. Factory Droid BYOK on /v1/responses). The fast path now returns `[]`
for the null flush while still passing real chunks through unchanged.

Reported-by: thaitryhand (https://github.com/decolua/9router/issues/1052)

Co-authored-by: thaitryhand <248103256+thaitryhand@users.noreply.github.com>

* fix(translator): strip assistant echo fields on the OpenAI target path (Mistral 422) (#4350)

Strict OpenAI-compatible upstreams (e.g. mistral/codestral-latest) reject
client-only assistant echo fields sent back as input with 422
extra_forbidden (the report hit messages[].assistant.reasoning_content via
Codex /responses). Only reasoning_content was stripped on the OpenAI target
path; the sibling fields reasoning / refusal / annotations / cache_control
leaked through. They are now all dropped on the non-reasoner OpenAI target
path. `audio` is intentionally preserved (OpenAI audio models reference a
prior assistant audio response by id; Mistral never emits audio).

Reported-by: xxy9468615 (https://github.com/decolua/9router/issues/1649)

Co-authored-by: xxy9468615 <63351664+xxy9468615@users.noreply.github.com>

* fix(cli): honor TAILSCALE_AUTHKEY for non-interactive tailscale login (#4343)

* fix(cli): honor TAILSCALE_AUTHKEY for non-interactive tailscale login (port from 9router#1263)

startTailscaleLogin built `tailscale up` without ever reading
process.env.TAILSCALE_AUTHKEY, so a pre-authenticated / headless daemon
waited for an interactive auth URL and timed out (~15s). When
TAILSCALE_AUTHKEY is set it is now passed via `--auth-key=` (an argv element
to spawn(binary, args) — no shell interpolation, Hard Rule #13); when unset,
behavior is unchanged. The arg builder is extracted into a pure exported
`tailscaleUpArgs()` for testing.

Reported-by: ipeterpetrus (https://github.com/decolua/9router/issues/1263)
Co-authored-by: ipeterpetrus <93033698+ipeterpetrus@users.noreply.github.com>

* chore(quality): rebaseline tailscaleTunnel.ts file-size to 1202 (#1263 +13)

---------

Co-authored-by: ipeterpetrus <93033698+ipeterpetrus@users.noreply.github.com>

* fix(dashboard): OAuth modal surfaces the real error on a non-JSON response (#4351)

* fix(dashboard): OAuth modal surfaces real error on non-JSON responses (port from 9router#1318)

The OAuth connect/reauth modal called `await res.json()` unconditionally, so
a non-JSON error response (e.g. a plain-text 500 page from a build/OAuth
endpoint) threw `Unexpected token 'I'...` and hid the real failure. New
shared helpers parseResponseBody / getErrorMessage (src/shared/utils/api.ts)
read the body safely (JSON when JSON, raw text otherwise) and produce a clean
message either way; every modal fetch site now uses them.

Reported-by: DNNYF (https://github.com/decolua/9router/issues/1318)
Co-authored-by: DNNYF <74033321+DNNYF@users.noreply.github.com>

* fix(dashboard): type OAuth modal response body as Record<string, unknown> (t11 any-budget)

Switch the parseResponseBody casts from Record<string, any> to
Record<string, unknown> so OAuthModal.tsx stays within its t11 explicit-any
budget. getErrorMessage already takes unknown; the success paths typecheck
clean under strict:false. No runtime change.

---------

Co-authored-by: DNNYF <74033321+DNNYF@users.noreply.github.com>

* fix(translator): accept AI SDK-style { type: image, image: data-URL } content parts (#4345)

* fix(translator): accept AI SDK-style { type: image, image: "data:..." } parts (port from 9router#1330)

Several OpenAI-input translators only recognized images shaped as
`image_url.url` (or an object with `.source`/`.url`), so an AI SDK-style
content part where `image` is a bare data-URL STRING was silently dropped
before reaching a vision provider (OpenCode is one affected client; the gap
is generic). The OpenAI->Claude, OpenAI->Kiro and OpenAI->Gemini/Antigravity
translators now parse a string `image` data URL into each provider's native
image shape (Claude base64 source, Kiro images[].source.bytes, Gemini
inlineData).

Reported-by: mugnimaestra (https://github.com/decolua/9router/issues/1330)
Co-authored-by: mugnimaestra <13349159+mugnimaestra@users.noreply.github.com>

* chore(quality): freeze openai-to-kiro.ts file-size at 807 (#1330 +9, over 800 cap)

---------

Co-authored-by: mugnimaestra <13349159+mugnimaestra@users.noreply.github.com>

* fix(dashboard): show a disabled connection's last error in the row (#4352)

* fix(dashboard): show a disabled connection's last error in the row (port from 9router#1447)

The provider card's error badge counts a disabled connection (isActive ===
false) that has an error — its effective status is still
error/expired/unavailable — but the connection row hid the lastError text for
disabled rows, so the operator saw the count without the cause. The row's
error-visibility decision is extracted into shouldShowConnectionLastError()
and now shows the error whenever there is one, regardless of the active
toggle.

Reported-by: ntdung6868 (https://github.com/decolua/9router/issues/1447)
Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* chore(quality): rebaseline ConnectionRow.tsx file-size to 942 (#1447 +1 import)

---------

Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* fix(providers): bound the OAuth connection-test probe with a timeout (#4347)

* fix(providers): bound OAuth connection-test probe with a timeout (port from 9router#1449)

The OAuth path of "Test Connection One-by-One" called bare fetch() with no
AbortController/signal, so a provider probe that accepted the socket but never
responded wedged the test queue forever. Both the initial probe and the
post-refresh retry are now bounded with AbortSignal.timeout(30s) — matching the
API-key path's existing budget — and a timed-out probe resolves as a failure
with a clear "Test timed out after 30s" message in the route's normal error shape.

Reported-by: ntdung6868 (https://github.com/decolua/9router/issues/1449)
Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* chore(quality): rebaseline providers test route file-size to 887 (#1449 + sibling #1444 growth)

---------

Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* fix(providers): label a deactivated account distinctly from a revoked token (#4353)

A Codex connection whose OAuth refresh is fully healthy but whose ChatGPT
account has been deactivated by the provider gets a 401 from the upstream
API. The connection test labeled that the same as a bad credential
("Token invalid or revoked" -> upstream_auth_error), so an operator could not
tell a deactivated account from a revoked token. The test now reads the
401/403 body and, when it indicates account deactivation, classifies it as
account_deactivated (which the dashboard already renders as "Account
Deactivated"); a plain auth 401 is unchanged.

Reported-by: ntdung6868 (https://github.com/decolua/9router/issues/1444)

Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* fix(db): cascade-delete orphaned model aliases when a provider is removed (#4348)

* fix(db): cascade-delete orphaned model aliases when a provider is removed (port from 9router#1409)

Deleting a custom provider removed its connections and node but left the
imported model-alias rows (key=<alias>, value="<providerId>/<model>") behind,
so re-importing the same provider was blocked by stale "already exists" aliases.
Add a deleteModelAliasesForProvider(providerId) DB helper that drops every alias
whose stored value begins with "<providerId>/", and call it from the provider-node
DELETE handler so a fresh import is unblocked.

Reported-by: nguyenvanhuy0612 (https://github.com/decolua/9router/issues/1409)
Co-authored-by: nguyenvanhuy0612 <57367674+nguyenvanhuy0612@users.noreply.github.com>

* chore(quality): rebaseline models.ts file-size to 1221 (#1409 + sibling #1294 growth)

---------

Co-authored-by: nguyenvanhuy0612 <57367674+nguyenvanhuy0612@users.noreply.github.com>

* fix(api): persist max_input_tokens/max_output_tokens when adding a custom model (#4349)

The POST /api/provider-models handler read the rest of the body but never
the two token-limit fields, and addCustomModel() had no parameter for them,
so the form values were dropped on write while the DB layer and /v1/models
catalog already round-trip inputTokenLimit/outputTokenLimit. Accept the two
optional limits in the schema, forward them through the handler, and persist
them in addCustomModel(). TDD: failing-then-passing unit test.

Reported-by: codename-zen (https://github.com/decolua/9router/issues/1294)

Co-authored-by: codename-zen <263238141+codename-zen@users.noreply.github.com>

* docs: feature-documentation catch-up (v3.8.20 → v3.8.30) (#4391)

One-time reconciliation of the docs with every user-facing feature shipped since
v3.8.20 (we had never done a dedicated pass, so debt had accumulated):

- README: new ' What's New' section (curated v3.8.20→v3.8.30 highlights).
- New guides: CLI-INTEGRATIONS (all setup-*/launch commands), MITM-TPROXY-DECRYPT
  (transparent-decrypt epic), CONTEXT_EDITING (delegated Anthropic clear_tool_uses).
- Refreshed: AUTO-COMBO (auto/<category>:<tier> + Arena-ELO), API_REFERENCE
  (x-omniroute-no-memory), MEMORY (int8 quantization + off-by-default), RESILIENCE
  (model-lockout success-decay), RTK, AGENTBRIDGE, TRAFFIC_INSPECTOR, GUARDRAILS,
  CLOUD_AGENT, ENVIRONMENT, SETUP_GUIDE, CLI-TOOLS, MCP-SERVER.
- Regenerated PROVIDER_REFERENCE (231 providers); synced the count in README/CLAUDE/AGENTS.
- Allowlisted external-tool env vars (OPENAI_API_BASE, PROMPTFOO_PROVIDER_KEY) and the
  STREAM_RECOVERY config-object name in the docs-accuracy gates.

All claims source-verified; check:docs-all (sync/counts/env/links/fabricated) passes.
Going forward this runs every release via generate-release step 6b.

* fix(executors): don't inject thinking when tool_choice forces a tool (native Claude) (#4389)

Forced tool_choice now strips the adaptive thinking injection to avoid Anthropic 400. Thanks @NomenAK.

* fix(translator): Gemini accepts HTTP/HTTPS image URLs (port from 9router#344) (#4373)

OpenAI-style `image_url` parts with an `http://` or `https://` URL reached
`convertOpenAIContentToParts` and were dropped with only a `console.warn`,
because Gemini's `inlineData` requires base64 (the helper is synchronous and
cannot fetch+encode upstream assets). Gemini's `Part` schema, however, natively
accepts `fileData: { fileUri }` for remote URIs — the model fetches the asset
itself.

The helper now emits a `fileData` part (`mimeType: "image/*"`, inferred upstream
on fetch) for HTTP/HTTPS URLs instead of silently dropping them. Vision requests
that pass a URL — not a data: URI — now reach Gemini intact.

No behavioral change for:
- `data:` URIs → still emitted as `inlineData` with the parsed media type.
- Unsupported schemes (e.g. `ftp:`) → still skipped (Gemini would reject them).

The openai-to-claude side already passed HTTP/HTTPS URLs through as
`source: { type: "url", url }` (lines 573–578) — the upstream PR's Claude-side
change was already covered.

Regression test: tests/unit/gemini-helper-http-image-url-port344.test.ts
(4 cases: https URL, http URL, data: URI no-regression, unsupported-scheme guard).


Inspired-by: https://github.com/decolua/9router/pull/344

Co-authored-by: Ibrahim Ryan <ryan@nuevanext.com>

* fix(executors): strip stream_options for qwen non-streaming / thinking Claude Code requests (port from 9router#663) (#4374)

Claude-Code-compatible providers force the executor-level `stream` flag on
via `upstreamStream = stream || isClaudeCodeCompatible`
(open-sse/handlers/chatCore.ts), but the outgoing body keeps the caller's
original `stream: false`. The shared `stream && targetFormat === "openai"`
branch in DefaultExecutor.transformRequest then injected
`stream_options: { include_usage: true }` onto a body that still said
`stream: false`, and qwen upstream rejected the request with
`400 "'stream_options' only set this when you set stream: true"`. The same
rejection surfaced when the body carried `thinking` / `enable_thinking`.

The qwen branch now skips the injection (and strips any client-sent
`stream_options`) when the body explicitly says `stream: false` or
requests thinking, leaving regular qwen streaming requests with the
include_usage injection intact. Other providers are unaffected.

Adds a TDD regression with 4 cases covering both opt-out paths and the
normal-streaming positive control.


Inspired-by: https://github.com/decolua/9router/pull/663

Co-authored-by: anuragg-saxenaa <anuragg.saxenaa@gmail.com>

* fix(security): scope OAuth callback postMessage to a trusted-origin allowlist (port from 9router#998) (#4372)

The OAuth callback at `/callback` previously fell back to
`window.opener.postMessage({ code, state, ... }, "*")` whenever the opener
was cross-origin. The fallback was intended to support remote-OmniRoute +
local-loopback callbacks (where opener and callback live on different
origins), but the same code path also delivers the OAuth code/state to any
hostile opener that pops the well-known callback URL — letting that
attacker complete the OAuth flow as the user.

Replace the wildcard fallback with iteration over a fixed allowlist:
`window.location.origin` (same-origin parent — the popup-mode dashboard)
plus Codex's fixed loopback helper (`http://localhost:1455` and the IPv4
literal `http://127.0.0.1:1455`). The browser drops `postMessage` to any
opener whose actual origin is not in `targetOrigin`, so the message reaches
only known parents and is silently dropped for any other. The same-origin
fallback path is unchanged — methods 2 (`BroadcastChannel`) and 3
(`localStorage` storage event) still cover same-origin openers that COOP
severed.

The `openerSameOrigin` probe stays in place to drive the auto-close vs
manual-copy UI decision (no behavior change for the success path).

Adds a regression test (`tests/unit/ui/oauth-callback-postmessage-scope.test.tsx`)
that mounts the page with a stubbed cross-origin opener and asserts no
`postMessage` call ever uses `"*"` and every call lands on an allowlisted
origin. The test failed against the pre-fix code (red), passes after the
fix (green) — TDD per CLAUDE.md hard rule #18.

Partial port of upstream decolua/9router#998: the upstream PR also
re-enabled TLS verification on a DNS-bypass fetch in `open-sse/utils/proxyFetch.js`;
that part is N/A here because OmniRoute's `proxyFetch.ts` never disabled
TLS verification (no `rejectUnauthorized: false` anywhere in the file).


Inspired-by: https://github.com/decolua/9router/pull/998

Co-authored-by: aeonframework <aeon@aeonframework.dev>

* fix(sse): default combo per-target timeout to 120s for fast failover (#4365)

Combo per-target timeout inherited the full FETCH_TIMEOUT_MS (600s) when a
combo did not set its own targetTimeoutMs, so a single hung/slow target stalled
the whole combo for up to 10 minutes before falling through to the next model.

Introduce DEFAULT_COMBO_TARGET_TIMEOUT_MS (120s) as the unset-default in
resolveComboTargetTimeoutMs (new 3rd arg) and wire it in phaseComboSetup. The
upstream ceiling (600s) and per-combo opt-out (targetTimeoutMs, up to the
ceiling) are preserved; single non-combo requests are unchanged. For streaming
requests this only bounds time-to-first-headers, so token generation is not cut
short.

TDD: failing-then-passing unit test in tests/unit/combo-config.test.ts.

* refactor(combo): de-dup exhausted-target skip predicate across both dispatchers (#4362)

Primeiro incremento da de-dup dos 2 dispatchers de combo (handleComboChat +
handleRoundRobinCombo). O bloco de pre-check #1731/#1731v2 (skip de target já
exhausted no provider/connection) era BYTE-IDÊNTICO nos dois (mesmas condições,
mesmas mensagens), diferindo só na tag de log e no control-flow.

- comboPredicates.ts: getExhaustedTargetSkipReason(target, exhaustedProviders,
  exhaustedConnections) — predicate PURO que retorna a mensagem de skip (ou null);
  cada dispatcher mantém seu próprio log-tag + control-flow (return null / continue)
  + fallbackCount. No mutate do stryker (cobertura de mutação).
- combo.ts: −20 linhas (os 2 blocos viram 1 chamada cada).
- 7 testes de caracterização travam condições + strings exatas.

Comportamento preservado: 376/376 testes combo (357 caracterização + 7 novos),
integração sse-correctness 5/5, typecheck 0, complexity neutro (1895), file-size
encolhe. Próximo incremento: de-dup do error-handling/exhausted-tracking (handleTargetError).

* refactor(combo): de-dup upstream-error exhaustion classification across both dispatchers (#4366)

Segundo incremento da de-dup dos 2 dispatchers (handleTargetError). Após cada erro
de target, ambos rodavam um bloco quase-idêntico que marca o provider exhausted
(#1731), a conexão connection-errored (#1731v2) ou o provider transiently rate-limited.

- combo/targetExhaustion.ts: applyComboTargetExhaustion(target, opts) — atualiza os
  3 Sets de exhaustion e retorna providerExhausted. As MUTAÇÕES de Set (que dirigem o
  skip de targets, lidas por getExhaustedTargetSkipReason) são BYTE-IDÊNTICAS nos dois;
  as diferenças reais viram parâmetros: tag, allAccountsRateLimited (termo extra do RR,
  false no handleComboChat), exhaustedLogLevel (info no handleComboChat, debug no RR).
  Connection-level extraído p/ markConnectionLevelExhaustion (privado, <15 complexity).
- combo.ts: −73 linhas; 4 imports órfãos removidos.
- 7 testes de caracterização travam as mutações + o return.

ÚNICA mudança de comportamento: o WORDING das mensagens de log do RR ganha o sufixo
'on remaining targets' (cosmético; mesmo #code, mesmas mutações, mesmos níveis de log).
376/376 combo (caracterização preservada), integração sse 5/5, typecheck 0, complexity
neutro (1895), file-size encolhe.

* refactor(chatCore): extract checkHeapPressureGuard leaf (god-file decomposition start) (#4371)

Primeiro incremento da decomposição do chatCore.ts (5127 LOC, hot-path mais quente).
O guard de memória do topo do handleChatCore (rejeita 503 quando o heap V8 passa o
threshold de shed) vira um leaf testável, co-locado com o threshold em heapPressure.ts.

- heapPressure.ts: checkHeapPressureGuard(heapUsedMb, thresholdMb) — retorna o result
  503 pronto ou null. Byte-idêntico ao guard inline (mesmo check, mesma 503, mesmo warn).
  A figura de heap fica em telemetria INTERNA, nunca no response do cliente (Hard Rule #12).
- chatCore.ts: o bloco inline (~22 ln) vira 3 linhas; import órfão de HEAP_PRESSURE_THRESHOLD_MB
  trocado por checkHeapPressureGuard.
- 3 testes novos (incl. assert Rule #12: o MB medido não vaza no payload).

complexity-baseline 1895->1896: drift de base pós-#4338 (medido com minhas mudanças
stashed = 1896); esta mudança é complexity-NEUTRA (helper complexity 2, handleChatCore só
perde código). 190/190 chatcore tests, typecheck 0, file-size encolhe.

* Localize CLI and stabilize fetch, memory, and coverage handling (#4383)

en-only i18n, fetch-start-timeout hardening, EngineConfigPage icon fix, CI build-artifact-reuse overhaul. Memory production hunk dropped as a no-op (tests kept). Thanks @JxnLexn.

* test(combo): reset circuit breakers between stream-readiness cases (restore green) (#4396)

The combo-dispatch cases in combo-stream-readiness-fallback.test.ts deliberately
fail `glm` (zombie streams / repeated 504s), which legitimately trips the
per-provider circuit breaker. That OPEN state is a module-level singleton, so it
leaked into the next test and combo.ts then SKIPPED `glm/*` targets entirely
("Skipping … circuit breaker OPEN"). That made "combo does not retry stream
readiness timeouts on the same model" never attempt glm/zombie — expected
['glm/zombie','openai/gpt-5.4-mini'] but got ['openai/gpt-5.4-mini'].

This was a pre-existing red on release/v3.8.31 (present at the cycle-open tip),
order-dependent: the test passes in isolation, fails after the preceding cases.
Add a test.beforeEach(resetAllCircuitBreakers) so each scenario starts from a
clean breaker slate. Test-isolation only — the breaker behavior is correct and
no production code or assertion changes. Full combo suite: 390/390 green.

* fix(cli): decline confirm() cleanly on non-interactive stdin + document contexts workflow

The `contexts remove` command already has `--yes` to skip confirmation, but when
run without it under a non-interactive stdin (pipe, CI, EOF) the [y/N] prompt could
never be answered — the readline question stayed pending and Node warned about an
"unsettled top-level await" at exit. confirm() now detects `!process.stdin.isTTY`
and declines cleanly (returns false), pointing at `--yes` for non-interactive use.
Exported confirm() for a regression test.

Docs: REMOTE-MODE.md gains a full "Managing contexts" section (list/current/use to
switch between remote and local, add/show/rename, remove with --yes, export/import),
fixes a `context current` -> `contexts current` typo, and the README remote-mode
snippet now shows switching back to local. Verified against the live CLI: command
signatures, --yes, and the non-TTY decline path all behave as documented.

Tests: cli-contexts.test.ts asserts confirm() declines on non-TTY stdin (RED before,
GREEN after). All docs gates (fabricated/links/symbols) pass.

* ci(t11): bump any-budget for executors/base.ts (2 false-positive "any" strings)

Unblocks the Fast Quality Gates on release/v3.8.31: `check:any-budget:t11` was red on
`open-sse/executors/base.ts` for ALL PRs (pre-existing base drift, unrelated to this
branch). The checker counts `\bany\b` after stripping comments but NOT strings, and the
native-Claude tool_choice logic uses the API value `"any"` in two string literals
(`tb.tool_choice === "any"`, `.type === "any"`). There are zero actual TypeScript
`any` types in the file — budget set to the matched count, mirroring the existing
cursor.ts false-positive entry right below it.

* perf: combos UI split + next config + 1-click redis + bifrost sidecar (#3932) (#4381)

Combos UI split + next.config perf + 1-click local Redis launcher + bifrost relay. Review fixes (co-author): --rm/--restart conflict, error sanitization, UI/route names, dead-guard re-doc, bifrost Zod, IPv6 + CLI test fixes. Thanks @KooshaPari.

* fix(plugin): prefix OC static-catalog combo+raw keys with providerId (#4384)

OC parses model ids on '/'; combo keys now carry 'omniroute/' (was 'combo/'). Live-validated against the VPS via OpenCode. Thanks @herjarsa.

* docs(env): document TAILSCALE_AUTHKEY (env/docs contract drift on .31)

Second pre-existing base-drift fix needed to get Fast Quality Gates green: the
`repository contract is in sync` test (check:env-doc-sync) was red on ALL .31 PRs.
PR #4343 added a code reference to `process.env.TAILSCALE_AUTHKEY`
(src/lib/tailscaleTunnel.ts) for non-interactive `tailscale up`, but never added the
var to .env.example / docs/reference/ENVIRONMENT.md — the contract requires code vars
to appear in both. Add the (commented) entry to .env.example next to TAILSCALE_BIN and
a row to ENVIRONMENT.md. Verified: `node scripts/check/check-env-doc-sync.mjs` → in sync.

Unrelated to this branch's confirm()/docs change; surfaced because touching a quality
script triggers the TIA fail-safe full unit suite.

* chore(release): v3.8.31 — 2026-06-20

Finalize the v3.8.31 release: reconcile the CHANGELOG (full commit-to-bullet
coverage, 26 bullets across Features/Fixed/Security/Maintenance), refresh the
README What's New section, back-fill the .30/.31 sections into the 41 i18n
CHANGELOG mirrors, and add TAILSCALE_AUTHKEY to the env contract
(.env.example + ENVIRONMENT.md).

* chore(release): align mitm-hosts test comment with main to clear merge conflict

The same CodeQL false-positive fix landed twice — #4386 on release/v3.8.31 and
#4387 directly on main — with only the explanatory comment differing (the
assertion is byte-identical). Adopt main's comment wording on the release branch
so the release PR merges without a comment-only conflict.

* test(translator): align stale openai->gemini remote-URL tests with #4373

Third pre-existing base-drift fix to get Fast Quality Gates green on .31. PR #4373
('Gemini accepts HTTP/HTTPS image URLs', port of 9router#344) intentionally changed
convertOpenAIContentToParts so remote http(s) image URLs pass through as a native
`fileData: { fileUri }` part instead of the old #2807 drop+warn — and added its own
test (gemini-helper-http-image-url-port344.test.ts) for the new behavior. But it left
three tests in translator-openai-to-gemini.test.ts asserting the OLD drop+warn
contract, so they fail deterministically (verified: the file fails in isolation on
clean .31). These only surface under the TIA fail-safe FULL suite, which a quality-
script touch triggers.

Align the three stale tests to the real, intended behavior (captured by running the
function): remote URLs -> fileData.fileUri (mimeType image/*), still never inlineData
(the sync path cannot fetch+encode). This is test-vs-code alignment to a deliberate,
separately-tested change — not a weakened assertion. 40/40 in the file; 94/94 across
the translator + env-doc combo that previously failed.

* chore(quality): reconcile complexity baseline 1896->1900 (/review-prs v3.8.31 batch) (#4410)

* fix(release): reconcile full-CI drift for v3.8.31 (gemini tests #4373, any-budget #4389, masking allowlist #4384)

The release PR's full CI surfaced cumulative cycle drift the per-PR fast gates
skip:
- tests/unit/translator-openai-to-gemini.test.ts: realign 3 cases to #4373's
  HTTP/HTTPS-URL fileData pass-through (they asserted the old warn-and-drop).
- scripts/check/check-t11-any-budget.mjs: base.ts budget 0→2 — #4389 compares
  tool_choice against the string literal "any" (not a TS any type).
- config/quality/test-masking-allowlist.json: allowlist #4384's opencode combos
  net-assert reduction (obsolete combo/ namespace removed).
No production behavior change.

* chore(release): re-trigger full CI for v3.8.31 finalization

Force a fresh pull_request CI run on the head carrying the cycle-drift fixes
(gemini #4373 tests, any-budget #4389, masking allowlist #4384) — the prior
synchronize event did not spawn a ci.yml run.

* chore: re-trigger CI (no Actions runs registered for prior push)

---------

Co-authored-by: Xiangzhe <32761048+xz-dev@users.noreply.github.com>
Co-authored-by: hydraromania <252583922+hydraromania@users.noreply.github.com>
Co-authored-by: Bian-Sh <24520547+Bian-Sh@users.noreply.github.com>
Co-authored-by: thaitryhand <248103256+thaitryhand@users.noreply.github.com>
Co-authored-by: xxy9468615 <63351664+xxy9468615@users.noreply.github.com>
Co-authored-by: ipeterpetrus <93033698+ipeterpetrus@users.noreply.github.com>
Co-authored-by: DNNYF <74033321+DNNYF@users.noreply.github.com>
Co-authored-by: mugnimaestra <13349159+mugnimaestra@users.noreply.github.com>
Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>
Co-authored-by: nguyenvanhuy0612 <57367674+nguyenvanhuy0612@users.noreply.github.com>
Co-authored-by: codename-zen <263238141+codename-zen@users.noreply.github.com>
Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com>
Co-authored-by: Ibrahim Ryan <ryan@nuevanext.com>
Co-authored-by: anuragg-saxenaa <anuragg.saxenaa@gmail.com>
Co-authored-by: aeonframework <aeon@aeonframework.dev>
Co-authored-by: Jan Leon <Jan.gaschler@gmail.com>
Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
2026-06-20 15:53:35 -03:00
diegosouzapw
a2490cb776 chore(release): open v3.8.32 development cycle 2026-06-20 15:29:52 -03:00
Diego Rodrigues de Sa e Souza
d0396c200d Release v3.8.31 (#4377)
Release v3.8.31 — see CHANGELOG.md [3.8.31] for full notes and contributors.

Merged over known non-blocking reds (all correctness gates green): Integration Tests (2/2) is env/flaky (polls a real upstream batch that did not complete in the poll window); SonarQube/SonarCloud is the advisory server-side new-code quality gate. Unit (8 shards), Coverage, Node 22/24/26, Lint, PR Test Policy, Quality Ratchet, Docs-Strict, Quality-Extended and all 4 CodeQL analyses are green.
2026-06-20 14:55:24 -03:00
Diego Rodrigues de Sa e Souza
3b2a2f02a9 test: exact host membership in MITM hosts test — CodeQL FP (#660)
Use exact array-element membership (.some((h) => h === host)) instead of Array.prototype.includes() in the MITM hosts unit test, so CodeQL's js/incomplete-url-substring-sanitization heuristic does not misread an Array.includes membership check as a String.includes URL-substring test. Functionally identical. Mirrors #4386 (already merged into release/v3.8.31). Closes the only open code-scanning alert (#660).
2026-06-20 11:23:07 -03:00
Diego Rodrigues de Sa e Souza
db362b0126 Release v3.8.30 (#4267)
Release v3.8.30 — see CHANGELOG.md [3.8.30] for the full release notes.
2026-06-20 07:09:43 -03:00
Diego Rodrigues de Sa e Souza
ab8096071c fix(deps): bump undici to 7.28.0 and dompurify to 3.4.11 (security) (#4304)
* fix(deps): bump undici to 7.28.0 and dompurify to 3.4.11 (security)

Resolves Dependabot alerts on package-lock.json and electron/package-lock.json:

- undici 7.x -> 7.28.0: TLS certificate validation bypass via dropped requestTls in SOCKS5 ProxyAgent (GHSA-vmh5-mc38-953g, HIGH) + cross-user information disclosure via shared-cache whitespace bypass (GHSA-pr7r-676h-xcf6, MEDIUM). Fixed in the root (jsdom transitive) and electron lockfiles.

- dompurify -> 3.4.11: permanent ALLOWED_ATTR pollution via setConfig() bypassing the hook clone-guard (GHSA-cmwh-pvxp-8882, MEDIUM). Bumped the overrides floor from ^3.4.9 to ^3.4.11.

Also bumps node-gyp's transitive undici 6.26.0 -> 6.27.0, clearing the <6.27.0 advisories (WebSocket DoS, Set-Cookie handling) surfaced by npm audit. Lockfile/override-only change; no production source touched.

* ci(quality): exclude dependency manifests/lockfiles from PR test-policy

The PR test-policy gate classifies any changed file under src/, open-sse/, electron/, or bin/ as production code requiring tests. This false-flags lockfile/manifest-only changes (e.g. this Dependabot security bump touching electron/package-lock.json), since a lockfile cannot have a meaningful unit test.

Adds package.json / package-lock.json to EXCLUDED_PATTERNS, consistent with the existing .md/.yaml/.yml exclusions. Real production-code changes remain flagged.
2026-06-19 18:27:04 -03:00
Diego Rodrigues de Sa e Souza
3c9883bb73 Release v3.8.29 (#4126)
OmniRoute v3.8.29 — 115 commits since v3.8.28. Full CHANGELOG + 41 i18n mirrors. All content quality gates green (build, unit 8/8, vitest 188/188, PR test policy, quality gates extended, docs sync, quality ratchet). Remaining red CI checks are pre-existing release flakes (coverage-shard/integration/node-compat teardown), a new transitive undici advisory in electron devDeps, and a workflow-level CodeQL fail (0 open alerts). VPS-validated by the operator.
2026-06-19 06:49:01 -03:00
Diego Rodrigues de Sa e Souza
dd5a3db55e fix(docs): move DOCUMENTATION_OVERHAUL_PLAN out of the fumadocs guides collection (#4123)
A cycle-internal docs housekeeping commit (635350ebb) relocated this internal
planning doc from docs/ root into docs/guides/, which `source.config.ts` globs
into the published fumadocs `docs` collection. The file has no frontmatter, so
`next build` (build:cli / Docker / Electron) failed to compile it:
`[MDX] invalid frontmatter … title: expected string, received undefined`,
breaking ALL three release-build fragments (npm/Docker/Electron) at release time.

Moving it back to docs/ root (which is NOT globbed by the collection) restores
the pre-cycle state and matches the documented intent — DOCUMENTATION_AUDIT_REPORT
lists it among docs/-root files that should never be published to the site.
Verified locally: `npm run build:cli` now completes green. No inbound links.
2026-06-17 19:58:28 -03:00
Diego Rodrigues de Sa e Souza
f165efcd0b Release v3.8.28 (#4053)
* chore(release): open v3.8.28 development cycle

* fix(ws): warm SSE auth import on LiveWS startup; relocate boot test to integration (#4063)

The live dashboard WebSocket sidecar lazily import()-ed the SSE auth module
inside the connection handler, only on the API-key path. That cold import pulls
in hundreds of transitive modules and takes ~7s under tsx, blocking the
single-threaded event loop. The first API-key WebSocket connection therefore
stalled the loop long enough that any connection arriving in that window — e.g.
a same-origin cookie client — could not complete its handshake and timed out.

This was deterministic, not an "env flake": the boot test fires an API-key
connection immediately followed by a cookie connection, so the cookie connection
always raced the cold import and timed out (reproduced 3/3 locally and red on
every CI run; proven via instrumented probes — reversing the order or warming
the module first makes both connections open in ~20ms).

Fix:
- Memoize the auth-module import and warm it once at startup (before listen), so
  connection handling never pays the cold-import cost. Real improvement: the
  first API-key client no longer stalls the event loop for concurrent clients.
- Relocate the boot test from tests/unit/cli to tests/integration. It spawns a
  real subprocess + WS server + SQLite (~9-11s); under the unit suite's
  --test-concurrency=20 it contended for CPU and destabilized the shard. The
  serial integration runner is its correct home; it still guards #4004's
  cookie-parse fix on every PR via the integration CI job.
- Bump the test's startup/overall timeouts to absorb the eager auth warm.

Makes `npm run test:unit` deterministically green (the only remaining unit red).

Validated: relocated test 3/3 green via the integration runner (was 3/3 red);
typecheck:core + eslint clean; confirmed it no longer matches the test:unit glob
and does match tests/integration/*.test.ts.

* fix(ws): start LiveWS sidecar with cwd at package root (#4055) (#4064)

* chore(deps): bump ossf/scorecard-action from 2.4.0 to 2.4.3 (#4045)

Integrado em release/v3.8.28. Patch de SHA do ossf/scorecard-action (2.4.0→2.4.3), mantém SHA-pin. Reds de CI são exclusivamente os shards flaky pré-existentes branch-wide (Unit 7/8, Integration, Coverage 7/8, Node 1/2) — não relacionados ao bump (PR deps-only).

* deps: bump electron from 42.4.0 to 42.4.1 in /electron (#4049)

Integrado em release/v3.8.28. Patch do electron (42.4.0→42.4.1). Reds de CI: shards flaky pré-existentes + PR Test Policy = falso-positivo (mudança deps-only sob electron/ não comporta teste de código) + Node 26(2/2) sem step (flake/infra). Precedente #3913/#3914 (electron dependabot mergeado nessas condições).

* fix(auto): resolve built-in auto catalog combos (#4058)

Integrado em release/v3.8.28. Resolve os IDs de catálogo `auto/*` built-in (combos virtuais) — corrige o 400 "No auto combos configured" em auto/best-coding etc. Ajuste de review: os mapas AUTO_TEMPLATE_VARIANTS/VALID_AUTO_VARIANTS duplicados em chat.ts e chatHelpers.ts foram extraídos para open-sse/services/autoCombo/builtinCatalog.ts (DRY), devolvendo chatHelpers.ts <800 LOC; baseline de chat.ts rebaselinado 1432→1458 (lógica nova). Fast QG + semgrep + dast verdes; 22/22 testes.

* chore(docs): update Discord invite link to a non-expiring one (#4067)

* chore(deps): freeze @huggingface/transformers in dependabot (hard-pin) (#4066)

Integrado em release/v3.8.28. Congela @huggingface/transformers no dependabot (pin exato 3.5.2, load-bearing p/ LLMLingua + memory embeddings, VPS-validado #4014). Fast QG + semgrep + dast verdes.

* ci(quality): flip TIA impacted-unit-tests gate from advisory to blocking (#4069)

The pre-existing release unit test-debt that kept the TIA "Impacted unit tests"
step advisory has been cleared:
- #4030 restored 16 lossless Zod/registry reds (from the oyi77 modularize refactors).
- #4063 fixed the last red — the LiveWS boot test — which was a real deterministic
  event-loop stall in the WS sidecar (cold ~7s lazy auth import racing a second
  connection), not an env flake; fixed (warm the import at startup) and relocated to
  the integration suite.

A full workflow_dispatch ci.yml run on release/v3.8.28 then showed all 8 Unit Tests
shards green. The remaining Integration Tests / Quality Ratchet reds are pre-existing
and unrelated (combo/resilience env-flakes; eslint/i18n baseline drift).

Removing continue-on-error makes PR->release block on unit-test regressions in the
TIA-selected impacted set (fail-safe still runs the full unit suite on hub/unmapped
changes). typecheck:core was already blocking. Closes the fast-gates "no tests on
PR->release" hole (Quality Gate v2 / Fase 9, P2).

* docs(compression): document LLMLingua optional deps + on-demand install (#4061)

Integrado em release/v3.8.28. Docs LLMLingua optional deps + on-demand install (F3.1).

* feat(dashboard): Combo Studio connection-cooldown badge (U1b Slice 2) (#4068)

Integrado em release/v3.8.28. Combo Studio connection-cooldown badge (U1b Slice 2 / F5.1).

* feat(compression): record Context Editing telemetry (engine: context-editing) (#4062)

Integrado em release/v3.8.28. Context Editing telemetry (F4.1).

* feat(sse): Context Editing relay coverage + 400-fallback (#4065)

Integrado em release/v3.8.28. Context Editing relay coverage (cc-*) + 400-fallback (F4.2/F4.3). Conflito de file-size-baseline.json (vs #4062) resolvido por união (ambas justificativas + base.ts 1292 + chatCore.ts 5898). Validado local no tree mergeado: typecheck:core ✓, eslint ✓, check:file-size ✓, 4/4 testes ✓; semgrep + semgrep-cloud verdes. Fast QG enfileirado (saturação de runner) — mergeado nos gates de política verificados (precedente #4034/#4020).

* feat(providers): add OrcaRouter (OpenAI-compatible routing gateway) (#4070)

Integrado em release/v3.8.28. Adiciona o provider OrcaRouter (OpenAI-compatible, API-key, DefaultExecutor). Ajuste de review: rebaseline de file-size de providers.ts 3147→3159 (+12 da entrada OrcaRouter). Validado local no tree sincronizado: provider-consistency ✓, docs-counts STRICT 227 ✓, typecheck:core ✓, teste 3/3 ✓, eslint ✓; semgrep + semgrep-cloud verdes. Fast QG/dast enfileirados (saturação de runner) — merge nos gates de política verificados (precedente #4034/#4065).

* test(infra): isolate DATA_DIR per test process; raise Stryker concurrency 1→4 (#4078)

* test(infra): isolate DATA_DIR per test process; raise Stryker concurrency 1→4

Every test process resolved DATA_DIR to the same default (~/.omniroute) when the env
var was unset (src/lib/dataPaths.ts::resolveDataDir), so concurrent test files opened
the SAME on-disk storage.sqlite. node:test spawns a process per file and Stryker spawns
one per sandbox, so this shared file caused cross-file state races:
- SQLite lock contention that hung `npm run test:unit` under high --test-concurrency
  (the ~95-min local hang), and
- the non-deterministic baseline that forced stryker.conf.json to concurrency: 1, which
  in turn could not finish the ~15k-mutant run inside the nightly timeout (the cancelled
  2026-06-16/17 nightly-mutation runs) — blocking Quality Gate v2 / Fase 9 Onda 2.

open-sse/utils/setupPolyfill.ts could NOT host the fix: it is imported by production
(bin/omniroute.mjs, proxyFetch.ts, proxyDispatcher.ts), where redirecting DATA_DIR would
point the live SQLite DB at a throwaway temp dir. So this adds a TEST-ONLY
tests/_setup/isolateDataDir.ts that gives each process its own temp DATA_DIR when none is
set (tests that set DATA_DIR explicitly still win), wired via --import into the test,
mutation and CI invocations.

Verified:
- Stryker dry-run A/B at concurrency=4: FAILS without the isolation import
  (account-fallback-service tap exit 9, a cross-file race) and PASSES with it.
- Full `npm run test:unit` green with isolation (0 fail; a one-off
  chatcore-translation-paths timeout flake did not reproduce and passes 3/3 isolated)
  and noticeably faster — the DB lock contention is gone.
- New tests/unit/isolate-datadir.test.ts guards the contract (unique temp DATA_DIR when
  unset; explicit DATA_DIR respected).

Wired the --import into: package.json (13 test scripts), stryker.conf.json (tap.nodeArgs
+ concurrency 1→4), .github/workflows/quality.yml (TIA step), ci.yml (the 5
unit/coverage/integration commands), and bumped nightly-mutation.yml timeout 120→180 for
the first cold run before the incremental cache is seeded.

* ci(quality): run the TIA gate at CI concurrency (4) to stop oversubscription flakes

The TIA "Impacted unit tests" step (made blocking in #4069) ran its fail-safe via
`npm run test:unit` — concurrency=20, tuned for multi-core dev machines. On a 4-vCPU CI
runner that is 5x oversubscribed, so timing-sensitive tests flake under the load (e.g.
`db-backup-extended` "The database connection is not open", `chatcore-translation-paths`
upstream-timeout). That intermittently fails a blocking gate on legitimate PRs — exactly
what surfaced on the DATA_DIR-isolation PR, whose package.json/workflow changes trip the
__RUN_ALL__ fail-safe.

Run both the impacted set and the fail-safe at --test-concurrency=4, matching the stable
ci.yml unit job. Adds a `test:unit:ci` script (test:unit at concurrency=4). The DATA_DIR
isolation in this PR keeps the parallel run race-free, so the only change here is matching
the runner's core count. Verified locally: db-backup-extended passes 8/8 in isolation
(5 with isolation, 3 without).

* docs(quality-gates): reconcile gate inventory with ci.yml + add ROI rationalization backlog (#4095)

The "authoritative" gate inventory in QUALITY_GATES.md had drifted from ci.yml: it omitted
9 wired gates — `audit:deps`, `check:tracked-artifacts`, `check:lockfile`, `check:licenses`
(lint job), `check:dead-code`, `check:cognitive-complexity`, `check:type-coverage`,
`check:codeql-ratchet` (quality-gate job), and `check:pr-evidence` (pr-test-policy job).
You can't rationalize an inventory you can't trust, so this reconciles it first.

Adds those 9 rows to their job tables and a "Rationalization Backlog (ROI review)" section
capturing the Fase 9 Onda 3 findings: mechanical merge/dedup candidates (CVE scanners
audit:deps↔osv, the two complexity ESLint passes, cycles↔circular-deps, the two /api
anti-hallucination gates, the doubly-run check:docs-sync, check:node-runtime ×11) and the
operator-only flip/drop decisions (typecheck:noimplicit vs the type-coverage ratchet,
test:vitest:ui parked fails, check:secrets frozen FPs, openapi-security-tiers, pr-evidence,
the orphaned semgrep baseline). Also flags the undocumented advisory docs-lint job and the
standalone scanner workflows.

Docs-only — no gate behavior changes. The merges (CI changes) and flips (policy) are
deferred to operator-scoped follow-ups; this PR only makes the map accurate.

* test(dashboard): smoke e2e for the Combo Live Studio page (#4075)

Integrated into release/v3.8.28

* fix(sse): friendly 413 message for ChatGPT web payload-too-large (#4080)

Integrated into release/v3.8.28

* feat(sse): port Claude Code quota-probe bypass + command meta-request helpers (#4083)

Integrated into release/v3.8.28

* feat(api): exact offline token counting for count_tokens fallback via tiktoken (#4087)

Integrated into release/v3.8.28

* feat(compression): RTK learn/discover (sample source + API + UI) (#4088)

Integrated into release/v3.8.28

* feat(dashboard): 2026-06-17 free-tier refresh — honest catalog, uncapped + boost tiers, Layout A budget table (#4089)

Integrated into release/v3.8.28

* feat(mitm): capture-pipeline self-test route (Gap 12) (#4093)

Integrated into release/v3.8.28

* fix(mitm): crash-safe system-state teardown + socket timeouts (ProxyBridge-inspired hardening) (#4084)

Integrated into release/v3.8.28 (Fast QG TIA red = 3 pre-existing timing flakes verified passing locally 82/82; PR own tests green)

* feat(mitm): attribute intercepted requests to originating process (Gap 1) (#4085)

Integrated into release/v3.8.28 (Fast QG TIA red = 3 pre-existing timing flakes verified passing locally 82/82; PR own tests green)

* fix(sse): route image requests only to confirmed-vision combo targets (#4071)

Integrated into release/v3.8.28

* fix(security): injection guard respects INJECTION_GUARD_MODE DB feature flag (#4077)

Integrated into release/v3.8.28

* fix(ws): proxy LAN /live-ws upgrades and add unset JWT_SECRET warning (#4079)

Integrated into release/v3.8.28

* fix(dev): force webpack in custom dev server (Turbopack 16.2.x panics) (#4092)

Integrated into release/v3.8.28

* ci(quality): dedup the doubly-run check:docs-sync + record validated ROI backlog (#4099)

Onda 3 (gate ROI-review) Phase 2. Two parts, both low-risk:

1. Remove the standalone `check:docs-sync` from the `lint` job — it already runs in the
   `docs-sync-strict` job (via `check:docs-all`) and the husky pre-commit hook, so the
   `lint`-job copy was a pure duplicate. No coverage lost.

2. Update the Rationalization Backlog in QUALITY_GATES.md with trust-but-verify findings:
   several "obvious" merges/flips from the ROI review turned out to hide debt and are NOT
   clean drop-ins —
   - CVE merge (audit:deps→osv): different semantics (hard high/critical vs regression-ratchet) — keep both.
   - cycles→circular-deps: dpdm reports 91 cycles (can't promote to blocking) and is broader-scope than the green curated check:cycles — keep both.
   - openapi-security-tiers flip: blocked by traffic-inspector routes missing the x-loopback-only annotation.
   - complexity + /api merges: valid but real config/script surgery — deferred.
   - node-runtime ×11: ~10s savings vs a cheap guard — low ROI, skip.

   The remaining flips (typecheck:noimplicit, test:vitest:ui, check:secrets, pr-evidence,
   semgrep) are operator policy decisions, left for the owner.

* chore(deps): bump actions/github-script from 7 to 9 (#4046)

Integrated into release/v3.8.28 (dependabot GH-Action bump; SHA-pin preserved)

* chore(deps): bump actions/setup-node from 4 to 6 (#4048)

Integrated into release/v3.8.28 (dependabot GH-Action bump; SHA-pin preserved)

* chore(deps): bump actions/upload-artifact from 4 to 7 (#4044)

Integrated into release/v3.8.28 (dependabot GH-Action bump; SHA-pin preserved)

* chore(deps): bump actions/cache from 4.3.0 to 5.0.5 (#4047)

Integrated into release/v3.8.28 (dependabot GH-Action bump; SHA-pin preserved)

* deps: bump the development group with 10 updates (#4051)

Integrated into release/v3.8.28 (dependabot dev group; cyclonedx 4->5 verified compatible with the SBOM invocation --ignore-npm-errors/--output-format JSON/--output-file)

* fix(dashboard): event-driven fail-open auto-refresh for embedded log views (#4054) (#4103)

The Request Logger gated each auto-refresh tick on a static
document.visibilityState === "visible" read. Hosts that report a permanent
non-"visible" state without ever firing a visibilitychange event (Docker
dashboard wrappers, embedded/proxied webviews) froze auto-refresh entirely —
only the manual Refresh button worked, a regression from 3.8.24's unconditional
polling.

The pause is now event-driven and fail-open: visibleRef starts true and is only
flipped to false on a real visibilitychange → hidden transition, so a host that
never signals a genuine background transition keeps polling, while normal
browser tabs still pause when actually backgrounded.

Regression test reproduces the misreporting-host case (RED) and the perf guard
is re-encoded under the event-driven semantics.

* fix(docker): raise build-stage Node heap to stop production-build OOM (#4076) (#4104)

The Docker builder stage ran `npm run build` with V8's default heap ceiling
(~2 GB). After #4052 forced the heavier webpack engine (Turbopack panics on this
Next.js version), the production optimization pass exceeded that ceiling and the
build died with "FATAL ERROR: ... JavaScript heap out of memory" at
[builder] npm run build.

The builder stage now sets NODE_OPTIONS=--max-old-space-size (default 4096 MB,
overridable via --build-arg OMNIROUTE_BUILD_MEMORY_MB) before the build; the
value propagates to the spawned next build (resolveNextBuildEnv spreads
process.env). Build-only — the runtime heap on the runner stage is unchanged,
and CI/local builds (which invoke npm run build directly) are unaffected.

Regression guard: tests/unit/dockerfile-build-heap-4076.test.ts asserts the
builder stage sets the heap ceiling, before npm run build, at >= 4096 MB.

* feat(agent-bridge): portable JSON import/export of config (Gap 4) (#4094)

Integrated into release/v3.8.28

* feat(cli): add 'omniroute launch' zero-config Claude Code launcher (#4097)

Integrated into release/v3.8.28 (Fast QG TIA red = pre-existing env-doc-contract drift [MITM_IDLE_TIMEOUT_MS/TURBOPACK from #4084/#4092] + opencode-plugin-dist env flake; #4097 own test 3/3 green)

* feat(mitm): loop-guard self-check + verbosity control in server.cjs (Gaps 14+15) (#4101)

Integrated into release/v3.8.28 (rebased onto release — dropped the already-squash-merged #4084 commits; only the Gaps 14+15 loop-guard/verbosity delta remains)

* feat(sse): generic 400 field-downgrade retry + Groq field stripping (#4096)

Integrated into release/v3.8.28

* feat(providers): add Wafer AI (Anthropic-compatible, Bearer auth) (#4098)

Integrated into release/v3.8.28

* chore(docs)

* fix(responses): clear /v1/responses keepalive timer on cancel/abort (timer + CPU leak) (#4105)

Integrated into release/v3.8.28 (r7).

* perf(gemini): cache reasoning close-tag regex instead of recompiling per token (#4106)

Integrated into release/v3.8.28 (r7).

* fix(usage): reap orphaned pending-request details (unbounded memory leak) (#4107)

Integrated into release/v3.8.28 (r7).

* perf(stream): use structuredClone instead of JSON round-trip for per-chunk reasoning split (#4108)

Integrated into release/v3.8.28 (r7).

* fix(dashboard): restore Update Available banner with npm-binary-free version fallback (#4100) (#4112)

getLatestNpmVersion() derived the latest version only from the npm CLI binary and returned null on any error, so Docker/desktop/locked-down installs without npm on PATH silently hid the home banner even when an update existed. Add resolveLatestVersion() (npm CLI -> registry HTTP fallback -> logged warning) and harden version parsing for v-prefix/pre-release strings. Extracted into testable src/lib/system/versionCheck.ts with TDD coverage.

* fix(auth): prune expired entries from login brute-force guard map (unbounded growth) (#4111)

Integrated into release/v3.8.28 (r8)

* fix(logger): hard-cap the error-dedup map to bound memory under unique-message bursts (#4113)

Integrated into release/v3.8.28 (r8)

* fix(circuit-breaker): enforce MAX_REGISTRY_SIZE (declared but never applied) (#4114)

Integrated into release/v3.8.28 (r8)

* perf(obfuscation): cache per-word regexes instead of recompiling every request (#4109)

Integrated into release/v3.8.28 (r8)

* perf(registry): precompute model->provider index in parseModelFromRegistry (#4110)

Integrated into release/v3.8.28 (r8)

* fix(timers): unref background interval timers so they don't block clean shutdown (#4117)

Integrated into release/v3.8.28 (r8)

* fix(webhook): clear abort timer in finally to avoid dangling timers on fetch error (#4115)

Integrated into release/v3.8.28 (r8)

* fix(combo): detach per-target listener from shared hedge abort signal (#4116)

Integrated into release/v3.8.28 (r8)

* chore(release): finalize v3.8.28 CHANGELOG + reconcile env-doc contract

- Build the complete [3.8.28] CHANGELOG section (55 bullets) covering every
  commit since v3.8.27, grouped by type with PR back-references and human
  contributor attribution (artickc's memory-leak/perf cluster, OrcaRouter,
  Wafer AI, MITM gaps, etc.); move the OrcaRouter bullet out of [Unreleased].
- Inject the EN [3.8.28] section into all 41 i18n CHANGELOG mirrors (parity).
- Reconcile the env/docs contract: document MITM_IDLE_TIMEOUT_MS + MITM_VERBOSE
  in .env.example and ENVIRONMENT.md; allowlist the framework-internal TURBOPACK
  and the Claude Code ANTHROPIC_AUTH_TOKEN in check-env-doc-sync.
- Fix 3 broken relative links in docs/providers/AGENTROUTER.md (regressed when
  the file was relocated this cycle) so docs-sync-strict passes.

* fix(quality): treat test→test renames as relocations, not deletions

The anti-test-masking gate's subcheck-1 collected deleted AND renamed test
files via `--diff-filter=DR --name-only` and flagged every one as "deleted —
human review required", contradicting its own documented contract ("DELETADOS
ou renomeados-e-NÃO-substituídos"): a rename test→test IS a substitution (the
test moved, coverage preserved). This false-positived on #4063's legitimate
relocation of live-ws-startup.test.ts (unit/cli → integration, asserts 2→2)
and would block every PR that relocates a test — surfacing only at release-day
because the Fast QG (PR→release) doesn't run test-masking.

The gate now parses `--name-status -M`: true deletions and test→non-test
renames still flag; a test→test rename is run through the assert-reduction
check across the move, so a clean relocation passes while gutting-via-rename
(dropped asserts / new tautologies / skips) still fires. Adds
partitionDeletedRenamed + 6 regression tests.

---------

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Demiurge The Single <megamen932@gmail.com>
Co-authored-by: jinhaosong-source <jinhao.song@myflashcloud.com>
Co-authored-by: diego-anselmo <contato@diegoanselmo.com.br>
Co-authored-by: Felipe Almeman <4226997+zhiru@users.noreply.github.com>
Co-authored-by: Rahul sharma <sharmaR0810@gmail.com>
Co-authored-by: Chirag Singhal <76880977+chirag127@users.noreply.github.com>
Co-authored-by: NOXX - Commiter <artur1992123@mail.ru>
2026-06-17 19:26:32 -03:00
Diego Rodrigues de Sa e Souza
8842414d8a fix(docker): build release image with webpack (Turbopack internal panic) (#4052)
The v3.8.27 release Docker build failed on BOTH linux/amd64 and linux/arm64 with a
non-recoverable Turbopack panic — `TurbopackInternalError: internal error: entered
unreachable code: there must be a path to a root` in `ImportTracer::get_traces` (during
issue reporting), at Dockerfile `RUN npm run build`. Deterministic (not transient), so a
re-run does not help. The webpack build is the proven engine — `build:release` (deployed
to the VPS), the CI `Build` job, and `npm run build:cli` all use it and are green. Switch
the Docker build to webpack (OMNIROUTE_USE_TURBOPACK=0); re-enable once the upstream
Turbopack tracer bug is fixed. Documented in QUALITY_GATE_PLAYBOOK Parte 6.
2026-06-17 04:46:25 -03:00
Diego Rodrigues de Sa e Souza
fa367dd99e Release v3.8.27 (#3968)
* chore(release): open v3.8.27 development cycle

* fix(security): polynomial ReDoS in comboAgentMiddleware regex (#3982)

* fix(security): eliminate polynomial ReDoS in comboAgentMiddleware <omniModel> regex (CodeQL js/polynomial-redos)

CACHE_TAG_PATTERN wrapped the tag in an unbounded `(?:\\n|\n|\r)*` prefix/suffix.
On an unanchored `.test()`/`.exec()` that is O(n²) on inputs with many newlines
(CodeQL js/polynomial-redos, alerts #612/#613). The surrounding runs are irrelevant
to detecting/capturing the tag, so the detection pattern now matches only the core
`<omniModel>([^<]+)</omniModel>`; the global strip pattern still consumes the
wrapping newlines (combo.ts streaming, #531) but BOUNDED ({0,16}) so it stays linear.

Behavior preserved: detection, model extraction, multi-tag stripping (#454) and
blank-line cleanup all unchanged (107 related tests green). Adds ReDoS-safety
regression tests (50k-newline inputs complete in <1ms).

* docs(changelog): add #3982 ReDoS fix to [3.8.27]

* ci(security): harden workflows — artipacked persist-credentials + cache-poisoning + SC2086 (#3965)

* Refine provider quota card display (#3969)

Integrated into release/v3.8.27

* feat: add sidebar group separator toggles (#3971)

Integrated into release/v3.8.27

* Gate control-plane proxy direct fallback (#3963)

Integrated into release/v3.8.27

* Capture actual upstream provider requests (#3941)

Integrated into release/v3.8.27

* ci(quality): flip require-tighten + osv + Trivy to blocking (v3.8.27 cycle-end) (#3984)

* fix(resilience): respect connection cooldown stored as numeric epoch (#3954) (#3995)

rate_limited_until is a TEXT column, but setConnectionRateLimitUntil (Antigravity full-quota path) persists a raw epoch number that SQLite coerces to a numeric string ("1781696905131.0"). The selection predicate isAccountUnavailable then did new Date("1781696905131.0") -> NaN, so the cooling connection was never skipped and the router kept dispatching to rate-limited accounts. Normalize numeric-epoch strings (and number/Date/ISO) via a shared cooldownUntilMs() helper in isAccountUnavailable / getEarliestRateLimitedUntil / filterAvailableAccounts / parseFutureDateMs. ISO behavior preserved.

* fix(providers): fetch live /models for LLM7 and BytePlus (#3976) (#3996)

llm7 and byteplus carry a real modelsUrl but were not classified by any live-fetch branch of the model-import route, so their hardcoded 4-entry registry catalog was served (source local_catalog) instead of the upstream catalog. Add both to NAMED_OPENAI_STYLE_PROVIDERS so the route probes <baseUrl>/models and serves the live list, falling back to the local catalog only on fetch failure.

* fix(dashboard): logs auto-refresh reads live visibility, not a stale mount ref (#3972) (#3997)

The auto-refresh interval gated each tick on visibleRef, seeded once at mount and updated only by a visibilitychange event. A tab mounted while document.visibilityState is 'hidden' (background load, bfcache, embedded/proxied webviews) with no later visibilitychange left the ref false forever, so the interval ticked but never fetched — only the manual button worked. Read the live document.visibilityState in the tick instead.

* feat(compression): add Indonesian caveman rules and language pack (#3975)

Integrated into release/v3.8.27

(cherry picked from commit c9b5b1a892)

* fix(combo): shuffle strict-random fallback remainder to spread load (#3959) (#3998)

strict-random shuffled only the deck-selected slot 0 and left the fallback remainder in fixed priority order, so after a failing deck pick the chain always fell through to the same top-priority model — a persistently-failing model was retried on essentially every request and fallback load never spread across peers. Shuffle the remainder too (like the random strategy).

* Add provider auth visibility controls (#3953)

Integrated into release/v3.8.27

* fix(claude): forward client tool-search-tool anthropic-beta on the Claude OAuth path (#3974) (#3999)

The client-negotiated anthropic-beta: tool-search-tool-2025-10-19 was dropped on both Claude code paths (default executor rebuilt from static ANTHROPIC_BETA_CLAUDE_OAUTH; selectBetaFlags only read the client beta to gate thinking/effort), so claude.ai rejected deferred-tool requests with 400 'Tool reference not found'. Add an allowlist-merge (mergeClientAnthropicBeta) that unions the client's allowlisted betas into the outbound set on both paths, preserving #3415 (no forced thinking/effort).

* feat(providers): add model search filter to provider dashboard (#3950)

Integrated into release/v3.8.27

* fix(vision-bridge): force bridge for tokenrouter deepseek models (#3946)

Integrated into release/v3.8.27

* fix(executor): strip stream_options on non-streaming requests (#3884) (#4000)

Clients that send stream_options:{include_usage:true} regardless of stream (e.g. the OpenAI Python SDK) had it passed through on non-streaming calls; NVIDIA NIM rejected it with 400 'Stream options can only be defined when stream=True'. DefaultExecutor.transformRequest only injected/cleared stream_options on the streaming branch and never stripped a client-sent value when stream=false. Add a !stream strip branch; the streaming injection path is unchanged. Global to openai-compat providers.

* fix(qwen-web): cookie validation false-positive - check response body for user object (#3958)

Integrated into release/v3.8.27

* fix(db): persist backup retention days (#3970)

Integrated into release/v3.8.27

* 大量UI显示和i18n优化 (#3973)

Integrated into release/v3.8.27

* deps: bump the npm_and_yarn group across 1 directory with 2 updates (#3943)

Integrated into release/v3.8.27

* deps: bump form-data from 4.0.5 to 4.0.6 (#3944)

Integrated into release/v3.8.27

* deps: bump vite from 8.0.5 to 8.0.16 (#3942)

Integrated into release/v3.8.27

* chore(quality): re-baseline validation.ts 4407->4428 (#3958 qwen body-check)

The qwen-web validation body-check merged in #3958 pushed validation.ts past its
frozen size on the integrated release tip. Bump the baseline with justification;
no logic is separately extractable from the existing qwen-web validation branch.

* deps: bump the production group with 13 updates (#3915)

Integrated into release/v3.8.27 — low-risk group (playwright 1.60→1.61 minor + transitive patches; fumadocs-core 16.9→16.10 minor).

* chore(deps): ignore jscpd major bumps (v5 Rust rewrite breaks the duplication gate)

Our duplication ratchet (scripts/check/check-duplication.mjs) is pinned to jscpd@4
and parses jscpd-report.json against a frozen baseline. jscpd v5 is a native Rust
binary with no Node.js API and a different report/bin, so a major bump would break
the gate. Migrate deliberately, not via dependabot. Closes the noise from #3916.

* fix(perplexity-web): parse schematized diff_block stream so answers aren't empty (#4001)

Integrated into release/v3.8.27 — schematized diff_block parsing follow-up to #3938.

* refactor: modularize providerRegistry.ts into 159 individual provider plugins (#3993)

Modularize provider registry (#3594). Integrated into release/v3.8.27 after rebase + behavior-preservation verification (provider-consistency gate 159/232/0, typecheck, registry tests, build 556/556).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* fix(registry): restore byteplus + mimocode dropped by #3993 modularization

The provider-registry modularization (#3993) was cut from a base predating the
byteplus (#3877) and mimocode (#3837) registry entries, so merging it silently
dropped both providers (getRegistryEntry returned undefined → validation reported
'not supported'). Re-add them as registry modules in the new structure; registered
count 159→161, provider-consistency 161/232/0.

Also align the pre-existing qwen-web validator test to #3958: since the validator
now requires a real `user` object in the 200 body, the mock must carry one.

* refactor: modularize schemas (non-stacked) (#3988)

Modularize validation schemas (#3594). Integrated into release/v3.8.27 after rebase (reconciled the merged hiddenSidebarGroupLabels #3971 + intelligenceSyncRequestSchema into the new modules) + behavior verification (typecheck, 195 schema/settings/validation tests, build 556/556).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* fix(default-executor): honor custom providerSpecificData.baseUrl for OpenAI-format providers (#4002)

Integrated into release/v3.8.27 — honor custom providerSpecificData.baseUrl in DefaultExecutor (openai-format), tested.

* feat(openai): honor custom base URL in model discovery + complete openai/codex pricing (#4005)

Integrated into release/v3.8.27 — openai model-discovery honors custom base URL (SSRF-guarded) + pricing rows for new openai/codex models. Tested + baselines bumped.

* fix(live-ws): bridge sidecar events to dashboard (#4004)

Integrated into release/v3.8.27 — repair LiveWS sidecar (startup, same-origin /live-ws, main→sidecar compression.completed bridge, early-msg queue). Fixed the cookie-parse regex (\s) + added a focused unit test; baseline bumped for the non-blocking chatCore bridge.

* docs(troubleshooting): note MITM proxy cannot intercept Windows-host apps under WSL (#4003)

Integrated into release/v3.8.27 — MITM/WSL troubleshooting note.

* fix(repo): untrack accidentally-committed root node_modules symlink + gitignore it

A worktree node_modules symlink (-> the main checkout's node_modules) was staged by a
`git add -A` during the #3988 merge and committed into 05213ac6a. The symlink points
at the repo's own node_modules path, so checking it out turns the main checkout's
node_modules into a self-referential symlink (breaking tsx/all node ops). Untrack it and
add a root-anchored /node_modules ignore so the symlink form can't be re-committed (the
existing 'node_modules/' only matches directories).

* fix(quality): allowlist socks dep (declared by #4004, never allowlisted)

socks@^2.8.7 was added to package.json in #4004 (LiveWS sidecar, 02302131f)
as a phantom-dep cleanup but never added to dependency-allowlist.json, so
check:deps has been red on the release tip ever since. socks is the standard
SOCKS proxy client (dep of fetch-socks), legitimate and years old.

* feat(sse): real LLMLingua-2 ONNX compression engine (stable) (#4014)

Integrated into release/v3.8.27.

Adjustments before merge:
- Synced with the current release tip (was 11 commits behind).
- Added the 3 LLMLingua-2 ONNX optional-runtime deps to dependency-allowlist.json
  (@atjsh/llmlingua-2, @tensorflow/tfjs, js-tiktoken) — the only gate that was red.
- socks was allowlisted directly on release (separate fix d7db5c73d; it was declared
  by #4004 but never allowlisted, leaving check:deps red release-wide).

Verified locally: check:deps OK, file-size OK, public-creds OK, provider-consistency
161/232/0, typecheck:core clean, 24/24 LLMLingua tests pass. The only remaining Fast-QG
red is the pre-existing #3972 orphan test (request-logger-autorefresh-visibility-3972.test.tsx),
which is release-wide and unrelated to this PR.

* test(dashboard): rehome #3972 logs auto-refresh test so a runner collects it

tests/unit/request-logger-autorefresh-visibility-3972.test.tsx (added by #3972
via #3997) sat at the top level of tests/unit/ as a .tsx vitest test, which NO
runner collects: the node runner only globs *.test.ts, and test:vitest:ui only
runs tests/unit/ui. So the #3972 regression guard never executed in CI and
check:test-discovery was red release-wide. Move it under tests/unit/ui/ (the
collected vitest:ui path) and fix the relative import depth. Verified: the test
now runs and passes (2/2), and check:test-discovery is green.

* feat(compression): capture per-engine analytics (#3960) + Lite schema fix (#3952) (#4018)

Captures the net-new value from #3960 (per-engine breakdown analytics) and #3952 (Lite engine schema fix) onto release/v3.8.27. Fast QG green; 622/622 compression+analytics tests pass.

* fix(sse): guard model-less registry entries in getUnsupportedParams (mimocode) (#4015)

Real bugfix: guard model-less registry entries (mimocode) in getUnsupportedParams so handleChatCore no longer throws 'entry.models is not iterable' / reports 'All models failed' for unrelated requests. Includes a regression test. Fast QG green.

* feat(ci): Quality Gate v2 — Onda 0 + Onda 1 (gate flips, TIA, SAST, DAST-smoke, mutation infra) (#4016)

* docs(ops): add quality-gate assessment + replication playbook (Fase 9 foundation)

* feat(ci): flip oasdiff breaking-change gate to blocking (ratchet)

* docs(ops): deliver main branch-protection ruleset for owner to apply

* fix(ci): run typecheck:core in PR->release fast-gates (close fast-gates hole, part 1)

* perf(mutation): enable Stryker incremental mode + cache (scales the 60/80 rollout)

* feat(ci): commit CodeQL advanced config (security-extended), replacing default-setup

* feat(ci): version semgrep SAST workflow (owasp/secrets), advisory

* feat(quality): TIA test-impact map builder (import-graph; map built at runtime, gitignored)

* feat(quality): TIA impacted-test selector with run-all fail-safe

* fix(ci): run TIA-impacted unit tests in PR->release fast-gates (build map at runtime, fail-safe full)

* feat(ci): DAST-smoke per-PR (schemathesis subset + promptfoo injection-guard, blocking)

* fix(ci): unbreak Fase 9 PR CI (MDX frontmatter, CodeQL conflict, dast-smoke advisory)

- Add MDX frontmatter to docs/ops/{BRANCH_PROTECTION_MAIN,QUALITY_GATE_PLAYBOOK}.md.
  fumadocs rejects frontmatter-less docs -> 'npm run build' failed -> broke dast-smoke's
  build step (the release fast-gates never runs build, so this only surfaced on the PR).
- codeql.yml: workflow_dispatch-only until the owner switches repo CodeQL Default->Advanced
  (advanced configs cannot be processed while default setup is enabled; documented inline).
- dast-smoke.yml: job-level continue-on-error (advisory) so this brand-new gate matures
  before it blocks (repo convention: advisory -> blocking).

* ci(quality): make TIA unit-test step advisory until release test-debt is cleared

release/v3.8.27 carries ~17 pre-existing failing unit tests (budget #3537, apiKey
#3552, several Zod schemas, Puter/Qwen executors, mimocode entry, etc.) unrelated to
this PR — the new 'run tests on PR->release' gate surfaced them. Per the repo's
advisory->blocking convention, this step enters advisory (it still runs + reports)
so pre-existing debt doesn't block the gate program. typecheck:core stays blocking.
Flip to blocking (remove continue-on-error) once the release suite is green.

* fix(sse): preserve Kiro streaming finish_reason tool_calls (#3980) (#4025)

* fix(guardrails): preserve original image when vision-bridge describe fails (#4012) (#4026)

* feat(api): advertise combo capabilities on import surfaces (#3979) (#4027)

* feat(sse): delegated Anthropic Context Editing for Claude (clear_tool_uses) (#4021)

Opt-in Claude-only delegated compression: injects context_management.clear_tool_uses_20250919 at the Claude pre-serialization chokepoint (composes with clear_thinking, thinking first), threaded via ExecuteInput from handleChatCore. Pure edit-builder + 11 tests (7 unit + 4 e2e fetch-capture). Beta context-management-2025-06-27 already advertised; allowlist done. Telemetry/400-fallback/claude-web coverage deferred.

* fix(opencode): map x-session-affinity to x-opencode-session for custom providers (#4022) (#4028)

* fix(dashboard): Playground Compare tab loading + HTTP method guard (#4024)

randomUUID non-HTTPS fallback + static CompareTab import; raw HTTP TRACE->405 method guard wired into dev + standalone servers. Integrated into release/v3.8.27.

* refactor(dashboard): settings UI layout + API Keys naming (#4020)

Presentation/relabel refactor of the Settings dashboard (API Manager -> API Keys), card relocations, Toggle adoption, present-but-disabled engine steps. Auth-file changes are string/comment-only (no behavior change). Integrated into release/v3.8.27.

* fix: restore unit regressions dropped by lossy schema/registry modularizations (#4030)

Restores schema fields (combo reasoningTokenBuffer, budget-0 #3537, openrouter preset, proxy family #3777, resilience degradation/providerCooldown), qwen-web v2 endpoint+catalog, mimocode models key — all dropped by #3988/#3993 — and aligns 3 tests to #3941/#3993. Verified: 8 failing regression tests on release tip -> 131/131 green on this branch. Integrated into release/v3.8.27.

* fix(api): return 400 (not 500) for malformed JSON on /api/auth/login (#4031)

Wrap request.json() so a malformed/non-JSON login body returns a structured 400 instead of falling through to the 500 catch. Fixes the schemathesis high-risk-endpoint DAST finding (verified: schemathesis step now passes). +TDD test. Integrated into release/v3.8.27.

* feat(dashboard): real circuit-breaker state in the Combo Live cascade (U1b) (#4029)

Overlays real provider circuit-breaker state (GET /api/monitoring/health) onto the Combo Live cascade as a 'CB: OPEN · 41s' badge. Pure enrichRunWithBreakers + fail-soft useProviderBreakerHealth poll; graceful when health is absent. +13 tests. Integrated into release/v3.8.27.

* Fix promptfoo security assertion parsing (#4032)

* chore(deps): dependabot security bumps + drop unused gray-matter (#4036)

Integrated into release/v3.8.27 — dependabot security bumps (form-data/js-yaml/protobufjs/dompurify/hono) + drop unused gray-matter. Unblocks the npm audit:deps gate (Lint) branch-wide.

* fix(ci): scope TIA to node:test unit files only (mirror test:unit glob) (#4035)

Integrated into release/v3.8.27 — scopes the advisory TIA step to the test:unit node:test glob, fixing the 99 false failures. +4 TDD.

* Refine compression settings, storage labels, and sidebar grouping (#4033)

Integrated into release/v3.8.27 — relocate Token Saver into Compression Settings (controlled component), reorder Security/Authz tabs, storage labels + i18n relabel. Thanks @rdself!

* [codex] add per-key local usage command (#4034)

Integrated into release/v3.8.27 — per-key local @@om-usage command (cached quota, no upstream routing). Rebased onto modularized schemas/keys.ts + file-size rebaseline. Thanks @Witroch4!

* chore(release): reconcile v3.8.27 CHANGELOG + i18n mirrors

* ci(quality): unblock v3.8.27 release gates (zizmor pin + test-masking allowlist)

- zizmor ratchet (151→139, no regression): SHA-pin every action ref ADDED this
  cycle — codeql/dast-smoke/semgrep (3 new workflows) + trivy-action (docker-publish)
  + actions/cache (nightly-mutation). Pre-existing tag refs keep the repo convention.
- test-masking: add config/quality/test-masking-allowlist.json + allowlist support in
  check-test-masking.mjs (exempts ONLY the net-assert-reduction signal; tautology/skip/
  deletion still fire). Allowlists 2 verified-legitimate reductions:
  appearance-widget-settings-schema (#4033 removed showTokenSaverOnEndpoint field) and
  dashboard-shell-tabs (#3973 tabs→redirect refactor, asserts replaced). +4 gate tests.

* test(quality): reword test-masking self-test comments to avoid literal masking patterns

The added allowlist-test comments contained the literal strings 'assert.ok(true)' and
'.skip' which the masking detector's own regexes match as text — making the gate flag
its own test file (net +1 tautology/skip/extended-tautology vs main). Reworded to plain
prose ('a new tautology', 'a new skip marker'); test logic unchanged (24/24 pass).

* fix(quality): unblock v3.8.27 release — align 3 stale tests + restore modularized settings-schema parity

Release-PR full CI surfaced 3 deterministic test failures (no live product regression),
all stale vs legitimate cycle changes:

- settings-schema parity (#3988): the modularized updateSettingsSchema barrel
  (schemas/settings.ts) had diverged from the canonical settingsSchemas.ts (45 vs 85
  fields — 40 dropped + 6 extra), a lossy-modularization dead-code copy. Re-export from
  the canonical source so the barrel can never diverge again (runtime already uses
  canonical). Parity test now passes.
- api-manager permissions modal: #4034 added a 4th self-service switch (per-key usage
  allowance); a11y invariant (every switch type="button") still holds. Updated the
  static count 3 -> 4.
- pack-artifact policy: dist/http-method-guard.cjs became a required runtime path;
  added it to the test's expected missing-paths list.

Also documents the gate gap for Fase 9 (QUALITY_GATE_PLAYBOOK Parte 6): G1 run the
deterministic unit layer + test-masking on PR->release (not just PR->main), G2 a
modularization-parity gate (would have caught the #3988 drop at its PR), G3 flake
quarantine. Env flakes (LiveWS startup timeout, integration server-startup cascade)
are pre-existing/CI-env, triaged separately.

---------

Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: Veier04 <118300867+Veier04@users.noreply.github.com>
Co-authored-by: Felipe Sartori <felipesartori.ti@gmail.com>
Co-authored-by: WormAlien <164898390+WormAlien@users.noreply.github.com>
Co-authored-by: thezukiru <121331256+thezukiru@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: NOXX - Commiter <artur1992123@mail.ru>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Demiurge The Single <megamen932@gmail.com>
Co-authored-by: Witroch4 <witalo_rocha@hotmail.com>
2026-06-17 02:43:21 -03:00
Veier04
c9b5b1a892 feat(compression): add Indonesian caveman rules and language pack (#3975)
Integrated into release/v3.8.27
2026-06-16 09:12:35 -03:00
Diego Rodrigues de Sa e Souza
7509a32e9d fix(security): polynomial ReDoS in comboAgentMiddleware regex → main (#3983)
Brings the release/v3.8.27 fix (#3982) to main so CodeQL alerts #612/#613 close
on the next scan. Code + regression test only; the [3.8.27] CHANGELOG bullet lives
on release/v3.8.27 and reaches main when v3.8.27 ships (identical file → no merge
conflict). Detection pattern drops the unbounded surrounding newline run; global
strip pattern bounds it ({0,16}). Behavior unchanged (107 related tests green).
2026-06-16 08:46:03 -03:00
Diego Rodrigues de Sa e Souza
ca1e17f740 test(opencode-plugin): ESM default-export test (#3967)
The plugin became ESM-only when the CJS bundle was dropped to fix the OpenCode loader
(#3883), so tests/scaffold.test.ts's 'CJS default export resolves via require()' test
fails at publish time with 'Cannot find module ../dist/index.cjs' (it only runs in the
npm-publish opencode-plugin job, so the cycle never caught it). Replaced with an ESM
import of the built dist/index.js asserting the same v1 { id, server } shape; dropped the
now-unused createRequire import. omniroute@3.8.26 itself already published fine.
2026-06-16 02:50:40 -03:00
Diego Rodrigues de Sa e Souza
d59cd14391 fix(ci): electron-release publish-npm contents:write (#3966)
The v3.8.25→v3.8.26 #3874 fix bumped npm-publish.yml's publish job to contents:write
(gh release upload for the SBOM). electron-release.yml calls that workflow as a reusable
job (publish-npm) but only granted contents:read — a reusable job cannot request more
than the caller grants, so GitHub rejected the v3.8.26 electron run at startup
(startup_failure). Aligns the caller permission to contents:write.
2026-06-16 02:38:06 -03:00
Diego Rodrigues de Sa e Souza
4d21044ba5 fix(release): post-merge quality gates to main for v3.8.26 (#3964)
Cherry-picks #3961 + #3962 from release/v3.8.26 to main (parity before tagging).
2026-06-16 02:33:19 -03:00
Diego Rodrigues de Sa e Souza
81a37b67ed Release v3.8.26 (#3875)
OmniRoute v3.8.26 — see CHANGELOG.md [3.8.26] for the full notes.

Highlights: Vertex AI media generation (#3929), GLM-5.2 effort-tier routing (#3885),
sticky round-robin combos (#3846), OpenRouter connection presets (#3878), compression
prompt-cache fix (#3936/#3890), and a security pass (form-data/vite + workflow hardening, #3949).

Co-authored-by: artickc <artickc@users.noreply.github.com>
Co-authored-by: rdself <rdself@users.noreply.github.com>
Co-authored-by: herjarsa <herjarsa@users.noreply.github.com>
Co-authored-by: Jack Smith <16862258+YunyunZhai@users.noreply.github.com>
Co-authored-by: dhaern <dhaern@users.noreply.github.com>
Co-authored-by: adivekar-utexas <adivekar-utexas@users.noreply.github.com>
Co-authored-by: megamen32 <megamen32@users.noreply.github.com>
Co-authored-by: zhiru <zhiru@users.noreply.github.com>
Co-authored-by: insoln <insoln@users.noreply.github.com>
Co-authored-by: diego-anselmo <diego-anselmo@users.noreply.github.com>
2026-06-16 01:00:40 -03:00
dependabot[bot]
1f87a9589c deps: bump electron from 42.3.3 to 42.4.0 in /electron (#3914)
electron 42.3.3->42.4.0; rebased onto main after #3913 to resolve the electron/package-lock.json conflict. /electron-only, Build green.
2026-06-15 21:16:57 -03:00
dependabot[bot]
f5706a6528 deps: bump electron-builder from 26.15.2 to 26.15.3 in /electron (#3913)
electron-only dependency bump; Build green, reds are pre-existing main-wide (mid-cycle). Verified the PR touches only electron/package*.json.
2026-06-15 21:13:43 -03:00
Diego Rodrigues de Sa e Souza
4066a2ca31 fix(ci): grant contents:write to npm publish job for SBOM attach (#3874)
Post-release v3.8.25 CI hotfix — SBOM attach needs contents:write.
2026-06-15 04:17:19 -03:00
Diego Rodrigues de Sa e Souza
35dbf0eea1 Release v3.8.25 (#3866)
* chore(release): continue v3.8.25 development cycle after main code-sync (r5)

main fast-forwarded to release/v3.8.25 (#3863): unblocked Build+Docker via
#3864, plus #3837 (mimocode proxy) and #3862 (trivy bump). This marker
re-opens the umbrella PR for further v3.8.25 work. No version bump.

* fix(db): persist the Keep-latest-backups retention setting (#3834) (#3867)

* fix(oauth): clear GitLab Duo setup message instead of 500 (#3861) (#3868)

* test(oauth): prove refresh_token preserved on real gemini-cli/antigravity dispatch (#3850) (#3869)

* feat(compression-ui): unified compression config UI — per-engine pages + combos editor + menu + WS default-on (#3860)

Integrated into release/v3.8.25 — feat(compression-ui): unified compression configuration UI (Compression Hub + per-engine Lite/Aggressive/Ultra pages + combos editor + sidebar entry + live-WS default-on). File-size re-baselined for sidebarVisibility.ts/chatCore.ts growth; orphan ws test relocated to a collected path.

* docs(changelog): complete the v3.8.25 release notes + credit all contributors

Audited every commit since v3.8.24 and filled the gaps the [3.8.25] section
was missing: a New Features section (compression engines + Compression Studios
#3848, compression UI #3860, injection-guard #3857, kiro discovery #3836, Veo
#3839, mimocode proxy #3837, Arena ELO flag #3821), 9 more Fixed entries
(#3811/#3807/#3759/#3849/#3838/#3835/#3814/#3820/#3819), a Security section
(CCR IDOR #3859, supply-chain #3824), and an Internal/Quality section. Every
contributor and issue reporter is now credited.

* docs(changelog): restore + complete the v3.8.25 release notes

Re-adds CHANGELOG.md (a prior server-side commit accidentally dropped it) with
the complete, audited [3.8.25] section: New Features, the full Fixed list,
Security & Hardening, and Internal/Quality — every contributor and issue
reporter credited.

* chore(release): finalize v3.8.25 — reconcile CHANGELOG + i18n mirrors, document OMNIROUTE_MAX_PENDING_MIGRATIONS, green the unit suite

Release-gate reconciliation for v3.8.25:
- CHANGELOG: dated 2026-06-14, linked #3826, rolled up file-size re-baselines (#3823/#3833),
  recorded the test-greening; re-synced all 41 i18n CHANGELOG mirrors.
- Documented OMNIROUTE_MAX_PENDING_MIGRATIONS (#3416) in .env.example + ENVIRONMENT.md.
- Greened the unit suite (was merged red on 4 CI shards): aligned 10 stale tests to this
  cycle's intended behavior (#3838/#3822/#3501/SOCKS5/Vertex-Express/Antigravity) and the
  same-provider 503 fall-through test; de-flaked the compression benchmark reproducibility
  and ServiceSupervisor crash tests. No production code changed.

* ci(security): clear OpenSSF Scorecard code-scanning noise + harden workflow token permissions

The Security tab held 155 open alerts, ALL from the advisory OpenSSF Scorecard tool
(#3824) — supply-chain/posture scores, not code vulnerabilities — which drowned out
real CodeQL findings.

- scorecard.yml: stop uploading SARIF to the code-scanning tab (drop the upload-sarif
  step + the now-unused security-events: write). The run still produces the OpenSSF
  badge (publish_results) and a downloadable SARIF artifact.
- TokenPermissions hardening (the high-severity, genuinely-valuable subset): set each
  workflow's top-level token to read-only and grant the exact writes at the job level
  that needs them — npm-publish (id-token/packages on publish jobs), docker-publish
  (packages on build), electron-release (contents on build/release, id-token/packages
  on publish-npm), build-fork (packages on build), claude (empty top-level; job grants
  its own). The 155 existing alerts were dismissed.

Not adopting repo-wide SHA-pinning (143 PinnedDependencies advisories) — declined.

* test(integration): align stale wiring/socks5 integration tests to this cycle's behavior

These were red on the CI Integration job (pre-existing). No production code changed:
- integration-wiring: the combos page no longer renders a per-page EmailPrivacyToggle
  (#3822 consolidated it into Settings → Appearance); the provider-detail test-result
  masking and upstream-proxy copy moved to decomposed components (#3501
  BatchTestResultsModal / UpstreamProxyCard) — assertions now read the owning files.
- api-routes-critical: SOCKS5 is now enabled by default (opt-out), so the disabled-
  rejection test must set ENABLE_SOCKS5_PROXY=false explicitly (an unset env now means
  enabled).

(The ~32 live-Gemini integration tests are gated on OMNIROUTE_API_KEY and skip in CI;
they only 'fail' locally when that key is present without a running server.)
2026-06-15 03:32:11 -03:00
Diego Rodrigues de Sa e Souza
b4180145e6 Merge release/v3.8.25 into main (#3863)
Code-sync release/v3.8.25 → main: unblocks main Build + Docker Hub (#3864 SUPPLY_CHAIN.md frontmatter) + mimocode per-account proxy (#3837) + trivy-action bump (#3862). i18n CHANGELOG drift left for /generate-release. Dev continues on release/v3.8.25.
2026-06-14 21:31:49 -03:00
dependabot[bot]
36baf77ad5 chore(deps): bump aquasecurity/trivy-action (#3862)
Integrated into release/v3.8.25 — chore(deps): bump aquasecurity/trivy-action 0.28.0→0.36.0 (supply-chain scan action, #3824 workflow).
2026-06-14 21:30:40 -03:00
PizzaV
f42e8fa751 feat(mimocode): per-account proxy support for multi-account round-robin (#3837)
Integrated into release/v3.8.25 — feat(mimocode): per-account proxy for multi-account round-robin (runWithProxyContext per account, keyed by fingerprint). Orphan test relocated to a collected vitest path (14/14 green).
2026-06-14 21:30:02 -03:00
Diego Rodrigues de Sa e Souza
337cd18932 fix(sse): clamp Gemini thinking budget to model cap (#3842) (#3865) 2026-06-14 21:27:25 -03:00
Diego Rodrigues de Sa e Souza
e068a63530 fix(docs): add MDX frontmatter to SUPPLY_CHAIN.md (unblocks main Build) (#3864)
Integrated into release/v3.8.25 — fix(docs): SUPPLY_CHAIN.md MDX frontmatter (unblocks main Build + Docker Hub).
2026-06-14 21:17:09 -03:00
Diego Rodrigues de Sa e Souza
9847684f0d chore(release): continue v3.8.25 development cycle after main code-sync
main was fast-forwarded to release/v3.8.25 (#3805); this marker re-opens the
umbrella PR so further v3.8.25 work keeps flowing to main. No version bump —
development continues on the current v3.8.25 line.
2026-06-14 18:14:34 -03:00
Diego Rodrigues de Sa e Souza
78a1fb40a0 Merge release/v3.8.25 into main (#3805)
Code-sync release/v3.8.25 → main: 40 commits (review-prs r1-r3 + Fase 7/8 quality-gates, supply-chain, resilience, injection-guard, CCR IDOR fix). No versioned publish (no tag/npm/Electron) — dev continues on release/v3.8.25.
2026-06-14 18:11:45 -03:00
Diego Rodrigues de Sa e Souza
cbb332d355 Fase 7 finalize — 3 catracas advisory→bloqueante + re-baseline consciente v3.8.25 (#3809)
Integrated into release/v3.8.25 — Fase 7 finalize: 3 catracas advisory→bloqueante (dead-code/cognitive-complexity/type-coverage) + re-baseline consciente.
2026-06-14 18:06:56 -03:00
Diego Rodrigues de Sa e Souza
c4f2af70f0 ci(quality): install advisory security scanners so Fase 7 gates run (gitleaks/osv/actionlint/zizmor) (#3858)
Integrated into release/v3.8.25 — Fase 7: scanners advisory no CI (gitleaks/osv/actionlint/zizmor).
2026-06-14 18:03:21 -03:00
Diego Rodrigues de Sa e Souza
931afe3482 Fase 8 · Bloco B — suíte de correção (property + golden + SSE-correctness) (#3808)
Integrated into release/v3.8.25 — Fase 8 Bloco B (property + golden + SSE-correctness).
2026-06-14 18:02:51 -03:00
Diego Rodrigues de Sa e Souza
cf5898205a fix(security): CCR cross-tenant IDOR — scope store per-principal + bound memory (#3859)
Integrated into release/v3.8.25 — fix(security): CCR cross-tenant IDOR (scope store per-principal + bound memory).
2026-06-14 18:02:36 -03:00
Diego Rodrigues de Sa e Souza
aa8fc4157d Fase 8 · Bloco A — supply-chain (provenance, SBOM, Trivy, Scorecard) advisory (#3824)
Integrated into release/v3.8.25 — Fase 8 Bloco A (supply-chain: provenance, SBOM, Trivy, Scorecard) advisory.
2026-06-14 18:02:21 -03:00
Diego Rodrigues de Sa e Souza
d728bfbb1e Fase 8 · Bloco D — injection-guard em todas as rotas LLM + red-team (#3857)
Integrated into release/v3.8.25 — Fase 8 Bloco D (injection-guard em todas as rotas LLM + red-team).
2026-06-14 18:02:18 -03:00
Diego Rodrigues de Sa e Souza
d3146a1751 Fase 8 · Bloco C — resiliência runtime (chaos + heap-growth + k6 soak) (#3854)
Integrated into release/v3.8.25 — Fase 8 Bloco C (resilience: chaos + heap-growth + k6 soak).
2026-06-14 18:02:08 -03:00
Diego Rodrigues de Sa e Souza
4ffc55cfe4 feat(compression): compression engines + async pipeline + Compression Studios (#3848)
Integrated into release/v3.8.25.
2026-06-14 10:45:22 -03:00
Diego Rodrigues de Sa e Souza
c8b9544d54 test(proxy): guard per-connection direct bypass over global proxy (#2996) (#3853) 2026-06-14 10:33:22 -03:00
Diego Rodrigues de Sa e Souza
7c080941d1 feat(connections): per-connection disable-cooldown opt-out (#2997) (#3852) 2026-06-14 10:32:26 -03:00
Abhishek Divekar
2670a0a819 docs(ui): clarify routing settings copy for strategy sync + sticky limit (#3843)
Clarifies that the Default Strategy control syncs both new combo defaults and global
account fallback routing, and updates the Round Robin sticky-limit helper text to call
out account-level fallback behavior. Copy-only change to ComboDefaultsTab + en.json.

Integrated into release/v3.8.25.

Co-authored-by: Abhishek Divekar <adivekar@utexas.edu>
2026-06-14 10:32:14 -03:00
NOXX - Commiter
948cf1f92c feat(kiro): live per-account model discovery via ListAvailableModels (#3836)
Kiro's catalog is per-account / per-tier (and admin-curated for IAM Identity Center
orgs), which the static registry can't reflect. The models route now discovers the
live list from the CodeWhisperer ListAvailableModels API with the stored OAuth token
(Builder ID / social and IdC accounts; profileArn only as a retry to avoid 403,
region-matched with us-east-1 fallback), falling back to the static registry catalog
when the token is missing/expired or the upstream is unavailable so import never breaks.

Integrated into release/v3.8.25.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-14 10:30:32 -03:00
NOXX - Commiter
ed0638c0f1 feat(gemini/vertex): surface Veo video models in dynamic discovery (#3839)
Gemini / Vertex / Vertex AI Express already discover their catalog dynamically from
v1beta/models, but video (Veo) models use predictLongRunning, which was not mapped —
so they never surfaced. parseGeminiModelsList now recognizes predictLongRunning and
exposes Veo video models alongside chat/image/embedding/audio.

Integrated into release/v3.8.25.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-14 10:28:48 -03:00
Abhishek Divekar
2a26fea530 fix(quota): surface OpenCode Go missing-quota-API as a latched diagnostic (#3838)
Diagnostic mitigation: OpenCode Go has no public quota API today (the configured
endpoints return 404 / Z.ai 401). The fetcher now logs a single latched (per-process)
404 warning pointing at the upstream tracking issues, caches the "endpoint unavailable"
result for 5 minutes to avoid hammering, and fails open. The dashboard messaging is
clarified with the OMNIROUTE_OPENCODE_GO_QUOTA_URL override hint.

Integrated into release/v3.8.25.

Co-authored-by: Abhishek Divekar <adivekar@utexas.edu>
2026-06-14 10:27:39 -03:00
lukmanc405
058946bd04 fix(models): don't auto-hide transient (rate-limited/timeout) failures on Test All (#3849)
With Auto-hide failed models on (default), a Test All sweep across 10+ models in
parallel reliably trips per-account rate limits on subscription-tier providers, and
the 429'd/timed-out models were auto-hidden — silently removing working models from
/v1/models with no easy recovery. evaluateTestAllEntry now surfaces transient failures
(rateLimited/isTimeout) as an 'error' icon but keeps them visible; only genuine
(non-transient) failures are still auto-hidden.

Integrated into release/v3.8.25.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-14 10:25:55 -03:00
NOXX - Commiter
bcb7ed00c7 fix(pricing): add missing Kiro model pricing rows (#3835)
The kiro table in DEFAULT_PRICING was missing models the Kiro registry serves
(most visibly claude-sonnet-4.6), so getPricingForModel() returned null and their
usage cost was reported as $0.00. Adds the missing rows.

Integrated into release/v3.8.25.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-14 10:24:24 -03:00
Ramel Tecnologia - Rafa Martins
315ac98b49 fix(i18n): translate missing embeddedServices keys across 37 locales (#3819)
Fills the previously-untranslated embeddedServices / embeddedServicesSubtitle keys
(__MISSING__ placeholders) with proper translations in 37 locale message files,
improving UI key coverage. JSON validated; i18n UI-coverage gate (threshold 65) passes.

Integrated into release/v3.8.25.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-14 09:49:52 -03:00
Ramel Tecnologia - Rafa Martins
f2f909bd7f fix(ui): expand request log table height with vertical resize (#3820)
The request log table is given a comfortable minimum height (~10 rows) and is
user-resizable vertically, replacing the previous flex/overflow-hidden constraints that
clipped it short. Pure layout change to the logs page and RequestLoggerV2 card.

Integrated into release/v3.8.25.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-14 09:45:25 -03:00
Ramel Tecnologia - Rafa Martins
ef07a19de6 fix(ui): render country flags via flagcdn SVGs for Windows compatibility (#3814)
Windows does not render regional-indicator flag emojis. The LanguageSelector now maps
a flag emoji's regional-indicator code points to an ISO country code and renders the
flag from flagcdn, falling back to the raw emoji span when the glyph is not a
two-letter regional pair or the image fails to load (onError).

Integrated into release/v3.8.25.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-14 09:44:34 -03:00
Tubagus
5ace548bc5 fix(combo): return replay response in round-robin streaming path (#3811)
A round-robin combo serving a streaming response returned a 500
(TypeError: ReadableStream is locked). validateResponseQuality() peeks streaming
bodies via getReader(), which locks result.body and returns an unlocked replay in
quality.clonedResponse. The priority strategy already returns
`quality.clonedResponse ?? result`, but the round-robin success path returned the
locked original. This mirrors the priority strategy so the body pipes downstream.

Added a regression test (#3811) that fails (body locked) without the fix.

Integrated into release/v3.8.25.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-14 09:42:59 -03:00
Randi
772e6ba493 Consolidate email privacy control into Settings (#3822)
Moves the account email visibility control into Settings › Appearance (above Show
Sidebar Items) and removes the page-level email reveal buttons from combos, logs,
provider detail, provider quota, quota sharing, and the edit-connection modal. The
global masking state is unchanged — existing account labels still consume the shared
emailPrivacyStore — so one toggle now governs masking everywhere. The old
EmailPrivacyToggle component is replaced by AccountEmailVisibilitySetting.

Integrated into release/v3.8.25.

Co-authored-by: R.D. <rogerproself@gmail.com>
2026-06-14 09:37:19 -03:00
Diego Rodrigues de Sa e Souza
70319eb831 fix(combo): sessionless combo stickiness + reasoning-aware readiness (#3825) (#3847) 2026-06-14 09:04:28 -03:00
Randi
31e4e46ef9 Expose Arena ELO sync in feature flags (#3821)
Adds ARENA_ELO_SYNC_ENABLED to the Dashboard Feature Flags registry (DB-overridable),
routes Arena ELO startup/status checks through the shared feature-flag resolver while
preserving the existing env fallback, and refreshes env docs (adds the missing
STREAM_READINESS_TIMEOUT_MS example) so env/doc sync stays green.

Integrated into release/v3.8.25.

Co-authored-by: R.D. <rogerproself@gmail.com>
2026-06-14 08:45:02 -03:00
Randi
ebf06b5e6c fix(reasoning): map max effort to xhigh by default (#3826)
Reuse the xhigh opt-out policy for OpenAI-compatible `max` normalization: non-Claude
providers map `max` to `xhigh` unless the target model explicitly opts out (DeepSeek
via OpenRouter supports xhigh), downgrading to `high` only on explicit opt-outs.
Resolves common Claude aliases (anthropic/claude-opus-4.6, anthropic.claude-opus-4-6,
short/dated/-thinking variants) back to the canonical Claude xhigh-support list, and
keeps literal `max` pass-through for native Claude/CC providers that support it. Also
moves the mistral/github reasoning-effort rejection ahead of normalization (it was
previously dead code for `max`). Includes the v3.8.25 release file-size re-baseline.

Integrated into release/v3.8.25.

Co-authored-by: R.D. <rogerproself@gmail.com>
2026-06-14 08:43:45 -03:00
Diego Rodrigues de Sa e Souza
9f2d062083 chore(quality): reconcile file-size baseline for prettier-inflated v3.8.25 fixes (#3833) 2026-06-14 02:09:26 -03:00
Diego Rodrigues de Sa e Souza
e2d171c63e test(combo): cover skipProviderBreaker consumer gate (#2743 gap d) (#3832) 2026-06-14 02:07:10 -03:00
Diego Rodrigues de Sa e Souza
5875c7993f fix(providers): surface real Devin error + fix Windsurf auth instructions (#3324) (#3829) 2026-06-14 02:05:22 -03:00
Diego Rodrigues de Sa e Souza
c9e24ae48c fix(grok-web): clearer 403 message for anti-bot/IP-reputation blocks (#3474) (#3830) 2026-06-14 02:04:12 -03:00
Diego Rodrigues de Sa e Souza
3e79d92744 fix(db): env-overridable mass-pending-migrations threshold (#3416) (#3827) 2026-06-14 02:02:56 -03:00
Diego Rodrigues de Sa e Souza
01c8f2d3dd test(proxy): cover Vercel-relay proxyFetch path (#2743 gap c) (#3831) 2026-06-14 02:02:30 -03:00
Diego Rodrigues de Sa e Souza
c29e83a0ba fix(cli): surface 'omniroute runtime repair' in native-module errors (#3476) (#3828) 2026-06-14 02:02:02 -03:00
Diego Rodrigues de Sa e Souza
01e6cabeff chore(quality): re-baseline file-size for chat.ts growth (#3758 follow-up) (#3823) 2026-06-14 01:42:09 -03:00
Diego Rodrigues de Sa e Souza
5b71b05a5e fix(antigravity): per-request Pro-family upstream-id fallback chain (#3786) (#3818)
* fix(antigravity): per-request Pro-family upstream-id fallback chain (#3786)

* chore(quality): re-baseline file-size for antigravity.ts growth (#3786)
2026-06-14 01:40:35 -03:00
Diego Rodrigues de Sa e Souza
826c533a59 fix(sse): retry once on STREAM_EARLY_EOF for single-model requests (#3758) (#3817) 2026-06-14 01:39:05 -03:00
Diego Rodrigues de Sa e Souza
ef324cd00e fix(models): preserve eye-hidden models across auto-sync (#3782) (#3816)
* fix(models): preserve eye-hidden models across auto-sync (#3782)

* chore(quality): re-baseline file-size for models.ts growth (#3782)
2026-06-14 01:38:38 -03:00
Diego Rodrigues de Sa e Souza
e952ae1406 fix(providers): correct lmarena cookie hint to arena-auth-prod-v1 (#3810) (#3815) 2026-06-14 01:37:30 -03:00
Randi
6781a843f2 fix: stream routed SSE chunks promptly (#3759)
Reworks stream readiness as a ping/zombie filter instead of a semantic content
gate: downstream streaming is released as soon as any structured non-ping SSE
event arrives, replaying the buffered prefix. Removes the fixed 2s first-byte
cap (readiness now inherits REQUEST_TIMEOUT_MS unless STREAM_READINESS_TIMEOUT_MS
is set) so slow first-byte reasoning providers no longer false-504.

Combo stream quality stays strict: validateResponseQuality still requires an
actual content_block / known non-Claude payload before accepting a routed target.
Also normalizes multi-line data: framing, metadata-prefixed events, and final
events that arrive without a trailing blank line.

Integrated into release/v3.8.25.

Co-authored-by: R.D. <rogerproself@gmail.com>
2026-06-14 01:10:24 -03:00
Felipe Almeman
cccb087011 fix(claude): strip reasoning-effort suffix from Claude model ids (#3807)
The Claude / Claude-Code model picker (VS Code Copilot's "Effort" slider)
advertises effort variants by appending a suffix to the base model id
(claude-...-{low,medium,high,xhigh,max}). Anthropic has no such model, so the
suffixed id was forwarded verbatim and 404'd upstream — repeated 404s then
tripped the account circuit breaker, surfacing to clients as a bogus
"rate limited" cooldown.

splitClaudeEffortSuffix() strips the suffix off Claude / Claude-Code targets
(the upstream receives the real base id) and surfaces the level as
reasoning_effort so the OpenAI->Claude translator / CC bridge convert it into
thinking/effort config. Explicit client effort is never overridden; native
Claude passthrough is untouched.

Integrated into release/v3.8.25.

Co-authored-by: Felipe Almeman <felipe@aireset.com.br>
2026-06-14 01:09:52 -03:00
Diego Rodrigues de Sa e Souza
e38d225124 fix(intelligence): run pricing + models.dev sync from the live startup path (#3806)
Wires initPricingSync + initModelsDevSync into instrumentation-node.ts (self-gated, opt-in preserved) so they actually run in the standalone runtime.
2026-06-13 22:42:00 -03:00
diegosouzapw
1a1f236b7d chore(release): open v3.8.25 development cycle 2026-06-13 22:06:01 -03:00
Diego Rodrigues de Sa e Souza
b7ac403c81 fix(intelligence): run Arena ELO sync from the live startup path (#3803)
Initializes initArenaEloSync() from instrumentation-node.ts (the Next standalone startup hook) instead of the never-executed server-init.ts, so the Free Provider Rankings page (#3799) actually gets data. On by default; opt out with ARENA_ELO_SYNC_ENABLED=false.
2026-06-13 20:49:54 -03:00
Diego Rodrigues de Sa e Souza
54b89e5c5b feat(intelligence): enable Arena ELO sync by default (#3802)
ARENA_ELO_SYNC_ENABLED flips to on-by-default (opt out with =false) so the Free Provider Rankings page (#3799) has data out of the box. Sync stays non-blocking/never-fatal.
2026-06-13 20:16:26 -03:00
Diego Rodrigues de Sa e Souza
c5e1102989 chore(test): drop duplicate free-provider-rankings test (#3801)
#3799 already shipped tests/unit/freeProviderRankings.test.ts (thorough pure-function
coverage). My #3800 file duplicated it; remove the redundant copy. The matching
algorithm (stripVersionSuffix/findMatchingIntelligence) stays covered by #3799's tests.
2026-06-13 19:33:31 -03:00
Diego Rodrigues de Sa e Souza
24c785a3ba fix(v3.8.24): plugins menu + proxy IP-family selector + CodeQL/test cleanup (#3800)
Surfaces the Plugins page (marketplace, #3656) in the sidebar; adds the proxy IP-family selector (auto/ipv4/ipv6) completing #3777's UI; clears the remaining CodeQL URL-substring alerts; covers #3799 with tests and fixes the costs-section count.
2026-06-13 19:30:31 -03:00
PizzaV
c8dee8c5f7 feat: free provider rankings page by Arena AI ELO scores (#3799)
Adds a dashboard page + /api/free-provider-rankings route ranking free providers by model ELO/intelligence scores. Pure computation over the existing provider registry + model-intelligence data (no external fetch). Sidebar entry included.
2026-06-13 18:50:54 -03:00
Diego Rodrigues de Sa e Souza
76a07cf7a5 Release v3.8.24 (#3747)
Release v3.8.24 — see CHANGELOG.md [3.8.24] for the full notes and the PR description for the contributors hall. Integration of release/v3.8.24 into main.
2026-06-13 17:27:40 -03:00
dependabot[bot]
420d62b420 deps: bump esbuild from 0.28.0 to 0.28.1 (#3746)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.28.0 to 0.28.1.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.0...v0.28.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-13 00:53:13 -03:00
diegosouzapw
ec78fe3d2c fix(publish): clean opencode-plugin node_modules after tsup build to prevent E415 hard-link tarball rejection 2026-06-13 00:07:23 -03:00
Diego Rodrigues de Sa e Souza
de60b4b171 Release v3.8.23
* chore(release): open v3.8.23 development cycle

* fix(anthropic): strip top_p when temperature is set to avoid 400 (#3691)

Integrated into release/v3.8.23

* fix(vertex): support Vertex AI Express-mode API keys (#3690)

Integrated into release/v3.8.23

* fix(stream): error on empty Claude SSE instead of synthetic success (#3689)

Integrated into release/v3.8.23

* fix(oauth): stop token-refresh invalidation loop + harden proxy resolution (#3692)

Integrated into release/v3.8.23

* docs: add FUNDING.yml and Support section to README (#3698)

Integrated into release/v3.8.23

* feat: gemini - handle known ratelimits (#3686)

Integrated into release/v3.8.23

* fix: stream combo fails over on empty content-filtered response (#3685) (#3702)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(antigravity): preserve gemini-3.1-pro high/low budget tiers (#3696) (#3703)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(auto-combo): add auto-updating model intelligence scoring (#3660)

Integrated into release/v3.8.23

* fix(gemini): context-mode fallback for signatureless tool calls (#3688) (#3704)

* chore(quality-gate): reconcile file-size baseline (27 files + providerLimits.ts) (#3705)

* feat(vertex): dynamic model discovery via Generative Language models API (#3712)

Integrated into release/v3.8.23. Vertex dynamic model discovery — surfaces image models (imagen-*, gemini-*-image), embeddings and audio from the live Generative Language catalog, with cached→static fallback and the shared parseGeminiModelsList helper. Validated: parser test 5/5, typecheck:core clean.

* fix(combo): gate reasoning token buffer (#3700)

Integrated into release/v3.8.23. Makes the #3588 reasoning token buffer safe and configurable: only inflates max_tokens when the model has a known, non-default output cap and the buffered value fits inside it; otherwise preserves/clamps the client limit. Adds the reasoningTokenBufferEnabled kill switch (default ON). Validated: combo-routing-engine 81/81, combo-config 25/25, combo-quality-validator-reasoning 12/12, phase1f 10/10, typecheck:core clean.

* refactor(#3501): god-component Phase 1g-1j — client 4062→3408 LOC (-654) (#3717)

Phase 1g-1j of #3501: client 4062→3408 LOC. Pure extraction (ProviderPlaygroundPanel, useCommandCodeAuth, useExternalLinkFlow+ExternalLinkModal, useAuthFileHandlers) + loadConnProxies ReferenceError fix + phase1f test path fix.

Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>

* refactor(#3501): god-component Phase 1k-1m — client 3408→2553 LOC (-855) (#3721)

Phase 1k-1m of #3501: client 3408→2553 LOC. Pure extraction (useModelImportHandlers+ImportProgressModal, useModelVisibilityHandlers, ProviderModelsSection).

Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>

* docs(changelog): restore #3590 bullet lost on the v3.8.20 release branch

The fix itself reached main pre-tag via cherry-pick #3591, but its changelog
bullet (commit e33fdd4ab) only ever existed on release/v3.8.20 after the
squash-merge. Restored under [3.8.20] per the 2026-06-12 release-branch
leftover audit (_tasks/release-audit/release-leftovers-audit-2026-06-12.md).

* fix(kiro): resolve quota for IAM Identity Center accounts missing a profileArn (#3722)

Integrated into release/v3.8.23

* refactor(#3501): god-component Phase 1n-1s — client 2553→1376 LOC (-1177) (#3725)

Phase 1n-1s of #3501: client 2553→1376 LOC. Pure extraction (ConnectionsListPanel, ConnectionsHeaderToolbar, ZedImportCard, BatchTestResultsModal, AdaptaTutorialModal, useApiKeySave + helpers).

Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>

* feat(model-lockout): settings UI, backend integration, error classification, and success-decay recovery (#3629)

Integrated into release/v3.8.23

* refactor(#3501): god-component Phase 1t — client 1376→781 LOC (≤800 TARGET REACHED ) (#3727)

Phase 1t of #3501: client 1376→781 LOC (≤800 reached). Original god-component 12,882→781 (−94%).

Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>

* fix: bundle @omniroute/opencode-plugin inside omniroute + add 'setup opencode' CLI command (#3726)

Integrated into release/v3.8.23

* feat(vertex): self-tracked USD spend since account added (#3724)

Integrated into release/v3.8.23

* fix(qwen-web): migrate to v2 chat API with full cookie-jar replay (#3288) (#3723)

Integrated into release/v3.8.23

* fix(sse): make safeLogEvents async — 'await' in a sync function broke every chatHelpers import

#3692 added a lazy 'await import(proxyEgress)' for egress-IP visibility inside
safeLogEvents, which is a sync function — an ES syntax error. It went unnoticed
because typecheck:core does not cover src/sse and no test in the merge gates
loaded chatHelpers via tsx; any consumer that did (chat-context-relay and
chat-route-coverage suites, integration harnesses) failed at module load with
'await can only be used inside an async function'.

safeLogEvents is fire-and-forget logging with an outer try/catch, so making it
async (and 'void'-ing the single chat.ts call site) preserves behavior exactly.

Validation: tests/unit/chat-context-relay.test.ts + chat-route-coverage.test.ts
went from failing-at-load to green (+14 tests destravados).

* fix(sse): remove cross-provider credential leak in emergency fallback + combo/proxy audit fixes (#3699)

Integrated into release/v3.8.23

* fix(executors): inject MiMoCode anti-abuse marker so free endpoint stops 403ing (#3728)

Integrated into release/v3.8.23

* fix(dashboard): repair "Test all models" — toast crash, status icons, auto-hide (#3729)

Integrated into release/v3.8.23

* chore(deps): bump actions/upload-artifact from 4 to 7 (#3735)

Integrated into release/v3.8.23 — aligns upload-artifact to v7 (already used across ci.yml).

* chore(deps): bump actions/cache from 4 to 5 (#3734)

Integrated into release/v3.8.23 — actions/cache v4→v5.

* chore(deps): bump actions/download-artifact from 4 to 8 (#3733)

Integrated into release/v3.8.23 — download-artifact v4→v8.

* feat(fallback): add OMNIROUTE_EMERGENCY_FALLBACK env switch (#3741)

Adds an OMNIROUTE_EMERGENCY_FALLBACK env switch to disable the emergency budget-exhaustion fallback (reroute to free nvidia/gpt-oss-120b). Default unchanged (enabled). Closes #3739, related #2879.

Integrated into release/v3.8.23.

* i18n: comprehensive zh-CN translation improvements (#3736)

Aligns zh-CN to en (hundreds of entries), translates batch-action labels + settings sidebar menu, adds categoryConfig/endpointTokenSaver keys, resolves __MISSING__ stubs. Sidebar/SidebarTab hardcoded strings replaced with t(). en.json purely additive (8 new sidebar.* keys, 0 removed); cli-i18n gate green.

Integrated into release/v3.8.23.

* chore(release): v3.8.23 — 2026-06-12

- CHANGELOG: complete v3.8.23 section (28 bullets, 27 commits)
- fix(webdav): resolve promise on writeStream finish, not req end — eliminates
  intermittent 500 on PUT update (writeStream may not have flushed at rename time)
- test(autoCombo): stub DB calls from PR #3660 in tieredRotation.test.ts to prevent
  5s timeout in vitest (getModelIntelligenceBySource DB init path)
- chore(env-sync): add XDG_DATA_HOME + OMNIROUTE_OPENCODE_PLUGIN_DIR to IGNORE_FROM_CODE
  allowlist (introduced by PR #3726 setup-open-code.mjs, not OmniRoute config vars)
- chore(cli): regenerated bin/cli/api-commands/*.mjs (7 new, 27 updated)

* fix(model-family): fallback lookup also tries bare model name with dots

getNextFamilyFallback normalized dots-to-hyphens ("gemini-3.1-pro-high" →
"gemini-3-1-pro-high") but MODEL_FAMILIES keys use the literal dot form. The
lookup always missed, returning null for any model whose dots are part of the
name rather than a version separator.

Fallback: try MODEL_FAMILIES[lookupKey] ?? MODEL_FAMILIES[bareModel] so both
naming conventions are covered. Fixes T30 test (pre-existing since v3.8.22).

* feat: expose API key cost drilldown + quota % used (#3742)

Adds all-time USD cost per API key in the API Key Manager, a per-key deep-link into the Cost Explorer (filtered + grouped by model), URL-param hydration of range/groupBy/apiKeyIds, and a '% used' quota display. Review adjustments: extracted URL-param parsers to a tested module (Rule #18), i18n'd the new strings (en + zh-CN), dropped the redundant webdav-handler entry already on release.

Integrated into release/v3.8.23.

* feat: add provider display modes — All / Configured / Compact (#3743)

Replaces the Providers page configured-only toggle with All/Configured/Compact display modes (Compact = flat deduped grid, no-auth last). Persists the preference and migrates the legacy localStorage key. Rebased onto release/v3.8.23.

Integrated into release/v3.8.23.

* fix(cache): scope semantic-cache signature to API key (#3740)

Adds the api_key_id dimension to generateSignature's SHA-256 hash so two callers with different API keys never receive each other's cached responses. Threads apiKeyId through checkSemanticCache + both write sites; migration 098 clears pre-existing key-less entries; unauthenticated requests stay isolated from keyed ones. 3 TDD tests.

Integrated into release/v3.8.23.

* fix(responses): apply OpenAI Responses API stream=false spec default (#3708)

resolveStreamFlag now applies the stream=false-when-omitted default for sourceFormat=openai-responses (same as the existing claude path), so spec-compliant /v1/responses upstreams that return JSON no longer fall through to the wildcard-Accept heuristic and trigger STREAM_EARLY_EOF / 502. Codex CLI (stream:true) and explicit text/event-stream clients unaffected.

Integrated into release/v3.8.23.

* chore(release): reconcile CI gates for v3.8.23

- file-size baseline: re-freeze 8 files grown by PRs #3742/#3743/#3740
  (cost drilldown, provider display modes, cache key isolation)
- ARCHITECTURE.md: update executor count 55→60 (check:docs-counts drift)
- .env.example: add OMNIROUTE_EMERGENCY_FALLBACK (#3741, env-doc-sync)
- CHANGELOG: add formatted bullets for #3742, #3743, #3708, #3740,
  model-family-fallback fix; remove duplicate raw ### Fixed section

* test: restore assert count to satisfy check:test-masking gate

Three test files had net assertion removals after behavior-changing PRs:
- chatcore-translation-paths: emergency fallback moved to routing layer
  (#3699) — add body error assertion + model-name guard
- executor-vertex-extended: non-JSON is now Express API key (#3690) —
  add projects/-path guard to the express-key URL test
- stream-utils: empty streams now emit error (#3685) — add code/message/
  status/completePayload guards to both passthrough and translate variants

All new assertions are meaningful (code enum value, 5xx range, non-empty
message, onComplete must-not-fire contract).

* fix(ci): move rtl-logical-classes test to ui/ so vitest:ui runner collects it

---------

Co-authored-by: Felipe Almeman <4226997+zhiru@users.noreply.github.com>
Co-authored-by: NOXX - Commiter <artur1992123@mail.ru>
Co-authored-by: Nick Sullivan <142708+TechNickAI@users.noreply.github.com>
Co-authored-by: Markus Hartung <mail@hartmark.se>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: PizzaV <103120356+pizzav-xyz@users.noreply.github.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Chewji <126886556+Chewji9875@users.noreply.github.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: Felipe Sartori <felipesartori.ti@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zois Pagoulatos <zpagoulatos@hotmail.com>
Co-authored-by: sdfsdfw2 <167810361+sdfsdfw2@users.noreply.github.com>
Co-authored-by: Witroch4 <witalo_rocha@hotmail.com>
2026-06-12 23:49:22 -03:00
Diego Rodrigues de Sa e Souza
b6c65efd28 fix(build): include webdav-handler.mjs in dist/ bundle (#3687)
server-ws.mjs imports ./webdav-handler.mjs but the assembleStandalone
pipeline did not copy it from scripts/dev/, causing a startup crash on
any fresh install of v3.8.22 (ERR_MODULE_NOT_FOUND).

Fix: add the copy entry to assembleStandalone.mjs, add it to
APP_STAGING_ALLOWED_EXACT_PATHS and PACK_ARTIFACT_REQUIRED_PATHS in
pack-artifact-policy.ts, and add a regression test.

Hotfix validated live on VPS (192.168.0.15): server-ws.mjs resolves the
module and the process starts healthy after the file was deployed.
2026-06-11 22:07:32 -03:00
Diego Rodrigues de Sa e Souza
9350a5d6c6 Release v3.8.22 (#3623)
* chore(release): open v3.8.22 development cycle

* refactor(dashboard): extract ProviderDetailPageClient — #3501 Phase 0 (#3633)

#3501 Phase 0: extract ProviderDetailPageClient + smoke test.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* refactor(dashboard): extract auth-import modals — #3501 Phase 1a (#3634)

#3501 Phase 1a: extract 3 auth-import modal clusters.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* fix(db): reclassify localDb unexported modules as intentionally-internal (#3499) (#3635)

Closes #3499 — reclassify localDb unexported modules as intentionally-internal (audit + honest gate framing).

* refactor(db): move call_logs aggregations into callLogStats db module (#3500) (#3636)

#3500 slice 1: call_logs aggregations → src/lib/db/callLogStats.ts (Rule #5). Byte-identical queries; TDD 6/6.

* refactor(dashboard): extract EditCompatibleNodeModal — #3501 Phase 1b (#3638)

#3501 Phase 1b: extract EditCompatibleNodeModal (cycle-safe via leaf constants module).

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* refactor(db): move community_servers SQL into gamification db module (#3500 slice 3) (#3639)

#3500 slice 3: community_servers SQL → gamification db module.

* refactor(db): move usage_history SQL into usageAnalytics module (#3500 slice 2) (#3644)

#3500 slice 2: usage_history/daily_usage_summary SQL → usageAnalytics db module.

* refactor(db): move skills UPDATE + db-backups SQL into db modules (#3500 slice 5) (#3647)

#3500 slice 5: skills UPDATE (allowlist) + db-backups SQL → db modules.

* refactor(db): move usage_logs/semantic_cache/proxy_logs SQL into db modules (#3500 slice 4) (#3648)

#3500 slice 4: usage_logs/semantic_cache/proxy_logs SQL → db modules. All internal routes done (2 external by-design remain).

* chore(db-gate): reclassify external-DB reads, fully close #3500 (#3649)

Closes #3500: reclassify external-DB reads; all internal raw-SQL migrated to db/ modules.

* refactor(dashboard): extract pure helpers to providerPageHelpers — #3501 Phase 2 (#3653)

#3501 Phase 2: extract pure helpers to providerPageHelpers (leaf, cycle-safe).

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* refactor(dashboard): extract remaining shared helpers to providerPageHelpers — #3501 Phase 2b (#3658)

#3501 Phase 2b: extract remaining shared helpers to providerPageHelpers (leaf, cycle-safe). Heavy modals unblocked.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* fix(reasoning): replay reasoning_content on plain DeepSeek turns (#1682) (#3632)

Integrated into release/v3.8.22

* fix(kiro): route enterprise IAM Identity Center accounts to their regional endpoint (#3631)

Integrated into release/v3.8.22

* refactor: small code cleanup (#3523)

Integrated into release/v3.8.22

* fix(combo): skip same-provider targets on 408/500/502/503/504/524 errors (#3637)

Integrated into release/v3.8.22 — circuit-breaker guard added in review (#1731v2)

* feat(providers): add MiMoCode free-tier provider with bootstrap JWT auth (#3659)

Integrated into release/v3.8.22 — page.tsx conflict resolved + NoAuthAccountCard re-applied to ProviderDetailPageClient in review. MiMoCode endpoint validated live.

* Log Responses WebSocket calls in history (#3616)

Integrated into release/v3.8.22 — Codex Responses WebSocket call history logging.

* Add Claude Code routing preference for unprefixed Claude models (#3540)

Integrated into release/v3.8.22 — page.tsx conflict resolved (re-applied toggle to ProviderDetailPageClient) + disable-test updated for catalog drift in review.

* docs(changelog): credit #3632/#3631/#3637/#3659/#3540/#3616/#3523 (v3.8.22 targeted review round)

* fix(mimocode): add required authHeader:"none" to registry entry (#3659 follow-up)

The mimocode RegistryEntry omitted the required authHeader field, which broke
typecheck:core (TS2741). Match the no-auth convention (authType:"none" + authHeader:"none")
used by veoaifree-web and other free providers. Follow-up to #3659 (@pizzav-xyz).

* fix(responses): detect stream readiness for tool-call-only and object-less chunks (#3612) (#3661)

Closes #3612

* fix(mitm): remove duplicated 'Command failed:' error prefix (#3641) (#3662)

Closes #3641

* fix(cli): honor HERMES_HOME for Hermes Agent config path (#3628) (#3663)

Closes #3628

* fix(api): fetch live OpenCode model catalog for no-auth model picker (#3611) (#3664)

Closes #3611

* fix(api): flag provider topology error state by current status, not stale history (#3619) (#3666)

Closes #3619

* fix(electron): launch peer-stamping server-ws.mjs entrypoint to avoid 403 LOCAL_ONLY (#3386) (#3665)

Closes #3386

* fix(dashboard): restore home topology live in-flight pulse (#3507) (#3667)

Closes #3507

* fix(oauth): name Kiro/AWS auto-imported accounts and dedupe by profileArn (#3615) (#3671)

Closes #3615

* fix(resilience): clear stale transient connection cooldowns on startup (#3625) (#3672)

Closes #3625

* fix(i18n): use logical CSS direction utilities for sidebar and key overlays (RTL #3541) (#3670)

Closes #3541

* fix(dashboard): honor auto-hide and switch to visible filter on passthrough Test-all (#3610) (#3669)

Closes #3610

* refactor(dashboard): extract AddApiKeyModal + EditConnectionModal — #3501 Phase 1c (#3674)

#3501 Phase 1c: extract AddApiKeyModal, EditConnectionModal, WebSessionCredentialGuide into components/; god-component 10,166->8,092 LOC. Reconciles the v3.8.22 file-size drift for this file.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* docs(changelog): reconcile v3.8.22 — credit #3621/#3622 + MiMoCode follow-up roll-up

* refactor(dashboard): extract ConnectionRow + ModelCompatPopover + SiliconFlowEndpointModal — #3501 Phase 1d (#3676)

#3501 Phase 1d: god-component 8,092->6,838 LOC.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* feat(obsidian): add WebDAV config route + encrypt creds at rest (#3485 part 1) (#3677)

Part 1 of #3485. Adds /api/settings/obsidian/webdav (GET/POST/DELETE) wiring the ready obsidianSync lib, encrypts webdav password + obsidian token at rest, removes the duplicate UI block, drops the KNOWN_MISSING entry. WebDAV file server is part 2.

* feat(obsidian): add /api/v1/webdav file server for Obsidian vault sync (#3485 part 2) (#3678)

Part 2 of #3485. WebDAV server (PROPFIND/GET/PUT/DELETE/MKCOL/MOVE/OPTIONS) handled in the custom server layer (standalone-server-ws.mjs) since the App Router cannot export WebDAV methods. Basic-Auth (constant-time), path-traversal hardened, password decrypt ported from encryption.ts (parity-tested), DATA_DIR resolution parity-tested against dataPaths.ts. End-to-end Obsidian-over-Tailscale validation is a live VPS step (Rule #18).

* fix(combo): stop premature context compaction — real auto-combo windows + per-target compression limit (#3680)

Integrated into release/v3.8.22

* feat(dashboard): deactivate/activate accounts from the quota overview (#3675)

Integrated into release/v3.8.22

* fix(dashboard): close review gaps in bulk provider connection actions (#3271 follow-up) (#3673)

Integrated into release/v3.8.22 — page.tsx conflict (god-component split #3501) resolved by re-applying the bulk-action deltas to ProviderDetailPageClient.tsx

* refactor(dashboard): extract useModelCompatState hook + model sections — #3501 Phase 1e (#3683)

#3501 Phase 1e: extract useModelCompatState hook (unblocks the model sections) + ModelRow/PassthroughModelsSection/PassthroughModelRow/CustomModelsSection/CompatibleModelsSection. god-component 6,838->4,921 LOC.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* refactor(dashboard): extract useProviderConnections/Settings/Models hooks — #3501 Phase 1f (#3684)

#3501 Phase 1f: god-component 4,948->4,062 LOC. Connection state+handlers, settings, and model metadata moved into hooks/.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* chore(release): v3.8.22 CHANGELOG + env-doc sync

- Set release date in CHANGELOG [3.8.22] to 2026-06-11
- Add HERMES_HOME to .env.example (from #3628/#3663)
- Add HERMES_HOME + OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS to ENVIRONMENT.md (#3628/#3540)

* docs(changelog): credit #3673 + #3675 — leninejunior bulk-actions + quota-toggle

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: Abhishek Divekar <adivekar@utexas.edu>
Co-authored-by: NOXX - Commiter <artur1992123@mail.ru>
Co-authored-by: Nicolas Lorin <androw95220@gmail.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: PizzaV <103120356+pizzav-xyz@users.noreply.github.com>
Co-authored-by: kkkayye <98376609+kkkayye@users.noreply.github.com>
Co-authored-by: Witroch4 <witalo_rocha@hotmail.com>
Co-authored-by: Lenine Júnior <lenine@engrene.com.br>
2026-06-11 18:52:29 -03:00
Diego Rodrigues de Sa e Souza
99397b4f41 fix(ci): align pack-artifact-policy with v3.8.21 package.json files expansion (#3624)
v3.8.21 broadened package.json files to include open-sse/ and src/*/*
directories for TypeScript-first imports, but pack-artifact-policy.ts
was not updated. Add the new directories to PACK_ARTIFACT_ROOT_ALLOWED_PATH_PREFIXES.
Also increase execFileSync maxBuffer to 64 MB to fix ENOBUFS on large packs.
2026-06-11 05:46:52 -03:00
Diego Rodrigues de Sa e Souza
a32e52eed6 fix(ci): increase execFileSync maxBuffer in validate-pack-artifact (#3622)
npm pack --dry-run --json on large packages exceeds the default 1 MB
buffer. Set maxBuffer to 64 MB so check:pack-artifact does not fail
with ENOBUFS on the CI runner.
2026-06-11 05:08:54 -03:00
Diego Rodrigues de Sa e Souza
88857237a2 fix(guardrails): use validateBody() in /api/guardrails/test route (#3621)
Replaces request.json() + TestRequestSchema.parse() pattern with the
canonical validateBody()/isValidationFailure() pattern from
@/shared/validation/helpers, satisfying check:route-validation:t06.
2026-06-11 05:06:58 -03:00
Diego Rodrigues de Sa e Souza
c315a2394c Release v3.8.21 (#3593)
* chore(release): open v3.8.21 development cycle

* fix: pass through valid max_tokens-truncated responses instead of fake 502 (#3572) (#3595)

* fix: /v1/completions returns legacy text-completion format, not chat (#3571) (#3596)

* fix: z.ai/GLM coding plan no longer shows Monthly 0% when no monthly cap (#3580) (#3597)

* docs: mark DISCOVERY_TOOL_DESIGN endpoints as Phase-2 not-yet-implemented (#3498) (#3599)

* fix(agent-bridge): add validate-only upstream-ca/test route (#3488) (#3600)

* fix(gamification): add level/badges/badges-earned profile routes (#3484)

* security(oauth): migrate 5 public client_ids to resolvePublicCred (#3493)

* fix(mcp): ship MCP server source closure in npm files + coverage gate (#3578)

* fix: add reasoning token buffer for combo routing (fixes #3587) (#3588)

Integrated into release/v3.8.21

* Refactor: Extract chatCore phases into modular files (#3598)

Integrated into release/v3.8.21 — chatCore phase modularization. Adjusted: re-derive idempotencyKey for the save path after the check moved into the module (co-authored). Thanks @oyi77!

* docs(changelog): credit #3598 (chatCore modularization) + #3588 (combo reasoning buffer)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(api): implement GET /api/guardrails + POST /api/guardrails/test, drop shadow/guardrails doc-fiction (#3496) (#3602)

Integrated into release/v3.8.21 — implements GET /api/guardrails + POST /api/guardrails/test, removes shadow/guardrails doc-fiction. TDD-validated (5/5) + check-docs-symbols/typecheck/eslint green.

* fix(gemini): isolate textual reasoning wrappers (#3605)

Split-out PR C from #3584. Isolates textual reasoning wrappers (<think>/<thinking>/<thought>/<internal_thought>, including malformed/open tags) into reasoning_content across both the non-streaming sanitizer and the Gemini streaming translator, with split-chunk buffering. Additive to the existing textual tool-call pipeline; does not touch the #3569 native functionResponse path. Integrated into release/v3.8.21. Thanks @dhaern!

* fix(antigravity): normalize Gemini 3.5 Flash tier IDs (#3603)

Split-out PR A from #3584. Normalizes the Antigravity/agy Gemini 3.5 Flash tier IDs to clean public names (gemini-3.5-flash-low/medium/high), maps them to the live upstream IDs at the executor boundary, and removes Antigravity from the global model resolver so the executor owns wire normalization. Maintainer follow-up: kept gemini-3.5-flash-preview as a hidden backward-compat alias routing to the High tier (so saved combos/configs keep working). Live-validated the tier set via the agy CLI catalog. Integrated into release/v3.8.21. Thanks @dhaern!

* fix(agent-bridge): surface real MITM startup-failure cause, not always port 443 (#3606) (#3608)

Integrated into release/v3.8.21 (#3606)

* fix(oauth): surface real Kiro import-token failure cause, not a bare 500 (#3589) (#3609)

Integrated into release/v3.8.21 (#3589)

* docs(opencode-provider): soft-deprecate in favor of @omniroute/opencode-plugin (#3419) (#3613)

Integrated into release/v3.8.21 (#3419)

* fix(usage): normalize Antigravity and agy provider quotas (#3604)

Split-out PR B from #3584. Normalizes Antigravity/agy provider quotas: prefers retrieveUserQuota for live consumption, falls back to fetchAvailableModels and local usage_history, sanitizes cached Provider Limits so retired upstream IDs are not re-exposed, and schedules a deduplicated post-usage refresh. Maintainer follow-up: decoupled the post-usage refresh via a lightweight usageEvents bus (usageHistory no longer dynamic-imports providerLimits) so it does not pull the executors/translator graph into the typecheck-core surface — typecheck:core stays at 0. Integrated into release/v3.8.21. Thanks @dhaern!

* feat(cli): add autostart on/off/toggle shorthand for headless serve mode (#3331) (#3614)

Integrated into release/v3.8.21 (#3331)

* docs(changelog): credit #3603 (Flash tier IDs) + #3604 (provider quotas) + #3605 (reasoning wrappers)

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* fix(review): resolve findings from /review-reviews battery (v3.8.21 hardening) (#3618)

Pre-release hardening from the /review-reviews battery — 15 findings resolved (L1-L13,L15) + L14 live-verified WONTFIX, convergence re-review clean. lint/typecheck:core/test:vitest(146)/build green; zero new test:unit failures vs baseline 797de433f.

* chore(release): v3.8.21 CHANGELOG + i18n + env-doc sync

---------

Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Raxxoor <manker_lol@hotmail.com>
2026-06-11 04:01:24 -03:00
Hernan Javier Ardila Sanchez
4bfd9e2845 fix: add reasoning token buffer for combo routing (fixes #3587) (#3588)
Integrated into release/v3.8.21
2026-06-10 21:29:11 -03:00
Diego Rodrigues de Sa e Souza
d6f008cdaf fix(resilience): expose providerCooldown in GET+PATCH /api/resilience (#3591) 2026-06-10 16:28:24 -03:00
Diego Rodrigues de Sa e Souza
bf8b56b29f Release v3.8.20 (#3547)
* chore(release): open v3.8.20 development cycle

* fix(images): prefer bare combos over image aliases (#3527)

Integrated into release/v3.8.20

* fix(translator): map Codex local_shell tool (#3534)

Integrated into release/v3.8.20

* fix(usage): make opencode-go quota fetcher fail-open instead of throwing 500 (#3522)

Integrated into release/v3.8.20

* Fix Runtime page breaker state rendering (#3533)

Integrated into release/v3.8.20

* Expose provider breaker degradation threshold setting (#3535)

Integrated into release/v3.8.20

* fix(executor): strip provider prefix from versioned built-in tool model field (#3532)

Integrated into release/v3.8.20

* feat(providers): add Claude Fable 5 support (#3524)

Integrated into release/v3.8.20

* feat(resilience): add global provider cooldown tracking to prevent combo re-walking (#3556)

Integrated into release/v3.8.20 (default OFF, opt-in)

* fix(translator): scope thoughtSignature bypass to Antigravity/CLI only (#3560)

Integrated into release/v3.8.20. Co-authored-by: Six7Day <six7day@gmail.com>

* fix(routing): normalize thinking:disabled for combo-substituted models that reject it (#3554) (#3563)

Integrated into release/v3.8.20

* fix(usage): accept 0/empty budget limits so the dashboard can save and clear (#3537) (#3564)

Integrated into release/v3.8.20

* docs(changelog): credit @Six7Day for #3560 thoughtSignature fix (#3414)

The #3560 squash co-author trailer landed inline (unparsed by GitHub), so add
an explicit CHANGELOG credit ensuring @Six7Day (original #3414) and @oyi77 are
on the public record for the Gemini thoughtSignature fix.

* fix(gamification): dedup badge unlock via user_badges so events don't re-fire every request (#3472) (#3565)

Integrated into release/v3.8.20

* fix(routing): pass through 'auto' keyword on codex /v1/responses instead of rewriting to codex/auto (#3509) (#3566)

Integrated into release/v3.8.20

* fix(cli-tools): normalize apiKey null in guide-settings schema so cloud-mode config saves (#3552) (#3567)

Integrated into release/v3.8.20

* fix(catalog): reclassify PublicAI from keyless to one-time-initial (requires API key) (#3558) (#3568)

Integrated into release/v3.8.20

* fix(gemini-web): surface missing Playwright browser as actionable 503 + cooldown hint, not a retryable 500 loop (#3516) (#3570)

Integrated into release/v3.8.20

* fix(security): sanitize raw err.message in web executors + embeddings/search response bodies (Rule #12) (#3494, #3495) (#3573)

Integrated into release/v3.8.20

* fix(dashboard): point CustomHostsManager + FeatureFlagsGrid at real routes (#3486, #3487) (#3574)

Integrated into release/v3.8.20

* chore(providers): remove dead krutrim entry (#3483) + docs(api): fix agent-bridge per-agent state route (#3489) (#3575)

Integrated into release/v3.8.20

* docs(api): correct API_REFERENCE.md paths for skills/plugins/admin/cache/acp/system-info (#3497) (#3577)

Integrated into release/v3.8.20

* fix(proxy): drive SOCKS5 UI option from runtime ENABLE_SOCKS5_PROXY, not build-time NEXT_PUBLIC (#3508) (#3579)

Integrated into release/v3.8.20

* fix(playground): filter playground models by node prefix so custom-endpoint models appear (#3505) (#3581)

Integrated into release/v3.8.20

* fix(usage): show an informative message instead of a blank Kiro quota card when no usage breakdown (#3506) (#3582)

Integrated into release/v3.8.20

* docs(changelog): add the #3506 Kiro quota entry (missed in #3582 due to a stale-base CHANGELOG anchor) (#3583)

Integrated into release/v3.8.20

* fix(auto-update): use stable PROJECT_ROOT walker, not frozen process.cwd() (#3561)

Integrated into release/v3.8.20. Auto-update PROJECT_ROOT now uses a stable __dirname-anchored upward walker instead of the no-op process.cwd() resolver.

* fix: address PR #3518 review comments (lifecycle hooks, regex, indentation, route params) (#3562)

Integrated into release/v3.8.20. Addresses #3518 review: regex literals, logs/[id] route params (Next 16), indentation, and wires plugin lifecycle hooks (onInstall/onActivate/onDeactivate/onUninstall) in the loader so manager.ts can register them. Adds Rule #18 regression test.

* docs(changelog): credit @ViFigueiredo (#3423) for PROJECT_ROOT + log #3561/#3562 (v3.8.20)

* fix: openai to gemini incorrectly translates historical tool calls into text (#3569)

Integrated into release/v3.8.20. Standard Gemini direct path now maps historical tool calls to native functionCall/functionResponse parts (signaturelessToolCallMode: native) instead of inert text — validated against the real Gemini API (gemini-2.5-flash returns 200 for signatureless native functionCall, even with tools+thinking; Hard Rule #18). Eliminates the text-serialization leak. Antigravity/CLI sentinel path (#3560) untouched.

* docs(changelog)+test: reconcile standard-Gemini native mode (#3569) — update round-2 rationale comment + log VPS validation

* docs(changelog): reconcile v3.8.20 — add 9 missing bullets + move [Unreleased] to versioned section

* docs(changelog): complete v3.8.20 reconciliation — 27 bullets, 11 contributors

---------

Co-authored-by: Alexander Averyanov <alex@averyan.ru>
Co-authored-by: Hakan Kurşun <bykamaka@gmail.com>
Co-authored-by: Wilson <pedbookmed@gmail.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: Giorgos Giakoumettis <giorgos@yiakoumettis.gr>
Co-authored-by: PizzaV <103120356+pizzav-xyz@users.noreply.github.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Markus Hartung <mail@hartmark.se>
2026-06-10 13:49:08 -03:00
Diego Rodrigues de Sa e Souza
630680067c fix(security): block cloud-metadata SSRF pivot in cli-tools catalog fetch (CodeQL #326 critical) (#3544)
assertSafeCatalogUrl() blocks the cloud-metadata/link-local SSRF→IAM pivot + non-http(s) + embedded creds before the user-controlled baseUrl reaches fetch; loopback (the legitimate OmniRoute target) and public OmniRoute Cloud stay allowed. Fetches the re-parsed (taint-severed) URL. TDD: 4 guard cases. CodeQL FP (custom-guard limitation) dismissed per Rule #14. Folded into v3.8.19.
2026-06-10 02:20:21 -03:00
Ramel Tecnologia - Rafa Martins
6a7a36c09e Update WhatsApp Brasil link in README.md (#3543)
Atualizaçao grupo Brasil
2026-06-10 01:15:56 -03:00
Diego Rodrigues de Sa e Souza
d65d8bb54f chore(quality): re-baseline complexity to the published v3.8.18 inheritance (1739→1746) (#3542)
Proven against pre-cycle base 5f2722bd6 (v3.8.19 cycle adds zero). Reduction = Phase 6A (2026-06-16).
2026-06-09 23:27:02 -03:00
Diego Rodrigues de Sa e Souza
68e4d0c599 Release v3.8.19 (#3526)
* chore(release): open v3.8.19 development cycle

* chore(release): sync electron lockfile to 3.8.19

* feat(quality): quality-gate ratchet + anti-hallucination/rule-enforcement guardrails (Phases 0-6) (#3471)

* feat(quality): generic ratchet comparator (multi-metric, regression-only)

* chore(ci): Fase 0 quality-gate fixes — reconcile coverage gate (40->60), tier npm audit, wire orphaned contract gates, re-enable cheap husky pre-commit

* feat(quality): ratchet engine (collector + frozen baseline + CI job) and provider-consistency gate

- collect-metrics.mjs: emits quality-metrics.json (ESLint warnings + coverage when present)
- quality-baseline.json: frozen baseline (eslintWarnings=3482, regression-only)
- ci.yml: quality-gate job (ratchet + step summary + artifact) and check:provider-consistency in lint job
- check-provider-consistency.ts: every REGISTRY id must be a canonical provider (found krutrim half-registered → allowlisted as known pre-existing, blocks any NEW orphan)
- TDD: 9 tests (5 ratchet + 4 provider-consistency)

* feat(quality): Fase 2 anti-hallucination gates — fetch-targets, openapi-routes, deps allowlist

- check-fetch-targets: every dashboard fetch(/api/...) resolves to a real route.ts; found 7 pre-existing dashboard->route mismatches frozen as KNOWN_MISSING for triage
- check-openapi-routes: every openapi.yaml path resolves to a real route; found 1 stale spec entry (agent-bridge agents/{id}/state) frozen as KNOWN_STALE_SPEC
- check-deps: anti-slopsquatting allowlist (105 deps); new deps need explicit human-reviewed entry
- all wired into CI lint/docs jobs; TDD +12 tests (21 total across 5 gates)

* docs(quality): add quality-gates report + implementation plan to repo root

* feat(quality): Fase 3a — file-size ratchet (freeze 91 files >800 LOC, cap 800 for new)

- check-file-size.mjs: frozen files can only shrink; new files must be <= cap (kills the next 12k-line god-component)
- file-size-baseline.json: 91 files frozen at current LOC (largest 12883)
- wired into CI lint job; TDD 5 tests; --update ratchets the baseline down on shrink

* feat(quality): Fase 3b — duplication ratchet (jscpd@4, baseline 5.72%)

- check-duplication.mjs: runs jscpd@4 (pinned; v5 is an incompatible Rust rewrite) over src+open-sse, fails if duplication % rises vs frozen baseline (5.72%, measured: 1358 clones / 22967 dup lines). Targets the executor copy-paste (48/50 override execute() wholesale)
- wired into the parallel quality-gate CI job (off the lint critical path); TDD 4 tests; --update ratchets down
- snapshot now complete: coverage ~82.6%, eslint 3482 (98.5% no-explicit-any), duplication 5.72%, 91 files >800 LOC

* feat(quality): Fase 4a — anti test-masking gate

- check-test-masking.mjs: for each MODIFIED test file in a PR, flags net assert removal + new assert.ok(true) tautologies (base...HEAD diff). Directly enforces CLAUDE.md 'never weaken asserts to go green'
- wired into pr-test-policy CI job (reuses base fetch); no-op outside PR; TDD 5 tests

* feat(quality): Fase 4b — coverage ratchet (conservative floors, CI consumes merged coverage)

- quality-baseline.json: coverage.{statements,lines,functions,branches} floors (80/80/82/73, real ~82.58/82.58/84.23/75.22 with margin; tighten via --update after a green main run)
- check-quality-ratchet.mjs: --allow-missing (local quality:gate skips coverage.* without a coverage run; CI runs strict)
- ci.yml quality-gate job: needs test-coverage + downloads merged coverage-report so the ratchet enforces 'coverage cannot drop'
- TDD +1 test (6 total)

* feat(quality): Fase 6 — 8 new gates (Rule #11/#12, migrations, known-symbols, route-guard, complexity, docs-symbols, db-rules)

Deterministic gates, each freezing pre-existing violations in a documented allowlist (ratchet) so they pass now and block only NEW regressions:
- check-error-helper (Rule #12): 7 executors/handlers forwarding raw err.message frozen
- check-public-creds (Rule #11): 5 literal client_ids (Claude/Codex/Qwen/Kimi/Copilot) frozen
- check-migration-numbering: gaps 026/055 + dup 041 frozen (prevents the git-rm-deleted-migration incident)
- check-known-symbols: 93 executors conformance + 15 combo strategies + 18 translator pairs
- check-route-guard-membership (#15/#17): all 25 spawn-capable routes verified local-only (0 gaps)
- check-complexity: cyclomatic>15 / fn-length>80 ratchet (baseline 1739)
- check-docs-symbols: 30 stale doc /api refs frozen (docs hallucination)
- check-db-rules (#2/#5): 25 unexported db modules + 15 raw-SQL routes frozen
Wired into CI (lint / docs-sync-strict / quality-gate jobs). 115 TDD tests, all green. ESLint ratchet held at 3482.

* docs(quality): Phase 7 plan (security/dead-code/mutation/community tooling) — GATED to 2026-06-16

Stored, not active. 7 suggested gates + all discussed OSS/Community tools (SonarQube Community + osv-scanner + CodeQL + knip + sonarjs + type-coverage + lockfile-lint + Stryker + size-limit + axe-core + semcheck + agent-lsp + Qlty). Activation gate: do not start before 2026-06-16 (use Phases 0-6 in production for 1 week, validate in practice, then evolve).

* docs(quality): Phase 6A critical-audit plan + Phase 7 additions — gated to 2026-06-16 (#3530)

PLANO-QUALITY-GATES-FASE6A.md (12-task audit of Phases 0-6: orphan tests, stale-allowlist enforcement, scope gaps) + Phase 7 additions (gitleaks, actionlint+zizmor, license compliance). Both stored, activation gated to 2026-06-16. Tasks 6A.1/6A.2 were fast-tracked separately (#3536).

* feat(quality): 6A.1+6A.2 — test-discovery gate, 135 orphan tests re-wired, 2 production bug fixes, vitest in CI (#3536)

check-test-discovery gate (TDD; 195 orphans found, 135 re-wired into the node runner, 60 frozen+annotated). Triage fixed 2 real production bugs: missing BYPASS_PREFIX_NOT_ALLOWED zod refine (spawn-capable prefixes accepted into the bypass list, Hard Rules #15/#17) and resetDbInstance not firing stateReset resetters (stale schema memo → 503 instead of 403; also hit backup-restore). New test-vitest CI job: test:vitest blocking (146/146), test:vitest:ui informational (14 pre-existing fails, triage 2026-06-16).

* chore: ignore generated yt-downloader artifact files

Add dated yt-downloader output files to .gitignore to prevent
local automation artifacts from being accidentally committed.

* chore(quality): green-light the quality-gate — conscious file-size + eslintWarnings re-baselines (#3538)

file-size: 9 files frozen at current sizes (v3.8.18-era growth + core.ts +7 from #3536 fix). eslintWarnings 3482→3501: the published v3.8.18 tag already measures 3501 (delta predates the quality-gate job); v3.8.19 cycle is neutral. Reduction + --require-tighten = Phase 6A (2026-06-16).

* fix(check): exclude internal planning docs (docs/superpowers/) from the docs-symbols gate

docs/superpowers/plans/*.md are historical implementation-plan snapshots that
may cite planned/abandoned routes — not claims about the current code. Three
such refs entered during the v3.8.18 cycle, before this gate was on the
pipeline, and would have blocked the v3.8.19 release merge.

* chore(release): v3.8.19 — 2026-06-09

CHANGELOG section for the quality-infrastructure release (7 commits, 1:1
coverage), [3.8.18] label corrected to its real release date, local prompt
artifacts ignored.

* test: hermetic auth context for 2 re-wired suites + real headroom on the breaker reset-timeout flake

CI shards exposed what the dev DATA_DIR was masking locally: detect.test.ts
and managementCliToken.test.ts asserted 401/403/reject outcomes that only
exist when login protection is configured — on a fresh CI DB isAuthRequired()
is false and the policy anonymous-allows. Both now create an isolated
DATA_DIR with requireLogin+password (the established pattern).

observability-fase04: the breaker reset-timeout test ran with a 5ms margin
(resetTimeout 10 / sleep 15) — lazy HALF_OPEN refresh under shard contention
flipped the first OPEN assert. Now 250/300ms.

* test: align bypass-prefix schema test to the restored layer-1 contract + real waitFor headroom

appearance-widget-settings-schema asserted that /api/cli-tools/runtime/ was
ACCEPTED into the bypass list — written against the buggy schema (missing
BYPASS_PREFIX_NOT_ALLOWED refine, restored in #3536) and consecrating the
bug the AC-8 orphan test guards against. Split into accept-safe +
reject-spawn-capable cases. chatcore waitFor ceiling 1500→10000ms (green
runs return immediately; observed 1580ms expiry on 2-core CI runners).

* test(chatcore): fix structurally-broken pending-detail predicate (flatten before find)

pendingRequests.details[connectionId] is Record<modelKey, PendingRequestDetail[]>
— the upstream-timeout test's waitFor tested each ARRAY's .providerRequest
(always undefined), so it could never resolve and expired (failed on 3 CI jobs;
reproduced deterministically isolated, including at the published v3.8.18 tag).
Flatten to the actual details + declare the call_log_pipeline_enabled dependency
explicitly + waitFor ceiling with real CI headroom.

* chore(quality): re-baseline coverage floors to the honest post-re-wire denominator + changelog coverage for the stabilization commits

The 135 re-wired tests import modules that were never loaded before, so the
c8 denominator grew: the old ~82.5% was inflated by never-imported modules
being invisible. CI merged coverage now measures 78.4/78.4/83.84/75.73 —
floors set ~2pt below (76.5/76.5; functions/branches floors already hold).
Tightening via --require-tighten is Phase 6A work (2026-06-16).
2026-06-09 22:57:12 -03:00
Diego Rodrigues de Sa e Souza
8169b97d84 Release v3.8.18 (#3482)
* chore(release): open v3.8.18 development cycle

* fix(catalog): stop Codex CLI model-catalog refresh from erroring (#3481)

Codex's model-catalog refresh (codex_models_manager) does
GET /v1/models?client_version=<v> and decodes a JSON object with a
TOP-LEVEL `models` array. OmniRoute answers in the OpenAI-standard
`{object,data}` shape, so codex fails with "missing field `models`"
and logs "failed to refresh available models" on every startup.

Detect codex clients via the `originator` / `user-agent` = `codex_*`
headers they send and add an EMPTY top-level `models: []` so the decode
succeeds. Non-codex OpenAI clients keep the byte-identical `{object,data}`
response.

The array is intentionally empty: codex replaces its built-in per-model
agent prompt (`base_instructions`, ~21k chars) with whatever a populated
entry carries for the selected model, so emitting our catalog would drop
the agent prompt to nothing and break codex's agent behaviour (verified
empirically against codex 0.137). An empty list keeps codex on its
built-in model info — same inference as before, minus the error.

Validated end-to-end with the real handler against codex 0.137:
"failed to refresh available models" → 0 occurrences, instructions
preserved (built-in Codex agent prompt, not empty).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: ignore quality reports and local prompt artifacts

Add generated quality gate reports, metrics files, and local setup prompt
artifacts to .gitignore to prevent committing environment-specific or
temporary files.

* fix(provider): detect Responses API format when body has `input` but … (#3490)

Integrated into release/v3.8.18

* fix(sse): normalize numeric provider ids to strings (#3451)

Integrated into release/v3.8.18

* feat(browserPool): resolve Playwright proxy from proxy_registry DB (#3492)

Integrated into release/v3.8.18

* fix(theoldllm): generate X-Request-Token server-side, drop Playwright (#3491)

Integrated into release/v3.8.18

* feat(plugins): add lifecycle hooks and theme-manager plugin (#3473)

Integrated into release/v3.8.18

* fix(combo): parallel pre-screen + circuit-breaker fast-exit for priority combos (#3169)

Integrated into release/v3.8.18

* feat(ui): unifi active and finished requests into single view #1422 (#3401)

Integrated into release/v3.8.18

* docs(changelog): record #3401, #3473, #3492, #3490, #3451, #3491, #3169 under v3.8.18

* feat(docs): add doc accuracy gate + refresh AGENTS.md counts (#3510)

Integrated into release/v3.8.18

* fix(sse): drop empty-choices chunks without usage instead of injecting retry text (#3513)

PR #3422 ('allow OpenAI usage-only empty choices chunks') reintroduced the
assistant-content injection '[OmniRoute] Upstream returned an empty response.
Please retry.' for empty `choices: []` chunks that carry no valid usage. Clients
(Goose/opencode) feed that text back as a turn and spin in a retry loop -- the
exact regression #3400 had fixed by dropping the chunk.

Restore the drop behavior for the no-usage case while preserving #3422's
standards-compliant forwarding of usage-only `include_usage` final chunks.
Realign the mislabeled stream-utils test (it asserted the injection) and add a
dedicated regression guard.

Reported-by: @mochizzan
Refs: #3502, #3388, #3400, #3422

* fix(authz): fall back to URL token when Authorization isn't a usable Bearer (#3504)

Integrated into release/v3.8.18

* fix(playground): authenticate via session, test key policy by id (#3503)

Integrated into release/v3.8.18

* docs(changelog): record #3510, #3504, #3503 under v3.8.18

* fix: llama base url normalization (#3519)

* docs(changelog): reconcile v3.8.18 — add #3519, #3513, #3435-repair, gitignore chore (full commit↔changelog coverage)

* fix(opencode-plugin): bound regex quantifiers in normaliseFreeLabel (polynomial-ReDoS)

CodeQL js/polynomial-redos: unbounded \s* before an anchored \s*$ allowed
O(n²) backtracking on attacker-influenced display names. Bounded to {0,8}/{1,8}
(ample for any real label spacing). Plugin builds + 254 tests green.

* fix(types): restore clean typecheck:core for v3.8.18 release gate

- getPendingRequests() typed to real shape (was widened to object) → fixes
  unknown 'count' in the unified-requests view (#3401)
- streamChunks log payload cast to its declared type (callLogs.ts)
- preScreenTargets aligned to canonical IsModelAvailable signature (#3169),
  Promise.resolve-normalized so .catch never hits a bare boolean

All 5 gates green: lint(0 err) + typecheck:core + cycles + docs-all + unit + vitest(146).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Andrey Borodulin <borodulin@gmail.com>
Co-authored-by: Dmitrii Safronov <zimniy@cyberbrain.cc>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: PizzaV <103120356+pizzav-xyz@users.noreply.github.com>
Co-authored-by: Markus Hartung <mail@hartmark.se>
Co-authored-by: Felipe Almeman <4226997+zhiru@users.noreply.github.com>
2026-06-09 15:56:24 -03:00
Nicolas Lorin
50ce13bce6 fix(providerRegistry): update Claude model entries to latest versions (#3521) 2026-06-09 14:29:11 -03:00
diegosouzapw
e1a9c61179 fix(opencode-plugin): remove duplicated blocks + wire missing schema fields (#3435 merge corruption)
PR #3435's branch shipped a corrupted index.ts that never built — the npm
publish-opencode-plugin job failed on DTS errors. Root causes:

- Duplicate apiFormat block (ensureV1Suffix/DEFAULT_ANTHROPIC_PREFIXES/
  resolveApiBlock) — kept the canonical #3420 copy (anthropic url WITHOUT /v1),
  removed the duplicate that wrongly appended /v1 to the Anthropic SDK base.
- Duplicate debug-logging block (DebugLogEntry + debugLog* + createDebugLoggingFetch)
  with mid-file imports — kept the canonical copy using top-of-file imports.
- Local normaliseFreeLabel def superseded by the naming.ts extraction —
  removed it, routed the lone caller to the imported _normaliseFreeLabel.
- sdkBaseURL → resolvedBaseURL (undefined identifier in the auth loader).
- featuresSchema missing startupDebug + logLevel (referenced but never declared).
- shortProviderLabel dropped the prefix on long displayName + no alias; now
  keeps the long label, matching the test intent.

Plugin builds (DTS clean) and all 254 tests pass.
2026-06-09 08:21:57 -03:00
diegosouzapw
2441a4f441 fix(docs): add ACP.md frontmatter and flatten docs/meta.json pages format
fumadocs-mdx requires a YAML title in every .md file and does not support
nested object entries in meta.json pages arrays — both were introduced by
PR #3438 and broke the webpack build.
2026-06-09 02:48:41 -03:00
Diego Rodrigues de Sa e Souza
6ebc493770 Release v3.8.17
Release v3.8.17
2026-06-09 02:12:29 -03:00
diegosouzapw
259486afb5 chore(release): v3.8.17 — 2026-06-09
CHANGELOG: 8 features, 15 bug fixes, 6 maintenance entries (29 bullets / 32 commits since v3.8.16).
i18n: sync [3.8.17] section to all 41 locale CHANGELOG files.

fix(translator): strip empty reasoning_content on non-tool-call kimi-k2 messages (#3433 regression)
fix(translator): update placeholder assertion for non-empty cache-miss behaviour (test alignment)
fix(executor): lmarena.ts return wrapper shape {response,url,headers,transformedBody} (#3421 regression)
test: align lmarena-provider + tool-request-sanitization to corrected executor contract
2026-06-09 01:51:51 -03:00
Randi
500197846d Add model catalog name feature flag (#3464)
Integrated into release/v3.8.17
2026-06-08 23:58:28 -03:00
diegosouzapw
ee0fdcb6c8 docs(env): document COMMAND_CODE_VERSION override (#3462 follow-up)
#3462 added a process.env.COMMAND_CODE_VERSION read but did not document it,
tripping the env-doc-sync gate on the release branch (PR-merges bypass the
pre-commit check-docs-sync hook). Add the var to .env.example + ENVIRONMENT.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:56:43 -03:00
Randi
5c3545b045 Add Endpoint Token Saver visibility setting (#3461)
Integrated into release/v3.8.17
2026-06-08 23:36:26 -03:00
Hevener Ancelmo Pereira
1cc2313a4f fix(command-code): align CLI version header (#3462)
Integrated into release/v3.8.17
2026-06-08 23:35:08 -03:00
Randi
49c11f0cea fix(browser): avoid bundling optional cloakbrowser import (#3460)
Integrated into release/v3.8.17
2026-06-08 23:34:07 -03:00
Diego Rodrigues de Sa e Souza
4f38167964 fix(catalog): surface imported models on no-auth providers in /api/v1/models (#3200) (#3463)
The custom-models loop in getUnifiedModelsResponse gated every model through hasEligibleConnectionForModel(getConnectionsForProvider(...)). no-auth providers (theoldllm, etc.) never create DB connection rows, so that returned [] and the gate dropped every imported/custom model for them — the Playground dropdown showed nothing for imported models while built-in/custom models on auth providers worked. Built-in models survived because they go through providerSupportsModel(), which already has a no-auth bypass (#2798).

The custom-model gate now applies the same no-auth bypass, keeping the eligibility check (with parentProviderType) intact for auth providers.

Co-authored-by: tjengbudi <tjengbudi@users.noreply.github.com>
Co-authored-by: a2belugin <a2belugin@users.noreply.github.com>
2026-06-08 22:53:22 -03:00
Diego Rodrigues de Sa e Souza
ff65652cdf fix(claude): respect client anthropic-beta instead of forcing thinking/effort betas (#3415) (#3458)
Claude Code -> claude-opus-4-8 turns intermittently died with 'tool call could not be parsed (retry also failed)'. OmniRoute's claude identity cloak rebuilt the anthropic-beta header from scratch and unconditionally forced interleaved-thinking-2025-05-14 (+ advanced-tool-use / effort for heavy agents), even when the client never negotiated them. The forced interleaved-thinking conflicts with tool_choice-forced turns, producing malformed opus tool_use streams (and sibling 400 'Thinking may not be enabled when tool_choice forces tool use').

selectBetaFlags now takes the client's inbound anthropic-beta: when present, thinking/effort betas are only emitted if the client requested them. Opaque clients (no header — the OAuth cloak path) keep the full set unchanged, so existing behavior and the #2454 model-tier gating are preserved.

Co-authored-by: Forcerecon <Forcerecon@users.noreply.github.com>
2026-06-08 20:59:38 -03:00
Diego Rodrigues de Sa e Souza
cc850122e3 fix(translator): strip function_call.id for Vertex AI provider (#3440) (#3457)
Vertex AI's FunctionCall/FunctionResponse protos have no id field; emitting it made Vertex reject tool calls with 400 'Unknown name id'. The id is now stripped only when the routed provider is vertex/vertex-partner (threaded via credentials._provider), preserving it for the public Gemini API where Gemini 3+ uses it for signature matching.

Co-authored-by: nullbytef0x <nullbytef0x@users.noreply.github.com>
2026-06-08 20:54:20 -03:00
sdfsdfw2
42887b65b2 feat: add connection pagination, health filter, batch delete confirmation, and custom banned keywords (#3454)
Integrated into release/v3.8.17
2026-06-08 20:41:06 -03:00
Paijo
1a98dfe8ed test(auto-combo): cover same-provider connection identity (#3378)
Integrated into release/v3.8.17
2026-06-08 19:10:40 -03:00
M.M
003e6a80b7 feat(plugin+api): auto combos + free model quota display + /api/combos/auto (#3435)
Integrated into release/v3.8.17
2026-06-08 19:08:17 -03:00
ViFigueiredo
617a648088 fix(translator): use non-empty reasoning_content placeholder on cache miss instead of empty string (#3433)
Integrated into release/v3.8.17
2026-06-08 19:04:36 -03:00
Hernan Javier Ardila Sanchez
1322411343 feat(opencode-plugin): per-prefix API format + debug logging + free-label normaliser (3 mrmm-fork backports) (#3420)
Integrated into release/v3.8.17
2026-06-08 18:51:22 -03:00
Ardem2025
da273d37e2 fix(stream): resolve index mismatch in textual tool-call slicing and deduplicate containsTextualToolCallMarker (#3413)
Integrated into release/v3.8.17
2026-06-08 18:50:53 -03:00
Hernan Javier Ardila Sanchez
dbd70ddd1f fix(catalog): make combos auto-compute context_length for any provider id form (#3417)
Integrated into release/v3.8.17
2026-06-08 18:50:21 -03:00
Xiangzhe
89a76d8c1c fix(stream): allow OpenAI usage-only empty choices chunks (#3422)
Integrated into release/v3.8.17
2026-06-08 18:50:05 -03:00
Paijo
a00366602b docs: close critical documentation gaps (ACP, router strategies, APIs, compression) (#3438)
Integrated into release/v3.8.17
2026-06-08 18:44:48 -03:00
Hernan Javier Ardila Sanchez
96e5ec9269 docs(opencode-plugin): lead with the why — make plugin the recommended path over @omniroute/opencode-provider (#3418)
Integrated into release/v3.8.17
2026-06-08 18:44:30 -03:00
Hernan Javier Ardila Sanchez
858b6742e8 fix(publish): remove onnxruntime CUDA binary from tarball to avoid 413 (#3437)
Integrated into release/v3.8.17
2026-06-08 18:44:13 -03:00
Benjamin
3c98e9f1ef fix: probe container bridge network IP in healthcheck (#3151) (#3434)
Integrated into release/v3.8.17
2026-06-08 18:43:50 -03:00
Paijo
2427df2f2c feat: add Gemini Business provider (Phase 2C of #3368) (#3436)
Integrated into release/v3.8.17
2026-06-08 18:43:30 -03:00
Paijo
07a81c8a40 feat: add LMArena provider (Phase 2A of #3368) (#3421)
Integrated into release/v3.8.17
2026-06-08 18:43:10 -03:00
Paijo
ea0c0d8499 feat: add ZenMux provider (Phase 2B of #3368) (#3429)
Integrated into release/v3.8.17
2026-06-08 18:42:50 -03:00
Nicolas Lorin
23f31faf38 fix claude-web and cleanup (#3449)
Integrated into release/v3.8.17
2026-06-08 18:42:31 -03:00
ReqX
1e4185edac fix(analytics): scope SQL named params per query context (#3446) (#3447)
Integrated into release/v3.8.17
2026-06-08 18:42:12 -03:00
Muhammad Nabil Muyassar Rahman
b3372e46c4 fix(command-code): revert chat endpoint to /alpha/generate and fix model sync discovery (#3432)
Integrated into release/v3.8.17
2026-06-08 18:41:52 -03:00
Dmitrii Safronov
fc437ddecd fix(sse): normalize provider ids to strings (#3427)
Integrated into release/v3.8.17
2026-06-08 18:41:33 -03:00
dependabot[bot]
70c6610fa8 deps: bump electron-builder from 26.14.0 to 26.15.2 in /electron (#3443)
Integrated into release/v3.8.17
2026-06-08 18:40:58 -03:00
dependabot[bot]
d3ff0b3bde deps: bump electron-updater from 6.8.8 to 6.8.9 in /electron (#3442)
Integrated into release/v3.8.17
2026-06-08 18:40:27 -03:00
dependabot[bot]
f01a0b0c6d deps: bump the development group with 4 updates (#3445)
Integrated into release/v3.8.17
2026-06-08 18:40:04 -03:00
dependabot[bot]
de2420a35c deps: bump the production group with 10 updates (#3444)
Integrated into release/v3.8.17
2026-06-08 18:40:00 -03:00
dependabot[bot]
eb8651780d deps: bump electron from 42.3.2 to 42.3.3 in /electron (#3441)
Integrated into release/v3.8.17
2026-06-08 18:39:52 -03:00
diegosouzapw
c0dcdcc12f chore(release): open v3.8.17 development cycle 2026-06-08 16:48:16 -03:00
diegosouzapw
ea9d22beda fix(docker): copy playwright from builder instead of npx fetch in runner-web
npx playwright falls back to a registry download when playwright is absent from
the slim runtime image's node_modules. On GitHub-hosted runners this download
fails with exit 127, breaking both amd64 and arm64 -web image builds.

Fix: COPY playwright and playwright-core from the builder stage and invoke
node node_modules/playwright/cli.js directly — no network access, same version,
and playwright remains available at runtime for web-session providers.
2026-06-08 16:14:26 -03:00
Diego Rodrigues de Sa e Souza
60fc41f638 Release v3.8.16
Release v3.8.16
2026-06-08 15:14:18 -03:00
diegosouzapw
ac4fd7e078 chore(release): cover missing agentSkills TS-overload fix in CHANGELOG 2026-06-08 15:14:02 -03:00
diegosouzapw
85351bc63d chore(release): finalize v3.8.16 CHANGELOG — 2026-06-08 2026-06-08 14:44:24 -03:00
diegosouzapw
ed3c188881 fix(e2e): use .first() on Close button to avoid strict-mode violation (2 elements) 2026-06-08 13:37:00 -03:00
diegosouzapw
11bd96ec5c fix(e2e): dismiss import-models modal after adding connection (sync-models mock + close) 2026-06-08 13:12:37 -03:00
diegosouzapw
f112bc966f fix(e2e): wait for add-dialog close before clicking Edit (backdrop race) 2026-06-08 12:44:30 -03:00
diegosouzapw
b60839b90c ci(e2e): increase E2E shard timeout 30→45min for slow runners 2026-06-08 11:55:26 -03:00
diegosouzapw
717f56bf93 docs: update Codex CLI profile naming guidance
Update Codex CLI docs and configuration skill to use the v0.137+
profile file naming format: ~/.codex/<name>.config.toml instead of
the deprecated profile- prefix.

Clarify that missing profile files silently fall back to defaults, and
rename the setup workflow heading to match the config-codex-cli skill.
2026-06-08 10:47:04 -03:00
diegosouzapw
3ea416350e fix(tests+ci): update 42→43 skill count, fix E2E artifact path
- Unit/integration tests: update hardcoded 42→43 in 7 test files
  (agentSkillTools-mcp, agentSkills-catalog, agentSkills-generator,
  agent-skills-content, agent-skills-discovery, listCapabilities-a2a)
  to match the 43rd skill (config-codex-cli) added in the previous commit.
- Include CONFIG_SKILL_IDS in integration content test ALL_IDS so
  skills/config-codex-cli/ is no longer "unexpected".
- listCapabilities.ts: change totalSkills from literal 42 to catalog.length
  so it adapts to catalog growth automatically.
- computeCoverage assertions: include config.have in totalSkills check.
- CI: switch E2E artifact from upload-artifact path (ambiguous stripping)
  to explicit tar archive. Fixes "Could not find a production build in
  ./.build/next" — the previous approach's download path was double-nested
  (.build/next/next/...) due to upload-artifact LCA computation. tar -czf
  stores .build/next/... relative to CWD; tar -xzf restores them verbatim.
- Also exclude .build/next/cache from the tar to keep archive lean.
- feat(translator): strip client_metadata in Responses→Chat translation
  (Mistral 422 extra_forbidden fix); add regression test.
2026-06-08 10:41:00 -03:00
diegosouzapw
1012603a1b fix(ci+tests): fix E2E artifact (exclude 558MB standalone/node_modules, cp after download) and update skill count to 43 2026-06-08 10:13:24 -03:00
diegosouzapw
b480e6c916 fix(agentSkills): cast next-fetch opts to satisfy TypeScript overload check 2026-06-08 09:58:52 -03:00
diegosouzapw
6ec4ca3f67 ci: speed up e2e shards with build and browser cache
Upload the Next.js build from the build job and reuse it across E2E
shards to avoid rebuilding in each shard. Increase Playwright sharding
from 6 to 9, cache Chromium browsers, and lower the E2E timeout to match
the faster expected runtime.

Add a Codex CLI configuration skill for OmniRoute setup and ignore local
credential-bearing setup prompts.
2026-06-08 09:49:28 -03:00
diegosouzapw
b145e41a42 fix(tests): align test suite to post-#3355/#3366/#3399 behavior
- Remove 429 from PROVIDER_BREAKER_FAILURE_STATUSES; 429 belongs to
  connection cooldown, not whole-provider breaker (CLAUDE.md §resilience).
  PR #3366 correctly added 429 to PROVIDER_FAILURE_ERROR_CODES in
  accountFallback.ts (combo infinite-retry fix) but the parallel change
  to chat.ts was wrong — the integration test from v3.8.10 confirms this.

- Align stream-utils tests to PR #3399 (SYNTHETIC_CLAUDE_EMPTY_RESPONSE_TEXT
  → "", message.content → null) and PR #3355 (malformed tool-call buffer
  now emitted as plain text, not suppressed).

- Align services-branch-hardening test to PR #3399 (pinnedModel always
  null from applyComboAgentMiddleware; server-side session pinning replaced
  client-side <omniModel> tag extraction).

- Align combo-routing-engine context-cache tests to PR #3399 (no <omniModel>
  tag in output, no X-OmniRoute-Model header, priority routing unchanged).
2026-06-08 09:07:50 -03:00
diegosouzapw
e328e257d1 fix(docs+ui): add MDX frontmatter to Codex CLI guide, fix setState-in-effect lint
- docs/guides/CODEX-CLI-CONFIGURATION.md was missing the YAML frontmatter
  block required by fumadocs (title/version/lastUpdated), causing the
  production build to fail with "invalid frontmatter" MDX error.
- CodexCliGuideModal.tsx called setLoading/setError synchronously in a
  useEffect body, triggering the react-hooks/set-state-in-effect lint error.
  Refactored to an internal async function with an `cancelled` guard to
  prevent state updates on unmounted components.
2026-06-08 08:37:51 -03:00
Diego Rodrigues de Sa e Souza
67d79f6c44 fix(sanitizer+stream): tighten textual tool-call detection, flush partial buffer (#3355) (#3410) 2026-06-08 02:14:08 -03:00
Diego Rodrigues de Sa e Souza
e0615a8194 fix(executor): strip trailing assistant text for Mistral (user-last required) (#3396) (#3409) 2026-06-08 02:09:20 -03:00
Diego Rodrigues de Sa e Souza
f688d1150f fix(mitm): getMitmStatus stub returns graceful status in Docker (#3390) (#3408) 2026-06-08 02:05:44 -03:00
diegosouzapw
fd6a2a7f95 docs: add Codex CLI configuration guide for OmniRoute
Add a comprehensive guide for configuring Codex CLI to use OmniRoute as an OpenAI-compatible backend.

Document ready-to-use config examples, Responses API routing behavior, context window settings, token limits, model profiles, and troubleshooting guidance to help users avoid direct-provider compatibility issues.
2026-06-08 01:59:01 -03:00
Paijo
ecdd5a36eb feat: add REST API for session pool health (dashboard interface) (#3404)
Integrated into release/v3.8.16
2026-06-08 01:25:38 -03:00
Paijo
71f6e8d312 feat: add bulk web-session credential import endpoint (#3403)
Integrated into release/v3.8.16
2026-06-08 01:24:27 -03:00
Diego Rodrigues de Sa e Souza
c9663d4f84 fix(sse): eliminate race window in usageTokenBuffer settings update (#3405)
Integrated into release/v3.8.16
2026-06-08 01:23:37 -03:00
k0valik
4c420b015d fix: server-side context cache pinning, stop proxy message leaks, persist context_cache_protection toggle (#3399)
Integrated into release/v3.8.16
2026-06-08 00:55:42 -03:00
Hernan Javier Ardila Sanchez
452e6cc937 feat(vision-bridge): auto-route to fastest vision model (#3377)
Integrated into release/v3.8.16
2026-06-08 00:51:29 -03:00
Tubagus
48ed42c6c3 fix(providers): refresh model list after provider sync (#3402)
Integrated into release/v3.8.16
2026-06-08 00:49:12 -03:00
Paijo
6df38155a4 feat: add web-session pool observability (MCP tool + health-matrix) (#3395)
Integrated into release/v3.8.16
2026-06-08 00:48:01 -03:00
Paijo
5b72dc6250 feat: adaptive keepalive threshold for web-session providers (#3397)
Integrated into release/v3.8.16
2026-06-08 00:47:34 -03:00
Tubagus
4adc1d087f fix(stream): drop empty choices chunks instead of emitting retry text (#3400)
Integrated into release/v3.8.16
2026-06-08 00:47:08 -03:00
Ardem2025
ee061d7a6d fix(stream): solve false positive textual tool-call marker truncation using emitted content state (#3382)
Integrated into release/v3.8.16
2026-06-08 00:46:20 -03:00
Felipe Almeman
ed275bb54b ci(docker): also build & publish the -web image variant (#3389)
Integrated into release/v3.8.16
2026-06-08 00:45:53 -03:00
Paijo
8505e0f2b7 fix(account-fallback): preserve provider cooldown dedupe state (#3381)
Integrated into release/v3.8.16
2026-06-08 00:45:29 -03:00
Nicolas Lorin
765964242c fix(featureFlags): update description for PRICING_SYNC_ENABLED to clarify environment variable requirement (#3394)
Integrated into release/v3.8.16
2026-06-08 00:45:06 -03:00
Nicolas Lorin
fc37c93a20 fix(env): correct casing of OMNIROUTE_TRACE in .env.example and related files (#3393)
Integrated into release/v3.8.16
2026-06-08 00:44:26 -03:00
Diego Rodrigues de Sa e Souza
a471d70c3c fix(ci): give the heavy E2E shard headroom + stream live progress (#3392)
The 35m bump still wasn't enough — shard 5/6 (responsive viewport matrix +
studio/smoke, ~24 serial tests after a ~5m build) was still cancelled at 35m,
and the `github` Playwright reporter buffers output so the cancelled log showed
no per-test results (couldn't tell which test was slow).

- e2e timeout-minutes 35 -> 50 (the shard observably needs >35m; other shards
  finish in ~7m so they're unaffected).
- Playwright CI reporter github -> line so per-test progress + timing stream
  live to the job log, making any genuinely slow/hung test diagnosable.
2026-06-07 15:59:28 -03:00
Diego Rodrigues de Sa e Souza
b4437dcee4 fix(ci): stop the E2E shard from being cancelled mid-run (timeout headroom) (#3387)
The heaviest E2E shard (5/6 — responsive viewport matrix + studio/smoke) overran
the job's 20m timeout-minutes because each shard re-runs `npm run build` (~5m)
before Playwright, then runs ~24 serial tests with retries:2. The job was killed
(CANCELLED mid-run, 'Terminate orphan process') instead of any test failing.

- Bump test-e2e timeout-minutes 20 -> 35 (cumulative build+tests headroom).
- Lower the Playwright per-test timeout 600s -> 180s so a genuine hang fails fast
  and visibly (a clear per-test timeout) instead of silently eating the job budget.
2026-06-07 14:57:29 -03:00
diegosouzapw
a8522cc13a chore(release): open v3.8.16 development cycle 2026-06-07 14:28:30 -03:00
Diego Rodrigues de Sa e Souza
929caeb910 Release v3.8.15 (#3373)
* chore(release): open v3.8.15 development cycle

Version bump 3.8.14 -> 3.8.15 (root + electron + open-sse + openapi + lockfiles)
and seed the v3.8.15 changelog placeholder (root + 41 i18n mirrors).

* fix(catalog): add getTokenLimit fallback for combo targets with unknown context (#3369)

Integrated into release/v3.8.15. Fixes applied on the contributor's branch: removed duplicate JSDoc opening in accountFallback.ts and dropped a test asserting unreachable catalog behavior (models with no registry/spec/synced source are filtered before the getTokenLimit fallback at catalog.ts:499).

* fix(combo): add 429 to PROVIDER_FAILURE_ERROR_CODES to prevent infinite retry loop (#3366)

Integrated into release/v3.8.15. Comment block reconciled on the contributor's branch to remove the contradictory 'intentionally excluded' text that remained from the original code.

* fix(auto-combo): include no-auth providers declaratively (#3365)

Integrated into release/v3.8.15. Cleanup applied on contributor's branch: removed duplicate migration 095 (already exists from PR #3338), reverted CHANGELOG.md and i18n changelogs to release versions (release process owns these), dropped package version-bump noise from stale fork base. Core feature — declarative no-auth via serviceKinds metadata, declarative VEO as 'video' provider, anonymousFallback flag for opencode-zen/opencode-go — integrated cleanly.

* fix(migrations): restore 095_provider_node_custom_headers migration

The squash merge of PR #3365 accidentally deleted this migration because
the cleanup commit on the contributor's branch included 'git rm' for the
file (which was a duplicate on their branch). The migration was merged
in v3.8.14 via PR #3338 and must be present in the release branch.

Restoring from git history.

* fix: update Command Code base URL from /alpha/ to /provider/v1/ (#3372)

Integrated into release/v3.8.15.

* feat(error-rules): provider-specific error classification with scope (#3370)

Integrated into release/v3.8.15. PR has genuine value beyond #3369: (1) getProviderErrorRuleMatch now accepts native Headers objects from fetch(); (2) checkFallbackError also uses the provider rule registry — the real end-to-end wiring in the combo fallback path; (3) S4 end-to-end test proving the wiring fires. Merge commit on contributor branch resolved the add/add conflict by taking the #3370 version throughout.

* fix(auto-combo): validate web-session credentials (#3371)

Integrated into release/v3.8.15. Core feature: provider-aware web-session credential validation — hasUsableWebSessionCredential() replaces the broad Object.keys check in virtualFactory.ts, ensuring only sessions with the required storageKeys are included in auto-combo. Cleanup: removed duplicate 095 migration, reverted CHANGELOG/i18n, dropped package bump noise.

* fix(migrations): restore 095_provider_node_custom_headers (deleted again by #3371 squash)

Same issue as after #3365: git rm in the contributor cleanup commit
was included in the squash, deleting this migration from release.
Permanent fix needed: use 'git checkout origin/release -- <file>'
instead of 'git rm' when cleaning up duplicate files in contributor branches.

* fix(kiro): probe Windows %APPDATA%\kiro\storage.db in auto-import (#3363) (#3375)

Integrated into release/v3.8.15. Test fix applied: kiro-windows-auto-import-3363.test.ts now sets DATA_DIR to a fresh temp dir before importing app modules, ensuring isAuthRequired() sees an empty settings DB (no password → auth not required). This fixed test 4 (synthetic SQLite) which was getting 401 due to settings DB state leakage.

* chore(release): finalize v3.8.15 changelog — 2026-06-07

---------

Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Muhammad Nabil Muyassar Rahman <65392758+TapZe@users.noreply.github.com>
Co-authored-by: kiro-agent[bot] <245459735+kiro-agent[bot]@users.noreply.github.com>
2026-06-07 12:16:33 -03:00
Diego Rodrigues de Sa e Souza
000d60b907 test(translator): align gemini-2.5-flash maxOutputTokens cap to 65536 (#3358) (#3367)
#3358 added the gemini-2.5-flash model spec with its real 65536 max-output cap
(previously the model had no spec and fell to an 8192 default). The Claude→Gemini
clamp test still asserted 8192, so it failed deterministically — the single real
failure behind the v3.8.14 CI red (Unit Tests 3/8, Coverage Shard 3/8, Node
24/26 Compatibility 1/2 all hit this one test; E2E 5/6 was fail-fast collateral).
2026-06-07 08:12:33 -03:00
Diego Rodrigues de Sa e Souza
591084052a fix(ci): drop explicit any on executeWithUpstreamStartTimeout call (t11 any-budget) (#3364)
The v3.8.14 merge introduced `executeWithUpstreamStartTimeout<any>(...)` in
chatCore.ts, pushing the file's explicit-any count to 1 over its budget of 0
(check:any-budget:t11, a blocking CI lint-job gate). The generic T is already
inferable from the `execute` callback's return type, so drop the explicit
`<any>` and let inference do it — no behavior change, typecheck:core stays clean.
2026-06-07 07:34:33 -03:00
Diego Rodrigues de Sa e Souza
35a30609dd docs(changelog): complete v3.8.14 — add #3356 + @nullbytef0x/@Ardem2025 to contributors (#3362)
The release PR #3340 was merged before these changelog lines landed: the #3356
Usage-Analytics-error bullet and the @nullbytef0x (#3357) / @Ardem2025 (#3358)
contributor rows. Code for all three was already in the squash; this only
completes the changelog/credits so the GitHub release notes are accurate.
2026-06-07 07:24:11 -03:00
Diego Rodrigues de Sa e Souza
7db430a352 Release v3.8.14 (#3340)
* chore(release): open v3.8.14 development cycle

Version bump 3.8.13 -> 3.8.14 (root + electron + open-sse + openapi + lockfiles).
Seed the v3.8.14 changelog with the four post-tag hotfixes that shipped to
Docker/Electron in v3.8.13 but missed the immutable npm 3.8.13 (#3336 SSRF /
CodeQL #323, #3334/#3335/#3339 Electron packaging). i18n CHANGELOG mirrors get
the in-progress placeholder section.

* feat: add per-provider custom headers support for OpenAI/Anthropic-compatible nodes (#3338)

Integrated into release/v3.8.14

* fix: Kiro Builder ID token import fails with Bad credentials (#3333)

Integrated into release/v3.8.14 — adds Builder ID cached-creds + OIDC refresh path for Kiro token import, with regression tests (#3333).

* Improve code quality: auto-pr/docstrings-1780792063 (#3337)

Integrated into release/v3.8.14 — docstring for context analytics route re-export.

* fix(catalog): remove minimaxai/minimax-m3 from NVIDIA NIM tier (404 upstream) (#3329) (#3341)

NVIDIA NIM does not host minimaxai/minimax-m3 — every request returns
404 page not found, while sibling minimaxai/minimax-m2.7 on the same provider
works. Advertising a model that 404s is a catalog bug; remove it from the nvidia
tier (it remains on the tiers that actually serve MiniMax M3). Re-add only once
NVIDIA serves it.

Co-authored-by: mikmaneggahommie <mikmaneggahommie@users.noreply.github.com>

* fix(cli): write OpenCode config to ~/.config on all platforms incl. Windows (#3330) (#3343)

resolveOpencodeConfigDir used %APPDATA% on Windows, but OpenCode reads its
config from XDG ~/.config/opencode/ on every platform (on Windows:
%USERPROFILE%\.config\opencode\, NOT %APPDATA%). So a Windows user who
configured OpenCode via the dashboard had the file written where OpenCode never
looks — it silently had no effect.

Use the XDG path (XDG_CONFIG_HOME || ~/.config) unconditionally. Update the UI
note + route JSDoc, and flip the three tests that encoded the old %APPDATA%
behavior (t40 per-platform + card-note, cli-runtime-extended getCliConfigPaths).

Co-authored-by: abdulkadirozyurt <abdulkadirozyurt@users.noreply.github.com>

* fix(proxy): make auto-selection fallback opt-in (#3332) (#3344)

selectWorkingProxyFallback (Step 11 of resolveProxyForConnection) listed ALL
registry proxies, ignoring assignments and per-connection proxy_enabled, and
returned the first working one with level:'autoSelect'. So a single proxy added
to the registry silently became a global fallback for every connection's traffic.

Gate it behind a new PROXY_AUTO_SELECT_ENABLED feature flag (default off): the
fallback now no-ops unless the operator opts in. No registry proxy becomes a
silent global default anymore.

Co-authored-by: hertznsk <hertznsk@users.noreply.github.com>

* fix(sse): treat MiniMax M3 as multimodal so vision isn't stripped (#3328) (#3342)

MiniMax M3 via the opencode provider (oc/minimax-m3-free) appeared blind:
image inputs didn't reach the model, while the same model in Cline could
see them. Verified empirically that MiniMax M3 on the opencode upstream IS
multimodal -- a base64 image is described correctly (it returns 403 only
for remote image URLs, which it doesn't accept).

Root cause: OmniRoute treated MiniMax M3 as a non-vision model in two
places, so when compression was active the image was replaced with a text
placeholder before dispatch:
- compression's modelSupportsVision() heuristic (lite.ts) only matched
  gpt-4/4o/claude-3/gemini/vision -- minimax was absent -> replaceImageUrls
  stripped the image.
- the opencode minimax-m3-free catalog entry lacked supportsVision, so the
  combo vision-capability gate could also exclude/mishandle it.

Add 'minimax-m3' to the vision heuristic and supportsVision: true to the
opencode minimax-m3-free entry. TDD: a failing-then-passing test in
compression/lite.test.ts proves replaceImageUrls now keeps images for
minimax-m3 ids, plus a registry assertion mirroring the #2822 qwen test.

Reported-by: @mikmaneggahommie

* docs(i18n): translate 25 core documentation files to Indonesian (#3348)

Integrated into release/v3.8.14 — Indonesian i18n docs.

* fix(review): resolve /review-reviews battery findings (LEDGER-1..11) on v3.8.14 (#3350)

Integrated into release/v3.8.14 — /review-reviews battery hardening (LEDGER-1..11) for #3338 custom-headers + #3333 kiro, plus cycle-test drift fixes (#3329/#3330/#3332).

* fix(provider-proxy): honor per-account proxy toggles (#3349)

Integrated into release/v3.8.14 — honor per-account proxy toggles + auto-fallback opt-in via PROXY_AUTO_SELECT_ENABLED.

* fix(dashboard): remove duplicate Distribute Proxies button on provider page (#3352)

* fix(providers): reduce proxy label noise (#3346)

Integrated into release/v3.8.14 — reduce proxy label noise + a11y (aria-label/sr-only).

* fix(duckduckgo): restore bare Response contract and rebase onto release/v3.8.14 (#3323)

Integrated into release/v3.8.14 — browser-backed cookie providers (duckduckgo/claude-web) with restored executor contract + unit tests.

* fix(noauth): expose only usable model aliases (#3345)

Integrated into release/v3.8.14 — noauth usable-alias filtering + registry alias plumbing (veo-free).

* fix(dashboard): stop infinite config-load loop on Hermes Agent detail page (#3353)

* fix(electron): tree-kill the server on exit/update to release the omniroute.exe lock (#3347) (#3354)

* chore(release): finalize v3.8.14 changelog + clear release-gate drift

- CHANGELOG: finalize the v3.8.14 section (date, full New Features/Bug Fixes/
  Maintenance coverage of all 16 cycle commits, Contributors hall of 12).
- docs: document OMNIROUTE_BROWSER_POOL + WEB_COOKIE_USE_BROWSER (#3323) in
  .env.example + ENVIRONMENT.md; regenerate the id/llm.txt strict mirror (#3348
  had translated it; llm.txt mirrors must match root).
- test(proxy-fetch): #3323 made tlsClient.available a computed getter — stub it
  via Object.defineProperty instead of assignment (5 tests were red on the base).

* fix(translator): coerce Gemini functionDeclaration parameters to an OBJECT schema (#3357) (#3360)

* fix(gemini): resolve truncation/suppression of false positive textual tool call markers in backticks (#3358)

Integrated into release/v3.8.14 — Gemini/Antigravity textual tool-call marker normalization (no false-positive suppression + split-chunk buffering).

* docs(changelog): add #3358 Gemini textual tool-call normalization to v3.8.14

* fix(dashboard): surface real analytics error instead of generic placeholder (#3356) (#3361)

The Analytics page discarded the server's error body on a non-OK response and
rendered a generic "An error occurred", so users (and maintainers) could not see
why /api/usage/analytics 500'd after an upgrade. Now the route returns the real
reason via buildErrorBody (sanitized, Hard Rule #12) and the page surfaces it via
a new readFetchErrorMessage helper that handles both the OpenAI-style and legacy
error shapes.

Reported-by: @superti4r

---------

Co-authored-by: PizzaV <103120356+pizzav-xyz@users.noreply.github.com>
Co-authored-by: Someres <168349709+quanturbo@users.noreply.github.com>
Co-authored-by: Dong Mengzhe <154944819+Lang-Qiu@users.noreply.github.com>
Co-authored-by: mikmaneggahommie <mikmaneggahommie@users.noreply.github.com>
Co-authored-by: abdulkadirozyurt <abdulkadirozyurt@users.noreply.github.com>
Co-authored-by: hertznsk <hertznsk@users.noreply.github.com>
Co-authored-by: Krisna Santosa <54174372+KrisnaSantosa15@users.noreply.github.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: Wilson <pedbookmed@gmail.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Ardem2025 <ardemb22@gmail.com>
2026-06-07 07:20:02 -03:00
Diego Rodrigues de Sa e Souza
630baa6c18 fix(electron): swallow auto-updater check rejection to avoid unhandled rejection (#3339)
checkForUpdates() is fired unawaited from a setTimeout at startup. The
underlying autoUpdater.checkForUpdates() rejects on a 404 (release update
manifest not published yet), offline, or rate-limit — and the uncaught
rejection surfaced as an "Unhandled Rejection", which the packaged-app smoke
test treats as fatal (failed the macOS-intel v3.8.13 build; passed elsewhere
only by timing race). The autoUpdater "error" event still notifies the user;
wrap the await so the promise rejection never escapes. Adds a regression test.
2026-06-06 21:42:30 -03:00
Diego Rodrigues de Sa e Souza
df2379053e fix(security): use trusted internal origin for provider auto-sync self-fetch (CodeQL #323 SSRF) (#3336)
POST /api/providers fires a credential-bearing self-fetch to the new
connection's /sync-models route (forwarding the management cookie + internal
sync auth headers). #3267 built that origin from new URL(request.url).origin —
the client-controlled Host header — so a (management-authenticated) caller
could redirect the internal request to an arbitrary host, exfiltrating the
internal sync auth token (CodeQL js/request-forgery, critical, alert #323).

Derive the origin from the trusted loopback/env-pinned base URL via a new
getModelSyncInternalBaseUrl() helper (same source the model-sync scheduler
already uses), never from the incoming request. Adds a regression test.
2026-06-06 21:20:26 -03:00
Diego Rodrigues de Sa e Souza
9535fa52a6 fix(startup): correct autoRefreshDaemon import alias (@/ -> @omniroute/open-sse) (#3292) (#3335)
instrumentation-node.ts imported the #3292 cookie auto-refresh daemon via
"@/open-sse/services/autoRefreshDaemon". The @/ alias maps to src/, but the
daemon lives in the open-sse workspace, so the import resolved to the
non-existent src/open-sse/... and threw "Cannot find module" at runtime in the
built standalone. A try/catch made it non-fatal (the daemon silently never
ran), which kept typecheck and the dev server green, but the packaged Electron
app's strict startup-log smoke test failed on the "Cannot find module" line.

Use the correct @omniroute/open-sse alias, plus a regression test banning
@/open-sse/* imports across src/.
2026-06-06 21:15:43 -03:00
Diego Rodrigues de Sa e Souza
cd89ce3cfa fix(electron): ship loginManager.js in the packaged app (#3292 regression) (#3334)
#3292 added electron/loginManager.js and a require("./loginManager") in
main.js but did not add it to electron-builder's build.files allowlist, so
the packaged app crashed at startup with "Cannot find module './loginManager'"
on the Linux/macOS smoke tests (v3.8.13 Electron release fragment).

Add loginManager.js to build.files, plus a regression test that asserts every
local require("./x") in the Electron entry points is shipped.
2026-06-06 20:50:02 -03:00
Diego Rodrigues de Sa e Souza
a25d5f1ef6 Release v3.8.13 (#3327)
* chore(release): open v3.8.13 development cycle

Bump 3.8.12 → 3.8.13 across package.json, lockfile, electron/, open-sse/, and
docs/reference/openapi.yaml; add the [3.8.13] cycle placeholder to the root
CHANGELOG and the 41 i18n mirrors. Integration branch for the v3.8.13 cycle —
fixes/features land here via per-issue PRs and it merges to main at release time.

* fix(ci): skip auto-deploy when VPS host is unreachable from the runner (#3299)

Integrated into release/v3.8.13

* fix(dev): auto-rebuild better-sqlite3 on Node ABI mismatch at dev startup (#3301)

Integrated into release/v3.8.13

* feat(api): accept path-scoped API keys on client API routes (#3300)

Integrated into release/v3.8.13

* fix(sse): harden against empty responses causing Copilot Chat failures (#3297)

Integrated into release/v3.8.13

* fix(api): remove Completions.me rickroll provider (discussion #3293) (#3302)

Integrated into release/v3.8.13

* fix(opencode-provider): extract contextLength from live model catalog (#3298)

Integrated into release/v3.8.13

* feat(web-cookie): self-service login infrastructure + auto-refresh daemon (#3292)

Integrated into release/v3.8.13

* docs(changelog): record the v3.8.13 PRs merged this round (#3292/#3300/#3297/#3298/#3301/#3302/#3299)

* fix(auth): harden URL token extraction — drop query-string fallback, gate to client routes (security follow-up to #3300) (#3309)

Security follow-up to #3300 — integrated into release/v3.8.13

* docs: rename resolve-issues → review-issues skill references

* fix(dashboard): keep no-auth providers visible under 'Show configured only' (#3290) (#3312)

no-auth providers (opencode, duckduckgo-web, theoldllm, veoaifree-web) never
create a DB connection row so stats.total stays 0, which the configured-only
filter treated as 'unconfigured' and hid them — even though they are always
usable and appear unconditionally in /v1/models. filterConfiguredProviderEntries
now treats displayAuthType === 'no-auth' as configured.

Co-authored-by: uniQta <uniQta@users.noreply.github.com>

* fix(cli): resolve update paths relative to script + recursive backup (#3295) (#3313)

omniroute update always failed on a global install:
- getCurrentVersion() read package.json from process.cwd(), which on a global
  npm/brew install is the user's working dir, not the package root → null →
  'Could not determine current version'.
- createBackup() resolved bin/ from cwd too, and passed the 'cli' directory to
  copyFileSync → EISDIR, swallowed by the catch → 'Failed to create backup'.

Both now resolve package.json/bin relative to the script via import.meta.url,
and the backup uses cpSync({recursive:true}) so the cli/ directory is copied.

Co-authored-by: uniQta <uniQta@users.noreply.github.com>

* fix(theoldllm): read upstream body once to avoid [502] body-already-read (#3296) (#3314)

On the cached-token path the executor never enters the refresh branch, so the
same upstream Response was read with .text() twice (token-rejection check +
final body). A Response body is single-use, so the second read threw
'Body is unusable: Body has already been read', caught and surfaced as [502].

Read the body once into finalBody and only re-read after a token-rejection
refetch.

Co-authored-by: onizukashonan14-png <onizukashonan14-png@users.noreply.github.com>

* fix(sse): strip leaked internal tool envelopes from streaming output (#3311)

Integrated into release/v3.8.13

* fix(sse): expose Claude + Gemini budget tiers in the antigravity catalog (#3184) (#3303)

Integrated into release/v3.8.13 (#3184)

* fix(catalog): compute combo context_length from known targets only (#3304)

Integrated into release/v3.8.13 — live contextLength + known-targets combo context (#3298 follow-up)

* chore(i18n): add message keys for proxy UI + vscode/ollama endpoint (#3307)

Integrated into release/v3.8.13 — i18n message keys for proxy UI + vscode/ollama

* feat(dashboard): i18n the proxy settings UI (#3310)

Integrated into release/v3.8.13 — i18n the proxy settings UI

* feat(api): model catalog enrichment + MCP model-catalog tools (#3306)

Integrated into release/v3.8.13 — model catalog enrichment + MCP model-catalog tools, reconciled with #3309 URL-token hardening

* test(catalog): align Antigravity preview-alias test with #3303 budget tiers

#3303 added the Gemini `-high`/`-low` budget tiers to ANTIGRAVITY_PUBLIC_MODELS
(user-callable on the Antigravity OAuth backend, verified via #3184), but did
not update the catalog-route test that asserted `antigravity/gemini-3.1-pro-high`
must NOT be exposed. The assertion now reflects the intended behavior — the
client-visible budget alias IS surfaced — while keeping the legacy
`gemini-claude-*` alias keys unexposed. Caught running the full catalog suite
on the merged release HEAD (the #3303 round only ran the antigravity-aliases
and usage-hardening files).

* docs(changelog): record the 6 PRs merged this review round into v3.8.13

#3306/#3307/#3310 (New Features — VS Code split: catalog+MCP, i18n keys, proxy
UI i18n), #3311/#3303/#3304 (Bug Fixes — SSE envelope sanitizer, antigravity
budget tiers, combo known-targets context_length).

* chore(release): finalize v3.8.13 changelog and cleanup

Finalize the v3.8.13 changelog with release date, maintenance notes,
and contributor credits. Update MCP docs to reference the correct tool
inventory diagram, exclude nested .claude worktrees from ESLint scans,
and tighten a response sanitizer type guard.

* fix(dashboard): refresh connections after provider auth import (#3320)

Integrated into release/v3.8.13 — refresh connections after provider auth import

* fix(codex): strip client-only params on native /responses passthrough (#3317) (#3325)

A /v1/responses request against the built-in codex/ provider does an
openai-responses -> openai-responses passthrough (CodexExecutor.transformRequest
returns the body early for _nativeCodexPassthrough). It forwarded client-only
fields verbatim and the Codex upstream rejected them with 400 Unsupported
parameter: prompt_cache_retention / safety_identifier / user — breaking Factory
Droid (which injects all three). The chat-completions path already strips these
(base.ts #1884, openai-responses translator #2770) but the passthrough skips
translation. Strip the three fields in the shared block before the passthrough
return; user is removed unconditionally since Codex /responses always rejects it.

Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com>

* fix(dashboard): normalize agent-bridge /state response to stop page crash (#3318) (#3326)

The Agent Bridge page seeded a well-shaped initialData default then replaced it
wholesale with the raw /api/tools/agent-bridge/state response. The route returns
{ server, agents } but the UI reads { serverState, agentStates, bypassPatterns,
mappings }, so serverState became undefined and AgentBridgeServerCard crashed on
serverState.running — surfaced as the full-page 'Internal Server Error' boundary
(client render error, not a real 5xx).

Add a shared normalizeAgentBridgeState() that maps the route shape into the page
contract (server.running/certExists -> serverState) and always returns safe
defaults (never undefined serverState). Wired into both the SSR loader (page.tsx)
and the polling hook. The legacy 'agents' entry shape differs from AgentStateEntry
so it is not coerced; full route<->page contract reconciliation (port, upstreamCa,
bypassPatterns, mappings, agentStates) is a follow-up.

Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com>

* docs: VS Code/Ollama endpoints + env & i18n tooling (#3319)

Integrated into release/v3.8.13 — VS Code/Ollama docs + env & i18n tooling

* feat(provider): test-all endpoint, rate-limit overrides, visibility f… (#3267)

Integrated into release/v3.8.13 — provider test-all endpoint, rate-limit overrides, model visibility

* feat: auto-combo optimization, playground model dropdown, only-configured toggle (#3322)

Integrated into release/v3.8.13 — auto-combo candidate expansion + playground dropdown + only-configured toggle

* feat(api): VS Code Copilot Ollama-compatible BYOK endpoint (#3316)

Integrated into release/v3.8.13 — VS Code Copilot Ollama-compatible BYOK endpoint (reconciled with #3306/#3309 auth hardening)

* chore(release): document #3320 in the v3.8.13 changelog + contributor credits

---------

Co-authored-by: Felipe Almeman <4226997+zhiru@users.noreply.github.com>
Co-authored-by: Wilson <pedbookmed@gmail.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: uniQta <uniQta@users.noreply.github.com>
Co-authored-by: onizukashonan14-png <onizukashonan14-png@users.noreply.github.com>
Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com>
Co-authored-by: Vinayrnani <vinayrnani@gmail.com>
2026-06-06 19:13:11 -03:00
Diego Rodrigues de Sa e Souza
78454eed5e Merge pull request #3264 from diegosouzapw/release/v3.8.12
Release v3.8.12
2026-06-06 08:09:11 -03:00
diegosouzapw
5bebf0e53c fix(quota,sse): clear SonarCloud new-reliability findings on the v3.8.12 diff
- chipotle/grokTls: explicit null checks instead of a Promise in a boolean
  conditional (behavior-preserving; clears the 2 MAJOR reliability bugs)
- sqliteQuotaStore.poolUsage: drop the unreachable dimMap scan loops (dimMap
  was never populated) — the lightweight snapshot already returns no
  dimensions; poolUsageWithDimensions() is the plan-aware path
- BudgetTab: presentation role + keyboard handler on the checkbox wrapper
2026-06-06 06:49:46 -03:00
diegosouzapw
7ab1ad85a1 chore(release): finalize v3.8.12 changelog + env-doc-sync + test drift 2026-06-06 06:14:57 -03:00
diegosouzapw
a8668ebd77 chore(governance): raise coverage gate 40 -> 60
test:coverage now enforces 60/60/60/60 (statements/lines/functions/branches);
real coverage is ~75-82% so this tightens the floor without new test work.
Updates the c8 --check-coverage thresholds in package.json and the matching
references in CLAUDE.md (Quick Start, testing table, Copilot policy, Hard
Rule #9). Salvaged from the never-pushed chore/skills-governance-tdd-vps
branch; the i18n CLAUDE.md mirrors carry a separate pre-existing drift and
are not gated by check-docs-sync.
2026-06-06 04:44:52 -03:00
diegosouzapw
27f6ea85f9 docs(changelog): add v3.8.12 entries for #3280/#3285/#3286/#3287
Quota Sharing Engine repair (#3280), MiniMax-M3 across 8 tiers (#3287),
emitHookBlocking payload chaining (#3286), Chipotle CodeQL hardening (#3285);
updated the contributors hall.
2026-06-06 04:28:38 -03:00
Paijo
1bc88d97ee fix(plugins): chain payload between emitHookBlocking handlers (#3286) (#3286)
Integrated into release/v3.8.12. Salvaged the emitHookBlocking payload-chaining fix from the now-closed plugins-v4 branch (#3221) and adapted it to the shipped release hooks.ts: each blocking handler now sees the body/metadata as mutated by previous handlers. TDD regression test included (RED before, GREEN after); existing plugins-hooks suites green (19+5), typecheck + lint clean.
2026-06-06 04:27:00 -03:00
Diego Rodrigues de Sa e Souza
3086894704 docs(readme): consolidate community at top (Discord + Telegram + WhatsApp) + promote Free-Token Budget section (#3289)
- Add the official Telegram group (t.me/omnirouteOficial) and gather Discord,
  Telegram and both WhatsApp groups into one community card block at the top;
  remove the scattered WhatsApp links from the nav line and the Support section
  (now a pointer to the top).
- Move the Free-Token Budget section from the bottom (before License) up to a
  hero section near the top, retitled '💰 ~1.9B Free Tokens / Month'.
2026-06-06 04:24:46 -03:00
Wilson
e1622ed88b feat(models): add MiniMax M3 across all provider tiers (#3110) (#3287)
Integrated into release/v3.8.12. Registers MiniMax-M3 (1M context, Anthropic-compatible) across 8 provider tiers (minimax, minimax-cn, opencode, opencode-go, opencode-zen, trae, ollama-cloud, nvidia). Validated: 8/8 new registry tests + 25 registry/model-catalog regression files green, typecheck + lint clean. Complements the #3141 max_tokens spec already on release.
2026-06-06 04:18:53 -03:00
Paijo
1344843a45 fix(security): use crypto.randomInt/randomUUID in chipotle + URL parser in test (#3285)
Integrated into release/v3.8.12. CodeQL hardening on the Chipotle executor: Math.random → crypto.randomInt/randomUUID, and a strict URL hostname check in the test. Fixed the node:crypto import (crypto.randomInt is not on the Web Crypto global → would crash at WS-connect) and added a regression guard exercising both helpers.
2026-06-06 04:16:51 -03:00
diegosouzapw
ba734b01b2 docs(changelog): credit @wilsonicdev for the Qoder 500-bypass diagnosis (#3282/#3247)
The #3247 fix shipped via #3283 (parallel session) 46s after @wilsonicdev
filed the same fix in #3282, leaving his PR stranded with no credit — the
#3242 credit-theft pattern. Repoint the entry to the merged #3283, credit
@wilsonicdev as co-author for the independent diagnosis, and note #3283
refined it to keep rejecting on an explicit-auth-signal 500.
2026-06-06 03:43:17 -03:00
Paijo
1c8f3bee97 fix(quota): resolve poolUsage dead code, burn rate, saturation signals, webhooks, and embeddings enforcement (#3280)
Integrated into release/v3.8.12. Quota Sharing Engine fixes: poolUsageWithDimensions promoted to the QuotaStore interface, single-snapshot burn rate, zero-weight normalization, Anthropic saturation signals, quota.exceeded webhook on block, and embeddings enforcement. Validated: 10/10 PR tests + 34 quota/embedding regression files green, typecheck + lint clean. Dropped the committed .omo/ agent-tooling artifacts.
2026-06-06 03:41:03 -03:00
Diego Rodrigues de Sa e Souza
7abb40c64c docs(free-tiers): richer budget-card image (28 models + first-month strip) + soften ToS framing to caution (#3284)
- Regenerate the README/dashboard mockup from the catalog: 28 pools in the grid
  (was 9), a balance-floored stacked bar (Mistral now ~40% of the bar, was ~90%),
  and a first-month signup-credit strip (~586M). Add the data-driven generator.
- FREE_TIERS.md: drop the alarming '🚫 Avoid / terms prohibit' framing — relabel
  those 19 providers as 'caution — worth checking', note their access is real and
  the OAuth/keyless ones aren't token-quantifiable (so out of the headline, not
  excluded as unusable).
2026-06-06 03:22:54 -03:00
Diego Rodrigues de Sa e Souza
a7e445edea fix(sse): don't mark a valid Qoder PAT expired on a generic Cosy 500 (#3247) (#3283)
A working Qoder PAT was reported as "expired". The validator probes the Cosy endpoint
(api1.qoder.sh) — which IS the correct PAT path (the executor falls back to it after the
expected 401 from api.qoder.com). The bug was the verdict: isCosyAppError (added by #2860)
marked ANY Cosy 500 with "success":false as an auth failure, including a generic
{..."msgCode":500,"message":"Internal Server Error"} server fault — contradicting the
older #1391 "5xx = valid bypass" rule.

Narrow it: a Cosy 500 only marks the PAT invalid when the body carries an EXPLICIT auth
signal (unauthorized/forbidden/expired/token invalid/...); a generic Internal Server Error
falls back to valid-bypass. #2860's protection for genuine auth rejections is preserved.

Regression test: tests/unit/qoder-cli.test.ts — the two pre-existing generic-500 cases now
assert valid:true (they encoded the #3247 bug) + a new explicit-auth-signal case asserts
valid:false. 13/13 green.
2026-06-06 03:11:58 -03:00
Diego Rodrigues de Sa e Souza
36932b62a7 fix(api): harden private webhook opt-in against cloud-metadata SSRF (#3269) (#3281)
Follow-up to the #3269 private-webhook opt-in. With the opt-in on, the private-host
check was bypassed entirely, leaving cloud-metadata endpoints (169.254.169.254,
metadata.google.internal, 100.100.100.200, link-local 169.254.0.0/16) reachable — the
classic SSRF -> IAM-credential pivot — and the webhook test endpoint returned the
upstream body, making it a content-exfiltration primitive against internal services.

- outboundUrlGuard: add isCloudMetadataHost(); parseAndValidateWebhookUrl blocks those
  hosts UNCONDITIONALLY, even when private targets are opted in.
- webhooks/[id]/test: redact responseBody for private targets (status + latency only).

Regression test: tests/unit/webhook-metadata-guard-3269.test.ts (RED before, GREEN after);
existing webhook SSRF/opt-in suites stay green (34/34).
2026-06-06 03:08:16 -03:00
Diego Rodrigues de Sa e Souza
a5d19bf4b9 fix(api): allow private webhook targets behind explicit opt-in (#3269) (#3279)
Webhooks hardcoded parseAndValidatePublicUrl, which blocks any RFC1918/loopback host —
breaking self-hosted setups that legitimately point webhooks at internal services
(n8n, Home Assistant, a LAN box). Provider URLs already had an opt-in
(OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS); webhooks now reuse it.

- outboundUrlGuard: add parseAndValidateWebhookUrl — gates the private-host check on
  arePrivateProviderUrlsAllowed() (default OFF); protocol + embedded-credential checks
  stay unconditional.
- swap all webhook call sites (create/update/test/validate-url + dispatcher x2) to it.

Regression test: tests/unit/webhook-private-optin-3269.test.ts (RED before, GREEN after);
existing webhook SSRF/dispatcher suites stay green (33/33).
2026-06-06 02:59:09 -03:00
diegosouzapw
a19cfd4036 docs(changelog): complete v3.8.12 audit — all 14 merged PRs + contributors hall
Audited every commit since v3.8.11 one-by-one. Added the missing v3.8.12
entries (features #3250/#3259/#3263/#3271, fixes #3248/#3249/#3261/#3256/#3274,
maintenance #3270), repointed the combo-rewrite and web-tools entries to their
actually-merged PRs (#3268, #3275) instead of the closed #3242/issue links, and
added the v3.8.12 Contributors hall. Also co-credited @ibanunmangun on the
v3.8.11 #3203 OAuth fix (independent first diagnosis via #3193).
2026-06-06 02:55:25 -03:00
Diego Rodrigues de Sa e Souza
e478ab23af fix(api): build /v1/images/edits multipart as Buffer, not global FormData (#3273) (#3278)
A custom OpenAI-compatible image-edit provider received an empty `model`. In production
`globalThis.fetch` is patched with node_modules/undici's fetch, whose `FormData` class
differs from `globalThis.FormData`; passing a native FormData made undici serialize it as
the string "[object FormData]" (text/plain), dropping every field including `model`.

handleOpenAIImageEdit now assembles the multipart body as a Buffer with an explicit
boundary + Content-Type, which every fetch impl accepts verbatim.

Regression test: tests/unit/image-edits-multipart-3273.test.ts reproduces the exact prod
condition (routes through undici's fetch) — RED before (upstream got text/plain
[object FormData]), GREEN after. Existing image suites stay green (50/50).
2026-06-06 02:52:44 -03:00
Diego Rodrigues de Sa e Souza
80c546eba9 fix(sse): strip reasoning_effort for non-reasoning Groq models (#3258) (#3277)
Regression of #764. Claude Code → Groq (llama-3.3-70b-versatile) returned HTTP 400
because the model was treated as reasoning-capable: supportsReasoning() defaulted to true,
so applyThinkingBudget did not strip reasoning params, and the claude→openai translator
forwarded reasoning_effort (and re-injected it from output_config.effort) — which Groq
rejects on non-reasoning models.

- providerRegistry: mark llama-3.3-70b-versatile + llama-4-scout supportsReasoning:false
  (gpt-oss / qwen3-32b keep reasoning — they accept reasoning_effort).
- stripThinkingConfig: also strip output_config.effort so the translator can't re-inject
  reasoning_effort downstream.

Regression test: tests/unit/thinking-budget-groq-3258.test.ts (RED before, GREEN after);
existing thinking-budget suites stay green (45/45).
2026-06-06 02:48:38 -03:00
Diego Rodrigues de Sa e Souza
41eb0091a2 fix(sse): parse <tool_call name=...> wrapper from web-cookie providers (#3260) (#3275)
ds-web/deepseek-v4-pro emits tool calls wrapped as
<tool_call name="skill">{"name":"customize-opencode"}</tool_call> instead of the
canonical <tool>{json}</tool>. webTools.ts only matched <tool>...</tool>, so the block
was silently dropped (and when arguments were present, the surrounding tag leaked into
content). Add TOOL_CALL_TAG_RE to capture the JSON body — the real tool name comes from
the body, never the tag's name= attribute — and extend the early-exit + range stripping.

Regression test: tests/unit/web-tools-translation-3260.test.ts (RED before, GREEN after).
Existing web-tools suites stay green (26/26).
2026-06-06 02:44:44 -03:00
Diego Rodrigues de Sa e Souza
30ebe0ae2e feat(free-tiers): per-model free-token budget + Monthly Budget dashboard card (#3263)
Free-token budget catalog + per-model budget + Monthly Budget dashboard card (joins #3257 + #3263 into one).

Integrated into release/v3.8.12.
2026-06-06 02:38:35 -03:00
Lenine Júnior
5d8f265192 feat(dashboard): bulk activate/deactivate/retest for selected provider connections (#3271)
Bulk activate/deactivate/retest for selected provider connections.

Integrated into release/v3.8.12. Thanks @leninejunior.
2026-06-06 02:33:17 -03:00
Felipe Almeman
6674f6a4f2 fix(db): detect SQLite driver-unavailable errors to avoid destructive rename (#3274)
Detect SQLite driver-unavailable errors to avoid destructive DB rename + optional FTS5 migration guard (split from #3073).

Integrated into release/v3.8.12. Thanks @zhiru.
2026-06-06 02:15:17 -03:00
Diego Rodrigues de Sa e Souza
6bfba384d8 fix(ci): deploy-vps recreates PM2 via bin + gates on /login 200 (#3270)
Synchronized deploy-vps hardening (PM2 recreate via bin + /api/monitoring/health gate + fail-on-unhealthy). Supersedes #3262.

Integrated into release/v3.8.12.
2026-06-06 02:11:21 -03:00
Diego Rodrigues de Sa e Souza
2ad2bcb13f fix(embeddings): block cross-dimension failover in embedding combos (#3256)
Block cross-dimension failover in embedding combos.

Integrated into release/v3.8.12.
2026-06-06 02:07:06 -03:00
Wilson
07d9010668 fix(v1/responses): skip codex rewrite for combo names (#3233, #3227) (#3268)
Regression test for the /v1/responses combo-name codex-rewrite guard (#3233, #3227).

Integrated into release/v3.8.12. Thanks @wilsonicdev.
2026-06-06 00:39:57 -03:00
Paijo
2db8de8232 feat(provider): add Chipotle Pepper AI — free provider via reverse-engineered Amelia protocol (#3250)
Add Chipotle Pepper AI free provider (Amelia protocol); sanitize executor error body (#12).

Integrated into release/v3.8.12. Thanks @oyi77.
2026-06-06 00:27:41 -03:00
Thiago Reis
583bceb53d fix(providers): improve refresh validation and model catalog UI (#3261)
Provider refresh/validation, OpenRouter catalog and proxy UI fixes — incl. NVIDIA NIM /models-suffix path fix (real-VPS validated).

Integrated into release/v3.8.12. Thanks @strangersp.
2026-06-06 00:26:40 -03:00
Paijo
e364764dc7 feat(web-cookie): add tool-call translation to 8 executors via shared webTools helpers (#3259)
Add tool-call translation to 8 web-cookie executors via shared webTools helpers.

Integrated into release/v3.8.12. Thanks @oyi77.
2026-06-06 00:22:27 -03:00
Wilson
925d838d3b fix(grok-web): add TLS fingerprint impersonation to bypass Cloudflare anti-bot (#3180) (#3249)
grok-web: TLS fingerprint impersonation to bypass Cloudflare anti-bot (#3180); sanitize executor error bodies (#12).

Integrated into release/v3.8.12. Thanks @wilsonicdev.
2026-06-06 00:20:51 -03:00
MikeTuev
c2520bf5b7 fix(sse): strip every <omniModel> tag, not just the first (#454) (#3248)
Strip ALL <omniModel> tags before forwarding to provider (global regex variant).

Integrated into release/v3.8.12. Thanks @MikeTuev.
2026-06-05 21:21:40 -03:00
diegosouzapw
1d28c0f13d docs(changelog): credit @wilsonicdev for the /v1/responses combo fix (#3242) 2026-06-05 21:20:51 -03:00
diegosouzapw
452e152703 chore(release): open v3.8.12 development cycle
Bump 3.8.11 → 3.8.12 across package.json, lockfile, electron/, open-sse/, and
docs/reference/openapi.yaml; add the [3.8.12] cycle placeholder to the root
CHANGELOG and the 41 i18n mirrors. Integration branch for the v3.8.12 cycle —
fixes/features land here via per-issue PRs and it merges to main at release time.
2026-06-05 20:43:47 -03:00
Diego Rodrigues de Sa e Souza
404cfcbbac Release v3.8.11 (#3190)
Release v3.8.11
2026-06-05 17:35:23 -03:00
diegosouzapw
c2d46776fd fix(security): clear CodeQL high alerts surfaced on the v3.8.11 release PR diff
CodeQL flagged 7 high alerts in code the cycle touched (the large release-PR diff
re-surfaces them). Resolved at the source — no dismissals:

- fix(images): resolveImageBaseUrl trimmed trailing slashes with `/\/+$/`, a
  polynomial-ReDoS pattern (js/polynomial-redos) on the configured node base URL.
  Replace it with a non-backtracking endsWith/slice loop.
- test(oauth): pin the Anthropic OAuth host with exact-equality asserts and a
  parsed-hostname negative check instead of substring `.includes()`
  (js/incomplete-url-substring-sanitization). The exact-equality assertions were
  already present, so coverage is unchanged.
- test(images): drop the redundant `!includes("generativelanguage.googleapis.com")`
  assert — the exact-equality assert on the resolved URL already guarantees it.
2026-06-05 16:48:13 -03:00
diegosouzapw
396a79f02a fix(api,dashboard): validate /v1/images/edits JSON body + drop duplicate proxy handler
Clear the two release-gate failures the CI Lint+Build jobs surfaced (release-branch
drift — PR merges bypassed pre-push):

- fix(api): /v1/images/edits parsed request.json() without a Zod guard
  (route-validation t06 / hard rule #7). Add ImageEditJsonSchema.safeParse so a
  malformed body (non-object / wrong types) is rejected with 400 instead of
  silently parsed; valid JSON/data-URL bodies behave exactly as before. (#3214, #3215)
- fix(dashboard): remove a duplicate handleToggleProxyEnabled /
  handleTogglePerKeyProxyEnabled / handleDistributeProxies block in
  providers/[id]/page.tsx — a bad merge of the proxy PRs declared all three twice,
  breaking the webpack build ("Identifier already declared"). The removed copy was
  byte-identical to the kept one. (#3170, #3171, #3172)
2026-06-05 16:03:24 -03:00
diegosouzapw
75bccccbef chore(release): finalize v3.8.11 changelog + repair release-gate test drift
Finalize the 3.8.11 cycle CHANGELOG and clear the failures the full test:unit
gate surfaced (release-branch drift — PR merges bypassed pre-push):

- CHANGELOG: date the [3.8.11] section (2026-06-05) + repo-housekeeping roll-up
- docs(env): document THEOLDLLM_NAV_TIMEOUT_MS in .env.example + ENVIRONMENT.md
  (env-doc-sync gate; #3217 added the var without docs)
- test(nvidia): exercise the #3226 bypass-fetch path via a local HTTP server and
  hoist the validator import (patching globalThis.fetch no longer intercepts the
  un-patched native fetch captured by proxyFetch)
- test(i18n): import the shipped normalizeComplianceEventTypes helper (#3185)
- test(model-caps): save synced metadata under the canonical gemini-3.1-pro key
  now that #3229 aliases gemini-3.1-pro-high/-low to it
- test(web-session): expect the grok-web "sso + sso-rw" credential hint (#3180)
- test(synced-models): isolate DATA_DIR so the #3199 hidden-override stops
  bleeding into the shared DB and breaking the re-run precondition
2026-06-05 15:38:06 -03:00
Diego Rodrigues de Sa e Souza
4fcc16fc6a docs(changelog): combo on /v1/responses (#3227/#3233) + agy gemini 400 (#3229) (#3246) 2026-06-05 14:20:57 -03:00
Diego Rodrigues de Sa e Souza
62e6336aad fix(antigravity): alias agy gemini-3.1-pro -high/-low + stop masking upstream 4xx (#3229) (#3245)
agy's gemini-3.1-pro-high/-low had no alias, so resolveAntigravityModelId sent the
speculative -high/-low suffix verbatim to upstream, which rejects it (400) for
gemini-3.x. Worse, the non-stream executor branch fed the 4xx response into the SSE
collector, returning a synthetic empty {object:chat.completion} envelope that masked
the error. Alias both to gemini-3.1-pro, and surface real upstream errors via
buildErrorBody for non-ok non-stream responses. + unit tests.
2026-06-05 14:20:05 -03:00
diegosouzapw
9c13d44cca docs(changelog): audit v3.8.11 — credit all 14 missing contributor PRs + add contributor hall
Consolidate the split [Unreleased]/[3.8.11] sections into one, add entries for
every merged contributor PR that was missing credit (#3170/#3171/#3172 @pizzav-xyz,
#3185/#3195 @zhiru, #3188 @xz-dev, #3189/#3203/#3204/#3241 @wilsonicdev, #3191 @bypanghu,
#3206 @juandisay, #3217 @oyi77, #3226 @miracuves, #3187/#3200 maintainer), drop stale
v3.8.8 leftovers (#2958/#2959 already shipped) and 3 empty v3.8.10 stub headers.
2026-06-05 14:19:49 -03:00
Diego Rodrigues de Sa e Souza
cf7f684bd8 fix(api): don't force combo names to codex/ on /v1/responses (#3227, #3233) (#3244)
The Codex CLI WS->HTTP fallback rewrite (resolveResponsesApiModel) prefixes a
bare model id with codex/ whenever codex/<id> resolves to codex. Codex accepts
arbitrary model strings, so a combo name with no slash (e.g. n8n-text,
paid-premium) was rewritten to codex/<combo> and sent to Codex instead of being
resolved as a combo — regressing combos via /v1/responses in v3.8.9+. Skip the
rewrite when the bare id is a combo (getComboByName). + unit test.
2026-06-05 14:16:10 -03:00
Wilson
b413774bdf fix(gemini): refresh AI Studio model fallback (#3241)
Refresh Gemini AI Studio static model fallback to current 3.x/2.5 models (closes #3231).

Integrated into release/v3.8.11. Thanks @wilsonicdev.
2026-06-05 14:13:05 -03:00
diegosouzapw
d985dace79 docs(changelog): credit @wilsonicdev for agy quota (#3232) and Hermes OpenCode Free picker (#3240) 2026-06-05 13:14:55 -03:00
Wilson
c222143071 fix(cli): show OpenCode Free in Hermes Agent picker (#3240)
Show OpenCode Free in the Hermes Agent model picker via alwaysIncludeProviders.

Integrated into release/v3.8.11. Thanks @wilsonicdev.
2026-06-05 13:12:52 -03:00
Wilson
5ec8fa222a fix(usage): route agy quota through antigravity (#3232)
Route agy quota through the Antigravity usage implementation (closes #3230).

Integrated into release/v3.8.11. Thanks @wilsonicdev.
2026-06-05 13:12:40 -03:00
diegosouzapw
8cdfee5d90 Remove deprecated SKILL files for capture-release-evidences and deployment workflows. These files are no longer needed as the workflows have been updated or replaced with new implementations. 2026-06-05 13:11:16 -03:00
diegosouzapw
af7a8b3b45 chore(gitignore): ignore generated coverage/ output dir 2026-06-05 12:10:54 -03:00
Felipe Almeman
2942ba874e Feat/codex device flow (#3195)
Codex public device-flow connect link (ticket-gated) + dashboard CTA. Integrated into release/v3.8.11.
2026-06-05 12:06:35 -03:00
diegosouzapw
0ac8539200 docs(changelog): credit @wilsonicdev for the Docker healthcheck.mjs COPY fix (#3201) 2026-06-05 11:55:19 -03:00
Wilson
fea2991fc0 fix(docker): copy healthcheck.mjs into runner-base image 2026-06-05 11:54:58 -03:00
MeAdityaB
4dbbbaacf1 fix: NVIDIA NIM API key validation timeout (bypass proxy fetch patch) (#3226)
NVIDIA NIM validation bypasses the proxy-patched fetch (504 fix) + combined with #3116 reliable probe model + test. Integrated into release/v3.8.11.
2026-06-05 11:52:27 -03:00
ipanghu
dfcaeba6d9 fix(sse): refine kimi thinking handling and add unit tests (#3191)
Refine Kimi thinking handling (reasoning_content for tool-call turns) + tests. Integrated into release/v3.8.11.
2026-06-05 11:48:03 -03:00
Paijo
5179b16596 feat(provider): add The Old LLM (theoldllm) — free Playwright-backed provider (#3217)
Add The Old LLM (theoldllm) free browser-backed provider. Integrated into release/v3.8.11.
2026-06-05 11:45:24 -03:00
Diego Rodrigues de Sa e Souza
b2887da1ca docs(changelog): v3.8.11 deep-triage fixes (#3198, #3116, #3180, #3091) (#3225) 2026-06-05 10:21:37 -03:00
Diego Rodrigues de Sa e Souza
b4d5610d86 fix(dashboard): correct misleading provider credential hints (#3180, #3091) (#3224)
- grok-web: the credential hint named only "sso" while Grok needs both "sso"
  and "sso-rw"; users pasted just sso and hit anti-bot 403s. Name both
  (credentialName + placeholder). The underlying Cloudflare anti-bot 403 is
  upstream and tracked separately on #3180.
- vertex: the Service Account JSON placeholder was an untranslated stub literal
  ("Vertex Service Account Placeholder") in 40 locales, making the field look
  broken even though SA-JSON auth is fully supported. Replace with real
  instructional text (zh localized; pt-BR already translated).

Guard test pins both hints.
2026-06-05 10:20:38 -03:00
Diego Rodrigues de Sa e Souza
e0c6fb9f8c fix(providers): use a reliable NVIDIA validation probe model (#3116) (#3223)
The NVIDIA key-validation chat probe used models[0] (z-ai/glm-5.1), which
requires the 'Public API Endpoints' account permission and has DEGRADED
windows. Accounts lacking that permission see the probe hang until the
validation timeout, surfacing as a misleading 'Upstream Error' on a valid key.
Probe the universally-available meta/llama-3.1-8b-instruct instead, still
overridable via providerSpecificData.validationModelId. + unit test.
2026-06-05 10:17:40 -03:00
Diego Rodrigues de Sa e Souza
5a2e93d20a fix(dashboard): show friendly provider name (not UUID) in home topology (#3198) (#3222)
getProviderConfig falls back to { name: providerId } for unknown ids, so the
label precedence config.name || p.name let the raw internal UUID of a custom
provider shadow the friendly name HomePageClient already resolved into p.name.
Extract resolveTopologyNodeLabel (entry name first) + unit test.
2026-06-05 10:14:06 -03:00
Diego Rodrigues de Sa e Souza
7786aa2c0e docs(changelog): record the v3.8.11 issue-fix batch 2 (#3025, #3214/#3215) (#3220) 2026-06-05 09:39:12 -03:00
Diego Rodrigues de Sa e Souza
ec4f8c4d42 feat(api): combo/alias resolution + OpenAI-compatible edits for image routes (#3214, #3215) (#3219)
Image routes now resolve a requested model the same way across /v1/images/generations
and /v1/images/edits, via a shared resolver: built-in id -> custom provider prefix ->
bare combo/alias name (e.g. "image" -> its single image target). Previously a bare
combo name fell through to "Invalid image model".

/v1/images/edits gains two capabilities for custom OpenAI-compatible providers:
- multipart edit forwarding to the node's {base_url}/images/edits (was hard-rejected
  unless chatgpt-web);
- JSON/data-URL edit input (images:[{image_url:"data:..."}]), converted to the same
  fields the multipart reader produces (was "Invalid multipart body").

The chatgpt-web conversation-continuation edit flow is unchanged.
2026-06-05 09:37:32 -03:00
Diego Rodrigues de Sa e Souza
c116bfbc7f fix(db): open import-validation DB via resilient driver factory (#3025) (#3218)
The db-backups import route statically imported better-sqlite3, which is
stripped from the Next standalone server's node_modules in the packaged
Electron app. Loading the route then crashed with "Cannot find module
'better-sqlite3'" on the Windows installer, even though node:sqlite was
available. Route the upload integrity-check through openDatabaseAsync
(better-sqlite3 -> node:sqlite -> sql.js), matching every other DB path.

Adds a guard test so no API route can reintroduce a direct native import.
2026-06-05 09:23:17 -03:00
Diego Rodrigues de Sa e Souza
5afb984425 docs(changelog): record the v3.8.11 issue-fix batch (#3202/#3205/#3151/#3197/#3199) (#3213) 2026-06-05 02:57:36 -03:00
Diego Rodrigues de Sa e Souza
6fe7c6b5b1 fix(provider-models): keep a deleted synced model deleted across re-fetch (#3199) (#3212)
Follow-up to #3204: deleting a synced (fetched) model removed it from the
synced set, but the DELETE route didn't mark it hidden and the re-import path
didn't skip hidden ids, so the next auto-fetch re-added it. Now the route
marks the id hidden on delete and replaceSyncedAvailableModelsForConnection
filters hidden ids, so the deletion sticks.

Co-authored-by: tjengbudi <tjengbudi@users.noreply.github.com>
2026-06-05 02:56:03 -03:00
Diego Rodrigues de Sa e Souza
143fb2ace4 fix(llama-cpp): fall back to local default base URL when none is set (#3197) (#3210)
Residual of #3136: a local connection with an empty baseUrl still resolved
to this.config.baseUrl (OpenAI). Fall back to the provider's localDefault
(127.0.0.1:8080/v1) before the OpenAI default for the local-provider group.

Co-authored-by: tjengbudi <tjengbudi@users.noreply.github.com>
2026-06-05 02:51:01 -03:00
Diego Rodrigues de Sa e Souza
9032a5a4ab fix(docker): healthcheck tries 127.0.0.1/localhost/::1 and surfaces errors (#3151) (#3209)
The healthcheck probed only 127.0.0.1 and swallowed every error (empty
Output), so containers binding to a non-loopback address always reported
unhealthy with no diagnostic. Add a multi-host probe helper that succeeds on
the first 2xx and prints the last error to stderr on total failure.

Co-authored-by: naimo84 <naimo84@users.noreply.github.com>
2026-06-05 02:50:57 -03:00
Diego Rodrigues de Sa e Souza
fa0aa1e25d fix(images): use provider node base_url + resolve prefix for custom image providers (#3205) (#3208)
The image-generation handler read credentials.baseUrl (always undefined),
so custom OpenAI-compatible image providers fell back to the Gemini endpoint
(401). Resolve from providerSpecificData.baseUrl like the chat path, and
rewrite prefix/model to the internal node id before the exact-id lookup.

Co-authored-by: ngocquynh85 <ngocquynh85@users.noreply.github.com>
2026-06-05 02:50:54 -03:00
Diego Rodrigues de Sa e Souza
9bc2c89924 fix(openrouter): report true upstream context_length for passthrough models (#3202) (#3207)
normalizeDiscoveredModels only copied record.inputTokenLimit, but OpenRouter
returns the window as context_length / top_provider.context_length, so every
synced model fell back to the 128K default. Read context_length (and
top_provider.max_completion_tokens for output) as a fallback.

Co-authored-by: pulyankote <pulyankote@users.noreply.github.com>
2026-06-05 02:50:51 -03:00
Diego Rodrigues de Sa e Souza
d3422c1c4d test(combo): guard same-provider cascade is short-circuited by connection cooldown (#3200) (#3194)
Integrated into release/v3.8.11
2026-06-05 02:50:32 -03:00
diegosouzapw
422b7b747c docs(changelog): credit @androw (#3167) as co-author of the i18n eventTypes fix (#3185)
#3185 (zhiru) and #3167 (androw / Nicolas Lorin) independently fixed the same
next-intl dotted-key bug. #3185 shipped (runtime normalize); #3167 is being
closed as superseded. Per the repo's contributor-credit policy, record the
parallel credit in the release notes so androw is co-credited even though their
PR is not the one that merged.

Co-authored-by: Nicolas Lorin <androw95220@gmail.com>
2026-06-05 02:49:45 -03:00
Wilson
005ee10a1e fix(oauth): use api.anthropic.com for Claude token exchange to avoid Cloudflare bot block on VPS (#3203)
Integrated into release/v3.8.11
2026-06-05 02:47:42 -03:00
Wilson
7b4bda13b1 fix(models): allow deleting synced/fetched models (e.g. llamacpp) via DELETE /api/provider-models (#3204)
Integrated into release/v3.8.11
2026-06-05 02:44:38 -03:00
‍juandisay
0ea925ac20 feat: validate client IDs against resolvePublicCred to correctly toggle OAuth redirect URI overrides (#3206)
Integrated into release/v3.8.11
2026-06-05 02:41:42 -03:00
PizzaV
c48e0851f7 feat(proxy): add proxy distribution UI with per-connection toggles (#3172)
Integrated into release/v3.8.11
2026-06-05 02:05:33 -03:00
PizzaV
de5c842301 feat(proxy): auto-fallback proxy selection when validation fails (#3171)
Integrated into release/v3.8.11
2026-06-05 01:57:40 -03:00
PizzaV
d8363a51f2 feat(proxy): per-key proxy toggle backend with DB schema (#3170)
Integrated into release/v3.8.11
2026-06-05 01:14:49 -03:00
Xiangzhe
4a5e123bad fix(auth): honor REQUIRE_API_KEY feature flag (#3188)
Integrated into release/v3.8.11
2026-06-05 00:53:27 -03:00
Wilson
ccc4425744 fix(auto-combo): include no-auth OpenCode Free (#3189)
Integrated into release/v3.8.11
2026-06-05 00:50:08 -03:00
Diego Rodrigues de Sa e Souza
ee62c4c38b refactor(build): single-source sidecar list + drop redundant Dockerfile COPYs (#3187)
Integrated into release/v3.8.11
2026-06-05 00:48:28 -03:00
Felipe Almeman
a7494e415e Fix/codex import auth (#3185)
Integrated into release/v3.8.11
2026-06-05 00:47:35 -03:00
diegosouzapw
264a2ccbc7 chore(release): mirror v3.8.11 cycle section to i18n CHANGELOGs 2026-06-04 21:05:49 -03:00
diegosouzapw
37218fd517 chore(release): open v3.8.11 development cycle 2026-06-04 21:05:05 -03:00
Diego Rodrigues de Sa e Souza
c27a32d432 fix(security/quality): clear CodeQL high + SonarCloud reliability gate for v3.8.10 (#3186)
v3.8.10 hardening: clear CodeQL high (URL substring → hostname) + SonarCloud reliability (remove dead BARE_PRO_IDS).
2026-06-04 20:12:33 -03:00
Diego Rodrigues de Sa e Souza
68d5a0ab27 Release v3.8.10 (#3140)
* chore(release): open v3.8.10 development cycle

Bump 3.8.9 → 3.8.10 across package.json, lockfile, electron, open-sse, and
docs/reference/openapi.yaml; add the [3.8.10] CHANGELOG section (root + 41 i18n
mirrors) as the integration target for the cycle. Entries land here as work
merges into release/v3.8.10; finalized by the release flow.

* fix(providers): resolve web provider alias collisions

Assign unique aliases to HuggingChat, Kimi Web, and Qwen Web so they no longer shadow primary providers or trigger startup warnings.

Add a unit test to enforce provider alias uniqueness and prevent future collisions. Also expand local ignore and VS Code exclude rules for agent, build, and worktree artifacts.

* fix(responses): normalize image_url parts across input paths (#3150)

Normalize image_url parts across all Responses input paths. Integrated into release/v3.8.10.

* fix(api-manager): preserve API key expiration local time (#3146)

Preserve API key expiration local time + clear button. Integrated into release/v3.8.10.

* Strip previous_response_id for stateless Responses upstreams (#3143)

Strip previous_response_id for stateless Responses upstreams (auto/strip/preserve). Integrated into release/v3.8.10.

* fix(opencode-plugin): map thinking cap to interleaved in model+combo (#3138)

Map caps.thinking to ModelV2.capabilities.interleaved for opencode-plugin. Integrated into release/v3.8.10.

* fix(providers): use synced models as fallback for all providers (#3148)

Use synced models as authoritative local catalog for all providers (+regression test). Integrated into release/v3.8.10.

* fix(qoder): bifurcate validation by token type — PAT→Cosy, regular API key→dashscope (#3149)

Bifurcate Qoder validation by token type (PAT→Cosy, regular→dashscope) +regression test. Integrated into release/v3.8.10.

* fix(antigravity): dynamic model resolution via MITM alias table (#3144)

Dynamic antigravity MITM model resolution in the executor (+bug fix +regression test; DB import dropped from client-reachable config). Integrated into release/v3.8.10.

* Feature/batch allow big (#3128)

Podman deployment options + larger upload body-size limits (+CONTAINER_HOST docs). Integrated into release/v3.8.10.

* fix(fireworks): preserve fully-qualified router/model IDs (#3133) (#3160)

Fireworks router IDs (accounts/fireworks/routers/...) were double-prefixed
with accounts/fireworks/models/ → upstream 404. Add optional
acceptedModelIdPrefixes to the registry entry and skip the prepend when the
model already starts with an accepted prefix.

Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>

* fix(llama-cpp): route to configured local baseUrl instead of OpenAI (#3136) (#3161)

llama-cpp was missing from the local-provider group in buildUrl(), so it
fell through to the OpenAI baseUrl and returned an OpenAI 401. Add the
case to resolve the connection's providerSpecificData.baseUrl.

Co-authored-by: tjengbudi <tjengbudi@users.noreply.github.com>

* fix(t3-chat-web): parse cookies + convexSessionId from stored credential (#3007) (#3162)

The executor read credentials.cookies/convexSessionId, but the pipeline
only stores the pasted string under apiKey → t3.chat always 400'd. Parse
both values from apiKey (fallback accessToken), mirroring validation.ts.

Co-authored-by: minhtran162 <minhtran162@users.noreply.github.com>

* fix(minimax): stop capping MiniMax-M3 / M2.7 max_tokens at 8192 (#3141) (#3163)

MiniMax-M3 had no MODEL_SPECS entry and capitalized MiniMax-M2.7 missed
its lowercase spec (case-sensitive lookup) → both fell to the 8192 default
cap. Add the M3 spec (512K output), alias the capitalized ids, and make
getModelSpec lookups case-insensitive.

Co-authored-by: totaltube <totaltube@users.noreply.github.com>

* fix(github-copilot): discover model catalog live from api.githubcopilot.com (#3120, #3121) (#3164)

The github (Copilot) provider had a static hardcoded catalog with no
discovery source, so Import Models never refreshed (#3120) and advertised
non-entitled models that 400 on use (#3121). Add a live /models fetch with
fallback to the static list.

Co-authored-by: gabrielmoreira <gabrielmoreira@users.noreply.github.com>

* fix(combo): invalidate nested-combo cache on edits + log DATA_DIR (#3147) (#3165)

Editing a combo did not invalidate the 10s nested-combo expansion caches
(chat.ts getCombosCachedForChat + chatCore.ts getCombosCached; the exported
clearCombosCache was dead code), so a removed nested target/model could be
served as a phantom for up to 10s. Wire a shared monotonic combos-cache
version in readCache (bumped by invalidateDbCache("combos") on every combo
write); both cache layers treat a version mismatch as a miss.

Also log the resolved DATA_DIR/SQLITE_FILE absolute path at DB init so the
reporter's 'persists across restart + volume wipe' symptom (a multi-replica
Docker volume/DATA_DIR mismatch, not a routing bug) is diagnosable from logs.

Includes consolidated CHANGELOG entries for #3133/#3136/#3007/#3141/#3120/#3121.

Co-authored-by: ViFigueiredo <ViFigueiredo@users.noreply.github.com>

* fix(web-tools): parse bare JSON tool calls (#3157)

Parse bare JSON tool calls for deepseek-web (#2820) + fuzzy tool-name matching. Integrated into release/v3.8.10.

* fix(misc): minor fixes across reasoning cache, account fallback, binary manager (#3177)

Misc: ProviderProfile export, DeepSeek reasoning regex, binary guard. Integrated into release/v3.8.10.

* fix(kiro): minor OAuth social exchange tweaks (#3176)

Kiro social OAuth: optional targetProvider passthrough. Integrated into release/v3.8.10.

* deps: bump hono from 4.12.18 to 4.12.23 (#3179)

Bump hono to 4.12.23. Integrated into release/v3.8.10.

* fix(providerRegistry): update kilocode format and executor (#3166)

kilocode: openai format + default executor (matches kilo-gateway) + registry test. Integrated into release/v3.8.10.

* feat(metrics): cross-request TTFT and gap latency after tool calls (#3173)

Cross-request TTFT + gap-after-tool latency metrics (+test). Integrated into release/v3.8.10.

* feat(dashboard): provider stats API endpoint and dashboard page (#3175)

Provider stats dashboard + API (SQL moved to db module per Hard Rule #5, +test). Integrated into release/v3.8.10.

* fix(usage): sequential+spaced OAuth quota sync, reactive force-refresh, actionable 401 (#3156)

Sequential+spaced OAuth quota sync, reactive force-refresh on 401, actionable 401 in UI. Integrated into release/v3.8.10.

* fix(healthcheck): per-provider proactive-refresh skip list (rescue short-TTL OAuth) (#3159)

Per-provider proactive-refresh skip list (OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS) to rescue short-TTL OAuth. Integrated into release/v3.8.10.

* feat(quota): show OAuth token expiry on provider cards (small, blue, informative) (#3178)

Show OAuth token expiry on provider cards (small, blue, informative). Integrated into release/v3.8.10.

* fix(providers): empty refresh must not resurface just-cleared synced models (#3181)

Empty refresh must not resurface just-cleared synced models (fixes the release-blocking provider-models-route test). Integrated into release/v3.8.10.

* chore(release): v3.8.10 — 2026-06-04 (finalize CHANGELOG)

---------

Co-authored-by: Wilson <pedbookmed@gmail.com>
Co-authored-by: Xiangzhe <32761048+xz-dev@users.noreply.github.com>
Co-authored-by: Jan Leon <Jan.gaschler@gmail.com>
Co-authored-by: M.M <mr.maatoug@gmail.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: Markus Hartung <mail@hartmark.se>
Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
Co-authored-by: tjengbudi <tjengbudi@users.noreply.github.com>
Co-authored-by: minhtran162 <minhtran162@users.noreply.github.com>
Co-authored-by: totaltube <totaltube@users.noreply.github.com>
Co-authored-by: gabrielmoreira <gabrielmoreira@users.noreply.github.com>
Co-authored-by: ViFigueiredo <ViFigueiredo@users.noreply.github.com>
Co-authored-by: PizzaV <103120356+pizzav-xyz@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Lorin <androw95220@gmail.com>
2026-06-04 20:05:38 -03:00
Diego Rodrigues de Sa e Souza
506a701a1a ci(electron): make macos-arm64 smoke best-effort (headless GPU crash) (#3137)
The headless GitHub macos-arm64 runner crashes Electron's GPU process
(gpu_process_host exit_code=15 → network service crash → no rendezvous
client), so the smoke can't reach 127.0.0.1:20128 in 60s and the whole
job fails → Create Release is skipped → no desktop binaries on the release.
The identical bundle is still smoke-gated on macos-intel + linux, so
per-OS packaging stays verified; extend the existing windows best-effort
exception to macos-arm64 so its runner flakiness no longer blocks releases.
2026-06-04 06:30:58 -03:00
Diego Rodrigues de Sa e Souza
5057454d21 Merge pull request #3135 from diegosouzapw/release/v3.8.9
Release v3.8.9 — final sync (static-asset fix + contributor credits)
2026-06-04 05:10:03 -03:00
diegosouzapw
6ce96cb664 refactor(build): reduce assembleStandalone cognitive complexity (SonarCloud gate)
Extract patchStandalonePackageJson / copyStaticAndPublic / copyNativeAssetsAndExtraModules
helpers so assembleStandalone drops from cognitive complexity 29 → ~12 (≤15 gate).
Also: replaceAll over replace, String.raw for the regex-escape replacement (2 minor smells).
Pure refactor — assemble-standalone.test.ts still green; no behavior change.
2026-06-04 04:46:37 -03:00
diegosouzapw
796267df3f docs(changelog): credit direct-to-main PRs (#3130/#3131/#3132) + static-asset build fix
- #3131 Kiro Opus 4.8 catalog (thanks @artickc)
- #3132 Kimi thinking-mode reasoning_content fix (thanks @bypanghu)
- #3130 connectionId fallback + kilo call logging (thanks @androw)
- build: standalone static-asset path fix (white login screen after build-output reorg)
- contributors hall: +@artickc +@bypanghu +@androw (15 total)
2026-06-04 04:21:09 -03:00
diegosouzapw
49dedecc42 fix(build): assemble static/server-files/chunks under distDir, not literal .next
CRITICAL white-screen bug from the build-output-isolation refactor: the standalone
server.js bakes distDir ("./.build/next") into its config and serves /_next/static
from <root>/.build/next/static — but assembleStandalone hard-coded the destination
to <outDir>/.next/static (+ sanitised/patched <outDir>/.next/{required-server-files,
server}). Result: the server's static dir was EMPTY → every JS/CSS chunk 404'd →
blank login page (health stayed 200, so it slipped past the health-only dry-run).

Mirror the distDir path (relative to projectRoot) for static, required-server-files
sanitization (was a silent no-op → 0 paths sanitised, now 11), and the Turbopack
chunk patch. Verified: booting the assembled bundle serves the webpack chunk 200.
Affects every consumer (npm/Docker/Electron/VPS).
2026-06-04 02:17:51 -03:00
Diego Rodrigues de Sa e Souza
872895c172 Merge pull request #3092 from diegosouzapw/release/v3.8.9
Release v3.8.9
2026-06-04 00:30:02 -03:00
diegosouzapw
b6bda19919 fix(quality): explicit promise-existence checks + protocol-aware WebDAV URL
SonarCloud new-code findings:
- combo.ts / sync-models route: `if (cachedPromise)` -> `!= null` (intentional
  in-flight-promise reuse; explicit existence check, no behaviour change).
- ObsidianSourceCard WebDAV URL: inherit window.location.protocol instead of
  hard-coding http:// (https when behind a TLS proxy) — clears the http hotspot.
2026-06-03 23:53:03 -03:00
NOXX - Commiter
223374221f feat(kiro): add Claude Opus 4.8 to the Kiro model catalog (#3131)
Kiro (AWS CodeWhisperer) tops out at Claude Opus 4.7 in the registry, but the latest Opus 4.8 is already served by the Claude Code provider and Kiro's executor passes the model id through to CodeWhisperer verbatim. Expose claude-opus-4.8 on the kiro provider so clients can select it via kiro/claude-opus-4.8.

- providerRegistry: add claude-opus-4.8 (1M context, 128k output) above 4.7

- pricing: add claude-opus-4.8 (and the previously-missing 4.7) to the kiro pricing block at Kiro's standard Opus rate so usage cost is non-zero

- tests: assert kiro exposes claude-opus-4.8 with matching context/output + pricing
2026-06-03 23:51:21 -03:00
ipanghu
74ce4fd76d Fix:Add KimiExecutor to fix the error of reasoning_content is missing in kimi thing mode (#3132)
* Fix the error of reasoning_content is missing in kimi thing mode

* fix: kimi always use default

* fix: add kimi-coding to use KimiExecutor
2026-06-03 23:50:53 -03:00
diegosouzapw
dd85309e64 fix(ci): hasStandaloneAppBundle dist/->app/ fallback + gate obsidian-plugin e2e
- hasStandaloneAppBundle now accepts the legacy app/ bundle too (mirrors serve
  CLI's dist/->app/ fallback), fixing postinstall-support.test.ts after #3124.
- obsidian-plugin-e2e: #3077 committed the e2e test but NEVER committed its
  dependency obsidian-plugin/src/server.ts (un-ignored but unstaged) nor the
  'obsidian' npm pkg, so it crashed with ERR_MODULE_NOT_FOUND on every fresh
  checkout. Load the runtime values dynamically and skip the suite when absent
  (unit sync logic stays covered by obsidian-plugin-sync.test.ts).
2026-06-03 22:48:24 -03:00
Nicolas Lorin
bb87a59125 fix(handler): provide fallback for connectionId when undefined (#3130) 2026-06-03 22:39:29 -03:00
diegosouzapw
dff836ae26 fix(ci): resolve remaining CI failures (typecheck + build-reorg test drift + #3100 dedup)
Full CI surfaced real failures that local subsets missed (gh-merged PRs bypass
the hooks that run these gates):
- typecheck:core (Lint job): 3 now-unused @ts-expect-error in mcp-server/server.ts
  (#3077 dynamic tool loops) → @ts-ignore (lenient, no TS2578).
- pack-artifact-policy.test.ts: build-reorg (#3124) renamed app/->dist/; the test
  still asserted app/ paths + REQUIRED order (it sorts alphabetically).
- electron-packaging.test.ts: extraResources from .next/electron-standalone ->
  .build/electron-standalone (#3124).
- glm-provider-model-import-route.test.ts: two GLM connections shared one apiKey,
  so #3100 (#3023) dedup collapsed them → only one discovery fetch. Distinct keys.

Remaining CI flakes (batch expiration, ModelSync self-fetch) pass in isolation —
concurrency/port flakiness under --test-concurrency=4, not real failures.
2026-06-03 22:12:17 -03:00
diegosouzapw
27229aa7eb test(cache): align stale cache-HIT tests with #2952 SSE-wrap behavior
#2952/#3108 made streaming cache hits SSE-wrapped (so streaming clients keep
content + reasoning_content), but two chatcore tests still asserted the pre-fix
'cache HIT returns JSON regardless of stream flag'. Update them to assert SSE
(text/event-stream) + verify the cached content appears in the SSE frames.
2026-06-03 21:12:32 -03:00
diegosouzapw
e1007acb7e fix(cli): bare omniroute (default serve) must provision STORAGE_ENCRYPTION_KEY
My #3129 gate wrongly skipped provisioning for a bare `omniroute` invocation —
but `serve` is isDefault:true, so bare runs the server, which needs the key.
Only --version/--help/help/completion skip now. Realigns with #1622: its bootstrap
test invoked `--help` (now correctly skipped), so it's switched to `config list
--json` (a real, fast, offline command) to exercise the provisioning path.
2026-06-03 21:08:01 -03:00
diegosouzapw
f1c42359a2 fix(ci): raise t11 any-budget for cursor.ts (false-positive) + server.ts (dynamic tools)
The Lint job's check:any-budget:t11 (string-blind /\bany\b/ regex) failed on:
- open-sse/executors/cursor.ts: the WORD 'any' in #3104's tool-commit/output-
  constraint prompt strings — zero real TS `any` in the file.
- open-sse/mcp-server/server.ts: 3 `(toolDef: any)` in dynamic memory/skill/
  compression tool-registration loops (#3077), guarded by existing @ts-ignore.
Both are v3.8.9-introduced and benign; set the per-file baseline to the count.
2026-06-03 20:31:34 -03:00
diegosouzapw
20331afeec fix(ci): allowlist build-time OMNIROUTE_BUILD_SHA in env-doc-sync
build:release injects OMNIROUTE_BUILD_SHA (git short SHA) read by
write-build-sha.mjs to stamp dist/BUILD_SHA — it's build-time only, never a
user .env var, so it belongs in IGNORE_FROM_CODE (like OMNIROUTE_CLI_SKIP_REPO_ENV)
rather than .env.example/ENVIRONMENT.md. Fixes the Docs Sync (Strict) CI job.
2026-06-03 20:08:44 -03:00
diegosouzapw
838c2cab88 test(sse-auth): unique default apiKey per seeded connection (align with #3023 dedup)
After #3100 (#3023) dedups provider connections by decrypted key value, the
seedConnection helper's shared 'sk-test' default collapsed multiple seeded
connections into one, breaking round-robin / least-used / fallback selection
tests (they saw 1 account instead of 2+). Default to a unique key per connection
(matching the existing unique-name default). Found via full test:unit — #3100 was
merged via gh, bypassing the pre-push test gate, so these never ran post-merge.
2026-06-03 20:05:57 -03:00
diegosouzapw
8dd9749a93 fix(build): correct modelResolution import path in responses route (#3113)
The webpack build failed: route.ts imported '../internal/codex-responses-ws/
modelResolution' (resolves to api/v1/internal/, which doesn't exist). The module
lives at api/internal/codex-responses-ws/. Switched to the @/app/api/... alias.
typecheck/tests passed (tsx resolves leniently; tests import the module directly),
only the production build caught it.
2026-06-03 19:47:42 -03:00
diegosouzapw
49bfe982c2 docs(changelog): finalize v3.8.9 — add session PR entries + contributors hall
Adds entries for #3097, #3101 (deepseek-web #2942/#2820), #3104, #3105, #3107,
#3109, #3111, #3113, #3115, #3122, #3125, #3127, #3129, plus a Contributors
section crediting all v3.8.9 contributors. Stamps the 3.8.9 release date.
2026-06-03 19:01:44 -03:00
diegosouzapw
cad93f35ce fix(lint): escape quotes in ObsidianSourceCard JSX (react/no-unescaped-entities) 2026-06-03 18:58:29 -03:00
diegosouzapw
652faeefc7 docs(readme): feature Discord community invite prominently at the top 2026-06-03 18:58:29 -03:00
Oğuzhan Sert
8dc93ade6d fix(i18n): Turkish locale-aware search and sorting (#3115)
* feat(i18n): add Turkish locale-aware text helpers (search/sort)

* test(i18n): cover null/whitespace edges + document compareTr/normalize contract

* fix(i18n): route dashboard search through Turkish-safe matchesSearch

* fix(i18n): sort user-visible lists with Turkish collation (compareTr)

* fix(i18n): keep providerId tiebreaker as ASCII sort (technical id)

* docs(i18n): document intentional lang=en in global-error boundary

* chore(lint): guard against locale-unsafe toLowerCase().includes search

* fix(i18n): migrate missed provider-name search + harden lint disable placement

* fix(i18n): downgrade no-restricted-syntax to warn (incremental adoption)

The rule errored on ~19 pre-existing toLowerCase().includes() call-sites in
src/app accumulated since this PR's base. Keep it as a warning so the guard-rail
guides future code without breaking the 0-errors lint gate (project policy:
0 errors, warnings tolerated).

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-03 18:58:01 -03:00
Diego Rodrigues de Sa e Souza
80c9ca7096 fix(cli): don't write STORAGE_ENCRYPTION_KEY to .env on informational commands (#3129)
Running any CLI command — even `omniroute --version` or `--help` — generated a
32-byte STORAGE_ENCRYPTION_KEY and created `~/.omniroute/.env` (or DATA_DIR/.env).
A read-only command should never mutate the data dir. Gate the provisioning
behind shouldProvisionStorageKey(): skip for --version/--help/help/completion and
bare invocations; still provision for real commands (serve, keys, …) so the
encryption key persists before storage is accessed (#1622).
2026-06-03 18:53:22 -03:00
Tubagus
a718558d68 perf(logs): fix browser freeze and network saturation on /dashboard/logs (#3109)
* feat(providers): implement bulk paste for extra API keys

Adds `parseExtraApiKeys` utility to process multi-line key inputs.
Integrates bulk paste functionality into the provider connection modal.
Users can now paste multiple API keys, one per line, into the input field.
Provides notifications for successfully added keys and ignored duplicates.
Adds a "Delete all" button to clear all extra API keys.
Updates i18n messages for new features and improved key masking/pluralization.

* refactor(providers): streamline API key bulk paste and i18n

Remove unused return from `handleAddParsedExtraKeys` to clean up code.
Adjust `onPaste` to allow default paste for single-line input, improving UX.
Remove obsolete bulk paste UI translation keys to reduce bundle size.
Update pluralization for bulk paste messages to ensure correct grammar.
Refine Portuguese (Brazil) API key translations for clarity.

* fix(logs): apply code review feedback - robust signature, immediate fetch on tab restore, reuse memoized apiKeyCount

* test(logs): extract pure polling/signature helpers + cover them (#3109)

Hard rule #8: the perf fix touched src/ without tests. Extract computeLogsSignature,
shouldAutoRefresh and resolveInitialVisibility into a pure module and unit-test
them (change-detection, first-page polling guard, SSR/hidden-tab visibility init).
Also fixes visibleRef to honor the real visibilityState on mount instead of
hardcoding true (no poll when mounted in a hidden tab).

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-03 18:35:09 -03:00
EmpRider
51345bf2e9 fix(cli): handle Windows exe healthchecks with spaces (#3111)
* fix(cli): handle Windows exe healthchecks with spaces

* Update src/shared/services/cliRuntime.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update tests/unit/cli-runtime-extended.test.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix(cli): pass command to spawn unquoted; export shouldUseShellForCommand

Remove the manual "${command}" interpolation into the shell command (hard rule
#13 violation, and redundant — Node quotes for cmd.exe when shell:true; .exe runs
with shell:false where the OS handles spaces via argv). Export the helper and add
a cross-platform test asserting non-Windows never uses the shell.

---------

Co-authored-by: Empire Rider <anuruddhawijesiri@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-03 18:30:03 -03:00
Ahmet Çetinkaya
84b5caeeb9 fix(sse): bound Antigravity 429 retry loop and lock quota-exhausted accounts for full reset window (#3122)
* fix(sse): bound Antigravity short-retry 429 loop per endpoint

A persistent 429 on the short-retry branch (retryAfterMs ≤ 60s) looped
forever on the same endpoint because the branch did `urlIndex--; continue`
without checking the shared retry counter. Production log showed 77
consecutive 429s on one daily endpoint/account with zero fallback.

Gate the short-retry branch on `retryAttemptsByUrl[urlIndex] < MAX_AUTO_RETRIES`
(mirroring the already-bounded sibling), so a persistent 429 retries at most
3× per endpoint across all 3 base URLs then returns the 429 to the account-
fallback layer.

Regression test: 'bounds a persistent short-retry 429' in
tests/unit/executor-antigravity.test.ts — asserts 12 total attempts
(3 endpoints × 4) and a returned 429 with zero hang.

* fix(sse): lock Antigravity quota-exhausted account for full reset window

After the retry-loop bound, OmniRoute fell over to the next account but
re-selected the exhausted one first on every subsequent request (~60s wasted
per request). Root cause: the 429 body 'Individual quota reached. Contact
your administrator to enable overages. Resets in 164h27m24s.' was not
recognized as quota exhaustion, so the model was locked for only ~5s instead
of the real 6.8-day reset window.

Two detector fixes (mirrors Antigravity-Manager rate_limit.rs set_lockout_until):

1. classify429.ts — add QUOTA_PATTERNS: /individual quota reached/i,
   /quota reached/i, /enable overages/i so looksLikeQuotaExhausted() fires.
2. accountFallback.ts — same patterns in classifyErrorText(); extend
   parseRetryFromErrorText() to parse 'Resets? in XhYmZs' (reusing the
   existing computeDurationMs helper) so the exact reset duration reaches
   recordModelLockoutFailure as exactCooldownMs (uncapped, per user choice).

The lockout machinery already stores until = now + cooldownMs with no clamp,
bypasses getScaledCooldown when exactCooldownMs > 0, and keeps the longer of
existing/new, so the full 164h window flows through intact.

New patterns stay specific — plain 'too many requests'/'rate limit exceeded'
messages still classify as rate_limit.

Verified end-to-end against the real message:
- classify429 → quota_exhausted
- parseRetryFromErrorText → 592044000 ms (164h27m24s exactly)
- checkFallbackError → usedUpstreamRetryHint: true, cooldownMs: 592044000

* refactor(account-fallback): simplify error parsing and add cooldown safety

- Implement a 30-day cap on parsed retry durations to prevent indefinite account lockouts.
- Replace manual string matching with `looksLikeQuotaExhausted` for more robust quota detection.
- Streamline regex logic in `parseRetryFromErrorText` for better readability.
- Add unit tests for extreme cooldown values and free-tier exhaustion scenarios.

* fix(429): drop over-broad /quota reached/ pattern, keep specific matches

The bare /quota reached/ would also flag transient per-minute limits like
'request quota reached, retry in 60s' as quota_exhausted (multi-hour lock).
The Antigravity message is still caught by /individual quota reached/. Added a
regression assertion proving the transient case stays a rate_limit.

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-03 18:29:59 -03:00
‍juandisay
656e73e1f0 Remove duplicate lowercase db-apikeys-crud.test.ts from tracking (#3125)
* remove duplicate lowercase db-apikeys-crud.test.ts from tracking

* Changed redirecURI for google OAuth

* revert: keep Google OAuth redirect on 127.0.0.1 (out-of-scope change)

This hotfix's purpose is removing the duplicate lowercase db-apikeys-crud.test.ts.
The 127.0.0.1 -> localhost OAuth redirect change is unrelated and reverses a
documented decision (Google native-app handoff prefers loopback IP; localhost can
resolve to ::1 and hit firewall/name-resolution edge cases). Keeping only the
test-file removal.

---------

Co-authored-by: juandisay <juandisay@example.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-03 18:29:55 -03:00
NMI
27b822e412 fix(tools): keep opaque object schemas open (#3097)
* fix(tools): keep opaque object schemas open

* test: align opaque-object-schema expectations with additionalProperties:true

The opaque-schema fix intentionally injects additionalProperties:true on empty
object schemas (incl. the web_search passthrough shim and null/missing parameter
fallbacks). Update the pre-fix snapshot assertions to match the new behavior.

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-03 18:29:31 -03:00
Diego Rodrigues de Sa e Souza
50896699d3 fix+feat(sse): per-model 403 lockout (#3027) + deepseek-web memory (#2942) & tool-calls (#2820) (#3101)
* fix(sse): per-model 403 on passthrough providers locks the model, not the connection (#3027)

A per-model subscription 403 from a passthrough / per-model-quota provider
(e.g. ollama-cloud "this model requires a subscription, upgrade for access" on
deepseek-v4-pro) cooled down the ENTIRE connection instead of locking out only
the paid model, knocking out the free models on the same key and escalating an
exponential connection-wide backoff on repeats.

markAccountUnavailable's per-model lockout gate only covered 404/429/>=500, and
the only 403 -> model-lockout special case was hard-coded to grok-web. Generalize
it: a model-scoped 403 on an isPerModelQuotaProvider becomes a model lockout
(connection stays active). Terminal whole-key 403s (permanent/banned, account
deactivated, credits exhausted, project-route) keep their connection-level path.

Test-first: reproduction (paid model locked, connection active, free model still
eligible), regression guard (deactivated key still terminal -> banned, not
downgraded), and backoff guard (repeated 403s do not escalate connection backoff).

* feat(sse): persistent session + rolling-window memory for deepseek-web (#2942)

The DeepSeek web API takes only a single `prompt` string (no messages array) and
the executor created a fresh chat session per request, deleting it afterward — so
agentic multi-turn clients got per-turn amnesia.

Add two opt-in, per-connection settings (providerSpecificData), both defaulting to
the legacy behavior so plain-chat users are unaffected:

- historyWindow (number, default 0): when > 0, messagesToPrompt stitches the last N
  non-system turns into a role-tagged transcript ("User:"/"Assistant:") so context
  carries across turns within the single prompt string.
- persistSession (bool, default false): reuse one upstream chat session per userToken
  (cached in the existing sessionCache) instead of creating/deleting one per request.
  A reused session that fails is treated as stale (deleted in the DeepSeek UI): the
  cache entry is dropped, a fresh session is created, and the completion is retried
  once. Error paths invalidate the cached session so the next turn self-heals.

Test-first: pure prompt-builder window semantics (legacy/window/cap/empty) and
execute()-level session behavior via mocked fetch (fresh-per-request default, reuse,
stale-session fresh retry, history threaded into the prompt). Full existing
deepseek-web suite (35 tests) still green.

Note: chat.deepseek.com is an unofficial reverse-engineered surface; the live
round-trip can't be exercised in CI (no userToken/session). Treated as best-effort
per the issue; the prompt/session logic is unit-tested in isolation.

* feat(sse): tool-call translation for deepseek-web (#2820)

deepseek-web previously hard-failed any request carrying tools[] with a 400 (#2848),
so agentic clients could not use it for function calling at all. Add a bidirectional
translation layer (new open-sse/translator/webTools.ts, reusable by the other
web-cookie executors):

- Request: serializeToolsToPrompt() turns the OpenAI tools[] into a <tool>{...}</tool>
  prompt contract, injected as a leading system message.
- Response: parseToolCallsFromText() extracts the model's <tool> blocks into OpenAI
  tool_calls (arguments as a JSON string), strips them from content, and the executor
  emits finish_reason "tool_calls" — for both non-stream and stream clients (the reply
  is buffered since a tool block must be parsed whole).

A plain reply (no <tool> block) still streams normally with finish_reason "stop".
The superseded #2848 400-contract test is updated to assert the new translation.

Test-first: pure serializer/parser units + execute() round-trip via mocked fetch
(no-400, prompt serialization, non-stream tool_calls, stream tool_calls, plain reply).

Note: chat.deepseek.com is an unofficial reverse-engineered surface and the exact
<tool> emission depends on the model following the injected contract; the live
round-trip can't be exercised in CI. Best-effort per the issue; translation logic is
unit-tested in isolation.

* fix(sse): drop duplicate per-model 403 block — already in release via #3096

The release branch already scopes per-model 403 to model lockout (PR #3096,
commit 7042d562c) with the canonical !terminalStatus guard + exactCooldownMs
upstream hint. This PR's separate isTerminalOrRoute403 block shadowed it and
omitted the retry hint. Keep only the deepseek-web (#2942) + webTools (#2820)
changes here; the #3027 regression test is retained as coverage for #3096.
2026-06-03 18:29:26 -03:00
payne
0594af6a6c feat(cursor): vision (image_url) input + tool-commit/output-constraint enhancements (#3104)
* feat(cursor): vision (image_url) input + tool-commit/output-constraint enhancements

Add image/vision input to the Cursor provider's agent.v1 endpoint, plus the
supporting prompt-engineering and resilience work developed alongside it.

Vision input
- Decode OpenAI `image_url` parts (base64 `data:` URIs and remote `http(s)` URLs)
  and inline them as `SelectedContext.selected_images[]` — field numbers pinned
  from the cursor-agent agent.v1 protobuf descriptor (SelectedImage.data oneof,
  uuid, optional Dimension, mime_type). Cross-checked against composer-api's shape.
- New `resolveCursorImages` helper: SSRF-guarded remote fetches via the repo's
  canonical `parseAndValidatePublicUrl` (always public-only for client URLs),
  <=1 MiB per image (pre-decode + streaming cap), `image/*` enforced, max 12
  images, sanitized `CursorImageError` (no stack/path leakage).
- `openai-to-cursor` translator now preserves `image_url` parts instead of
  dropping them; executor `buildRequest` resolves images and attaches them to
  the user turn. The no-image path is byte-identical to before (test-asserted).

Supporting cursor enhancements
- Tool-commit directive (raises composer-2.5 tool-call rate ~53% -> ~88%),
  `tool_choice` none/required/specific handling, and output constraints
  (`response_format` / `max_tokens` / `stop` surfaced as prompt instructions).
- `cursorSessionManager`: clear pending tool-call mappings on session close.
- `cursorVersionDetector`: export `FALLBACK_VERSION` as a single source of truth.

Tests & docs
- New unit suite for the image encoder + resolver (field layout, byte-identical
  no-image path, SSRF / oversize / bad-base64 / too-many rejections, sanitized
  error body), translator image-preservation tests, and live e2e tests
  (base64 + remote URL, gated on `CURSOR_E2E_TOKEN`).
- Documented `CURSOR_TOOL_DIRECTIVE` and `CURSOR_IMAGE_FETCH_TIMEOUT_MS` in
  `.env.example` and `docs/reference/ENVIRONMENT.md`.

* fix(cursor): address review — redirect SSRF, large-payload guard, stream OOM, case/NaN nits

Resolves the gemini-code-assist review on #3104:
- SSRF via redirect (critical): fetchImageBytes now uses redirect:"manual" and
  re-validates every hop through parseAndValidatePublicUrl, so a public URL can't
  30x-redirect to a private/link-local address. Bounded to 3 redirects.
- Large data URL (high): reject on raw payload length before the whitespace-strip
  regex, so an oversized data URL can't burn CPU.
- Stream read (high): readCapped consumes the body as an async iterable (Node
  Readable + Web Streams) or via getReader, capping mid-read; uncapped
  arrayBuffer() is only a last resort.
- data: scheme (medium): match case-insensitively (RFC 2397) while preserving the
  original payload.
- NaN timeouts (medium): CURSOR_IMAGE_FETCH_TIMEOUT_MS and CURSOR_STREAM_TIMEOUT_MS
  fall back to defaults when the env value isn't a positive integer.

Adds tests: redirect-to-private blocked, redirect-to-public followed, too-many-
redirects rejected, uppercase DATA: accepted.

* fix(cursor): defend image fetch against DNS-rebinding SSRF

Address the @codex review on #3104: parseAndValidatePublicUrl only checks the
hostname string, so a public-looking host that (re)resolves to a private /
link-local / metadata IP would still be fetched. Each hop now resolves the host
via dns.lookup({all:true}) and rejects if ANY answer is private (isPrivateHost),
before connecting. IP literals are skipped (already validated by the URL guard).

This narrows but doesn't fully close the TOCTOU window vs fetch's own
resolution; a connection-time IP filter on the shared outbound guard would
close it for every caller. Adds unit tests for the IP gate and a mocked
DNS-rebinding case (public host -> 127.0.0.1, fetch never reached).
2026-06-03 18:24:41 -03:00
Max Garmash
0331e8126d fix: add AbortController timeout to fetchImageEndpoint (#3105)
* fix: add AbortController timeout to fetchImageEndpoint

fetchImageEndpoint uses raw fetch() without timeout control.
Long-running image generation requests (~20-30s) are killed
by Next.js default timeout, producing upstream_error responses.

Replace fetch() with fetchWithTimeout() from shared utils,
defaulting to FETCH_TIMEOUT_MS (120s via OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS).

* fix: return 504 for image fetch timeout, add tests

Address review feedback from gemini-code-assist:
- Import FetchTimeoutError and catch it in fetchImageEndpoint
- Return 504 (Gateway Timeout) instead of 502 for timeout/AbortError
- Add 3 focused unit tests for timeout, non-timeout, and success paths

Clarifies timeout semantics: timeout is a gateway timeout (504), not
a bad gateway (502). Non-timeout fetch errors remain 502.

---------

Co-authored-by: mgarmash <mgarmash@37bytes.com>
2026-06-03 18:19:49 -03:00
Aoxiong Yin
261a910820 fix(codex): preserve native Responses passthrough tools and history (#3107)
* fix(codex): preserve tool_search hosted tool

* fix(codex): preserve native custom tools

* fix(codex): preserve native assistant commentary history
2026-06-03 18:12:54 -03:00
Diego Rodrigues de Sa e Souza
ed170229e7 fix(responses): resolve bare ChatGPT model ids to codex on HTTP fallback path (#3113)
When the Codex CLI falls back from WebSocket to HTTP (after 1008 Policy
Violation or reconnect exhaustion), it POSTs to /v1/responses with the
bare model id it was configured with (e.g. "gpt-5.5") — never the
provider-prefixed form. OmniRoute's normal routing resolved that bare id
to openrouter, not codex, producing:

  "No credentials for provider: openrouter"

Fix: add resolveResponsesApiModel() (extending modelResolution.ts) and
call it in /v1/responses before delegating to handleChat. The function
applies the same codex-preference logic as resolveCodexWsModelInfo:

  bare "gpt-5.5" → codex/gpt-5.5 has a codex provider → rewrite 
  bare "gpt-4o"  → codex/gpt-4o not in registry      → pass through 
  "anthropic/x"  → has "/" → skip resolution           → pass through 

Errors are caught; original request is returned on any failure.
8 unit tests added (TDD — watched each fail before implementing).

Refs: openai/codex#15492, openai/codex#13041, openai/codex#13039
2026-06-03 18:12:34 -03:00
Diego Rodrigues de Sa e Souza
c9620eb741 chore(build): re-apply build-reorg follow-ups (compat fallback + deploy docs) (#3127)
* build(compat): serve CLI falls back dist/ -> app/ for upgrade safety

Backward-compat hardening: the npm CLI prefers the new dist/ standalone but falls
back to the legacy app/ location, so an upgrade over a partially-replaced install
(or a package built before the app/->dist/ rename) still boots. Deployed runtimes
(VPS app/, Docker /app, Electron resources/app) are unchanged by design.

* docs(deploy): stop pm2 before rsync --delete to avoid transient chunk error

rsync --delete removing chunk files under a live server produced the transient [Shutdown] Cannot find module ./chunks/NNNNN.js. Stop the process first, then start with a clean module graph.

* test(cli): cover serve APP_DIR dist/->app/ backward-compat fallback
2026-06-03 18:12:30 -03:00
diegosouzapw
0231fbb335 build(verify): close ralph-loop-1 gaps — align tests + fix stale .next/app refs
- tests/unit/build-next-isolated.test.ts: align to .build/next distDir + removed
  app-snapshot transient entry (was RED 4/7 -> 7/7); legitimate alignment to the
  intentional Layer-1 behavior change, not masking.
- scripts/dev/run-next-playwright.mjs: testDistDir() default .next -> .build/next
  (E2E start runner found the standalone at the wrong path).
- prepublish.ts: stale log 'app/docs/' -> 'dist/docs/'.
- electron/README.md: '.next/standalone' -> '.build/next/standalone'.
- remove dead scripts/build/paths.mjs (created in L1, imported by nothing).
Verified: build-next-isolated 7/7, run-next-playwright 3/3, assemble 1/1, lint clean.
2026-06-03 16:06:59 -03:00
diegosouzapw
e390a8d633 build(layer2+3): propagate .build/+dist/ to Docker/Electron/CI; codify light deploy; docs
Layer 3: Dockerfile COPY .next/standalone -> .build/next/standalone (+cache mount);
electron stage -> .build/electron-standalone + extraResources; CI asserts dist/server.js;
eslint ignores .build/**+dist/**. Layer 2: deploy-vps-* skills use build:release + rsync(dist)
-> remote app/ + pm2 restart + BUILD_SHA verify (drop npm-i-g/legacy-peer-deps/manual-wreq).
Docs (RELEASE_CHECKLIST/CONTRIBUTING/AGENTS/CHANGELOG) describe src/+.build/+dist/ layout.
Verified: docker build exit 0 + health 200; electron stage OK; no stale build-output refs.
2026-06-03 15:51:26 -03:00
diegosouzapw
5efeeb183f build(layer1): add build:release clean rebuild + HEAD sentinel guard
- package.json: add "build:release" script that cleans .build/ + dist/,
  passes OMNIROUTE_BUILD_SHA env through the build, runs build + build:cli,
  then writes the HEAD sentinel
- scripts/build/write-build-sha.mjs: writes dist/BUILD_SHA and
  .build/next/standalone/BUILD_SHA; exits 1 if standalone dir is missing
  (guards against stale-cache shipping)
- scripts/build/pack-artifact-policy.ts: allow "BUILD_SHA" in staging exact paths
  and add it to the allowed set

Smoke: npm run build:release completes successfully; cat dist/BUILD_SHA ==
git rev-parse --short HEAD (BUILD_SHA_MATCH_OK)
2026-06-03 15:16:28 -03:00
diegosouzapw
b7fdcdddf8 build(layer1): rename standalone output app/ -> dist/; delete both App-Router move hacks
- scripts/build/prepublish.ts: APP_DIR -> DIST_DIR; remove Step 1 and Step 2.5
  hack blocks; fix MCP esbuild outfile (app/ -> dist/); update all log messages
- scripts/build/build-next-isolated.mjs: remove legacy-app-snapshot entry from
  getTransientBuildPaths() (App-Router collision hack deleted)
- scripts/build/assembleStandalone.mjs: fix standalone package.json after copy —
  removes "type":"module" so Next.js standalone server.js (CJS) loads correctly;
  also adds .build/next/ to allowed staging prefixes so server bundles are kept
- scripts/build/pack-artifact-policy.ts: app/ -> dist/ in all PACK_ARTIFACT_* paths;
  add ".build/next/" to APP_STAGING_ALLOWED_PATH_PREFIXES (Layer 1 distDir change)
- scripts/build/validate-pack-artifact.ts: dist/ check instead of app/
- scripts/build/postinstall.mjs: all app/ paths -> dist/
- scripts/build/postinstallSupport.mjs: hasStandaloneAppBundle checks dist/server.js
- bin/cli/commands/serve.mjs: APP_DIR -> dist/
- package.json: files[] "app/" -> "dist/"
- .gitignore: remove both /app and /app/ entries (no longer needed)

Smoke: NO_APP_DIR_OK; DIST_OK; check:pack-artifact PASS; health 200 from dist/
2026-06-03 14:59:20 -03:00
diegosouzapw
5b484737bb build(layer1): isolate Next output to .build/next; gitignore .build/ dist/ .next/
- next.config.mjs: distDir default ".next" → ".build/next" (NEXT_DIST_DIR override kept)
- .gitignore: add /.build/, /dist/, /.next/; remove loose dist/ under #dependencies
- scripts/build/paths.mjs: new shared module exporting ROOT, DIST_DIR, STANDALONE_DIR
- scripts/build/build-next-isolated.mjs: default ".next" → ".build/next" in distDir,
  resetStandaloneOutput fallback, and pruneStandaloneArtifacts
- scripts/build/prepublish.ts: NEXT_DIST default ".next" → ".build/next"
- scripts/build/assembleStandalone.mjs: legacy syncStandalone* helpers updated
  to resolve distDir via NEXT_DIST_DIR || ".build/next"
- tsconfig.json: Next.js auto-added .build/next/types includes (generated on build)

Smoke: .build/next/standalone/server.js exists; BUNDLE_OK confirmed;
no .next directory created.
2026-06-03 14:04:30 -03:00
diegosouzapw
03b8aa1f6d build(layer0): electron prepare uses assembleStandalone (keep ABI native-strip)
prepare-electron-standalone.mjs delegates standalone+static+public copy and abs-path
sanitization to assembleStandalone(); keeps the electron-unique steps (nested-bundle
resolution, symlink guard, dist-electron strip, and the better-sqlite3+keytar native
strip for electron-builder ABI rebuild). Smoke: stage has server.js/static/public,
better-sqlite3+keytar stripped, wreq-js+@swc/helpers retained. -93/+43.
2026-06-03 13:46:37 -03:00
diegosouzapw
27c08178d7 build(layer0): prepublish consumes single build via assembleStandalone (drop 2nd next build)
prepublish.ts no longer runs npm install + a second full 'next build'; it asserts
the .next/standalone produced by 'npm run build' exists (builds once if missing) and
assembles the npm staging app/ via assembleStandalone({sanitizePaths,patchTurbopackChunks}).
All npm-unique steps kept (MITM tsc, MCP/CLI esbuild, doc/sidecar copies, mkdir data,
prune+validate). A publish now runs exactly ONE next build (was 2). Verified: 1 build,
check:pack-artifact green (6467 entries), npm pack -> fresh install -> boot -> health 200.
-211/+40 lines.
2026-06-03 13:46:37 -03:00
diegosouzapw
05c6335292 build(layer0): unify standalone assembly into assembleStandalone.mjs
Extract the shared copy/sync/sanitize logic (native assets, extra modules,
static/public, optional path-sanitize + turbopack-chunk-patch) from the three
divergent assembly scripts into one module. Wire build-next-isolated.mjs to call
it (in-place .next/standalone). Output is byte-identical to before — pure refactor.
Golden test + existing build-next-isolated tests (7/7) green; standalone retains
all natives + sidecars.
2026-06-03 13:46:37 -03:00
diegosouzapw
277f530f0e Remove obsolete files: deleted unused test-debug.ts, .semgrep rules, documentation images, and various markdown files related to skill generation and audit reports. 2026-06-03 12:49:08 -03:00
diegosouzapw
aa647768c4 docs(claude): add bug fix validation protocol + both test runners rule
Hard Rule #18: every issue fix must have TDD (failing test → pass)
or a documented live VPS test (192.168.0.15) when TDD is not possible.
Also clarifies that test:unit + test:vitest must both pass (non-overlapping
coverage).
2026-06-03 12:33:33 -03:00
Diego Rodrigues de Sa e Souza
d5f2586513 fix(sse): emit reasoning/content as separate SSE deltas to avoid duplication (#3089 follow-up) (#3112)
synthesizeOpenAiSseFromJson combined role+content+reasoning_content in one delta, which the openai→openai translator re-split, duplicating reasoning_content across chunks. Now emits role, reasoning_content, content and tool_calls as separate sequential deltas (reasoning before content) — the shape a real reasoning model streams — so the translator passes them through with no duplication. Tests assert each field appears exactly once and reasoning precedes content.
2026-06-03 12:08:43 -03:00
Diego Rodrigues de Sa e Souza
b57afb5bbe fix(sse): handle non-SSE JSON upstream body on streaming path + SSE-wrap cache hits (#3089, #2952) (#3108)
#3089: reasoning openai-compatible upstreams that ignore stream:true and return application/json produced STREAM_EARLY_EOF because readiness only scans SSE data: frames. chatCore now detects a non-SSE JSON upstream body on the streaming path and synthesizes an equivalent OpenAI SSE stream (new synthesizeOpenAiSseFromJson util), preserving content + reasoning_content. #2952: semantic-cache hits returned application/json regardless of stream flag, so streaming clients lost reasoning_content; stream requests now SSE-wrap the cached completion via the same helper. Unit tests for the converter (4).
2026-06-03 10:23:00 -03:00
Diego Rodrigues de Sa e Souza
f0f776c310 fix(i18n): fill missing zh-CN and ru UI translations (#3026, #3067) (#3103)
zh-CN and ru were each missing 9 whole sections (~823 keys: quotaPlans, activity, agentBridge, trafficInspector, cliCommon, cliCode, cliAgents, acpAgents, agentSkills) added after the last sweep, so those UI strings fell back to English. Filled via the canonical i18n sync+translate pipeline (scripts/i18n/sync-ui-keys.mjs --translate-markers). Both catalogs are now at full key parity with en.json (70 sections / 8025 keys), 0 __MISSING__ markers, valid JSON, Prettier-clean.
2026-06-03 08:04:17 -03:00
Diego Rodrigues de Sa e Souza
5f7f74dc6a fix(dashboard): qualify vendor-namespaced Playground models with provider prefix (#3050) (#3102)
The provider Playground (LlmChatCard) only added the providerId/ prefix to models without a slash, so vendor-namespaced ids (moonshotai/kimi-k2.6, nvidia/zyphra/...) were sent bare and rejected with 'Ambiguous model' when the same id exists under multiple providers. Extracted qualifyPlaygroundModel() which always prefixes with the provider unless already qualified. Bug 1 ('unhashable type: dict') is an upstream NVIDIA NIM server error, not OmniRoute. Tests: 4 cases for the qualifier.
2026-06-03 07:56:16 -03:00
Diego Rodrigues de Sa e Souza
5f3b1e8cde fix(db): dedup duplicate API keys per provider on connection create (#3023) (#3100)
createProviderConnection deduped apikey connections only by (provider, name). Adding the same key under a different/blank name created a duplicate row. It now also matches by the decrypted key value (AES-GCM ciphertext is non-deterministic, so we decrypt+compare plaintext, trimmed) and updates the existing connection instead. Tests cover same-key dedup, whitespace-variant dedup, and distinct-key separation.
2026-06-03 07:52:10 -03:00
Diego Rodrigues de Sa e Souza
8a25d9e229 fix(dashboard): make 'Import from /models' work for no-auth providers (#3047) (#3099)
No-auth providers (OpenCode Free) have no connection row, so handleImportModels returned early and /api/providers/[id]/models 404'd — the import button silently no-op'd. The route now serves the provider's registry/static model catalog when called with a no-auth provider id, and handleImportModels falls back to the provider id when there is no connection. Test: route returns the opencode catalog (source local_catalog) and still 404s for unknown ids.
2026-06-03 07:48:19 -03:00
Diego Rodrigues de Sa e Souza
365c29a115 fix(providers): forward Grok sso-rw cookie to fix anti-bot 403 (#3063) (#3098)
grok-web only sent the 'sso' cookie; Grok's anti-bot now rejects (403 code 7) requests missing the paired 'sso-rw' write cookie. Adds buildGrokCookieHeader() which emits sso plus sso-rw when the pasted blob carries it (never a phantom sso-rw from a bare value), used by both the executor and the connection validator. Updates the grok-web authHint. Tests: 6 new buildGrokCookieHeader cases; grok-web 62/62 unchanged.
2026-06-03 07:39:02 -03:00
Diego Rodrigues de Sa e Souza
7042d562c4 fix(sse): scope ollama-cloud per-model 403 to model lockout, not connection cooldown (#3027) (#3096)
A per-model subscription/permission 403 from a passthrough provider (hasPerModelQuota) now locks only the failing model instead of cooling the whole connection, so free models on the same key keep serving and repeated paid-model 403s don't escalate a connection-wide backoff. Generalizes the grok-web 403 precedent; terminal/credential 403s still deactivate the connection (guarded by resolveTerminalConnectionStatus). TDD: 3 tests in sse-auth.test.ts (2 reproductions fail pre-fix, regression guard passes both ways).
2026-06-03 07:25:50 -03:00
diegosouzapw
470df2df77 docs(changelog): add SiliconFlow fix entry (#3094) 2026-06-03 07:21:36 -03:00
Xiangzhe
948f232517 fix: sync SiliconFlow models from configured endpoint (#3094)
Integrated into release/v3.8.9. SiliconFlow tests pass.
2026-06-03 07:19:38 -03:00
diegosouzapw
42821ee620 test: update web-session-credentials to reflect veoaifree-web NOAUTH reclassification
veoaifree-web was moved from WEB_COOKIE_PROVIDERS to NOAUTH_PROVIDERS
in PR #3090 — it no longer appears in WEB_SESSION_CREDENTIAL_REQUIREMENTS.
2026-06-03 07:11:20 -03:00
diegosouzapw
51d2ca8151 feat(db): add apiKeyContextSources module + migration 092
Implements per-API-key context source configuration table and CRUD
functions required by the Obsidian PR. Restores getApiKeyContextSource
in obsidian.ts (was stubbed to null during conflict resolution).
11/11 tests pass in obsidian-config.test.ts.
2026-06-03 07:04:52 -03:00
diegosouzapw
a296c34a95 docs(changelog): add v3.8.9 entries for merged PRs 2026-06-03 07:00:47 -03:00
Diego Rodrigues de Sa e Souza
28116c71f8 fix(cache): preserve client cache_control for Xiaomi MiMo (#3088) (#3093)
Add xiaomi-mimo to the prompt-caching provider allowlist so Claude Code (via cc-switch) cache_control breakpoints are preserved instead of stripped by the OpenAI-format translator. Restores cache hits that worked when calling Xiaomi directly.
2026-06-03 06:55:28 -03:00
diegosouzapw
3c8646a400 fix(mcp): remove duplicate notionTools registration introduced by merge
Obsidian PR added a second notionTools.forEach block; release branch
already had the first one. Removed the duplicate (second occurrence).
2026-06-03 06:54:41 -03:00
dependabot[bot]
e6db182dcf deps: bump the production group with 21 updates (#3085)
Integrated into release/v3.8.9. 21 production dependencies updated. Typecheck clean.
2026-06-03 06:47:38 -03:00
dependabot[bot]
c4fa7add1b deps: bump the development group with 5 updates (#3086)
Integrated into release/v3.8.9. Reverted concurrently from 10.0.3 to 9.2.1 (v10 requires Node >=22, project supports Node 20). Other 4 updates applied: eslint-config-next 16.2.7, lint-staged 17.0.7, typescript-eslint 8.60.1, vitest 4.1.8.
2026-06-03 06:46:16 -03:00
Brandon Bennett
9db306e2f8 feat(observability): add Obsidian context source with 24 MCP tools (#3077)
Integrated into release/v3.8.9. Fixes applied: removed .serena/ from repo (gitignored), resolved import conflicts preserving agentSkillTools, fixed TS errors.
2026-06-03 06:45:04 -03:00
diegosouzapw
e7f064d916 deps(electron): bump electron-builder to 26.14.0
Applies dependabot PR #3082. Security hardening in auto-update flow,
pure-JS migration for blockmap/icon commands. electron 42.3.2 and
electron-updater 6.8.8 were already in the release branch.
2026-06-03 06:39:04 -03:00
dependabot[bot]
8a5feacc88 deps: bump electron from 42.2.0 to 42.3.2 in /electron (#3083)
Integrated into release/v3.8.9. electron 42.3.2 patch: crash fix + performance improvements.
2026-06-03 06:37:05 -03:00
dependabot[bot]
6999566ce1 deps: bump electron-updater from 6.8.6 to 6.8.8 in /electron (#3084)
Integrated into release/v3.8.9. electron-updater security patch: harden auto-update flow against path traversal and env var intercepts.
2026-06-03 06:37:02 -03:00
Paijo
43b1392876 fix(autoCombo): rotate across all connections, never waste provider capacity (#3078)
Integrated into release/v3.8.9. Clean merge, all 146 vitest tests pass.
2026-06-03 06:36:47 -03:00
Paijo
9141e98458 fix(providers): fix claude-web 403, move no-auth providers out of web-cookie (#3090)
Integrated into release/v3.8.9 — resolved conflicts with release branch (allowAutoSolve:true preserved, duckduckgo-web correctly kept in NOAUTH_PROVIDERS).
2026-06-03 06:35:45 -03:00
diegosouzapw
c42591f400 chore(release): open v3.8.9 development cycle
Bump 3.8.8 → 3.8.9 across package.json, lockfile, electron, open-sse, and
docs/reference/openapi.yaml; add the [3.8.9] CHANGELOG section (root + 40 i18n
mirrors) as the integration target for the cycle. Entries land here as work
merges into release/v3.8.9; finalized by the release flow.
2026-06-03 06:31:51 -03:00
Diego Rodrigues de Sa e Souza
d37693de98 Merge release/v3.8.8 into main (#2930)
Release/v3.8.8
2026-06-03 02:19:13 -03:00
2787 changed files with 306933 additions and 67566 deletions

View File

@@ -1,52 +0,0 @@
---
name: capture-release-evidences-ag
description: Automatically run a browser-automation agent to visually validate all new UI features from the current release and capture evidence WebP recordings of the changes.
---
# Capture Release Evidences Workflow
Use this workflow to automatically drive a browser-automation agent to explore the newly deployed or locally running application and record evidence of the UI changes introduced in the latest release.
> **Tool mapping note (v3.8):** The `browser_subagent` tool referenced below is specific to an earlier agent runtime. In Claude Code, substitute with the available browser MCP tools (e.g. `mcp__claude-in-chrome__*`) for navigation/screenshots, plus the `Write` tool for saving artifacts. The high-level steps remain the same regardless of the browser-automation surface in use.
## Prerequisites
- OmniRoute must be actively running and accessible (e.g. locally at `http://localhost:20128` or on the Local VPS at `http://192.168.0.15:20128`).
- The user must provide the target URL to be tested, or default to `http://192.168.0.15:20128`.
## Workflow Steps
### 1. Identify Target Features
Review the `CHANGELOG.md` for the latest version to map out the new UI elements. For example:
- **CLI Tools Settings**
- **New Provider/Model Listings (e.g., Gemini 3.1, Qoder PAT)**
- **New Feature Modals**
### 2. Run the Browser Subagent
For each identified feature, invoke the `browser_subagent` using the `default_api:browser_subagent` tool.
**Important Task Guidelines for the Subagent:**
- `TaskName`: Give it a clear name like "Validate CLIProxyAPI Tool Tab".
- `TaskSummary`: "Navigate to the CLI Tools tab and verify the new Integration settings."
- `Task`: Provide unambiguous instructions for the subagent, such as: "Navigate to http://192.168.0.15:20128/dashboard. Click on the 'Settings' or 'CLI Tools' nav link. Scroll down to find the CLIProxyAPI integration card. Hover over it to trigger UI state. Verify the components render correctly and exit."
- `RecordingName`: Ensure it describes the feature (e.g. `v3_4_5_cli_proxy_api`). This is required and strictly automatically saved as a WebP artifacts video by the system.
_(Note: The `browser_subagent` automatically creates a WebP recording named by the `RecordingName` parameter. No additional tools for screenshots are needed.)_
### 3. Generate Report Artifact
After the `browser_subagent` finishes its sessions, generate a final Markdown artifact (using `Write` and `IsArtifact=true`) to present the recordings inline to the user using the `![caption](/absolute/path/to/media.webp)` syntax.
### Example Invocation
\```json
{
"TaskName": "Validating Qoder PAT Configuration UI",
"TaskSummary": "Validates the Qoder provider configuration modal",
"Task": "Go to http://192.168.0.15:20128/dashboard. Click on the 'Providers' tab. Find 'Qoder' in the list. Click 'Add Token' or 'Configure'. Type 'test_token' and submit. Return when done.",
"RecordingName": "qoder_pat_ui_validation"
}
\```

View File

@@ -1,52 +0,0 @@
---
name: capture-release-evidences-cc
description: Automatically run a browser-automation agent to visually validate all new UI features from the current release and capture evidence WebP recordings of the changes.
---
# Capture Release Evidences Workflow
Use this workflow to automatically drive a browser-automation agent to explore the newly deployed or locally running application and record evidence of the UI changes introduced in the latest release.
> **Tool mapping note (v3.8):** This workflow references a `browser_subagent` tool that was specific to an earlier agent runtime. In Claude Code, substitute with the available browser MCP tools (e.g. `mcp__claude-in-chrome__*`) for navigation/screenshots, plus the `Write` tool for saving artifacts. The high-level steps below remain the same regardless of which browser-automation surface is used.
## Prerequisites
- OmniRoute must be actively running and accessible (e.g. locally at `http://localhost:20128` or on the Local VPS at `http://192.168.0.15:20128`).
- The user must provide the target URL to be tested, or default to `http://192.168.0.15:20128`.
## Workflow Steps
### 1. Identify Target Features
Review the `CHANGELOG.md` for the latest version to map out the new UI elements. For example:
- **CLI Tools Settings**
- **New Provider/Model Listings (e.g., Gemini 3.1, Qoder PAT)**
- **New Feature Modals**
### 2. Run the Browser Subagent
For each identified feature, invoke the `browser_subagent` using the `default_api:browser_subagent` tool.
**Important Task Guidelines for the Subagent:**
- `TaskName`: Give it a clear name like "Validate CLIProxyAPI Tool Tab".
- `TaskSummary`: "Navigate to the CLI Tools tab and verify the new Integration settings."
- `Task`: Provide unambiguous instructions for the subagent, such as: "Navigate to http://192.168.0.15:20128/dashboard. Click on the 'Settings' or 'CLI Tools' nav link. Scroll down to find the CLIProxyAPI integration card. Hover over it to trigger UI state. Verify the components render correctly and exit."
- `RecordingName`: Ensure it describes the feature (e.g. `v3_4_5_cli_proxy_api`). This is required and strictly automatically saved as a WebP artifacts video by the system.
_(Note: The `browser_subagent` automatically creates a WebP recording named by the `RecordingName` parameter. No additional tools for screenshots are needed.)_
### 3. Generate Report Artifact
After the `browser_subagent` finishes its sessions, generate a final Markdown artifact (using `Write` and `IsArtifact=true`) to present the recordings inline to the user using the `![caption](/absolute/path/to/media.webp)` syntax.
### Example Invocation
\```json
{
"TaskName": "Validating Qoder PAT Configuration UI",
"TaskSummary": "Validates the Qoder provider configuration modal",
"Task": "Go to http://192.168.0.15:20128/dashboard. Click on the 'Providers' tab. Find 'Qoder' in the list. Click 'Add Token' or 'Configure'. Type 'test_token' and submit. Return when done.",
"RecordingName": "qoder_pat_ui_validation"
}
\```

View File

@@ -1,52 +0,0 @@
---
name: capture-release-evidences-cx
description: Automatically run a browser-automation agent to visually validate all new UI features from the current release and capture evidence WebP recordings of the changes.
---
# Capture Release Evidences Workflow
Use this workflow to automatically drive a browser-automation agent to explore the newly deployed or locally running application and record evidence of the UI changes introduced in the latest release.
> **Tool mapping note (v3.8):** The `browser_subagent` tool referenced below is specific to an earlier agent runtime. In Claude Code, substitute with the available browser MCP tools (e.g. `mcp__claude-in-chrome__*`) for navigation/screenshots, plus the `Write` tool for saving artifacts. The high-level steps remain the same regardless of the browser-automation surface in use.
## Prerequisites
- OmniRoute must be actively running and accessible (e.g. locally at `http://localhost:20128` or on the Local VPS at `http://192.168.0.15:20128`).
- The user must provide the target URL to be tested, or default to `http://192.168.0.15:20128`.
## Workflow Steps
### 1. Identify Target Features
Review the `CHANGELOG.md` for the latest version to map out the new UI elements. For example:
- **CLI Tools Settings**
- **New Provider/Model Listings (e.g., Gemini 3.1, Qoder PAT)**
- **New Feature Modals**
### 2. Run the Browser Subagent
For each identified feature, invoke the `browser_subagent` using the `default_api:browser_subagent` tool.
**Important Task Guidelines for the Subagent:**
- `TaskName`: Give it a clear name like "Validate CLIProxyAPI Tool Tab".
- `TaskSummary`: "Navigate to the CLI Tools tab and verify the new Integration settings."
- `Task`: Provide unambiguous instructions for the subagent, such as: "Navigate to http://192.168.0.15:20128/dashboard. Click on the 'Settings' or 'CLI Tools' nav link. Scroll down to find the CLIProxyAPI integration card. Hover over it to trigger UI state. Verify the components render correctly and exit."
- `RecordingName`: Ensure it describes the feature (e.g. `v3_4_5_cli_proxy_api`). This is required and strictly automatically saved as a WebP artifacts video by the system.
_(Note: The `browser_subagent` automatically creates a WebP recording named by the `RecordingName` parameter. No additional tools for screenshots are needed.)_
### 3. Generate Report Artifact
After the `browser_subagent` finishes its sessions, generate a final Markdown artifact (using `Write` and `IsArtifact=true`) to present the recordings inline to the user using the `![caption](/absolute/path/to/media.webp)` syntax.
### Example Invocation
\```json
{
"TaskName": "Validating Qoder PAT Configuration UI",
"TaskSummary": "Validates the Qoder provider configuration modal",
"Task": "Go to http://192.168.0.15:20128/dashboard. Click on the 'Providers' tab. Find 'Qoder' in the list. Click 'Add Token' or 'Configure'. Type 'test_token' and submit. Return when done.",
"RecordingName": "qoder_pat_ui_validation"
}
\```

View File

@@ -1,40 +0,0 @@
---
name: deploy-vps-akamai-cc
description: Deploy the latest OmniRoute code to the Akamai VPS (69.164.221.35)
---
# Deploy to Akamai VPS Workflow
Deploy OmniRoute to the Akamai VPS using `npm pack + scp` + PM2.
**Akamai VPS:** `69.164.221.35`
**Process manager:** PM2 (`omniroute`)
**Port:** `20128`
## Steps
### 1. Build + pack locally
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
```
### 2. Copy to Akamai VPS and install
// turbo-all
```bash
scp omniroute-*.tgz root@69.164.221.35:/tmp/
```
```bash
ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'"
```
### 3. Verify the deployment
```bash
curl -s -o /dev/null -w 'AKAMAI HTTP %{http_code}\n' http://69.164.221.35:20128/
```

View File

@@ -1,50 +0,0 @@
---
name: deploy-vps-both-cc
description: Deploy the latest OmniRoute code to BOTH the Akamai VPS and the Local VPS
---
# Deploy to VPS (Both) Workflow
Deploy OmniRoute to the production VPSs using `npm pack + scp` + PM2.
**Akamai VPS:** `69.164.221.35`
**Local VPS:** `192.168.0.15`
**Process manager:** PM2 (`omniroute`)
**Port:** `20128`
**PM2 entry:** `/usr/lib/node_modules/omniroute/app/server.js`
> [!IMPORTANT]
> The npm registry rejects packages > 100MB, so deployment uses **npm pack + scp**.
## Steps
### 1. Build + pack locally
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
```
### 2. Copy to both VPS and install
// turbo-all
```bash
scp omniroute-*.tgz root@69.164.221.35:/tmp/ && scp omniroute-*.tgz root@192.168.0.15:/tmp/
```
```bash
ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'"
```
```bash
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
```
### 3. Verify the deployment
```bash
curl -s -o /dev/null -w 'AKAMAI HTTP %{http_code}\n' http://69.164.221.35:20128/
curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/
```

View File

@@ -1,40 +0,0 @@
---
name: deploy-vps-local-ag
description: Deploy the latest OmniRoute code to the Local VPS (192.168.0.15)
---
# Deploy to Local VPS Workflow
Deploy OmniRoute to the Local VPS using `npm pack + scp` + PM2.
**Local VPS:** `192.168.0.15`
**Process manager:** PM2 (`omniroute`)
**Port:** `20128`
## Steps
### 1. Build + pack locally
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
```
### 2. Copy to Local VPS and install
// turbo-all
```bash
scp omniroute-*.tgz root@192.168.0.15:/tmp/
```
```bash
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
```
### 3. Verify the deployment
```bash
curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/
```

View File

@@ -1,40 +0,0 @@
---
name: deploy-vps-local-cc
description: Deploy the latest OmniRoute code to the Local VPS (192.168.0.15)
---
# Deploy to Local VPS Workflow
Deploy OmniRoute to the Local VPS using `npm pack + scp` + PM2.
**Local VPS:** `192.168.0.15`
**Process manager:** PM2 (`omniroute`)
**Port:** `20128`
## Steps
### 1. Build + pack locally
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
```
### 2. Copy to Local VPS and install
// turbo-all
```bash
scp omniroute-*.tgz root@192.168.0.15:/tmp/
```
```bash
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
```
### 3. Verify the deployment
```bash
curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/
```

View File

@@ -1,45 +0,0 @@
---
name: deploy-vps-local-cx
description: Deploy the latest OmniRoute code to the Local VPS (192.168.0.15)
---
# Deploy to Local VPS Workflow
Deploy OmniRoute to the Local VPS using `npm pack + scp` + PM2.
## Codex Execution Notes
- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` only for independent commands. Do not parallelize dependent build, copy, install, restart, and verification steps.
- Report each remote result explicitly before finishing.
**Local VPS:** `192.168.0.15`
**Process manager:** PM2 (`omniroute`)
**Port:** `20128`
## Steps
### 1. Build + pack locally
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
```
### 2. Copy to Local VPS and install
// turbo-all
```bash
scp omniroute-*.tgz root@192.168.0.15:/tmp/
```
```bash
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
```
### 3. Verify the deployment
```bash
curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/
```

View File

@@ -1,427 +0,0 @@
---
name: generate-release-ag
description: Create a new release, bump version up to the .999 patch threshold, generate a complete CHANGELOG (with PR co-authors + every commit since the last tag), and manage Pull Requests
---
# Generate Release Workflow
Bump version, build a **complete CHANGELOG** from every commit since the last tag (with PR back-reference and contributor attribution), commit, open a **PR to main** and wait for user confirmation before tagging, publishing, and deploying.
> **VERSION RULE: Always use PATCH bumps (3.x.y → 3.x.y+1)**
> NEVER use `npm version minor` or `npm version major`.
> Always use: `npm version patch --no-git-tag-version`
> The threshold rule: when `y` reaches 1000, bump to `3.(x+1).0` — e.g. `3.8.999` → `3.9.0`.
> **🔴 INTEGRATION BRANCH RULE**: The `release/vX.Y.Z` branch is the **integration target** for the entire release cycle. Bug fixes and feature implementations land here **via per-issue PRs from short-lived `fix/<ISSUE>-<short>` or `feat/<ISSUE>-<short>` worktrees** (see `/resolve-issues`, `/implement-features`). Contributor PRs from `/review-prs` likewise merge into this branch. The release branch is then merged to `main` via a single release PR at the end of the cycle.
---
## ⚠️ Four-Phase Flow
```
Phase 0 → security audit (npm + CodeQL + Dependabot)
Phase 1 → bump → full quality gate → changelog from commits → commit → push → open PR
↕ 🛑 STOP: notify user, wait for PR merge
Phase 2 → deploy main to Local VPS for homologation
↕ 🛑 STOP: notify user, wait for OK
Phase 3 → tag → GitHub release → Docker → npm → Akamai
Phase 4 → monitor CI pipelines and validate artifacts
```
**NEVER push directly to main or create tags before the user confirms the PR.**
---
## Phase 0: Security Verification (MANDATORY)
```bash
# 1. Local dependency audit
npm audit --production --audit-level=high
# 2. GitHub CodeQL alerts (open + high severity)
gh api '/repos/diegosouzapw/OmniRoute/code-scanning/alerts?state=open&severity=high' \
--jq '.[] | {rule: .rule.id, path: .most_recent_instance.location.path, msg: .most_recent_instance.message.text}' \
2>/dev/null || echo "(no CodeQL access or no alerts)"
# 3. Dependabot alerts (open + high/critical)
gh api '/repos/diegosouzapw/OmniRoute/dependabot/alerts?state=open' \
--jq '.[] | select(.security_advisory.severity == "high" or .security_advisory.severity == "critical") | {pkg: .dependency.package.name, sev: .security_advisory.severity, summary: .security_advisory.summary}' \
2>/dev/null || echo "(no Dependabot access or no alerts)"
```
Fix or justify (per Hard Rule #14) any `high`/`critical` findings before proceeding.
---
## Phase 1: Pre-Merge
### 1. Create or confirm release branch
```bash
# To create a new release branch (MUST always be created from main):
git checkout main
git pull origin main
git checkout -b release/v3.9.0
# If continuing the current cycle, just verify:
git branch --show-current
```
### 2. Determine and sync version
```bash
grep '"version"' package.json
```
> **🔴 BRANCH-VERSION PARITY GATE**:
```bash
BRANCH=$(git branch --show-current)
BRANCH_VER=${BRANCH#release/v}
PKG_VER=$(node -p "require('./package.json').version")
if [[ "$BRANCH" != release/v* ]]; then
echo "❌ Not on a release/v* branch (current: $BRANCH). Aborting."; exit 1
fi
echo "Branch target: $BRANCH_VER"
echo "package.json: $PKG_VER"
```
> **⚠️ ATOMIC COMMIT RULE** — bump and feature/fix code MUST land in the same commit so that `git show vX.Y.Z` always contains both. NEVER commit features first and bump in a separate commit.
```bash
npm version patch --no-git-tag-version
```
### 3. Regenerate lock file (REQUIRED after version bump)
```bash
npm install
```
### 4. Build CHANGELOG from EVERY commit since the last tag
> **🎯 Goal**: produce a complete CHANGELOG section — emoji-grouped sections, PR back-reference, and `— thanks @user` attribution. Nothing must slip through.
> **🔴 NO MIXUPS RULE**: do not mix backlog of the previous version. The new section must contain ONLY commits whose merge/landing happened after the previous tag.
#### 4a. Collect raw commit log since last tag
```bash
LAST_TAG=$(git describe --tags --abbrev=0)
NEW_VERSION=$(node -p "require('./package.json').version")
TODAY=$(date -u +%F)
echo "Range: $LAST_TAG..HEAD → v$NEW_VERSION ($TODAY)"
git log --no-merges "$LAST_TAG..HEAD" --pretty=format:'%h %s' > /tmp/release_commits.txt
wc -l /tmp/release_commits.txt
git log --merges "$LAST_TAG..HEAD" --pretty=format:'%h %s%n author=%an <%ae>' > /tmp/release_merges.txt
git log "$LAST_TAG..HEAD" --pretty=format:'---%n%h | %s%n author=%an <%ae>%n body=%b' > /tmp/release_detailed.txt
```
#### 4b. Enrich with PR metadata + co-authors
```bash
grep -oE '#[0-9]+' /tmp/release_commits.txt | sort -u > /tmp/release_prs.txt
> /tmp/release_pr_meta.json
while read -r PR; do
N=${PR#\#}
gh pr view "$N" --repo diegosouzapw/OmniRoute \
--json number,title,author,mergeCommit,body \
>> /tmp/release_pr_meta.json 2>/dev/null || echo "(skip $PR — not found)"
echo "" >> /tmp/release_pr_meta.json
done < /tmp/release_prs.txt
```
#### 4c. Assemble the new CHANGELOG section
Using `/tmp/release_commits.txt` + `/tmp/release_pr_meta.json` + `/tmp/release_detailed.txt`, build a new entry that:
1. **Covers every commit** — read the full list and group by Conventional Commit type. A commit is "covered" iff it appears (or is intentionally rolled-up) in the new section.
2. **Groups using these section headers**:
- `### ✨ New Features``feat(*)`
- `### 🔧 Bug Fixes``fix(*)`
- `### 📝 Maintenance``chore(*)`, `refactor(*)`, `docs(*)`, `test(*)`, `ci(*)`, `build(*)`
- `### 🔒 Security` — security-flagged commits (only if any)
3. **Entry format**:
```
- **type(scope):** human-friendly description — extra context if useful. ([#PR](https://github.com/diegosouzapw/OmniRoute/pull/PR) — thanks @author / @coauthor1 / @coauthor2)
```
- No PR referenced (direct commit on release branch): `(thanks @author)`.
- PR closed an external contributor's PR via cherry-pick or re-implementation: attribute BOTH (`thanks @originalAuthor / @diegosouzapw`).
- **Co-authors** extracted from merge commit body and from PR participants who supplied commits.
4. **Coverage check** — diff the section against `/tmp/release_commits.txt`. Any unlisted commit must either be explicitly added or consolidated under a roll-up bullet. Do NOT silently drop commits.
Layout in `CHANGELOG.md` (right below `## [Unreleased]`):
```markdown
## [Unreleased]
---
## [3.9.0] — 2026-05-27
### ✨ New Features
- **feat(scope):** description ([#1234](https://github.com/diegosouzapw/OmniRoute/pull/1234) — thanks @author)
### 🔧 Bug Fixes
- **fix(scope):** description ([#1235](https://github.com/diegosouzapw/OmniRoute/pull/1235) — thanks @author / @diegosouzapw)
### 📝 Maintenance
- **chore(scope):** description (thanks @diegosouzapw)
---
## [3.8.999] — 2026-05-20
```
#### 4d. Coverage assertion
```bash
NEW_VERSION=$(node -p "require('./package.json').version")
COMMITS=$(wc -l < /tmp/release_commits.txt)
BULLETS=$(awk "/^## \\[$NEW_VERSION\\]/{flag=1;next} /^## \\[/{flag=0} flag" CHANGELOG.md | grep -c '^- ')
echo "Commits in range: $COMMITS"
echo "Changelog bullets: $BULLETS"
if [ "$BULLETS" -lt $(( COMMITS / 3 )) ]; then
echo "⚠️ Bullet count looks low (< commits/3). Re-review /tmp/release_commits.txt for missed entries."
fi
```
### 5. Sync versioned files ⚠️ MANDATORY
```bash
VERSION=$(node -p "require('./package.json').version")
sed -i "s/ version: .*/ version: $VERSION/" docs/reference/openapi.yaml
echo "✓ openapi.yaml → $VERSION"
for dir in electron open-sse; do
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
(cd "$dir" && npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null)
echo "✓ $dir/package.json → $VERSION"
fi
done
npm install
```
### 6. Sync README.md and i18n docs
No `/update-docs` workflow exists (deprecated in v3.8). Apply manually OR via parallel agents:
1. Apply the substantive change to `README.md` first (feature table row + "What's new in vX.Y.Z" section).
2. Capture the diff: `git diff README.md > /tmp/readme.patch`.
3. Dispatch 5-10 parallel agents, each handling a slice of the 40 `docs/i18n/*/README.md`, translating the diff into the target language.
4. Update `docs/<AREA>.md` if architecture/counts changed.
5. Validate: `npm run check:docs-sync && npm run check:docs-all`.
### 7. Full quality gate (MANDATORY — replaces the old `npm test`)
> **Precedent**: v3.8.2 landed with 49 broken tests because only `npm test` was running. Lint + typecheck + cycles caught zero of those regressions.
```bash
set -e
npm run lint
npm run typecheck:core
npm run check:cycles
npm run check:docs-all
npm test
```
All five must pass before opening the PR.
### 8. Stage, commit, and push (atomic — bump + features + changelog + i18n in ONE commit)
```bash
VERSION=$(node -p "require('./package.json').version")
git add -A
git commit -m "chore(release): v$VERSION — $(date -u +%F)"
git push origin "release/v$VERSION"
```
> **NEVER** include `Co-Authored-By:` trailers in the release commit (Hard Rule #16). Attribution lives inside the CHANGELOG entries.
### 9. Open PR to main
```bash
VERSION=$(node -p "require('./package.json').version")
awk "/^## \\[$VERSION\\]/{flag=1; print; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md > /tmp/changelog_body.txt
{
echo ""
echo "---"
echo ""
echo "### Quality Gate"
echo "- lint: pass"
echo "- typecheck:core: pass"
echo "- check:cycles: pass"
echo "- check:docs-all: pass"
echo "- tests: pass"
echo ""
echo "### Coverage of commits since previous tag"
LAST_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "(no previous tag)")
COMMITS=$(git rev-list --no-merges "$LAST_TAG..HEAD" | wc -l)
echo "- Range: \`$LAST_TAG..HEAD\`"
echo "- Commits inspected: $COMMITS"
echo ""
echo "### ⚠️ After merging: run Phase 2 (Local VPS homologation) before tagging."
} >> /tmp/changelog_body.txt
gh pr create \
--repo diegosouzapw/OmniRoute \
--base main \
--head "release/v$VERSION" \
--title "Release v$VERSION" \
--body-file /tmp/changelog_body.txt
```
### 10. 🛑 STOP — Notify user & await PR confirmation
Present the report and stop. Provide:
- PR URL
- Summary of changes (top 5 from CHANGELOG)
- Quality gate results
- `git diff --stat $LAST_TAG..HEAD`
- Coverage count vs commits-in-range
**DO NOT proceed to Phase 2 until the user confirms.**
---
## Phase 2: Post-Merge Validation (Local VPS)
> Run only AFTER the user has merged the PR into `main` and all CI jobs pass.
### 11. Deploy `main` to the Local VPS
Delegate to the `deploy-vps-local-ag` workflow (single source of truth — do NOT inline SCP/SSH here):
```
/deploy-vps-local-ag
```
### 12. 🛑 STOP — Notify user & await final OK
Provide smoke-test checklist:
- [ ] `GET /` returns 200
- [ ] Dashboard login works (`/dashboard`)
- [ ] `/v1/chat/completions` with default provider returns a stream
- [ ] No critical errors in `pm2 logs omniroute --lines 100`
- [ ] Any release-specific UI features are reachable
Wait for user **OK** before Phase 3.
---
## Phase 3: Official Launch
### 13. Create git tag and GitHub Release
```bash
git checkout main
git pull origin main
VERSION=$(node -p "require('./package.json').version")
NOTES=$(awk "/^## \\[$VERSION\\]/{flag=1; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
[ -z "$NOTES" ] && NOTES="OmniRoute v$VERSION Release"
git tag -a "v$VERSION" -m "Release v$VERSION"
git push origin "v$VERSION"
gh release create "v$VERSION" \
--repo diegosouzapw/OmniRoute \
--title "v$VERSION" \
--notes "$NOTES" \
--target main \
|| gh release edit "v$VERSION" \
--repo diegosouzapw/OmniRoute \
--title "v$VERSION" \
--notes "$NOTES"
```
### 14. 🐳 Trigger / verify Docker Hub build
```bash
gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit 3
gh run watch --repo diegosouzapw/OmniRoute
```
### 15. Publish to npm (usually CI)
```bash
npm publish
npm info omniroute version
```
### 16. Deploy to Akamai VPS (Production)
Delegate to `deploy-vps-akamai-ag` workflow if available, or run the inline equivalent of `deploy-vps-local-ag` against `69.164.221.35`. Do NOT duplicate the procedure here.
### 17. Rollback playbook (use only if Phase 3 fails after tag push)
```bash
VERSION=$(node -p "require('./package.json').version")
PREV=$(git describe --tags --abbrev=0 "v$VERSION^")
gh release edit "v$VERSION" --repo diegosouzapw/OmniRoute --prerelease
git checkout "$PREV" && /deploy-vps-akamai-ag
npm deprecate "omniroute@$VERSION" "broken release — use $PREV"
```
---
## Phase 4: Release Monitoring & Artifact Validation
### 18. Monitor CI pipelines
```bash
gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit 1
gh run list --repo diegosouzapw/OmniRoute --workflow electron-release.yml --limit 1
gh run watch <RUN_ID>
npm info omniroute version
```
### 19. Handle failures
```bash
gh run view <RUN_ID> --log-failed
VERSION=$(node -p "require('./package.json').version")
gh workflow run <workflow.yml> --repo diegosouzapw/OmniRoute --ref "v$VERSION"
```
### 20. Preserve release branch
Branch is kept for historical purposes. Do not delete.
---
## Notes
- Ensure CHANGELOG, README and `docs/*` are current BEFORE this workflow — run `npm run check:docs-all` first.
- The `prepublishOnly` script runs `npm run build:cli` automatically during `npm publish`.
- After npm publish, verify with `npm info omniroute version`.
- Lock file sync errors are caused by skipping `npm install` after version bump.
- Use `gh auth switch -u diegosouzapw` if `git push` fails with the wrong account.
- Deploy procedures live in dedicated workflows (`deploy-vps-local-ag`, `deploy-vps-akamai-ag` if present). Never inline SCP/SSH commands here.
## Known CI Pitfalls
| CI failure | Cause | Fix |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| `[docs-sync] FAIL - OpenAPI version differs from package.json` | Skipped step 5 — `docs/reference/openapi.yaml` version not updated | Run step 5 (`sed -i ...`) and commit |
| `[docs-sync] FAIL - CHANGELOG.md first section must be "## [Unreleased]"` | `## [Unreleased]` missing or not at top of CHANGELOG | Add `## [Unreleased]\n\n---\n` before the first versioned `## [x.y.z]` |
| Electron Linux `.deb` build fails (`FpmTarget` error) | `fpm` Ruby gem not installed on `ubuntu-latest` runner | Already fixed in `electron-release.yml` (`gem install fpm` step) |
| Docker Hub `502 error writing layer blob` | Transient Docker Hub network error during ARM64 push | Re-run the Docker publish workflow; no code change needed |
| Coverage gate fails (statements/lines < 75% or branches < 70%) | Production code changed without tests | Add tests, re-run `npm run test:coverage` (see CLAUDE.md hard rule #9) |

View File

@@ -1,513 +0,0 @@
---
name: generate-release-cc
description: Create a new release, bump version up to the .999 patch threshold, generate a complete CHANGELOG (with PR co-authors + every commit since the last tag), and manage Pull Requests
---
# Generate Release Workflow
Bump version, build a **complete CHANGELOG** from every commit since the last tag (with PR back-reference and contributor attribution), commit, open a **PR to main** and wait for user confirmation before tagging, publishing, and deploying.
> **VERSION RULE: Always use PATCH bumps (3.x.y → 3.x.y+1)**
> NEVER use `npm version minor` or `npm version major`.
> Always use: `npm version patch --no-git-tag-version`
> The threshold rule: when `y` reaches 1000, bump to `3.(x+1).0` — e.g. `3.8.999` → `3.9.0`.
> **🔴 INTEGRATION BRANCH RULE**: The `release/vX.Y.Z` branch is the **integration target** for the entire release cycle. Bug fixes and feature implementations land here **via per-issue PRs from short-lived `fix/<ISSUE>-<short>` or `feat/<ISSUE>-<short>` worktrees** (see `/resolve-issues`, `/implement-features`). Contributor PRs from `/review-prs` likewise merge into this branch. The release branch is then merged to `main` via a single release PR at the end of the cycle.
---
## ⚠️ Four-Phase Flow
```
Phase 0 → security audit (npm + CodeQL + Dependabot)
Phase 1 → bump → full quality gate → changelog from commits → commit → push → open PR
↕ 🛑 STOP: notify user, wait for PR merge
Phase 2 → deploy main to Local VPS for homologation
↕ 🛑 STOP: notify user, wait for OK
Phase 3 → tag → GitHub release → Docker → npm → Akamai
Phase 4 → monitor CI pipelines and validate artifacts
```
**NEVER push directly to main or create tags before the user confirms the PR.**
---
## Phase 0: Security Verification (MANDATORY)
Before creating the release, ensure the codebase and supply chain are clean.
```bash
# 1. Local dependency audit
npm audit --production --audit-level=high
# 2. GitHub CodeQL alerts (open + high severity)
gh api '/repos/diegosouzapw/OmniRoute/code-scanning/alerts?state=open&severity=high' \
--jq '.[] | {rule: .rule.id, path: .most_recent_instance.location.path, msg: .most_recent_instance.message.text}' \
2>/dev/null || echo "(no CodeQL access or no alerts)"
# 3. Dependabot alerts (open + high/critical)
gh api '/repos/diegosouzapw/OmniRoute/dependabot/alerts?state=open' \
--jq '.[] | select(.security_advisory.severity == "high" or .security_advisory.severity == "critical") | {pkg: .dependency.package.name, sev: .security_advisory.severity, summary: .security_advisory.summary}' \
2>/dev/null || echo "(no Dependabot access or no alerts)"
```
Fix or justify (with `vulnerability-scanner` skill or dismissal comment per Hard Rule #14) any `high`/`critical` findings before proceeding.
---
## Phase 1: Pre-Merge
### 1. Create or confirm release branch
```bash
# To create a new release branch (MUST always be created from main):
git checkout main
git pull origin main
git checkout -b release/v3.9.0
# If continuing the current cycle, just verify:
git branch --show-current
```
### 2. Determine and sync version
```bash
grep '"version"' package.json
```
> **🔴 BRANCH-VERSION PARITY GATE** — auto-checked before any work:
// turbo
```bash
BRANCH=$(git branch --show-current)
BRANCH_VER=${BRANCH#release/v}
PKG_VER=$(node -p "require('./package.json').version")
if [[ "$BRANCH" != release/v* ]]; then
echo "❌ Not on a release/v* branch (current: $BRANCH). Aborting."; exit 1
fi
# Allow first-bump scenario (branch declares a not-yet-bumped target)
echo "Branch target: $BRANCH_VER"
echo "package.json: $PKG_VER"
```
> **⚠️ ATOMIC COMMIT RULE** — bump and feature/fix code MUST land in the same commit so that `git show vX.Y.Z` always contains both.
>
> **CORRECT order**: bump → (or already-staged changes) → single commit.
> **NEVER**: commit features first, then bump in a separate commit.
```bash
npm version patch --no-git-tag-version
```
### 3. Regenerate lock file (REQUIRED after version bump)
```bash
npm install
```
Skipping this causes `@swc/helpers` lock mismatch and CI failures.
### 4. Build CHANGELOG from EVERY commit since the last tag
> **🎯 Goal**: produce a complete CHANGELOG section following the format of PR #2617 — emoji-grouped sections, PR back-reference, and `— thanks @user` attribution. Nothing must slip through.
> **🔴 NO MIXUPS RULE**: do not mix backlog of the previous version. The new section must contain ONLY commits whose merge/landing happened after the previous tag.
#### 4a. Collect raw commit log since last tag
// turbo
```bash
LAST_TAG=$(git describe --tags --abbrev=0)
NEW_VERSION=$(node -p "require('./package.json').version")
TODAY=$(date -u +%F)
echo "Range: $LAST_TAG..HEAD → v$NEW_VERSION ($TODAY)"
# Full commit list (oneline)
git log --no-merges "$LAST_TAG..HEAD" --pretty=format:'%h %s' > /tmp/release_commits.txt
wc -l /tmp/release_commits.txt
# Merge commits (preserve PR numbers + authors)
git log --merges "$LAST_TAG..HEAD" --pretty=format:'%h %s%n author=%an <%ae>' > /tmp/release_merges.txt
# Per-commit detailed list (PR refs, co-authors, body)
git log "$LAST_TAG..HEAD" --pretty=format:'---%n%h | %s%n author=%an <%ae>%n body=%b' > /tmp/release_detailed.txt
```
#### 4b. Enrich with PR metadata + co-authors
For each commit referencing a PR (e.g. `(#2617)` or merge commit `Merge pull request #N`), fetch the PR author and any additional contributors so the entry follows the model below.
// turbo
```bash
# Extract all PR numbers referenced in the range
grep -oE '#[0-9]+' /tmp/release_commits.txt | sort -u > /tmp/release_prs.txt
echo "PRs in range:"; cat /tmp/release_prs.txt
# Fetch author + co-author info for every PR
> /tmp/release_pr_meta.json
while read -r PR; do
N=${PR#\#}
gh pr view "$N" --repo diegosouzapw/OmniRoute \
--json number,title,author,mergeCommit,body \
>> /tmp/release_pr_meta.json 2>/dev/null || echo "(skip $PR — not found)"
echo "" >> /tmp/release_pr_meta.json
done < /tmp/release_prs.txt
```
#### 4c. Assemble the new CHANGELOG section
Using `/tmp/release_commits.txt` + `/tmp/release_pr_meta.json` + `/tmp/release_detailed.txt`, build a new entry that:
1. **Covers every commit** — read the full list and group by Conventional Commit type. A commit is "covered" iff it appears (or is intentionally rolled-up) in the new section.
2. **Groups using these section headers (model from PR #2617)**:
- `### ✨ New Features``feat(*)`
- `### 🔧 Bug Fixes``fix(*)`
- `### 📝 Maintenance``chore(*)`, `refactor(*)`, `docs(*)`, `test(*)`, `ci(*)`, `build(*)`
- `### 🔒 Security` — security-flagged commits (only if any)
3. **Entry format**:
```
- **type(scope):** human-friendly description — extra context if useful. ([#PR](https://github.com/diegosouzapw/OmniRoute/pull/PR) — thanks @author / @coauthor1 / @coauthor2)
```
- When **no PR** is referenced (direct commit on release branch): `(thanks @author)`.
- When the PR closed an external contributor's PR via cherry-pick or re-implementation, attribute BOTH the original author AND the implementer: `thanks @originalAuthor / @diegosouzapw`.
- **Co-authors** must be extracted from the merge commit body (`Co-Authored-By:` lines that pre-date Hard Rule #16) and from PR participants who supplied commits.
4. **Coverage check** — after drafting, diff the section against `/tmp/release_commits.txt`. Any unlisted commit must either be explicitly added or consolidated under a roll-up bullet (e.g. "various lint and test alignments"). Do NOT silently drop commits.
Place the new section in `CHANGELOG.md` right below `## [Unreleased]`, separated by `---`:
```markdown
## [Unreleased]
---
## [3.9.0] — 2026-05-27
### ✨ New Features
- **feat(scope):** description ([#1234](https://github.com/diegosouzapw/OmniRoute/pull/1234) — thanks @author)
- ...
### 🔧 Bug Fixes
- **fix(scope):** description ([#1235](https://github.com/diegosouzapw/OmniRoute/pull/1235) — thanks @author / @diegosouzapw)
- ...
### 📝 Maintenance
- **chore(scope):** description (thanks @diegosouzapw)
- ...
---
## [3.8.999] — 2026-05-20
```
#### 4d. Coverage assertion
// turbo
```bash
NEW_VERSION=$(node -p "require('./package.json').version")
# Count commits in range
COMMITS=$(wc -l < /tmp/release_commits.txt)
# Count bullets under the new section
BULLETS=$(awk "/^## \\[$NEW_VERSION\\]/{flag=1;next} /^## \\[/{flag=0} flag" CHANGELOG.md | grep -c '^- ')
echo "Commits in range: $COMMITS"
echo "Changelog bullets: $BULLETS"
if [ "$BULLETS" -lt $(( COMMITS / 3 )) ]; then
echo "⚠️ Bullet count looks low (< commits/3). Re-review /tmp/release_commits.txt for missed entries."
fi
```
> If a commit cannot be matched to a bullet, EITHER add it or explicitly justify the omission in this session before continuing.
### 5. Sync versioned files ⚠️ MANDATORY
> **CI will fail** if `docs/reference/openapi.yaml` version ≠ `package.json` version (`check:docs-sync` enforces this).
// turbo
```bash
VERSION=$(node -p "require('./package.json').version")
sed -i "s/ version: .*/ version: $VERSION/" docs/reference/openapi.yaml
echo "✓ openapi.yaml → $VERSION"
for dir in electron open-sse; do
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
(cd "$dir" && npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null)
echo "✓ $dir/package.json → $VERSION"
fi
done
# Re-run install so workspace lockfile picks up the bumps
npm install
```
### 6. Sync README.md and i18n docs
There is **no `/update-docs` slash command** (deprecated in v3.8). Updates must happen manually OR via parallel subagents.
**Recommended automation** — dispatch parallel agents to apply the same diff across the 40 translations (see `superpowers:dispatching-parallel-agents`):
1. Apply the substantive change to `README.md` first (feature table row + "What's new in vX.Y.Z" section).
2. Capture the diff: `git diff README.md > /tmp/readme.patch`.
3. Dispatch 5-10 parallel agents, each handling a slice of the 40 `docs/i18n/*/README.md`, translating the diff into the target language.
4. Update `docs/<AREA>.md` if architecture/counts changed (e.g. `docs/frameworks/MCP-SERVER.md` when MCP tools change).
5. Validate: `npm run check:docs-sync && npm run check:docs-all`.
### 7. Full quality gate (MANDATORY — replaces the old `npm test`)
> **Precedent**: the v3.8.2 cycle landed with 49 broken tests because only `npm test` was running. Lint + typecheck + cycles caught zero of those regressions.
// turbo
```bash
set -e
npm run lint
npm run typecheck:core
npm run check:cycles
npm run check:docs-all
npm test
```
All five must pass before opening the PR. If any fail, fix and re-run.
### 8. Stage, commit, and push (atomic — bump + features + changelog + i18n in ONE commit)
// turbo
```bash
VERSION=$(node -p "require('./package.json').version")
git add -A
git commit -m "chore(release): v$VERSION — $(date -u +%F)"
git push origin "release/v$VERSION"
```
> **NEVER** include `Co-Authored-By:` trailers in the release commit (Hard Rule #16). Co-author attribution lives inside the CHANGELOG entries.
### 9. Open PR to main
// turbo
```bash
VERSION=$(node -p "require('./package.json').version")
# Extract the exact changelog entry for this version
awk "/^## \\[$VERSION\\]/{flag=1; print; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md > /tmp/changelog_body.txt
# Append PR-only metadata (test status + reviewer instructions)
{
echo ""
echo "---"
echo ""
echo "### Quality Gate"
echo "- lint: pass"
echo "- typecheck:core: pass"
echo "- check:cycles: pass"
echo "- check:docs-all: pass"
echo "- tests: pass"
echo ""
echo "### Coverage of commits since previous tag"
LAST_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "(no previous tag)")
COMMITS=$(git rev-list --no-merges "$LAST_TAG..HEAD" | wc -l)
echo "- Range: \`$LAST_TAG..HEAD\`"
echo "- Commits inspected: $COMMITS"
echo ""
echo "### ⚠️ After merging: run Phase 2 (Local VPS homologation) before tagging."
} >> /tmp/changelog_body.txt
gh pr create \
--repo diegosouzapw/OmniRoute \
--base main \
--head "release/v$VERSION" \
--title "Release v$VERSION" \
--body-file /tmp/changelog_body.txt
```
### 10. 🛑 STOP — Notify user & await PR confirmation
Present in the final response and stop. Do not continue to Phase 2 until the user explicitly approves.
Provide:
- PR URL
- Summary of changes (top 5 from CHANGELOG)
- Quality gate results
- List of files changed (`git diff --stat $LAST_TAG..HEAD`)
- Coverage count vs commits-in-range
**DO NOT proceed to Phase 2 until the user confirms the PR looks good and merges it.**
---
## Phase 2: Post-Merge Validation (Local VPS)
> Run only AFTER the user has merged the PR into `main` and all CI jobs pass.
### 11. Deploy `main` to the Local VPS
Delegate to the `deploy-vps-local-cc` skill (single source of truth for the deploy procedure — do NOT duplicate the SCP/SSH commands here):
```
/deploy-vps-local-cc
```
The skill handles: checkout `main`, `npm pack`, scp to `192.168.0.15`, install, pm2 restart, and HTTP probe.
### 12. 🛑 STOP — Notify user & await final OK
Inform the user that `main` is running on `192.168.0.15:20128`. Provide a smoke-test checklist:
- [ ] `GET /` returns 200
- [ ] Dashboard login works (`/dashboard`)
- [ ] `/v1/chat/completions` with default provider returns a stream
- [ ] No critical errors in `pm2 logs omniroute --lines 100`
- [ ] Any release-specific UI features are reachable
Wait for user **OK** before Phase 3.
---
## Phase 3: Official Launch
> Run only AFTER the user gives the final OK from Phase 2.
### 13. Create git tag and GitHub Release
// turbo
```bash
git checkout main
git pull origin main
VERSION=$(node -p "require('./package.json').version")
# Extract release notes section from CHANGELOG
NOTES=$(awk "/^## \\[$VERSION\\]/{flag=1; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
[ -z "$NOTES" ] && NOTES="OmniRoute v$VERSION Release"
git tag -a "v$VERSION" -m "Release v$VERSION"
git push origin "v$VERSION"
gh release create "v$VERSION" \
--repo diegosouzapw/OmniRoute \
--title "v$VERSION" \
--notes "$NOTES" \
--target main \
|| gh release edit "v$VERSION" \
--repo diegosouzapw/OmniRoute \
--title "v$VERSION" \
--notes "$NOTES"
```
### 14. 🐳 Trigger / verify Docker Hub build
> **CRITICAL**: Docker Hub and npm MUST publish the same version.
```bash
VERSION=$(node -p "require('./package.json').version")
gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit 3
gh run watch --repo diegosouzapw/OmniRoute
```
### 15. Publish to npm (usually CI)
`prepublishOnly` runs `npm run build:cli`. Manual fallback:
```bash
npm publish
npm info omniroute version # verify
```
### 16. Deploy to Akamai VPS (Production)
Delegate to the `deploy-vps-akamai-cc` skill:
```
/deploy-vps-akamai-cc
```
The skill handles: build, pack, scp to `69.164.221.35`, install, pm2 restart, HTTP probe.
### 17. Rollback playbook (use only if Phase 3 fails after tag push)
If a fatal regression surfaces after the tag is pushed:
```bash
VERSION=$(node -p "require('./package.json').version")
PREV=$(git describe --tags --abbrev=0 "v$VERSION^")
# 1. Mark GitHub release as pre-release (do not delete history)
gh release edit "v$VERSION" --repo diegosouzapw/OmniRoute --prerelease
# 2. Re-deploy previous version to Akamai
git checkout "$PREV" && /deploy-vps-akamai-cc
# 3. Deprecate the broken npm version
npm deprecate "omniroute@$VERSION" "broken release — use $PREV"
# 4. Open follow-up issue and start a new patch cycle from main
```
---
## Phase 4: Release Monitoring & Artifact Validation
> Actively monitor the CI pipelines until all artifacts succeed. If any fail, stop and fix before continuing.
### 18. Monitor CI pipelines
Verify successful completion of:
1. **Docker Hub Publish**
2. **Electron Build**
3. **npm Registry Publish**
```bash
gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit 1
gh run list --repo diegosouzapw/OmniRoute --workflow electron-release.yml --limit 1
gh run watch <RUN_ID>
npm info omniroute version
```
### 19. Handle failures
```bash
gh run view <RUN_ID> --log-failed
# Fix on main, then re-trigger:
VERSION=$(node -p "require('./package.json').version")
gh workflow run <workflow.yml> --repo diegosouzapw/OmniRoute --ref "v$VERSION"
```
### 20. Preserve release branch
Branch is kept for historical purposes. Do not delete.
---
## Notes
- Ensure CHANGELOG, README and `docs/*` are current BEFORE this workflow — run `npm run check:docs-all` first.
- The `prepublishOnly` script runs `npm run build:cli` automatically during `npm publish`.
- After npm publish, verify with `npm info omniroute version`.
- Lock file sync errors are caused by skipping `npm install` after version bump.
- Use `gh auth switch -u diegosouzapw` if `git push` fails with the wrong account.
- Deploy procedures live in dedicated skills (`deploy-vps-local-cc`, `deploy-vps-akamai-cc`, `deploy-vps-both-cc`) — never inline the SCP/SSH commands here, to avoid drift.
## Known CI Pitfalls
| CI failure | Cause | Fix |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| `[docs-sync] FAIL - OpenAPI version differs from package.json` | Skipped step 5 — `docs/reference/openapi.yaml` version not updated | Run step 5 (`sed -i ...`) and commit |
| `[docs-sync] FAIL - CHANGELOG.md first section must be "## [Unreleased]"` | `## [Unreleased]` missing or not at top of CHANGELOG | Add `## [Unreleased]\n\n---\n` before the first versioned `## [x.y.z]` |
| Electron Linux `.deb` build fails (`FpmTarget` error) | `fpm` Ruby gem not installed on `ubuntu-latest` runner | Already fixed in `electron-release.yml` (`gem install fpm` step) |
| Docker Hub `502 error writing layer blob` | Transient Docker Hub network error during ARM64 push | Re-run the Docker publish workflow; no code change needed |
| Coverage gate fails (statements/lines < 75% or branches < 70%) | Production code changed without tests | Add tests, re-run `npm run test:coverage` (see CLAUDE.md hard rule #9) |

View File

@@ -1,515 +0,0 @@
---
name: generate-release-cx
description: Create a new release, bump version up to the .999 patch threshold, generate a complete CHANGELOG (with PR co-authors + every commit since the last tag), and manage Pull Requests
---
# Generate Release Workflow
Bump version, build a **complete CHANGELOG** from every commit since the last tag (with PR back-reference and contributor attribution), commit, open a **PR to main** and wait for user confirmation before tagging, publishing, and deploying.
## Codex Execution Notes
- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` for independent reads, checks, and GitHub calls.
- When the workflow says `notify_user` or `BlockedOnUser: true`, present the report/status in the final response and stop. Do not continue into the next phase until the user explicitly approves.
> **VERSION RULE: Always use PATCH bumps (3.x.y → 3.x.y+1)**
> NEVER use `npm version minor` or `npm version major`.
> Always use: `npm version patch --no-git-tag-version`
> The threshold rule: when `y` reaches 1000, bump to `3.(x+1).0` — e.g. `3.8.999` → `3.9.0`.
> **🔴 INTEGRATION BRANCH RULE**: The `release/vX.Y.Z` branch is the **integration target** for the entire release cycle. Bug fixes and feature implementations land here **via per-issue PRs from short-lived `fix/<ISSUE>-<short>` or `feat/<ISSUE>-<short>` worktrees** (see `/resolve-issues`, `/implement-features`). Contributor PRs from `/review-prs` likewise merge into this branch. The release branch is then merged to `main` via a single release PR at the end of the cycle.
---
## ⚠️ Four-Phase Flow
```
Phase 0 → security audit (npm + CodeQL + Dependabot)
Phase 1 → bump → full quality gate → changelog from commits → commit → push → open PR
↕ 🛑 STOP (BlockedOnUser: true): notify user, wait for PR merge
Phase 2 → deploy main to Local VPS for homologation
↕ 🛑 STOP (BlockedOnUser: true): notify user, wait for OK
Phase 3 → tag → GitHub release → Docker → npm → Akamai
Phase 4 → monitor CI pipelines and validate artifacts
```
**NEVER push directly to main or create tags before the user confirms the PR.**
---
## Phase 0: Security Verification (MANDATORY)
// turbo
```bash
# 1. Local dependency audit
npm audit --production --audit-level=high
# 2. GitHub CodeQL alerts (open + high severity)
gh api '/repos/diegosouzapw/OmniRoute/code-scanning/alerts?state=open&severity=high' \
--jq '.[] | {rule: .rule.id, path: .most_recent_instance.location.path, msg: .most_recent_instance.message.text}' \
2>/dev/null || echo "(no CodeQL access or no alerts)"
# 3. Dependabot alerts (open + high/critical)
gh api '/repos/diegosouzapw/OmniRoute/dependabot/alerts?state=open' \
--jq '.[] | select(.security_advisory.severity == "high" or .security_advisory.severity == "critical") | {pkg: .dependency.package.name, sev: .security_advisory.severity, summary: .security_advisory.summary}' \
2>/dev/null || echo "(no Dependabot access or no alerts)"
```
Fix or justify (with `vulnerability-scanner` skill, or dismissal comment per Hard Rule #14) any `high`/`critical` findings before proceeding.
---
## Phase 1: Pre-Merge
### 1. Create or confirm release branch
```bash
# To create a new release branch (MUST always be created from main):
git checkout main
git pull origin main
git checkout -b release/v3.9.0
# If continuing the current cycle, just verify:
git branch --show-current
```
### 2. Determine and sync version
```bash
grep '"version"' package.json
```
> **🔴 BRANCH-VERSION PARITY GATE** — auto-checked before any work:
// turbo
```bash
BRANCH=$(git branch --show-current)
BRANCH_VER=${BRANCH#release/v}
PKG_VER=$(node -p "require('./package.json').version")
if [[ "$BRANCH" != release/v* ]]; then
echo "❌ Not on a release/v* branch (current: $BRANCH). Aborting."; exit 1
fi
echo "Branch target: $BRANCH_VER"
echo "package.json: $PKG_VER"
```
> **⚠️ ATOMIC COMMIT RULE** — bump and feature/fix code MUST land in the same commit so that `git show vX.Y.Z` always contains both. NEVER commit features first and bump in a separate commit.
```bash
npm version patch --no-git-tag-version
```
### 3. Regenerate lock file (REQUIRED after version bump)
```bash
npm install
```
Skipping causes `@swc/helpers` lock mismatch and CI failures.
### 4. Build CHANGELOG from EVERY commit since the last tag
> **🎯 Goal**: produce a complete CHANGELOG section following the format of PR #2617 — emoji-grouped sections, PR back-reference, and `— thanks @user` attribution. Nothing must slip through.
> **🔴 NO MIXUPS RULE**: do not mix backlog of the previous version. The new section must contain ONLY commits whose merge/landing happened after the previous tag.
#### 4a. Collect raw commit log since last tag
// turbo
```bash
LAST_TAG=$(git describe --tags --abbrev=0)
NEW_VERSION=$(node -p "require('./package.json').version")
TODAY=$(date -u +%F)
echo "Range: $LAST_TAG..HEAD → v$NEW_VERSION ($TODAY)"
# Full commit list (oneline)
git log --no-merges "$LAST_TAG..HEAD" --pretty=format:'%h %s' > /tmp/release_commits.txt
wc -l /tmp/release_commits.txt
# Merge commits (preserve PR numbers + authors)
git log --merges "$LAST_TAG..HEAD" --pretty=format:'%h %s%n author=%an <%ae>' > /tmp/release_merges.txt
# Per-commit detailed list (PR refs, co-authors, body)
git log "$LAST_TAG..HEAD" --pretty=format:'---%n%h | %s%n author=%an <%ae>%n body=%b' > /tmp/release_detailed.txt
```
#### 4b. Enrich with PR metadata + co-authors
For each commit referencing a PR (e.g. `(#2617)` or merge commit `Merge pull request #N`), fetch the PR author and any additional contributors. Use `multi_tool_use.parallel` to fan out the `gh pr view` calls.
// turbo
```bash
# Extract all PR numbers referenced in the range
grep -oE '#[0-9]+' /tmp/release_commits.txt | sort -u > /tmp/release_prs.txt
echo "PRs in range:"; cat /tmp/release_prs.txt
# Fetch author + co-author info for every PR
> /tmp/release_pr_meta.json
while read -r PR; do
N=${PR#\#}
gh pr view "$N" --repo diegosouzapw/OmniRoute \
--json number,title,author,mergeCommit,body \
>> /tmp/release_pr_meta.json 2>/dev/null || echo "(skip $PR — not found)"
echo "" >> /tmp/release_pr_meta.json
done < /tmp/release_prs.txt
```
#### 4c. Assemble the new CHANGELOG section
Using `/tmp/release_commits.txt` + `/tmp/release_pr_meta.json` + `/tmp/release_detailed.txt`, build a new entry that:
1. **Covers every commit** — read the full list and group by Conventional Commit type. A commit is "covered" iff it appears (or is intentionally rolled-up) in the new section.
2. **Groups using these section headers (model from PR #2617)**:
- `### ✨ New Features``feat(*)`
- `### 🔧 Bug Fixes``fix(*)`
- `### 📝 Maintenance``chore(*)`, `refactor(*)`, `docs(*)`, `test(*)`, `ci(*)`, `build(*)`
- `### 🔒 Security` — security-flagged commits (only if any)
3. **Entry format**:
```
- **type(scope):** human-friendly description — extra context if useful. ([#PR](https://github.com/diegosouzapw/OmniRoute/pull/PR) — thanks @author / @coauthor1 / @coauthor2)
```
- When **no PR** is referenced (direct commit on release branch): `(thanks @author)`.
- When the PR closed an external contributor's PR via cherry-pick or re-implementation, attribute BOTH the original author AND the implementer: `thanks @originalAuthor / @diegosouzapw`.
- **Co-authors** must be extracted from the merge commit body (`Co-Authored-By:` lines that pre-date Hard Rule #16) and from PR participants who supplied commits.
4. **Coverage check** — after drafting, diff the section against `/tmp/release_commits.txt`. Any unlisted commit must either be explicitly added or consolidated under a roll-up bullet (e.g. "various lint and test alignments"). Do NOT silently drop commits.
Place the new section in `CHANGELOG.md` right below `## [Unreleased]`, separated by `---`:
```markdown
## [Unreleased]
---
## [3.9.0] — 2026-05-27
### ✨ New Features
- **feat(scope):** description ([#1234](https://github.com/diegosouzapw/OmniRoute/pull/1234) — thanks @author)
- ...
### 🔧 Bug Fixes
- **fix(scope):** description ([#1235](https://github.com/diegosouzapw/OmniRoute/pull/1235) — thanks @author / @diegosouzapw)
- ...
### 📝 Maintenance
- **chore(scope):** description (thanks @diegosouzapw)
- ...
### 🏆 Hall of Contributors
A special thanks to everyone who contributed code, reviews, and tests for this release:
@user1, @user2, @user3
---
## [3.8.999] — 2026-05-20
```
> **🔴 HALL OF CONTRIBUTORS RULE**: After drafting all section bullets, parse every `@username` mention from the bullets (PR authors AND co-authors), deduplicate, sort, and append them as a `### 🏆 Hall of Contributors` block at the end of the new release section (before the trailing `---`).
#### 4d. Coverage assertion
// turbo
```bash
NEW_VERSION=$(node -p "require('./package.json').version")
# Count commits in range
COMMITS=$(wc -l < /tmp/release_commits.txt)
# Count bullets under the new section
BULLETS=$(awk "/^## \\[$NEW_VERSION\\]/{flag=1;next} /^## \\[/{flag=0} flag" CHANGELOG.md | grep -c '^- ')
echo "Commits in range: $COMMITS"
echo "Changelog bullets: $BULLETS"
if [ "$BULLETS" -lt $(( COMMITS / 3 )) ]; then
echo "⚠️ Bullet count looks low (< commits/3). Re-review /tmp/release_commits.txt for missed entries."
fi
```
> If a commit cannot be matched to a bullet, EITHER add it or explicitly justify the omission in this session before continuing.
### 5. Sync versioned files ⚠️ MANDATORY
> **CI will fail** if `docs/reference/openapi.yaml` version ≠ `package.json` version (`check:docs-sync` enforces this).
// turbo
```bash
VERSION=$(node -p "require('./package.json').version")
sed -i "s/ version: .*/ version: $VERSION/" docs/reference/openapi.yaml
echo "✓ openapi.yaml → $VERSION"
for dir in electron open-sse; do
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
(cd "$dir" && npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null)
echo "✓ $dir/package.json → $VERSION"
fi
done
# Re-run install so workspace lockfile picks up the bumps
npm install
```
### 6. Sync README.md and i18n docs
There is **no `/update-docs` workflow** (deprecated in v3.8). Updates must happen manually OR via parallel agents.
**Recommended automation** — fan out via `multi_tool_use.parallel`:
1. Apply the substantive change to `README.md` first (feature table row + "What's new in vX.Y.Z" section).
2. Capture the diff: `git diff README.md > /tmp/readme.patch`.
3. Dispatch 5-10 parallel sub-tasks, each handling a slice of the 40 `docs/i18n/*/README.md`, translating the diff into the target language.
4. Update `docs/<AREA>.md` if architecture/counts changed (e.g. `docs/frameworks/MCP-SERVER.md` when MCP tools change).
5. Validate: `npm run check:docs-sync && npm run check:docs-all`.
### 7. Full quality gate (MANDATORY — replaces the old `npm test`)
> **Precedent**: the v3.8.2 cycle landed with 49 broken tests because only `npm test` was running. Lint + typecheck + cycles caught zero of those regressions.
// turbo
```bash
set -e
npm run lint
npm run typecheck:core
npm run check:cycles
npm run check:docs-all
npm test
```
All five must pass before opening the PR. If any fail, fix and re-run.
### 8. Stage, commit, and push (atomic — bump + features + changelog + i18n in ONE commit)
// turbo-all
```bash
VERSION=$(node -p "require('./package.json').version")
git add -A
git commit -m "chore(release): v$VERSION — $(date -u +%F)"
git push origin "release/v$VERSION"
```
> **NEVER** include `Co-Authored-By:` trailers in the release commit (Hard Rule #16). Co-author attribution lives inside the CHANGELOG entries and the Hall of Contributors block.
### 9. Open PR to main
// turbo
```bash
VERSION=$(node -p "require('./package.json').version")
# Extract the exact changelog entry for this version
awk "/^## \\[$VERSION\\]/{flag=1; print; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md > /tmp/changelog_body.txt
# Append PR-only metadata (test status + reviewer instructions)
{
echo ""
echo "---"
echo ""
echo "### Quality Gate"
echo "- lint: pass"
echo "- typecheck:core: pass"
echo "- check:cycles: pass"
echo "- check:docs-all: pass"
echo "- tests: pass"
echo ""
echo "### Coverage of commits since previous tag"
LAST_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "(no previous tag)")
COMMITS=$(git rev-list --no-merges "$LAST_TAG..HEAD" | wc -l)
echo "- Range: \`$LAST_TAG..HEAD\`"
echo "- Commits inspected: $COMMITS"
echo ""
echo "### ⚠️ After merging: run Phase 2 (Local VPS homologation) before tagging."
} >> /tmp/changelog_body.txt
gh pr create \
--repo diegosouzapw/OmniRoute \
--base main \
--head "release/v$VERSION" \
--title "Release v$VERSION" \
--body-file /tmp/changelog_body.txt
```
### 10. 🛑 STOP — Notify user & await PR confirmation (`BlockedOnUser: true`)
Present in the final response and stop. Do not continue to Phase 2 until the user explicitly approves.
Provide:
- PR URL
- Summary of changes (top 5 from CHANGELOG)
- Quality gate results
- List of files changed (`git diff --stat $LAST_TAG..HEAD`)
- Coverage count vs commits-in-range
**DO NOT proceed to Phase 2 until the user confirms the PR looks good and merges it.**
---
## Phase 2: Post-Merge Validation (Local VPS)
> Run only AFTER the user has merged the PR into `main` and all CI jobs pass.
### 11. Deploy `main` to the Local VPS
Delegate to the `deploy-vps-local-cx` skill (single source of truth for the deploy procedure — do NOT duplicate SCP/SSH commands here):
```
/deploy-vps-local-cx
```
The skill handles: checkout `main`, `npm pack`, scp to `192.168.0.15`, install, pm2 restart, and HTTP probe.
### 12. 🛑 STOP — Notify user & await final OK (`BlockedOnUser: true`)
Inform the user that `main` is running on `192.168.0.15:20128`. Provide a smoke-test checklist:
- [ ] `GET /` returns 200
- [ ] Dashboard login works (`/dashboard`)
- [ ] `/v1/chat/completions` with default provider returns a stream
- [ ] No critical errors in `pm2 logs omniroute --lines 100`
- [ ] Any release-specific UI features are reachable
Wait for user **OK** before Phase 3.
---
## Phase 3: Official Launch
> Run only AFTER the user gives the final OK from Phase 2.
### 13. Create git tag and GitHub Release
// turbo
```bash
git checkout main
git pull origin main
VERSION=$(node -p "require('./package.json').version")
# Extract release notes section from CHANGELOG
NOTES=$(awk "/^## \\[$VERSION\\]/{flag=1; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
[ -z "$NOTES" ] && NOTES="OmniRoute v$VERSION Release"
git tag -a "v$VERSION" -m "Release v$VERSION"
git push origin "v$VERSION"
gh release create "v$VERSION" \
--repo diegosouzapw/OmniRoute \
--title "v$VERSION" \
--notes "$NOTES" \
--target main \
|| gh release edit "v$VERSION" \
--repo diegosouzapw/OmniRoute \
--title "v$VERSION" \
--notes "$NOTES"
```
### 14. 🐳 Trigger / verify Docker Hub build
> **CRITICAL**: Docker Hub and npm MUST publish the same version.
```bash
VERSION=$(node -p "require('./package.json').version")
gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit 3
gh run watch --repo diegosouzapw/OmniRoute
```
### 15. Publish to npm (usually CI)
`prepublishOnly` runs `npm run build:cli`. Manual fallback:
```bash
npm publish
npm info omniroute version # verify
```
### 16. Deploy to Akamai VPS (Production)
Delegate to the `deploy-vps-akamai-cx` skill if present, or run the inline equivalent of `deploy-vps-local-cx` against `69.164.221.35`. Do NOT duplicate the procedure here.
### 17. Rollback playbook (use only if Phase 3 fails after tag push)
If a fatal regression surfaces after the tag is pushed:
```bash
VERSION=$(node -p "require('./package.json').version")
PREV=$(git describe --tags --abbrev=0 "v$VERSION^")
# 1. Mark GitHub release as pre-release (do not delete history)
gh release edit "v$VERSION" --repo diegosouzapw/OmniRoute --prerelease
# 2. Re-deploy previous version to Akamai
git checkout "$PREV" && /deploy-vps-akamai-cx
# 3. Deprecate the broken npm version
npm deprecate "omniroute@$VERSION" "broken release — use $PREV"
# 4. Open follow-up issue and start a new patch cycle from main
```
---
## Phase 4: Release Monitoring & Artifact Validation
> Actively monitor the CI pipelines until all artifacts succeed. If any fail, stop and fix before continuing.
### 18. Monitor CI pipelines
Verify successful completion of:
1. **Docker Hub Publish**
2. **Electron Build**
3. **npm Registry Publish**
```bash
gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit 1
gh run list --repo diegosouzapw/OmniRoute --workflow electron-release.yml --limit 1
gh run watch <RUN_ID>
npm info omniroute version
```
### 19. Handle failures
```bash
gh run view <RUN_ID> --log-failed
# Fix on main, then re-trigger:
VERSION=$(node -p "require('./package.json').version")
gh workflow run <workflow.yml> --repo diegosouzapw/OmniRoute --ref "v$VERSION"
```
### 20. Preserve release branch
Branch is kept for historical purposes. Do not delete.
---
## Notes
- Ensure CHANGELOG, README and `docs/*` are current BEFORE this workflow — run `npm run check:docs-all` first.
- The `prepublishOnly` script runs `npm run build:cli` automatically during `npm publish`.
- After npm publish, verify with `npm info omniroute version`.
- Lock file sync errors are caused by skipping `npm install` after version bump.
- Use `gh auth switch -u diegosouzapw` if `git push` fails with the wrong account.
- Deploy procedures live in dedicated skills (`deploy-vps-local-cx`, `deploy-vps-akamai-cx` if present) — never inline the SCP/SSH commands here, to avoid drift.
## Known CI Pitfalls
| CI failure | Cause | Fix |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| `[docs-sync] FAIL - OpenAPI version differs from package.json` | Skipped step 5 — `docs/reference/openapi.yaml` version not updated | Run step 5 (`sed -i ...`) and commit |
| `[docs-sync] FAIL - CHANGELOG.md first section must be "## [Unreleased]"` | `## [Unreleased]` missing or not at top of CHANGELOG | Add `## [Unreleased]\n\n---\n` before the first versioned `## [x.y.z]` |
| Electron Linux `.deb` build fails (`FpmTarget` error) | `fpm` Ruby gem not installed on `ubuntu-latest` runner | Already fixed in `electron-release.yml` (`gem install fpm` step) |
| Docker Hub `502 error writing layer blob` | Transient Docker Hub network error during ARM64 push | Re-run the Docker publish workflow; no code change needed |
| Coverage gate fails (statements/lines < 75% or branches < 70%) | Production code changed without tests | Add tests, re-run `npm run test:coverage` (see CLAUDE.md hard rule #9) |

View File

@@ -1,891 +0,0 @@
---
name: implement-features-ag
description: Analyze open feature request issues, implement viable ones on dedicated branches, and respond to authors
---
# /implement-features — Feature Request Harvest, Research & Implementation Workflow
## Overview
A **5-phase** workflow that systematically harvests feature requests from GitHub issues, creates structured idea files, researches solutions across the internet and Git repositories, presents a consolidated report for user approval, then generates detailed implementation plans and executes them.
**Output directory structure:**
```
_ideia/
├── viable/ # ✅ Approved, awaiting implementation
│ ├── 1046-native-playground.md
│ └── 1046-native-playground.requirements.md
├── implemented/ # ✅ Implemented but release PR not yet merged to main (transient)
│ └── 1046-native-playground.md
├── need_details/ # ❓ Issue OPEN — awaiting author clarification (permanent archive)
│ └── 1015-warp-terminal-mitm.md
├── defer/ # ⏭️ Issue CLOSED — good idea, deferred for future cycles (permanent)
│ └── 1041-smart-auto-combos.md
├── notfit/ # ❌ Issue CLOSED — out of scope (permanent)
│ └── 945-telegram-integration.md
├── exists/ # 🔁 Issue CLOSED — feature already shipped (permanent, kept separate from notfit)
│ └── 812-rate-limit-dashboard.md
└── in_flight/ # 🚧 Issue OPEN — third-party PR already addresses it (permanent until reclaim or merge)
└── 988-batch-export.md
_tasks/features-vX.Y.Z/ # Implementation plans (per-release)
└── 1046-native-playground.plan.md
```
> **LIFECYCLE RULE:**
> - `viable/` files are **MOVED** to `implemented/` once code lands on the release branch.
> - `implemented/` files are **DELETED** only after the release PR is merged to `main`.
> - All other buckets — `need_details/`, `defer/`, `notfit/`, `exists/`, `in_flight/` — are **permanent archives**. Even when the upstream issue is CLOSED, the local file stays. Future cycles can revisit any of them (Phase 1.7 stale-reclaim turns `in_flight/` and `need_details/` back into VIABLE after 15 days of upstream inactivity).
> - This preserves recovery context if implementation fails partially AND lets us re-evaluate old decisions when the project matures.
> **BRANCH RULE**: All implementation work MUST happen on the current `release/vX.Y.Z` branch. Never create separate `feat/` branches. If no release branch exists yet, delegate creation to `/generate-release` (see Phase 1.2) — do NOT reimplement bump logic here.
> **LANGUAGE RULE** (per `feedback_reply_language` memory): GitHub comments MUST match the language of the original issue body. Detect language by sampling the issue body + first 2 comments. Default to English when uncertain. All comment templates below are in English — translate to the detected language before posting. Internal docs, plan files, and idea files stay in English regardless.
---
## Phase 1 — Harvest: Collect & Catalog Feature Ideas
### 1.1 Identify the Repository
// turbo
- Run: `git -C <project_root> remote get-url origin` to extract owner/repo.
### 1.2 Ensure Release Branch Exists
Before doing any work, ensure you are on the current release branch:
```bash
git branch --show-current
```
**Decision tree:**
- If already on a `release/vX.Y.Z` branch → continue working there.
- If on `main` or any other branch → **delegate to `/generate-release`** by invoking its Phase 1 (steps 15: detect current version, bump, create branch, install). Do NOT reimplement the bump formula here — `/generate-release` owns the canonical version policy (patch bumps allowed up to `.999`; minor bump only when patch reaches `999`).
> **Why delegate?** Duplicating the bump formula caused divergence in the past. `/generate-release` is the single source of truth for version arithmetic and now allows patches up to `.999` before bumping minor.
### 1.3 Fetch ALL Open Feature Requests
// turbo-all
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below.
**Step 1 — Get Issue numbers only** (small output, never truncated):
```bash
# Fetch issues with feature/enhancement labels
gh issue list --repo <owner>/<repo> --state open -l "enhancement" --limit 500 --json number --jq '.[].number'
# Also check for [Feature] in title (common pattern when no labels are set)
gh issue list --repo <owner>/<repo> --state open --limit 500 --json number,title --jq '.[] | select(.title | test("\\[Feature\\]|\\[feature\\]|feature request"; "i")) | .number'
```
- Merge both lists, deduplicate. Count and confirm the total.
- If the count hits the `--limit 500` ceiling, raise the limit and re-run — never proceed with a truncated set.
**Step 2 — Fetch full metadata for each Issue** (one call per issue):
```bash
gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author,assignees
```
- Read the **entire body** — including description, use cases, screenshots, mockups, and any embedded images.
- Read **ALL comments** — community discussion, agreements, restrictions, owner responses, and linked PRs.
- **Images**: If the body or comments contain image URLs (`![...](...)` or `https://...png/jpg/gif`), **download and analyze them with the Read tool** (Claude can read PNG/JPG/GIF directly). Mockups and wireframes are often the most informative artifact — do NOT just "note" them, actually inspect their content and incorporate findings into the refined description.
- **Detect issue language** from body + first 2 comments and record it in the idea file front-matter (`reply_lang: pt-BR | en | es | ...`). This will drive comment translation in Phases 2.5 and 5.
- You may batch these into parallel calls (up to 4 at a time).
- Sort by oldest first (FIFO).
### 1.4 Create Idea Files (initially in `_ideia/` root)
For each feature request, create a structured idea file in `<project_root>/_ideia/`:
**Filename convention**: `<NUMBER>-<kebab-case-short-title>.md`
Example: `1046-native-playground.md`, `1041-smart-auto-combos.md`
#### 1.4a — If the idea file does NOT exist yet, create it:
```markdown
---
reply_lang: <detected-lang, e.g. pt-BR | en | es>
---
# Feature: <Title from Issue>
> GitHub Issue: #<NUMBER> — opened by @<author> on <date>
> Status: 📋 Cataloged | Priority: TBD
## 📝 Original Request
<Paste the FULL issue body here, preserving all formatting, images, and code blocks>
## 💬 Community Discussion
<Summarize ALL comments chronologically, noting who said what and any decisions or objections raised>
### Participants
- @<author> — Original requester
- @<commenter1> — <brief role/opinion>
- ...
### Key Points
- <bullet list of the most important discussion points>
- <agreements reached>
- <objections raised>
## 🖼️ Mockup / Image Analysis
<For each image embedded in the issue, summarize what it depicts: UI layout, data flow, architecture diagram, etc. Cite source URL.>
## 🎯 Refined Feature Description
<YOUR interpretation and enrichment of the feature request. Expand on what was asked, fill in logical gaps, provide concrete examples of how it would work. This section should be MORE detailed and clearer than the original request.>
### What it solves
- <problem 1>
- <problem 2>
### How it should work (high level)
1. <step 1>
2. <step 2>
3. ...
### Affected areas
- <list of codebase areas, modules, files likely affected>
## 📎 Attachments & References
- <any image URLs, mockup links, or external references from the issue>
## 🔗 Related Ideas
- <links to related \_ideia/ files if any overlap found>
```
#### 1.4b — If the idea file ALREADY exists, update it:
- Append new comments from the issue to the **Community Discussion** section.
- Update the **Refined Feature Description** if new information changes the understanding.
- Add any new **Related Ideas** cross-references found.
- Re-detect `reply_lang` only if the issue language clearly changed (uncommon).
- **Do NOT overwrite** existing content — append and enrich it.
### 1.5 Cross-Reference & Deduplication
After processing all issues:
- Scan all `_ideia/*.md` files for overlapping features.
- If two features are substantially the same, add `🔗 Related Ideas` cross-references to both.
- If one is a strict subset of another, note it in the smaller file: `> This feature is a subset of #<OTHER_NUMBER>. Consider implementing together.`
### 1.6 Detect In-Flight Work (avoid duplicate effort)
For each issue number, check whether an open PR or branch already targets it:
```bash
# Open PRs that link the issue
gh pr list --repo <owner>/<repo> --state open --search "linked:#<NUMBER>" --json number,title,headRefName,updatedAt,author
# Local branches that mention the issue number
git branch -a | grep -E "(^|/)(feat|fix|refactor)/.*-?<NUMBER>(-|$)" || true
```
If a PR or branch already exists:
- Mark the idea file with `> ⚠️ In-flight: PR #<PR_NUMBER> by @<author> / branch <name> (last activity <date>)` near the top.
- **Skip Phase 2 research and Phase 4 planning** for this feature — the implementation is already in motion.
- In the Phase 3 report, list it under a separate "🚧 Already in progress" bucket; do NOT count it as VIABLE for implementation.
- The idea file will be moved to `_ideia/in_flight/` in Phase 2.5.2 (it stays there permanently, but Phase 1.7 may reclaim it later).
### 1.7 Stale Reclaim (15-day rule)
Some issues sit in `in_flight/` or `need_details/` forever — third-party PRs go cold, authors disappear, the world moves on. This phase reclaims them when they go quiet.
**Trigger conditions** (run for each issue currently in `_ideia/in_flight/` or `_ideia/need_details/`):
```bash
# For IN FLIGHT — last activity on the linked PR (commit OR comment)
gh pr view <PR_NUMBER> --repo <owner>/<repo> --json updatedAt,commits,comments \
--jq '[.updatedAt, (.commits[-1].committedDate // ""), (.comments[-1].createdAt // "")] | max'
# For NEEDS DETAIL — last activity from the issue author (any comment by them)
gh issue view <NUMBER> --repo <owner>/<repo> --json comments,author \
--jq '.author.login as $a | [.comments[] | select(.author.login == $a) | .createdAt] | max // (.createdAt)'
```
Compute the gap in days between the timestamp above and today.
**Reclaim rule:**
| Bucket | Trigger | Action |
| --------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| 🚧 IN FLIGHT | ≥15 days since last PR activity (commit OR comment by PR author) | Post **intent-to-take-over comment** (template below), wait **48h**, then reclaim if no response |
| ❓ NEEDS DETAIL | ≥15 days since last comment by the issue author | Post **gentle nudge** (template below), wait **48h**, then reclaim as VIABLE if no response |
**Intent-to-take-over comment (🚧 IN FLIGHT path)** — translate to `reply_lang`:
```markdown
Hi @<pr_author> and @<issue_author>! 👋
This PR (#<PR>) addressing issue #<NUMBER> hasn't had updates in <N> days. We'd love to ship this feature in our next release.
**Plan:** if there are no updates in the next **48 hours**, our team will take over the work and merge it as part of `release/vX.Y.Z`. The original PR will be referenced and authorship preserved in the commit trailer.
If you're still working on it, just drop a comment here and we'll hold off. Thanks for the contribution either way! 🙏
```
**Gentle nudge (❓ NEEDS DETAIL path)** — translate to `reply_lang`:
```markdown
Hi @<author>! 👋
It's been <N> days since we asked for more details on this feature request. We'd still love to move forward.
**Plan:** if we don't hear back in the next **48 hours**, we'll proceed with our best interpretation of the original request and add it to our backlog for implementation. We'll tag you on the implementation PR so you can review before it ships.
If you still want to provide the details, just reply here — we'll wait. 🙏
```
**Reclaim execution** (only after the 48h grace period, with no new author/PR-author activity):
1. Move the idea file to `_ideia/viable/` (preserve any prior content + add a `> ♻️ Reclaimed on <date> after 15-day inactivity` banner near the top).
2. If it was IN FLIGHT and a research file does not yet exist, run Phase 2 (Research) for it now.
3. Otherwise create the requirements file based on the existing content + a quick research pass.
4. Add a `viable_origin: stale_reclaim` line to the front-matter so the Phase 3 report can flag it.
5. In Phase 5 (commit / PR), include a commit trailer crediting the original PR author if applicable:
```
Originally-proposed-by: @<pr_author> in #<original_pr_number>
```
(This is NOT `Co-Authored-By` — hard rule #16 still applies. It is a free-form trailer that preserves credit without GitHub re-attributing the commit.)
> **Why 15 days + 48h grace?** Long enough that the original contributor has truly moved on; short enough that the feature still ships in the same release cycle. Grace period is documented in `feedback_issue_triage_independence` so we don't default to "trust prior triage" — we verify the silence is real.
---
## Phase 2 — Research: Find Solutions & Build Requirements
For each cataloged idea that is **viable** (aligns with the project's goals) AND not already in flight (per 1.6):
### 2.1 Viability Pre-Check
Before investing in research, quickly assess:
- [ ] Does this feature align with the project's goals and architecture?
- [ ] Is it technically feasible with the current codebase?
- [ ] Does it duplicate existing functionality?
- [ ] Would it introduce breaking changes or security risks?
- [ ] Is there enough detail to understand what's needed?
**Verdict options:**
| Verdict | When | Action |
| --------------------- | ------------------------------------- | --------------------------- |
| ✅ **VIABLE** | Good idea, enough context | Proceed to Research |
| ❓ **NEEDS DETAIL** | Good idea, insufficient spec | Skip research, ask author |
| ⏭️ **DEFER** | Good idea, too complex for this cycle | Catalog only, skip research |
| ❌ **NOT FIT** | Doesn't fit the project | Explain why |
| 🔁 **ALREADY EXISTS** | Feature already implemented | Point to existing feature |
| 🚧 **IN FLIGHT** | PR/branch already exists (from 1.6) | Skip — track only |
### 2.2 Internet Research (for VIABLE features)
For each viable feature, perform systematic research with an **early-stopping criterion**:
> **Stop as soon as EITHER condition is met:**
> - 3 reference implementations show a consistent pattern, OR
> - 1 high-quality repo (≥1k stars, updated within the last 12 months) already solves the problem cleanly.
>
> Cap at 10 repos total. Do NOT exhaustively browse — depth over breadth.
**Step 1 — Web search for similar implementations:**
```
WebSearch("how to implement <feature description> in <tech stack>")
WebSearch("<feature keyword> implementation nextjs typescript 2025 2026")
WebSearch("<feature keyword> open source library npm")
```
**Step 2 — Find reference Git repositories:**
```
WebSearch("site:github.com <feature keyword> <tech stack> stars:>100")
WebSearch("github <feature keyword> implementation recently updated 2026")
```
- Sort by most recently updated.
- For each repository (until stop criterion hit):
- Note the repo URL, star count, last commit date
- Read its README and relevant source files via `WebFetch`
- Extract the architectural approach, patterns used, and key code snippets
**Step 3 — Read API docs and standards:**
If the feature involves an external API, protocol, or standard:
- Find and read the official documentation
- Note version requirements, authentication patterns, rate limits
### 2.3 Create Requirements File
For each researched feature, create a requirements file alongside its idea file:
**Filename**: `<NUMBER>-<kebab-case-short-title>.requirements.md`
```markdown
# Requirements: <Feature Title>
> Feature Idea: [#<NUMBER>](./<NUMBER>-<kebab-case-short-title>.md)
> Research Date: <YYYY-MM-DD>
> Verdict: ✅ VIABLE
## 🔍 Research Summary
<Brief summary of what was found during research>
## 📚 Reference Implementations
| # | Repository | Stars | Last Updated | Approach | Relevance |
| --- | ---------------- | ----- | ------------ | -------- | ------------ |
| 1 | [repo/name](url) | ⭐ N | YYYY-MM-DD | <brief> | High/Med/Low |
| 2 | ... | | | | |
### Key Patterns Found
- <pattern 1 with code snippet or link>
- <pattern 2>
## 📐 Proposed Solution Architecture
### Approach
<Describe the chosen approach based on research findings>
### New Files
| File | Purpose |
| --------------------- | ------------- |
| `path/to/new/file.ts` | <description> |
### Modified Files
| File | Changes |
| -------------------------- | -------------- |
| `path/to/existing/file.ts` | <what changes> |
### Database Changes
- <migrations needed, if any>
### API Changes
- <new/modified endpoints, if any>
### UI Changes
- <new/modified pages/components, if any>
## ⚙️ Implementation Effort
- **Estimated complexity**: Low / Medium / High / Very High
- **Estimated files changed**: ~N
- **Dependencies needed**: <new npm packages, if any>
- **Breaking changes**: Yes/No — <details>
- **i18n impact**: <number of new translation keys>
- **Test coverage needed**: <brief description>
## ⚠️ Open Questions
- <question 1>
- <question 2>
## 🔗 External References
- <documentation URLs>
- <API references>
```
---
## Phase 2.5 — Organize: Sort Files into Category Directories
> **⚠️ This phase only moves files. It does NOT post comments or close issues.** All GitHub-visible actions are deferred to Phase 3.2 (after human approval).
### 2.5.1 Create Directory Structure
// turbo
```bash
mkdir -p <project_root>/_ideia/viable
mkdir -p <project_root>/_ideia/implemented
mkdir -p <project_root>/_ideia/need_details
mkdir -p <project_root>/_ideia/defer
mkdir -p <project_root>/_ideia/notfit
mkdir -p <project_root>/_ideia/exists
mkdir -p <project_root>/_ideia/in_flight
```
> **Permanent archives**: `need_details/`, `defer/`, `notfit/`, `exists/`, `in_flight/`. Even after the upstream issue is closed, the local file stays — future cycles may revisit.
### 2.5.2 Move Idea Files to Category Subdirectories
After classification, move EVERY idea file to its correct subdirectory (still local-only — no GitHub side-effects):
```bash
# ✅ VIABLE — move idea + requirements files
mv _ideia/<NUMBER>-*.md _ideia/viable/
mv _ideia/<NUMBER>-*.requirements.md _ideia/viable/
# ❓ NEEDS DETAIL — viable but waiting for author response (issue stays OPEN)
mv _ideia/<NUMBER>-*.md _ideia/need_details/
# ⏭️ DEFER — issue will be CLOSED but file is kept permanently for future re-evaluation
mv _ideia/<NUMBER>-*.md _ideia/defer/
# ❌ NOT FIT — issue will be CLOSED but file is kept permanently
mv _ideia/<NUMBER>-*.md _ideia/notfit/
# 🔁 ALREADY EXISTS — issue will be CLOSED but file is kept permanently (separate bucket from NOT FIT)
mv _ideia/<NUMBER>-*.md _ideia/exists/
# 🚧 IN FLIGHT — issue stays OPEN, third-party PR is handling it; file kept permanently for Phase 1.7 stale-reclaim
mv _ideia/<NUMBER>-*.md _ideia/in_flight/
```
No idea files should remain in `_ideia/` root after this step.
---
## Phase 3 — Report: Present Findings & Get Human Approval
### 3.1 🛑 MANDATORY STOP — Present Consolidated Report
After completing Phase 1, Phase 2, and Phase 2.5, **STOP and present the following report** in the chat. **No comments have been posted to GitHub yet** — that happens in 3.2 after approval.
Present a structured report containing:
#### 3.1a — Feature Summary Table
| # | Issue | Title | Verdict | Local Location | Planned GitHub Action |
| --- | ----- | ----- | ----------------- | ----------------------- | -------------------------------------- |
| 1 | #N | Title | ✅ VIABLE | `_ideia/viable/` | Comment + keep OPEN |
| 2 | #N | Title | ⏭️ DEFER | `_ideia/defer/` | Comment + CLOSE |
| 3 | #N | Title | ❌ NOT FIT | `_ideia/notfit/` | Comment + CLOSE |
| 4 | #N | Title | 🔁 EXISTS | `_ideia/exists/` | Comment with location + CLOSE |
| 5 | #N | Title | ❓ NEEDS DETAIL | `_ideia/need_details/` | Comment with questions + keep OPEN |
| 6 | #N | Title | 🚧 IN FLIGHT | `_ideia/in_flight/` | None — PR #M handles it |
| 7 | #N | Title | ♻️ RECLAIMED | `_ideia/viable/` | Intent comment posted in Phase 1.7 |
#### 3.1b — Viable Features Detail
For each VIABLE feature, provide a brief paragraph:
- What was found during research (with stop reason: "3-pattern consistency" or "dominant repo")
- The proposed approach
- Key risks or unknowns
- Which reference repositories were most useful
#### 3.1c — Issues Requiring Author Feedback
For features marked ❓ NEEDS DETAIL, list:
- What specific information is missing
- What examples or repository references would help
- Detected `reply_lang` for the question post
#### 3.1d — Ask for User Confirmation
End the report with:
> **Ready to proceed?**
>
> Approving will (a) post comments on GitHub in the detected language of each issue and (b) close DEFER / NOT FIT / EXISTS issues. VIABLE and NEEDS DETAIL stay open.
>
> - Reply **"sim"** / **"yes"** to post all comments AND generate implementation plans for all VIABLE features.
> - Reply **"only comments"** to post comments without generating plans yet.
> - Reply with specific issue numbers to scope the action.
> - Reply **"não"** / **"no"** to stop without touching GitHub.
### 3.2 Post GitHub Comments & Close Issues (only after approval)
> **⚠️ Do NOT execute this step without explicit user approval from 3.1d.**
For each issue, translate the appropriate template below into the `reply_lang` recorded in its idea file front-matter, then post. The English templates are reference only — never post the English version verbatim to a non-English issue.
---
#### For 🔁 ALREADY EXISTS — Comment + CLOSE issue
The feature already exists in the system. Explain WHERE it is and HOW to use it.
```markdown
Hi @<author>! Thanks for the suggestion! 🙏
Great news — this functionality **already exists** in OmniRoute:
**📍 Where to find it:** <exact dashboard path or settings location>
**🔧 How to use it:**
1. <step 1>
2. <step 2>
3. <step 3>
If you have any trouble finding or using it, feel free to ask in a Discussion. We're always happy to help!
Closing this as the feature is already available. 🎉
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
---
#### For ⏭️ DEFER — Comment + CLOSE issue
Thank the user, explain the idea was cataloged, and that we'll study it before implementing.
```markdown
Hi @<author>! Thanks for this thoughtful feature request! 🙏
We really appreciate the detailed proposal. We've **cataloged your idea** and it's now part of our improvement backlog.
Due to the **significant architectural impact** of this feature, we'll need to conduct thorough use-case studies and architectural analysis before we start development. This ensures we build it right and don't introduce regressions.
**What happens next:**
- Your idea is saved in our internal feature backlog
- We'll conduct architecture studies when this area is prioritized
If you want to track progress, please **subscribe to the repository releases** — every implemented feature is announced in the CHANGELOG.
Thank you for contributing to OmniRoute's roadmap! 🚀
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
---
#### For ❌ NOT FIT — Comment + CLOSE issue
Politely explain why the feature doesn't fit the project scope.
```markdown
Hi @<author>! Thanks for the suggestion! 🙏
After careful analysis, we've determined that this feature **falls outside OmniRoute's core scope** as a proxy/router.
**Reason:** <explain why — e.g., "Telegram integration belongs in the application/orchestrator layer that consumes OmniRoute's API, not inside the router itself.">
**Alternative:** <suggest an alternative approach if possible>
We appreciate you thinking of ways to improve OmniRoute! If you'd like to discuss this further, feel free to open a Discussion. 🙏
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
---
#### For ❓ NEEDS DETAIL — Comment (keep OPEN)
Ask for the specific missing details needed.
```markdown
Hi @<author>! Thanks for the feature request — it's an interesting idea and we'd love to explore it further. 🙏
To move forward, we need a few more details:
1. <specific question 1>
2. <specific question 2>
3. <specific question 3>
If you know of any **open-source projects or repositories** that implement something similar, please share links — it would help us design the best solution.
Looking forward to your response! 🚀
```
---
#### For ✅ VIABLE — Comment (keep OPEN)
Thank the user, confirm we've cataloged their idea, and explain that progress is tracked in releases.
```markdown
Hi @<author>! Thanks for the great feature suggestion! 🙏
We've analyzed your request and it aligns well with OmniRoute's roadmap. We've **cataloged this feature** and it's in our implementation backlog.
**Status:** 📋 Cataloged for future implementation
This issue will be **closed automatically by the merge commit** when the feature ships. To follow along, you can subscribe to repository releases or watch this issue.
Thank you for helping improve OmniRoute! 🚀
```
**⚠️ Do NOT close viable issues — they remain OPEN until the implementation PR closes them via commit message.**
---
## Phase 4 — Plan: Generate Implementation Plans (after user says "yes")
> **⚠️ Do NOT enter this phase without explicit user approval from Phase 3.**
### 4.1 Pre-Plan Context Load (mandatory)
Before writing ANY plan, read:
1. `docs/architecture/REPOSITORY_MAP.md` — to know which directory owns what.
2. `docs/architecture/CODEBASE_DOCUMENTATION.md` — for the engineering reference.
3. The matching "Adding a New X" scenario from `CLAUDE.md` (provider, API route, DB module, MCP tool, A2A skill, cloud agent, embedded service, guardrail, eval, skill, webhook event).
4. Any docs linked from the requirements file's "External References" section.
This ensures plans cite real paths and follow the established add-a-X recipe, instead of inventing structure.
### 4.2 Create Task Directory
```bash
mkdir -p <project_root>/_tasks/features-vX.Y.Z/
```
### 4.3 Generate One Implementation Plan Per Feature
For each VIABLE feature approved by the user, create:
**Filename**: `_tasks/features-vX.Y.Z/<NUMBER>-<kebab-case-title>.plan.md`
```markdown
# Implementation Plan: <Feature Title>
> Issue: #<NUMBER>
> Idea: [\_ideia/viable/<NUMBER>-title.md](../../_ideia/viable/<NUMBER>-title.md)
> Requirements: [\_ideia/viable/<NUMBER>-title.requirements.md](../../_ideia/viable/<NUMBER>-title.requirements.md)
> Branch: `release/vX.Y.Z`
> Matching CLAUDE.md recipe: <e.g. "Adding a New Provider">
## Overview
<Brief description of what will be built>
## Pre-Implementation Checklist
- [ ] Read all related source files listed below
- [ ] Confirm no conflicts with in-flight PRs (re-run Phase 1.6 lookup)
- [ ] Verify database migration numbering (next free integer in `src/lib/db/migrations/`)
## Implementation Steps
### Step 1: <Title>
**Files:**
- `path/to/file.ts` — <what to change>
**Details:**
<Detailed description of the change, including code patterns to follow, function signatures, etc.>
### Step 2: <Title>
...
### Step N: Tests (MANDATORY per CLAUDE.md hard rule #8)
**New test files:**
- `tests/unit/<test-file>.test.mjs` — <what to test>
**Test cases:**
- [ ] <test case 1>
- [ ] <test case 2>
- [ ] Coverage check: confirm overall coverage stays ≥75% statements/lines/functions, ≥70% branches (hard rule #9)
### Step N+1: i18n
**Translation keys to add:**
- `<namespace>.<key>` — "<English value>"
### Step N+2: Documentation
- [ ] Update CHANGELOG.md (current release section)
- [ ] Update relevant docs/ files
- [ ] If touching error responses, follow `docs/security/ERROR_SANITIZATION.md`
- [ ] If touching upstream credentials, follow `docs/security/PUBLIC_CREDS.md`
## Verification Plan (Trust-but-Verify — mandatory before declaring done)
1. `git status` + `git diff --stat` — review every changed file; flag anything outside the plan's declared scope
2. `npm run lint` — 0 new errors
3. `npm run typecheck:core` — clean
4. `npm run typecheck:noimplicit:core` — clean
5. `npm run check:cycles` — no new circular deps
6. `npm run build` — must pass
7. `npm run test:coverage` — coverage gate respected
8. `npm run check-docs-sync` (via pre-commit hook) — passes
9. Manual UI verification if the feature touches frontend (start dev server, exercise golden path + 1 edge case)
## Commit Plan
```
feat: <description> (#<NUMBER>)
```
```
### 4.4 Present Plans for Final Approval
Present a summary of all generated plans:
> **Implementation plans generated:**
>
> | # | Feature | Plan File | Steps | Effort | CLAUDE.md recipe |
> | --- | ------- | ---------------------------------------- | ------- | ------ | ---------------------- |
> | 1 | <title> | `_tasks/features-vX.Y.Z/N-title.plan.md` | N steps | Medium | Adding a New Provider |
>
> Reply **"sim"** / **"yes"** to begin implementation of all features.
> Reply with specific issue numbers to implement only certain ones.
---
## Phase 5 — Execute: Implement the Plans (after user says "yes")
> **⚠️ Do NOT enter this phase without explicit user approval from Phase 4.**
### 5.1 Implement Each Feature
For each approved plan, execute it step by step:
1. **Follow the plan** — implement exactly as specified in the `.plan.md` file
2. **Mark progress** — flip checkboxes to `[x]` in the plan as each step completes
### 5.2 Trust-but-Verify Audit (mandatory before commit)
> Aligned with `~/.claude/CLAUDE.md` global rule: never trust a subagent's summary alone.
Run the full audit checklist from the plan's "Verification Plan" section AND inspect the diff yourself:
```bash
git status
git diff --stat
git diff # full diff, scan for out-of-scope changes
npm run lint
npm run typecheck:core
npm run typecheck:noimplicit:core
npm run check:cycles
npm run build
npm run test:coverage
```
**Block-on-failure checklist:**
- [ ] No files changed outside the plan's declared scope (or scope expansion explicitly justified)
- [ ] No deleted symbols/routes/files without a documented replacement (grep to confirm)
- [ ] No weakened or removed test assertions (only additions or alignments with real behavior)
- [ ] Coverage gate green (75/75/75/70)
- [ ] All commands above exit 0
- [ ] If UI was touched: manual smoke test passed and noted
If any item fails, **fix root cause** before committing. Do NOT bypass with `--no-verify` (hard rule #10).
### 5.3 Commit (one feature, one commit)
```bash
git add <only files in the plan>
git commit -m "feat: <description> (#<NUMBER>)"
```
> **No `Co-Authored-By` trailers** (hard rule #16). Commits go solely under `diegosouzapw`.
Then move (do NOT delete yet) the idea file to `_ideia/implemented/`:
```bash
mv _ideia/viable/<NUMBER>-<title>.md _ideia/implemented/
mv _ideia/viable/<NUMBER>-<title>.requirements.md _ideia/implemented/ 2>/dev/null || true
```
> **Why move, not delete?** If the release PR is reverted or rebased, we still have the context. The file is deleted only after the PR merges to `main` (see 5.6).
Continue to the next feature on the same branch — do NOT switch branches between features.
### 5.4 Respond to Authors
For each implemented feature, post a final close-comment **translated into the issue's `reply_lang`**:
```markdown
✅ **Implemented in `release/vX.Y.Z`!**
Hi @<author>! Great news — your feature request has been implemented! 🎉
**What was done:**
- <bullet list of what was built>
**How to try it (after the release PR merges):**
```bash
git fetch origin && git checkout main && git pull
npm install && npm run dev
```
This will be included in the upcoming **vX.Y.Z** release. Feel free to reopen if you spot any issues! 🚀
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
### 5.5 Finalize the Release Branch
After implementing all approved features:
1. **Update CHANGELOG.md** on the release branch with all new feature entries
2. Push: `git push origin release/vX.Y.Z`
3. Hand off to `/generate-release` for the "Tests → Commit → Push → PR to main" stage. Refer to it **by stage name**, not step number, so this command does not break if `/generate-release` renumbers steps.
### 5.6 Post-Merge Cleanup (only after release PR merges to main)
Once the release PR is merged:
```bash
# Now safe to delete — commit history + CHANGELOG are the source of truth
rm _ideia/implemented/<NUMBER>-*.md
```
> If running this command before the merge: STOP at 5.5 and skip 5.6. Re-enter the workflow later just for the cleanup.
### 5.7 Final Summary Report
Present a final summary report to the user:
| Issue | Title | Verdict | Action | Commit |
| ----- | ----- | ---------------- | --------------------------------------------------------------- | --------- |
| #N | Title | ✅ Implemented | Issue closed, idea file in `_ideia/implemented/` (until merge) | `abc1234` |
| #N | Title | ♻️ Reclaimed | Was IN FLIGHT / NEEDS DETAIL, reclaimed after 15d → implemented | `abc1234` |
| #N | Title | ⏭️ Deferred | Issue closed + permanent archive in `_ideia/defer/` | — |
| #N | Title | ❌ Not Fit | Issue closed + permanent archive in `_ideia/notfit/` | — |
| #N | Title | 🔁 Exists | Issue closed + permanent archive in `_ideia/exists/` | — |
| #N | Title | ❓ Needs Detail | Issue OPEN, archive in `_ideia/need_details/` | — |
| #N | Title | 🚧 In Flight | Issue OPEN, archive in `_ideia/in_flight/`, tracked by PR #M | — |
Include:
- Total features harvested
- Total ideas archived per bucket (`need_details/` / `defer/` / `notfit/` / `exists/` / `in_flight/`)
- Total features implemented (idea files in `_ideia/implemented/`, awaiting post-merge cleanup)
- Total reclaimed via Phase 1.7 (stale 15-day rule)
- Total issues closed
- Total issues left open (NEEDS DETAIL + VIABLE-pending + IN FLIGHT)
- Audit results: lint / typecheck / cycles / build / coverage (pass-count per phase)
- Languages used in posted comments (e.g. "3× pt-BR, 5× en, 1× es")

View File

@@ -1,903 +0,0 @@
---
name: implement-features-cc
description: Analyze open feature request issues, implement viable ones on dedicated branches, and respond to authors
---
# /implement-features — Feature Request Harvest, Research & Implementation Workflow
## Overview
A **5-phase** workflow that systematically harvests feature requests from GitHub issues, creates structured idea files, researches solutions across the internet and Git repositories, presents a consolidated report for user approval, then generates detailed implementation plans and executes them.
**Output directory structure:**
```
_ideia/
├── viable/ # ✅ Approved, awaiting implementation
│ ├── 1046-native-playground.md
│ └── 1046-native-playground.requirements.md
├── implemented/ # ✅ Implemented but release PR not yet merged to main (transient)
│ └── 1046-native-playground.md
├── need_details/ # ❓ Issue OPEN — awaiting author clarification (permanent archive)
│ └── 1015-warp-terminal-mitm.md
├── defer/ # ⏭️ Issue CLOSED — good idea, deferred for future cycles (permanent)
│ └── 1041-smart-auto-combos.md
├── notfit/ # ❌ Issue CLOSED — out of scope (permanent)
│ └── 945-telegram-integration.md
├── exists/ # 🔁 Issue CLOSED — feature already shipped (permanent, kept separate from notfit)
│ └── 812-rate-limit-dashboard.md
└── in_flight/ # 🚧 Issue OPEN — third-party PR already addresses it (permanent until reclaim or merge)
└── 988-batch-export.md
_tasks/features-vX.Y.Z/ # Implementation plans (per-release)
└── 1046-native-playground.plan.md
```
> **LIFECYCLE RULE:**
> - `viable/` files are **MOVED** to `implemented/` once code lands on the release branch.
> - `implemented/` files are **DELETED** only after the release PR is merged to `main`.
> - All other buckets — `need_details/`, `defer/`, `notfit/`, `exists/`, `in_flight/` — are **permanent archives**. Even when the upstream issue is CLOSED, the local file stays. Future cycles can revisit any of them (Phase 1.7 stale-reclaim turns `in_flight/` and `need_details/` back into VIABLE after 15 days of upstream inactivity).
> - This preserves recovery context if implementation fails partially AND lets us re-evaluate old decisions when the project matures.
> **BRANCH RULE**: All implementation work MUST happen on the current `release/vX.Y.Z` branch. Never create separate `feat/` branches. If no release branch exists yet, delegate creation to `/generate-release` (see Phase 1.2) — do NOT reimplement bump logic here.
> **LANGUAGE RULE** (per `feedback_reply_language` memory): GitHub comments MUST match the language of the original issue body. Detect language by sampling the issue body + first 2 comments. Default to English when uncertain. All comment templates below are in English — translate to the detected language before posting. Internal docs, plan files, and idea files stay in English regardless.
---
## Phase 1 — Harvest: Collect & Catalog Feature Ideas
### 1.1 Identify the Repository
// turbo
- Run: `git -C <project_root> remote get-url origin` to extract owner/repo.
### 1.2 Ensure Release Branch Exists
Before doing any work, ensure you are on the current release branch:
```bash
git branch --show-current
```
**Decision tree:**
- If already on a `release/vX.Y.Z` branch → continue working there.
- If on `main` or any other branch → **delegate to `/generate-release`** by invoking its Phase 1 (steps 15: detect current version, bump, create branch, install). Do NOT reimplement the bump formula here — `/generate-release` owns the canonical version policy (patch bumps allowed up to `.999`; minor bump only when patch reaches `999`).
> **Why delegate?** Duplicating the bump formula caused divergence in the past. `/generate-release` is the single source of truth for version arithmetic and now allows patches up to `.999` before bumping minor.
### 1.3 Fetch ALL Open Feature Requests
// turbo-all
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below.
**Step 1 — Get Issue numbers only** (small output, never truncated):
```bash
# Fetch issues with feature/enhancement labels
gh issue list --repo <owner>/<repo> --state open -l "enhancement" --limit 500 --json number --jq '.[].number'
# Also check for [Feature] in title (common pattern when no labels are set)
gh issue list --repo <owner>/<repo> --state open --limit 500 --json number,title --jq '.[] | select(.title | test("\\[Feature\\]|\\[feature\\]|feature request"; "i")) | .number'
```
- Merge both lists, deduplicate. Count and confirm the total.
- If the count hits the `--limit 500` ceiling, raise the limit and re-run — never proceed with a truncated set.
**Step 2 — Fetch full metadata for each Issue** (one call per issue):
```bash
gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author,assignees
```
- Read the **entire body** — including description, use cases, screenshots, mockups, and any embedded images.
- Read **ALL comments** — community discussion, agreements, restrictions, owner responses, and linked PRs.
- **Images**: If the body or comments contain image URLs (`![...](...)` or `https://...png/jpg/gif`), **download and analyze them with the Read tool** (Claude can read PNG/JPG/GIF directly). Mockups and wireframes are often the most informative artifact — do NOT just "note" them, actually inspect their content and incorporate findings into the refined description.
- **Detect issue language** from body + first 2 comments and record it in the idea file front-matter (`reply_lang: pt-BR | en | es | ...`). This will drive comment translation in Phases 2.5 and 5.
- You may batch these into parallel calls (up to 4 at a time).
- Sort by oldest first (FIFO).
### 1.4 Create Idea Files (initially in `_ideia/` root)
For each feature request, create a structured idea file in `<project_root>/_ideia/`:
**Filename convention**: `<NUMBER>-<kebab-case-short-title>.md`
Example: `1046-native-playground.md`, `1041-smart-auto-combos.md`
#### 1.4a — If the idea file does NOT exist yet, create it:
```markdown
---
reply_lang: <detected-lang, e.g. pt-BR | en | es>
---
# Feature: <Title from Issue>
> GitHub Issue: #<NUMBER> — opened by @<author> on <date>
> Status: 📋 Cataloged | Priority: TBD
## 📝 Original Request
<Paste the FULL issue body here, preserving all formatting, images, and code blocks>
## 💬 Community Discussion
<Summarize ALL comments chronologically, noting who said what and any decisions or objections raised>
### Participants
- @<author> — Original requester
- @<commenter1> — <brief role/opinion>
- ...
### Key Points
- <bullet list of the most important discussion points>
- <agreements reached>
- <objections raised>
## 🖼️ Mockup / Image Analysis
<For each image embedded in the issue, summarize what it depicts: UI layout, data flow, architecture diagram, etc. Cite source URL.>
## 🎯 Refined Feature Description
<YOUR interpretation and enrichment of the feature request. Expand on what was asked, fill in logical gaps, provide concrete examples of how it would work. This section should be MORE detailed and clearer than the original request.>
### What it solves
- <problem 1>
- <problem 2>
### How it should work (high level)
1. <step 1>
2. <step 2>
3. ...
### Affected areas
- <list of codebase areas, modules, files likely affected>
## 📎 Attachments & References
- <any image URLs, mockup links, or external references from the issue>
## 🔗 Related Ideas
- <links to related \_ideia/ files if any overlap found>
```
#### 1.4b — If the idea file ALREADY exists, update it:
- Append new comments from the issue to the **Community Discussion** section.
- Update the **Refined Feature Description** if new information changes the understanding.
- Add any new **Related Ideas** cross-references found.
- Re-detect `reply_lang` only if the issue language clearly changed (uncommon).
- **Do NOT overwrite** existing content — append and enrich it.
### 1.5 Cross-Reference & Deduplication
After processing all issues:
- Scan all `_ideia/*.md` files for overlapping features.
- If two features are substantially the same, add `🔗 Related Ideas` cross-references to both.
- If one is a strict subset of another, note it in the smaller file: `> This feature is a subset of #<OTHER_NUMBER>. Consider implementing together.`
### 1.6 Detect In-Flight Work (avoid duplicate effort)
For each issue number, check whether an open PR or branch already targets it:
```bash
# Open PRs that link the issue
gh pr list --repo <owner>/<repo> --state open --search "linked:#<NUMBER>" --json number,title,headRefName,updatedAt,author
# Local branches that mention the issue number
git branch -a | grep -E "(^|/)(feat|fix|refactor)/.*-?<NUMBER>(-|$)" || true
```
If a PR or branch already exists:
- Mark the idea file with `> ⚠️ In-flight: PR #<PR_NUMBER> by @<author> / branch <name> (last activity <date>)` near the top.
- **Skip Phase 2 research and Phase 4 planning** for this feature — the implementation is already in motion.
- In the Phase 3 report, list it under a separate "🚧 Already in progress" bucket; do NOT count it as VIABLE for implementation.
- The idea file will be moved to `_ideia/in_flight/` in Phase 2.5.2 (it stays there permanently, but Phase 1.7 may reclaim it later).
### 1.7 Stale Reclaim (15-day rule)
Some issues sit in `in_flight/` or `need_details/` forever — third-party PRs go cold, authors disappear, the world moves on. This phase reclaims them when they go quiet.
**Trigger conditions** (run for each issue currently in `_ideia/in_flight/` or `_ideia/need_details/`):
```bash
# For IN FLIGHT — last activity on the linked PR (commit OR comment)
gh pr view <PR_NUMBER> --repo <owner>/<repo> --json updatedAt,commits,comments \
--jq '[.updatedAt, (.commits[-1].committedDate // ""), (.comments[-1].createdAt // "")] | max'
# For NEEDS DETAIL — last activity from the issue author (any comment by them)
gh issue view <NUMBER> --repo <owner>/<repo> --json comments,author \
--jq '.author.login as $a | [.comments[] | select(.author.login == $a) | .createdAt] | max // (.createdAt)'
```
Compute the gap in days between the timestamp above and today.
**Reclaim rule:**
| Bucket | Trigger | Action |
| --------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| 🚧 IN FLIGHT | ≥15 days since last PR activity (commit OR comment by PR author) | Post **intent-to-take-over comment** (template below), wait **48h**, then reclaim if no response |
| ❓ NEEDS DETAIL | ≥15 days since last comment by the issue author | Post **gentle nudge** (template below), wait **48h**, then reclaim as VIABLE if no response |
**Intent-to-take-over comment (🚧 IN FLIGHT path)** — translate to `reply_lang`:
```markdown
Hi @<pr_author> and @<issue_author>! 👋
This PR (#<PR>) addressing issue #<NUMBER> hasn't had updates in <N> days. We'd love to ship this feature in our next release.
**Plan:** if there are no updates in the next **48 hours**, our team will take over the work and merge it as part of `release/vX.Y.Z`. The original PR will be referenced and authorship preserved in the commit trailer.
If you're still working on it, just drop a comment here and we'll hold off. Thanks for the contribution either way! 🙏
```
**Gentle nudge (❓ NEEDS DETAIL path)** — translate to `reply_lang`:
```markdown
Hi @<author>! 👋
It's been <N> days since we asked for more details on this feature request. We'd still love to move forward.
**Plan:** if we don't hear back in the next **48 hours**, we'll proceed with our best interpretation of the original request and add it to our backlog for implementation. We'll tag you on the implementation PR so you can review before it ships.
If you still want to provide the details, just reply here — we'll wait. 🙏
```
**Reclaim execution** (only after the 48h grace period, with no new author/PR-author activity):
1. Move the idea file to `_ideia/viable/` (preserve any prior content + add a `> ♻️ Reclaimed on <date> after 15-day inactivity` banner near the top).
2. If it was IN FLIGHT and a research file does not yet exist, run Phase 2 (Research) for it now.
3. Otherwise create the requirements file based on the existing content + a quick research pass.
4. Add a `viable_origin: stale_reclaim` line to the front-matter so the Phase 3 report can flag it.
5. In Phase 5 (commit / PR), include a commit trailer crediting the original PR author if applicable:
```
Originally-proposed-by: @<pr_author> in #<original_pr_number>
```
(This is NOT `Co-Authored-By` — hard rule #16 still applies. It is a free-form trailer that preserves credit without GitHub re-attributing the commit.)
> **Why 15 days + 48h grace?** Long enough that the original contributor has truly moved on; short enough that the feature still ships in the same release cycle. Grace period is documented in `feedback_issue_triage_independence` so we don't default to "trust prior triage" — we verify the silence is real.
---
## Phase 2 — Research: Find Solutions & Build Requirements
For each cataloged idea that is **viable** (aligns with the project's goals) AND not already in flight (per 1.6):
### 2.1 Viability Pre-Check
Before investing in research, quickly assess:
- [ ] Does this feature align with the project's goals and architecture?
- [ ] Is it technically feasible with the current codebase?
- [ ] Does it duplicate existing functionality?
- [ ] Would it introduce breaking changes or security risks?
- [ ] Is there enough detail to understand what's needed?
**Verdict options:**
| Verdict | When | Action |
| --------------------- | ------------------------------------- | --------------------------- |
| ✅ **VIABLE** | Good idea, enough context | Proceed to Research |
| ❓ **NEEDS DETAIL** | Good idea, insufficient spec | Skip research, ask author |
| ⏭️ **DEFER** | Good idea, too complex for this cycle | Catalog only, skip research |
| ❌ **NOT FIT** | Doesn't fit the project | Explain why |
| 🔁 **ALREADY EXISTS** | Feature already implemented | Point to existing feature |
| 🚧 **IN FLIGHT** | PR/branch already exists (from 1.6) | Skip — track only |
### 2.2 Internet Research (for VIABLE features)
For each viable feature, perform systematic research with an **early-stopping criterion**:
> **Stop as soon as EITHER condition is met:**
> - 3 reference implementations show a consistent pattern, OR
> - 1 high-quality repo (≥1k stars, updated within the last 12 months) already solves the problem cleanly.
>
> Cap at 10 repos total. Do NOT exhaustively browse — depth over breadth.
**Step 1 — Web search for similar implementations:**
```
WebSearch("how to implement <feature description> in <tech stack>")
WebSearch("<feature keyword> implementation nextjs typescript 2025 2026")
WebSearch("<feature keyword> open source library npm")
```
**Step 2 — Find reference Git repositories:**
```
WebSearch("site:github.com <feature keyword> <tech stack> stars:>100")
WebSearch("github <feature keyword> implementation recently updated 2026")
```
- Sort by most recently updated.
- For each repository (until stop criterion hit):
- Note the repo URL, star count, last commit date
- Read its README and relevant source files via `WebFetch`
- Extract the architectural approach, patterns used, and key code snippets
**Step 3 — Read API docs and standards:**
If the feature involves an external API, protocol, or standard:
- Find and read the official documentation
- Note version requirements, authentication patterns, rate limits
### 2.3 Create Requirements File
For each researched feature, create a requirements file alongside its idea file:
**Filename**: `<NUMBER>-<kebab-case-short-title>.requirements.md`
```markdown
# Requirements: <Feature Title>
> Feature Idea: [#<NUMBER>](./<NUMBER>-<kebab-case-short-title>.md)
> Research Date: <YYYY-MM-DD>
> Verdict: ✅ VIABLE
## 🔍 Research Summary
<Brief summary of what was found during research>
## 📚 Reference Implementations
| # | Repository | Stars | Last Updated | Approach | Relevance |
| --- | ---------------- | ----- | ------------ | -------- | ------------ |
| 1 | [repo/name](url) | ⭐ N | YYYY-MM-DD | <brief> | High/Med/Low |
| 2 | ... | | | | |
### Key Patterns Found
- <pattern 1 with code snippet or link>
- <pattern 2>
## 📐 Proposed Solution Architecture
### Approach
<Describe the chosen approach based on research findings>
### New Files
| File | Purpose |
| --------------------- | ------------- |
| `path/to/new/file.ts` | <description> |
### Modified Files
| File | Changes |
| -------------------------- | -------------- |
| `path/to/existing/file.ts` | <what changes> |
### Database Changes
- <migrations needed, if any>
### API Changes
- <new/modified endpoints, if any>
### UI Changes
- <new/modified pages/components, if any>
## ⚙️ Implementation Effort
- **Estimated complexity**: Low / Medium / High / Very High
- **Estimated files changed**: ~N
- **Dependencies needed**: <new npm packages, if any>
- **Breaking changes**: Yes/No — <details>
- **i18n impact**: <number of new translation keys>
- **Test coverage needed**: <brief description>
## ⚠️ Open Questions
- <question 1>
- <question 2>
## 🔗 External References
- <documentation URLs>
- <API references>
```
---
## Phase 2.5 — Organize: Sort Files into Category Directories
> **⚠️ This phase only moves files. It does NOT post comments or close issues.** All GitHub-visible actions are deferred to Phase 3.2 (after human approval).
### 2.5.1 Create Directory Structure
// turbo
```bash
mkdir -p <project_root>/_ideia/viable
mkdir -p <project_root>/_ideia/implemented
mkdir -p <project_root>/_ideia/need_details
mkdir -p <project_root>/_ideia/defer
mkdir -p <project_root>/_ideia/notfit
mkdir -p <project_root>/_ideia/exists
mkdir -p <project_root>/_ideia/in_flight
```
> **Permanent archives**: `need_details/`, `defer/`, `notfit/`, `exists/`, `in_flight/`. Even after the upstream issue is closed, the local file stays — future cycles may revisit.
### 2.5.2 Move Idea Files to Category Subdirectories
After classification, move EVERY idea file to its correct subdirectory (still local-only — no GitHub side-effects):
```bash
# ✅ VIABLE — move idea + requirements files
mv _ideia/<NUMBER>-*.md _ideia/viable/
mv _ideia/<NUMBER>-*.requirements.md _ideia/viable/
# ❓ NEEDS DETAIL — viable but waiting for author response (issue stays OPEN)
mv _ideia/<NUMBER>-*.md _ideia/need_details/
# ⏭️ DEFER — issue will be CLOSED but file is kept permanently for future re-evaluation
mv _ideia/<NUMBER>-*.md _ideia/defer/
# ❌ NOT FIT — issue will be CLOSED but file is kept permanently
mv _ideia/<NUMBER>-*.md _ideia/notfit/
# 🔁 ALREADY EXISTS — issue will be CLOSED but file is kept permanently (separate bucket from NOT FIT)
mv _ideia/<NUMBER>-*.md _ideia/exists/
# 🚧 IN FLIGHT — issue stays OPEN, third-party PR is handling it; file kept permanently for Phase 1.7 stale-reclaim
mv _ideia/<NUMBER>-*.md _ideia/in_flight/
```
No idea files should remain in `_ideia/` root after this step.
---
## Phase 3 — Report: Present Findings & Get Human Approval
### 3.1 🛑 MANDATORY STOP — Present Consolidated Report
After completing Phase 1, Phase 2, and Phase 2.5, **STOP and present the following report** in the chat. **No comments have been posted to GitHub yet** — that happens in 3.2 after approval.
Present a structured report containing:
#### 3.1a — Feature Summary Table
| # | Issue | Title | Verdict | Local Location | Planned GitHub Action |
| --- | ----- | ----- | ----------------- | ----------------------- | -------------------------------------- |
| 1 | #N | Title | ✅ VIABLE | `_ideia/viable/` | Comment + keep OPEN |
| 2 | #N | Title | ⏭️ DEFER | `_ideia/defer/` | Comment + CLOSE |
| 3 | #N | Title | ❌ NOT FIT | `_ideia/notfit/` | Comment + CLOSE |
| 4 | #N | Title | 🔁 EXISTS | `_ideia/exists/` | Comment with location + CLOSE |
| 5 | #N | Title | ❓ NEEDS DETAIL | `_ideia/need_details/` | Comment with questions + keep OPEN |
| 6 | #N | Title | 🚧 IN FLIGHT | `_ideia/in_flight/` | None — PR #M handles it |
| 7 | #N | Title | ♻️ RECLAIMED | `_ideia/viable/` | Intent comment posted in Phase 1.7 |
#### 3.1b — Viable Features Detail
For each VIABLE feature, provide a brief paragraph:
- What was found during research (with stop reason: "3-pattern consistency" or "dominant repo")
- The proposed approach
- Key risks or unknowns
- Which reference repositories were most useful
#### 3.1c — Issues Requiring Author Feedback
For features marked ❓ NEEDS DETAIL, list:
- What specific information is missing
- What examples or repository references would help
- Detected `reply_lang` for the question post
#### 3.1d — Ask for User Confirmation
End the report with:
> **Ready to proceed?**
>
> Approving will (a) post comments on GitHub in the detected language of each issue and (b) close DEFER / NOT FIT / EXISTS issues. VIABLE and NEEDS DETAIL stay open.
>
> - Reply **"sim"** / **"yes"** to post all comments AND generate implementation plans for all VIABLE features.
> - Reply **"only comments"** to post comments without generating plans yet.
> - Reply with specific issue numbers to scope the action.
> - Reply **"não"** / **"no"** to stop without touching GitHub.
### 3.2 Post GitHub Comments & Close Issues (only after approval)
> **⚠️ Do NOT execute this step without explicit user approval from 3.1d.**
For each issue, translate the appropriate template below into the `reply_lang` recorded in its idea file front-matter, then post. The English templates are reference only — never post the English version verbatim to a non-English issue.
---
#### For 🔁 ALREADY EXISTS — Comment + CLOSE issue
The feature already exists in the system. Explain WHERE it is and HOW to use it.
```markdown
Hi @<author>! Thanks for the suggestion! 🙏
Great news — this functionality **already exists** in OmniRoute:
**📍 Where to find it:** <exact dashboard path or settings location>
**🔧 How to use it:**
1. <step 1>
2. <step 2>
3. <step 3>
If you have any trouble finding or using it, feel free to ask in a Discussion. We're always happy to help!
Closing this as the feature is already available. 🎉
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
---
#### For ⏭️ DEFER — Comment + CLOSE issue
Thank the user, explain the idea was cataloged, and that we'll study it before implementing.
```markdown
Hi @<author>! Thanks for this thoughtful feature request! 🙏
We really appreciate the detailed proposal. We've **cataloged your idea** and it's now part of our improvement backlog.
Due to the **significant architectural impact** of this feature, we'll need to conduct thorough use-case studies and architectural analysis before we start development. This ensures we build it right and don't introduce regressions.
**What happens next:**
- Your idea is saved in our internal feature backlog
- We'll conduct architecture studies when this area is prioritized
If you want to track progress, please **subscribe to the repository releases** — every implemented feature is announced in the CHANGELOG.
Thank you for contributing to OmniRoute's roadmap! 🚀
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
---
#### For ❌ NOT FIT — Comment + CLOSE issue (soft-archive)
Politely explain the current limitation, but make clear the idea is **archived, not discarded**. If the situation changes (provider opens a public API, scope shifts, etc.), we revisit and tag the author.
```markdown
Hi @<author>! Thanks for the suggestion! 🙏
After researching, we've determined this feature isn't viable right now:
**Reason:** <explain why — e.g., "CodeBuddy has no public API; YepApi is fronted by Cloudflare bot detection that we won't evade.">
**Alternative:** <suggest an alternative if one exists, otherwise omit this line>
That said, **we've saved your suggestion** to our internal archive rather than discarding it. If circumstances change (a public API is released, the provider opens up, our scope shifts, etc.), we'll revisit it and tag you here.
Closing for now, but the idea isn't lost — we'll let you know if things change. 🙏
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
---
#### For ❓ NEEDS DETAIL — Comment (keep OPEN)
Ask for the specific missing details needed.
```markdown
Hi @<author>! Thanks for the feature request — it's an interesting idea and we'd love to explore it further. 🙏
To move forward, we need a few more details:
1. <specific question 1>
2. <specific question 2>
3. <specific question 3>
If you know of any **open-source projects or repositories** that implement something similar, please share links — it would help us design the best solution.
Looking forward to your response! 🚀
```
---
#### For ✅ VIABLE — Comment + CLOSE issue (cataloged for future implementation)
When we **know how to implement** the feature, we accept + catalog + close the issue right away (to keep the open-issue list focused on items still awaiting input). A separate post-implementation comment will reopen the conversation later when code ships. Include a 1-2 sentence summary of what we plan to build so the author knows we understood the request.
```markdown
Hi @<author>! Thanks for the great feature suggestion! 🙏
We've analyzed your request — it aligns with OmniRoute's roadmap and we have a clear implementation path:
> <one to two sentence summary of what we plan to build>
We've **cataloged it internally** and it will be picked up in an upcoming release.
**Status:** ✅ Accepted — cataloged for future implementation
We'll respond here and tag you once the implementation lands so you can test it before it ships.
Closing for now to keep our open-issue list focused on items still awaiting input. The feature is tracked in our internal backlog and won't be forgotten.
Thank you for helping improve OmniRoute! 🚀
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
**⚠️ Important**: The VIABLE comment **CLOSES** the issue. When implementation ships later, Phase 5.4 will REOPEN the issue, post the implementation comment, and CLOSE it again. The author still gets the @-mention notification.
---
## Phase 4 — Plan: Generate Implementation Plans (after user says "yes")
> **⚠️ Do NOT enter this phase without explicit user approval from Phase 3.**
### 4.1 Pre-Plan Context Load (mandatory)
Before writing ANY plan, read:
1. `docs/architecture/REPOSITORY_MAP.md` — to know which directory owns what.
2. `docs/architecture/CODEBASE_DOCUMENTATION.md` — for the engineering reference.
3. The matching "Adding a New X" scenario from `CLAUDE.md` (provider, API route, DB module, MCP tool, A2A skill, cloud agent, embedded service, guardrail, eval, skill, webhook event).
4. Any docs linked from the requirements file's "External References" section.
This ensures plans cite real paths and follow the established add-a-X recipe, instead of inventing structure.
### 4.2 Create Task Directory
```bash
mkdir -p <project_root>/_tasks/features-vX.Y.Z/
```
### 4.3 Generate One Implementation Plan Per Feature
For each VIABLE feature approved by the user, create:
**Filename**: `_tasks/features-vX.Y.Z/<NUMBER>-<kebab-case-title>.plan.md`
```markdown
# Implementation Plan: <Feature Title>
> Issue: #<NUMBER>
> Idea: [\_ideia/viable/<NUMBER>-title.md](../../_ideia/viable/<NUMBER>-title.md)
> Requirements: [\_ideia/viable/<NUMBER>-title.requirements.md](../../_ideia/viable/<NUMBER>-title.requirements.md)
> Branch: `release/vX.Y.Z`
> Matching CLAUDE.md recipe: <e.g. "Adding a New Provider">
## Overview
<Brief description of what will be built>
## Pre-Implementation Checklist
- [ ] Read all related source files listed below
- [ ] Confirm no conflicts with in-flight PRs (re-run Phase 1.6 lookup)
- [ ] Verify database migration numbering (next free integer in `src/lib/db/migrations/`)
## Implementation Steps
### Step 1: <Title>
**Files:**
- `path/to/file.ts` — <what to change>
**Details:**
<Detailed description of the change, including code patterns to follow, function signatures, etc.>
### Step 2: <Title>
...
### Step N: Tests (MANDATORY per CLAUDE.md hard rule #8)
**New test files:**
- `tests/unit/<test-file>.test.mjs` — <what to test>
**Test cases:**
- [ ] <test case 1>
- [ ] <test case 2>
- [ ] Coverage check: confirm overall coverage stays ≥75% statements/lines/functions, ≥70% branches (hard rule #9)
### Step N+1: i18n
**Translation keys to add:**
- `<namespace>.<key>` — "<English value>"
### Step N+2: Documentation
- [ ] Update CHANGELOG.md (current release section)
- [ ] Update relevant docs/ files
- [ ] If touching error responses, follow `docs/security/ERROR_SANITIZATION.md`
- [ ] If touching upstream credentials, follow `docs/security/PUBLIC_CREDS.md`
## Verification Plan (Trust-but-Verify — mandatory before declaring done)
1. `git status` + `git diff --stat` — review every changed file; flag anything outside the plan's declared scope
2. `npm run lint` — 0 new errors
3. `npm run typecheck:core` — clean
4. `npm run typecheck:noimplicit:core` — clean
5. `npm run check:cycles` — no new circular deps
6. `npm run build` — must pass
7. `npm run test:coverage` — coverage gate respected
8. `npm run check-docs-sync` (via pre-commit hook) — passes
9. Manual UI verification if the feature touches frontend (start dev server, exercise golden path + 1 edge case)
## Commit Plan
```
feat: <description> (#<NUMBER>)
```
```
### 4.4 Present Plans for Final Approval
Present a summary of all generated plans:
> **Implementation plans generated:**
>
> | # | Feature | Plan File | Steps | Effort | CLAUDE.md recipe |
> | --- | ------- | ---------------------------------------- | ------- | ------ | ---------------------- |
> | 1 | <title> | `_tasks/features-vX.Y.Z/N-title.plan.md` | N steps | Medium | Adding a New Provider |
>
> Reply **"sim"** / **"yes"** to begin implementation of all features.
> Reply with specific issue numbers to implement only certain ones.
---
## Phase 5 — Execute: Implement the Plans (after user says "yes")
> **⚠️ Do NOT enter this phase without explicit user approval from Phase 4.**
### 5.1 Implement Each Feature
For each approved plan, execute it step by step:
1. **Follow the plan** — implement exactly as specified in the `.plan.md` file
2. **Mark progress** — flip checkboxes to `[x]` in the plan as each step completes
### 5.2 Trust-but-Verify Audit (mandatory before commit)
> Aligned with `~/.claude/CLAUDE.md` global rule: never trust a subagent's summary alone.
Run the full audit checklist from the plan's "Verification Plan" section AND inspect the diff yourself:
```bash
git status
git diff --stat
git diff # full diff, scan for out-of-scope changes
npm run lint
npm run typecheck:core
npm run typecheck:noimplicit:core
npm run check:cycles
npm run build
npm run test:coverage
```
**Block-on-failure checklist:**
- [ ] No files changed outside the plan's declared scope (or scope expansion explicitly justified)
- [ ] No deleted symbols/routes/files without a documented replacement (grep to confirm)
- [ ] No weakened or removed test assertions (only additions or alignments with real behavior)
- [ ] Coverage gate green (75/75/75/70)
- [ ] All commands above exit 0
- [ ] If UI was touched: manual smoke test passed and noted
If any item fails, **fix root cause** before committing. Do NOT bypass with `--no-verify` (hard rule #10).
### 5.3 Commit (one feature, one commit)
```bash
git add <only files in the plan>
git commit -m "feat: <description> (#<NUMBER>)"
```
> **No `Co-Authored-By` trailers** (hard rule #16). Commits go solely under `diegosouzapw`.
Then move (do NOT delete yet) the idea file to `_ideia/implemented/`:
```bash
mv _ideia/viable/<NUMBER>-<title>.md _ideia/implemented/
mv _ideia/viable/<NUMBER>-<title>.requirements.md _ideia/implemented/ 2>/dev/null || true
```
> **Why move, not delete?** If the release PR is reverted or rebased, we still have the context. The file is deleted only after the PR merges to `main` (see 5.6).
Continue to the next feature on the same branch — do NOT switch branches between features.
### 5.4 Respond to Authors
For each implemented feature, post a final close-comment **translated into the issue's `reply_lang`**:
```markdown
✅ **Implemented in `release/vX.Y.Z`!**
Hi @<author>! Great news — your feature request has been implemented! 🎉
**What was done:**
- <bullet list of what was built>
**How to try it (after the release PR merges):**
```bash
git fetch origin && git checkout main && git pull
npm install && npm run dev
```
This will be included in the upcoming **vX.Y.Z** release. Feel free to reopen if you spot any issues! 🚀
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
### 5.5 Finalize the Release Branch
After implementing all approved features:
1. **Update CHANGELOG.md** on the release branch with all new feature entries
2. Push: `git push origin release/vX.Y.Z`
3. Hand off to `/generate-release` for the "Tests → Commit → Push → PR to main" stage. Refer to it **by stage name**, not step number, so this command does not break if `/generate-release` renumbers steps.
### 5.6 Post-Merge Cleanup (only after release PR merges to main)
Once the release PR is merged:
```bash
# Now safe to delete — commit history + CHANGELOG are the source of truth
rm _ideia/implemented/<NUMBER>-*.md
```
> If running this command before the merge: STOP at 5.5 and skip 5.6. Re-enter the workflow later just for the cleanup.
### 5.7 Final Summary Report
Present a final summary report to the user:
| Issue | Title | Verdict | Action | Commit |
| ----- | ----- | ---------------- | --------------------------------------------------------------- | --------- |
| #N | Title | ✅ Implemented | Issue closed, idea file in `_ideia/implemented/` (until merge) | `abc1234` |
| #N | Title | ♻️ Reclaimed | Was IN FLIGHT / NEEDS DETAIL, reclaimed after 15d → implemented | `abc1234` |
| #N | Title | ⏭️ Deferred | Issue closed + permanent archive in `_ideia/defer/` | — |
| #N | Title | ❌ Not Fit | Issue closed + permanent archive in `_ideia/notfit/` | — |
| #N | Title | 🔁 Exists | Issue closed + permanent archive in `_ideia/exists/` | — |
| #N | Title | ❓ Needs Detail | Issue OPEN, archive in `_ideia/need_details/` | — |
| #N | Title | 🚧 In Flight | Issue OPEN, archive in `_ideia/in_flight/`, tracked by PR #M | — |
Include:
- Total features harvested
- Total ideas archived per bucket (`need_details/` / `defer/` / `notfit/` / `exists/` / `in_flight/`)
- Total features implemented (idea files in `_ideia/implemented/`, awaiting post-merge cleanup)
- Total reclaimed via Phase 1.7 (stale 15-day rule)
- Total issues closed
- Total issues left open (NEEDS DETAIL + VIABLE-pending + IN FLIGHT)
- Audit results: lint / typecheck / cycles / build / coverage (pass-count per phase)
- Languages used in posted comments (e.g. "3× pt-BR, 5× en, 1× es")

View File

@@ -1,899 +0,0 @@
---
name: implement-features-cx
description: Analyze open feature request issues, implement viable ones on dedicated branches, and respond to authors
---
# /implement-features — Feature Request Harvest, Research & Implementation Workflow
## Overview
A **5-phase** workflow that systematically harvests feature requests from GitHub issues, creates structured idea files, researches solutions across the internet and Git repositories, presents a consolidated report for user approval, then generates detailed implementation plans and executes them.
## Codex Execution Notes
- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` for independent reads, checks, and GitHub calls.
- Approval gates (Phase 3 and Phase 4 → 5) are hard stops. Present the report/plan in the final response and do not move to implementation phases until the user explicitly approves.
- Keep harvest/research bounded enough to produce the approval report quickly; do not start implementation while still in report phases.
- The trust-but-verify audit in Phase 5.2 is mandatory before any commit — full lint + typecheck + cycles + build + coverage, plus a real `git diff` review for out-of-scope changes.
- Phase 1.7 stale-reclaim (15-day rule) is opt-in per run: only execute when the user asks for a "reclaim pass" or when the harvest report explicitly flags eligible IN FLIGHT / NEEDS DETAIL items.
**Output directory structure:**
```
_ideia/
├── viable/ # ✅ Approved, awaiting implementation
│ ├── 1046-native-playground.md
│ └── 1046-native-playground.requirements.md
├── implemented/ # ✅ Implemented but release PR not yet merged to main (transient)
│ └── 1046-native-playground.md
├── need_details/ # ❓ Issue OPEN — awaiting author clarification (permanent archive)
│ └── 1015-warp-terminal-mitm.md
├── defer/ # ⏭️ Issue CLOSED — good idea, deferred for future cycles (permanent)
│ └── 1041-smart-auto-combos.md
├── notfit/ # ❌ Issue CLOSED — out of scope (permanent)
│ └── 945-telegram-integration.md
├── exists/ # 🔁 Issue CLOSED — feature already shipped (permanent, kept separate from notfit)
│ └── 812-rate-limit-dashboard.md
└── in_flight/ # 🚧 Issue OPEN — third-party PR already addresses it (permanent until reclaim or merge)
└── 988-batch-export.md
_tasks/features-vX.Y.Z/ # Implementation plans (per-release)
└── 1046-native-playground.plan.md
```
> **LIFECYCLE RULE:**
> - `viable/` files are **MOVED** to `implemented/` once code lands on the release branch.
> - `implemented/` files are **DELETED** only after the release PR is merged to `main`.
> - All other buckets — `need_details/`, `defer/`, `notfit/`, `exists/`, `in_flight/` — are **permanent archives**. Even when the upstream issue is CLOSED, the local file stays. Future cycles can revisit any of them (Phase 1.7 stale-reclaim turns `in_flight/` and `need_details/` back into VIABLE after 15 days of upstream inactivity).
> - This preserves recovery context if implementation fails partially AND lets us re-evaluate old decisions when the project matures.
> **BRANCH RULE**: All implementation work MUST happen on the current `release/vX.Y.Z` branch. Never create separate `feat/` branches. If no release branch exists yet, delegate creation to `/generate-release` (see Phase 1.2) — do NOT reimplement bump logic here.
> **LANGUAGE RULE** (per `feedback_reply_language` memory): GitHub comments MUST match the language of the original issue body. Detect language by sampling the issue body + first 2 comments. Default to English when uncertain. All comment templates below are in English — translate to the detected language before posting. Internal docs, plan files, and idea files stay in English regardless.
---
## Phase 1 — Harvest: Collect & Catalog Feature Ideas
### 1.1 Identify the Repository
// turbo
- Run: `git -C <project_root> remote get-url origin` to extract owner/repo.
### 1.2 Ensure Release Branch Exists
Before doing any work, ensure you are on the current release branch:
```bash
git branch --show-current
```
**Decision tree:**
- If already on a `release/vX.Y.Z` branch → continue working there.
- If on `main` or any other branch → **delegate to `/generate-release`** by invoking its Phase 1 (steps 15: detect current version, bump, create branch, install). Do NOT reimplement the bump formula here — `/generate-release` owns the canonical version policy (patch bumps allowed up to `.999`; minor bump only when patch reaches `999`).
> **Why delegate?** Duplicating the bump formula caused divergence in the past. `/generate-release` is the single source of truth for version arithmetic and now allows patches up to `.999` before bumping minor.
### 1.3 Fetch ALL Open Feature Requests
// turbo-all
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below.
**Step 1 — Get Issue numbers only** (small output, never truncated):
```bash
# Fetch issues with feature/enhancement labels
gh issue list --repo <owner>/<repo> --state open -l "enhancement" --limit 500 --json number --jq '.[].number'
# Also check for [Feature] in title (common pattern when no labels are set)
gh issue list --repo <owner>/<repo> --state open --limit 500 --json number,title --jq '.[] | select(.title | test("\\[Feature\\]|\\[feature\\]|feature request"; "i")) | .number'
```
- Merge both lists, deduplicate. Count and confirm the total.
- If the count hits the `--limit 500` ceiling, raise the limit and re-run — never proceed with a truncated set.
**Step 2 — Fetch full metadata for each Issue** (one call per issue):
```bash
gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author,assignees
```
- Read the **entire body** — including description, use cases, screenshots, mockups, and any embedded images.
- Read **ALL comments** — community discussion, agreements, restrictions, owner responses, and linked PRs.
- **Images**: If the body or comments contain image URLs (`![...](...)` or `https://...png/jpg/gif`), **download and analyze them with the Read tool** (Claude can read PNG/JPG/GIF directly). Mockups and wireframes are often the most informative artifact — do NOT just "note" them, actually inspect their content and incorporate findings into the refined description.
- **Detect issue language** from body + first 2 comments and record it in the idea file front-matter (`reply_lang: pt-BR | en | es | ...`). This will drive comment translation in Phases 2.5 and 5.
- You may batch these into parallel calls (up to 4 at a time).
- Sort by oldest first (FIFO).
### 1.4 Create Idea Files (initially in `_ideia/` root)
For each feature request, create a structured idea file in `<project_root>/_ideia/`:
**Filename convention**: `<NUMBER>-<kebab-case-short-title>.md`
Example: `1046-native-playground.md`, `1041-smart-auto-combos.md`
#### 1.4a — If the idea file does NOT exist yet, create it:
```markdown
---
reply_lang: <detected-lang, e.g. pt-BR | en | es>
---
# Feature: <Title from Issue>
> GitHub Issue: #<NUMBER> — opened by @<author> on <date>
> Status: 📋 Cataloged | Priority: TBD
## 📝 Original Request
<Paste the FULL issue body here, preserving all formatting, images, and code blocks>
## 💬 Community Discussion
<Summarize ALL comments chronologically, noting who said what and any decisions or objections raised>
### Participants
- @<author> — Original requester
- @<commenter1> — <brief role/opinion>
- ...
### Key Points
- <bullet list of the most important discussion points>
- <agreements reached>
- <objections raised>
## 🖼️ Mockup / Image Analysis
<For each image embedded in the issue, summarize what it depicts: UI layout, data flow, architecture diagram, etc. Cite source URL.>
## 🎯 Refined Feature Description
<YOUR interpretation and enrichment of the feature request. Expand on what was asked, fill in logical gaps, provide concrete examples of how it would work. This section should be MORE detailed and clearer than the original request.>
### What it solves
- <problem 1>
- <problem 2>
### How it should work (high level)
1. <step 1>
2. <step 2>
3. ...
### Affected areas
- <list of codebase areas, modules, files likely affected>
## 📎 Attachments & References
- <any image URLs, mockup links, or external references from the issue>
## 🔗 Related Ideas
- <links to related \_ideia/ files if any overlap found>
```
#### 1.4b — If the idea file ALREADY exists, update it:
- Append new comments from the issue to the **Community Discussion** section.
- Update the **Refined Feature Description** if new information changes the understanding.
- Add any new **Related Ideas** cross-references found.
- Re-detect `reply_lang` only if the issue language clearly changed (uncommon).
- **Do NOT overwrite** existing content — append and enrich it.
### 1.5 Cross-Reference & Deduplication
After processing all issues:
- Scan all `_ideia/*.md` files for overlapping features.
- If two features are substantially the same, add `🔗 Related Ideas` cross-references to both.
- If one is a strict subset of another, note it in the smaller file: `> This feature is a subset of #<OTHER_NUMBER>. Consider implementing together.`
### 1.6 Detect In-Flight Work (avoid duplicate effort)
For each issue number, check whether an open PR or branch already targets it:
```bash
# Open PRs that link the issue
gh pr list --repo <owner>/<repo> --state open --search "linked:#<NUMBER>" --json number,title,headRefName,updatedAt,author
# Local branches that mention the issue number
git branch -a | grep -E "(^|/)(feat|fix|refactor)/.*-?<NUMBER>(-|$)" || true
```
If a PR or branch already exists:
- Mark the idea file with `> ⚠️ In-flight: PR #<PR_NUMBER> by @<author> / branch <name> (last activity <date>)` near the top.
- **Skip Phase 2 research and Phase 4 planning** for this feature — the implementation is already in motion.
- In the Phase 3 report, list it under a separate "🚧 Already in progress" bucket; do NOT count it as VIABLE for implementation.
- The idea file will be moved to `_ideia/in_flight/` in Phase 2.5.2 (it stays there permanently, but Phase 1.7 may reclaim it later).
### 1.7 Stale Reclaim (15-day rule)
Some issues sit in `in_flight/` or `need_details/` forever — third-party PRs go cold, authors disappear, the world moves on. This phase reclaims them when they go quiet.
**Trigger conditions** (run for each issue currently in `_ideia/in_flight/` or `_ideia/need_details/`):
```bash
# For IN FLIGHT — last activity on the linked PR (commit OR comment)
gh pr view <PR_NUMBER> --repo <owner>/<repo> --json updatedAt,commits,comments \
--jq '[.updatedAt, (.commits[-1].committedDate // ""), (.comments[-1].createdAt // "")] | max'
# For NEEDS DETAIL — last activity from the issue author (any comment by them)
gh issue view <NUMBER> --repo <owner>/<repo> --json comments,author \
--jq '.author.login as $a | [.comments[] | select(.author.login == $a) | .createdAt] | max // (.createdAt)'
```
Compute the gap in days between the timestamp above and today.
**Reclaim rule:**
| Bucket | Trigger | Action |
| --------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| 🚧 IN FLIGHT | ≥15 days since last PR activity (commit OR comment by PR author) | Post **intent-to-take-over comment** (template below), wait **48h**, then reclaim if no response |
| ❓ NEEDS DETAIL | ≥15 days since last comment by the issue author | Post **gentle nudge** (template below), wait **48h**, then reclaim as VIABLE if no response |
**Intent-to-take-over comment (🚧 IN FLIGHT path)** — translate to `reply_lang`:
```markdown
Hi @<pr_author> and @<issue_author>! 👋
This PR (#<PR>) addressing issue #<NUMBER> hasn't had updates in <N> days. We'd love to ship this feature in our next release.
**Plan:** if there are no updates in the next **48 hours**, our team will take over the work and merge it as part of `release/vX.Y.Z`. The original PR will be referenced and authorship preserved in the commit trailer.
If you're still working on it, just drop a comment here and we'll hold off. Thanks for the contribution either way! 🙏
```
**Gentle nudge (❓ NEEDS DETAIL path)** — translate to `reply_lang`:
```markdown
Hi @<author>! 👋
It's been <N> days since we asked for more details on this feature request. We'd still love to move forward.
**Plan:** if we don't hear back in the next **48 hours**, we'll proceed with our best interpretation of the original request and add it to our backlog for implementation. We'll tag you on the implementation PR so you can review before it ships.
If you still want to provide the details, just reply here — we'll wait. 🙏
```
**Reclaim execution** (only after the 48h grace period, with no new author/PR-author activity):
1. Move the idea file to `_ideia/viable/` (preserve any prior content + add a `> ♻️ Reclaimed on <date> after 15-day inactivity` banner near the top).
2. If it was IN FLIGHT and a research file does not yet exist, run Phase 2 (Research) for it now.
3. Otherwise create the requirements file based on the existing content + a quick research pass.
4. Add a `viable_origin: stale_reclaim` line to the front-matter so the Phase 3 report can flag it.
5. In Phase 5 (commit / PR), include a commit trailer crediting the original PR author if applicable:
```
Originally-proposed-by: @<pr_author> in #<original_pr_number>
```
(This is NOT `Co-Authored-By` — hard rule #16 still applies. It is a free-form trailer that preserves credit without GitHub re-attributing the commit.)
> **Why 15 days + 48h grace?** Long enough that the original contributor has truly moved on; short enough that the feature still ships in the same release cycle. Grace period is documented in `feedback_issue_triage_independence` so we don't default to "trust prior triage" — we verify the silence is real.
---
## Phase 2 — Research: Find Solutions & Build Requirements
For each cataloged idea that is **viable** (aligns with the project's goals) AND not already in flight (per 1.6):
### 2.1 Viability Pre-Check
Before investing in research, quickly assess:
- [ ] Does this feature align with the project's goals and architecture?
- [ ] Is it technically feasible with the current codebase?
- [ ] Does it duplicate existing functionality?
- [ ] Would it introduce breaking changes or security risks?
- [ ] Is there enough detail to understand what's needed?
**Verdict options:**
| Verdict | When | Action |
| --------------------- | ------------------------------------- | --------------------------- |
| ✅ **VIABLE** | Good idea, enough context | Proceed to Research |
| ❓ **NEEDS DETAIL** | Good idea, insufficient spec | Skip research, ask author |
| ⏭️ **DEFER** | Good idea, too complex for this cycle | Catalog only, skip research |
| ❌ **NOT FIT** | Doesn't fit the project | Explain why |
| 🔁 **ALREADY EXISTS** | Feature already implemented | Point to existing feature |
| 🚧 **IN FLIGHT** | PR/branch already exists (from 1.6) | Skip — track only |
### 2.2 Internet Research (for VIABLE features)
For each viable feature, perform systematic research with an **early-stopping criterion**:
> **Stop as soon as EITHER condition is met:**
> - 3 reference implementations show a consistent pattern, OR
> - 1 high-quality repo (≥1k stars, updated within the last 12 months) already solves the problem cleanly.
>
> Cap at 10 repos total. Do NOT exhaustively browse — depth over breadth.
**Step 1 — Web search for similar implementations:**
```
WebSearch("how to implement <feature description> in <tech stack>")
WebSearch("<feature keyword> implementation nextjs typescript 2025 2026")
WebSearch("<feature keyword> open source library npm")
```
**Step 2 — Find reference Git repositories:**
```
WebSearch("site:github.com <feature keyword> <tech stack> stars:>100")
WebSearch("github <feature keyword> implementation recently updated 2026")
```
- Sort by most recently updated.
- For each repository (until stop criterion hit):
- Note the repo URL, star count, last commit date
- Read its README and relevant source files via `WebFetch`
- Extract the architectural approach, patterns used, and key code snippets
**Step 3 — Read API docs and standards:**
If the feature involves an external API, protocol, or standard:
- Find and read the official documentation
- Note version requirements, authentication patterns, rate limits
### 2.3 Create Requirements File
For each researched feature, create a requirements file alongside its idea file:
**Filename**: `<NUMBER>-<kebab-case-short-title>.requirements.md`
```markdown
# Requirements: <Feature Title>
> Feature Idea: [#<NUMBER>](./<NUMBER>-<kebab-case-short-title>.md)
> Research Date: <YYYY-MM-DD>
> Verdict: ✅ VIABLE
## 🔍 Research Summary
<Brief summary of what was found during research>
## 📚 Reference Implementations
| # | Repository | Stars | Last Updated | Approach | Relevance |
| --- | ---------------- | ----- | ------------ | -------- | ------------ |
| 1 | [repo/name](url) | ⭐ N | YYYY-MM-DD | <brief> | High/Med/Low |
| 2 | ... | | | | |
### Key Patterns Found
- <pattern 1 with code snippet or link>
- <pattern 2>
## 📐 Proposed Solution Architecture
### Approach
<Describe the chosen approach based on research findings>
### New Files
| File | Purpose |
| --------------------- | ------------- |
| `path/to/new/file.ts` | <description> |
### Modified Files
| File | Changes |
| -------------------------- | -------------- |
| `path/to/existing/file.ts` | <what changes> |
### Database Changes
- <migrations needed, if any>
### API Changes
- <new/modified endpoints, if any>
### UI Changes
- <new/modified pages/components, if any>
## ⚙️ Implementation Effort
- **Estimated complexity**: Low / Medium / High / Very High
- **Estimated files changed**: ~N
- **Dependencies needed**: <new npm packages, if any>
- **Breaking changes**: Yes/No — <details>
- **i18n impact**: <number of new translation keys>
- **Test coverage needed**: <brief description>
## ⚠️ Open Questions
- <question 1>
- <question 2>
## 🔗 External References
- <documentation URLs>
- <API references>
```
---
## Phase 2.5 — Organize: Sort Files into Category Directories
> **⚠️ This phase only moves files. It does NOT post comments or close issues.** All GitHub-visible actions are deferred to Phase 3.2 (after human approval).
### 2.5.1 Create Directory Structure
// turbo
```bash
mkdir -p <project_root>/_ideia/viable
mkdir -p <project_root>/_ideia/implemented
mkdir -p <project_root>/_ideia/need_details
mkdir -p <project_root>/_ideia/defer
mkdir -p <project_root>/_ideia/notfit
mkdir -p <project_root>/_ideia/exists
mkdir -p <project_root>/_ideia/in_flight
```
> **Permanent archives**: `need_details/`, `defer/`, `notfit/`, `exists/`, `in_flight/`. Even after the upstream issue is closed, the local file stays — future cycles may revisit.
### 2.5.2 Move Idea Files to Category Subdirectories
After classification, move EVERY idea file to its correct subdirectory (still local-only — no GitHub side-effects):
```bash
# ✅ VIABLE — move idea + requirements files
mv _ideia/<NUMBER>-*.md _ideia/viable/
mv _ideia/<NUMBER>-*.requirements.md _ideia/viable/
# ❓ NEEDS DETAIL — viable but waiting for author response (issue stays OPEN)
mv _ideia/<NUMBER>-*.md _ideia/need_details/
# ⏭️ DEFER — issue will be CLOSED but file is kept permanently for future re-evaluation
mv _ideia/<NUMBER>-*.md _ideia/defer/
# ❌ NOT FIT — issue will be CLOSED but file is kept permanently
mv _ideia/<NUMBER>-*.md _ideia/notfit/
# 🔁 ALREADY EXISTS — issue will be CLOSED but file is kept permanently (separate bucket from NOT FIT)
mv _ideia/<NUMBER>-*.md _ideia/exists/
# 🚧 IN FLIGHT — issue stays OPEN, third-party PR is handling it; file kept permanently for Phase 1.7 stale-reclaim
mv _ideia/<NUMBER>-*.md _ideia/in_flight/
```
No idea files should remain in `_ideia/` root after this step.
---
## Phase 3 — Report: Present Findings & Get Human Approval
### 3.1 🛑 MANDATORY STOP — Present Consolidated Report
After completing Phase 1, Phase 2, and Phase 2.5, **STOP and present the following report** in the chat. **No comments have been posted to GitHub yet** — that happens in 3.2 after approval.
Present a structured report containing:
#### 3.1a — Feature Summary Table
| # | Issue | Title | Verdict | Local Location | Planned GitHub Action |
| --- | ----- | ----- | ----------------- | ----------------------- | -------------------------------------- |
| 1 | #N | Title | ✅ VIABLE | `_ideia/viable/` | Comment + keep OPEN |
| 2 | #N | Title | ⏭️ DEFER | `_ideia/defer/` | Comment + CLOSE |
| 3 | #N | Title | ❌ NOT FIT | `_ideia/notfit/` | Comment + CLOSE |
| 4 | #N | Title | 🔁 EXISTS | `_ideia/exists/` | Comment with location + CLOSE |
| 5 | #N | Title | ❓ NEEDS DETAIL | `_ideia/need_details/` | Comment with questions + keep OPEN |
| 6 | #N | Title | 🚧 IN FLIGHT | `_ideia/in_flight/` | None — PR #M handles it |
| 7 | #N | Title | ♻️ RECLAIMED | `_ideia/viable/` | Intent comment posted in Phase 1.7 |
#### 3.1b — Viable Features Detail
For each VIABLE feature, provide a brief paragraph:
- What was found during research (with stop reason: "3-pattern consistency" or "dominant repo")
- The proposed approach
- Key risks or unknowns
- Which reference repositories were most useful
#### 3.1c — Issues Requiring Author Feedback
For features marked ❓ NEEDS DETAIL, list:
- What specific information is missing
- What examples or repository references would help
- Detected `reply_lang` for the question post
#### 3.1d — Ask for User Confirmation
End the report with:
> **Ready to proceed?**
>
> Approving will (a) post comments on GitHub in the detected language of each issue and (b) close DEFER / NOT FIT / EXISTS issues. VIABLE and NEEDS DETAIL stay open.
>
> - Reply **"sim"** / **"yes"** to post all comments AND generate implementation plans for all VIABLE features.
> - Reply **"only comments"** to post comments without generating plans yet.
> - Reply with specific issue numbers to scope the action.
> - Reply **"não"** / **"no"** to stop without touching GitHub.
### 3.2 Post GitHub Comments & Close Issues (only after approval)
> **⚠️ Do NOT execute this step without explicit user approval from 3.1d.**
For each issue, translate the appropriate template below into the `reply_lang` recorded in its idea file front-matter, then post. The English templates are reference only — never post the English version verbatim to a non-English issue.
---
#### For 🔁 ALREADY EXISTS — Comment + CLOSE issue
The feature already exists in the system. Explain WHERE it is and HOW to use it.
```markdown
Hi @<author>! Thanks for the suggestion! 🙏
Great news — this functionality **already exists** in OmniRoute:
**📍 Where to find it:** <exact dashboard path or settings location>
**🔧 How to use it:**
1. <step 1>
2. <step 2>
3. <step 3>
If you have any trouble finding or using it, feel free to ask in a Discussion. We're always happy to help!
Closing this as the feature is already available. 🎉
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
---
#### For ⏭️ DEFER — Comment + CLOSE issue
Thank the user, explain the idea was cataloged, and that we'll study it before implementing.
```markdown
Hi @<author>! Thanks for this thoughtful feature request! 🙏
We really appreciate the detailed proposal. We've **cataloged your idea** and it's now part of our improvement backlog.
Due to the **significant architectural impact** of this feature, we'll need to conduct thorough use-case studies and architectural analysis before we start development. This ensures we build it right and don't introduce regressions.
**What happens next:**
- Your idea is saved in our internal feature backlog
- We'll conduct architecture studies when this area is prioritized
If you want to track progress, please **subscribe to the repository releases** — every implemented feature is announced in the CHANGELOG.
Thank you for contributing to OmniRoute's roadmap! 🚀
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
---
#### For ❌ NOT FIT — Comment + CLOSE issue
Politely explain why the feature doesn't fit the project scope.
```markdown
Hi @<author>! Thanks for the suggestion! 🙏
After careful analysis, we've determined that this feature **falls outside OmniRoute's core scope** as a proxy/router.
**Reason:** <explain why — e.g., "Telegram integration belongs in the application/orchestrator layer that consumes OmniRoute's API, not inside the router itself.">
**Alternative:** <suggest an alternative approach if possible>
We appreciate you thinking of ways to improve OmniRoute! If you'd like to discuss this further, feel free to open a Discussion. 🙏
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
---
#### For ❓ NEEDS DETAIL — Comment (keep OPEN)
Ask for the specific missing details needed.
```markdown
Hi @<author>! Thanks for the feature request — it's an interesting idea and we'd love to explore it further. 🙏
To move forward, we need a few more details:
1. <specific question 1>
2. <specific question 2>
3. <specific question 3>
If you know of any **open-source projects or repositories** that implement something similar, please share links — it would help us design the best solution.
Looking forward to your response! 🚀
```
---
#### For ✅ VIABLE — Comment (keep OPEN)
Thank the user, confirm we've cataloged their idea, and explain that progress is tracked in releases.
```markdown
Hi @<author>! Thanks for the great feature suggestion! 🙏
We've analyzed your request and it aligns well with OmniRoute's roadmap. We've **cataloged this feature** and it's in our implementation backlog.
**Status:** 📋 Cataloged for future implementation
This issue will be **closed automatically by the merge commit** when the feature ships. To follow along, you can subscribe to repository releases or watch this issue.
Thank you for helping improve OmniRoute! 🚀
```
**⚠️ Do NOT close viable issues — they remain OPEN until the implementation PR closes them via commit message.**
---
## Phase 4 — Plan: Generate Implementation Plans (after user says "yes")
> **⚠️ Do NOT enter this phase without explicit user approval from Phase 3.**
### 4.1 Pre-Plan Context Load (mandatory)
Before writing ANY plan, read:
1. `docs/architecture/REPOSITORY_MAP.md` — to know which directory owns what.
2. `docs/architecture/CODEBASE_DOCUMENTATION.md` — for the engineering reference.
3. The matching "Adding a New X" scenario from `CLAUDE.md` (provider, API route, DB module, MCP tool, A2A skill, cloud agent, embedded service, guardrail, eval, skill, webhook event).
4. Any docs linked from the requirements file's "External References" section.
This ensures plans cite real paths and follow the established add-a-X recipe, instead of inventing structure.
### 4.2 Create Task Directory
```bash
mkdir -p <project_root>/_tasks/features-vX.Y.Z/
```
### 4.3 Generate One Implementation Plan Per Feature
For each VIABLE feature approved by the user, create:
**Filename**: `_tasks/features-vX.Y.Z/<NUMBER>-<kebab-case-title>.plan.md`
```markdown
# Implementation Plan: <Feature Title>
> Issue: #<NUMBER>
> Idea: [\_ideia/viable/<NUMBER>-title.md](../../_ideia/viable/<NUMBER>-title.md)
> Requirements: [\_ideia/viable/<NUMBER>-title.requirements.md](../../_ideia/viable/<NUMBER>-title.requirements.md)
> Branch: `release/vX.Y.Z`
> Matching CLAUDE.md recipe: <e.g. "Adding a New Provider">
## Overview
<Brief description of what will be built>
## Pre-Implementation Checklist
- [ ] Read all related source files listed below
- [ ] Confirm no conflicts with in-flight PRs (re-run Phase 1.6 lookup)
- [ ] Verify database migration numbering (next free integer in `src/lib/db/migrations/`)
## Implementation Steps
### Step 1: <Title>
**Files:**
- `path/to/file.ts` — <what to change>
**Details:**
<Detailed description of the change, including code patterns to follow, function signatures, etc.>
### Step 2: <Title>
...
### Step N: Tests (MANDATORY per CLAUDE.md hard rule #8)
**New test files:**
- `tests/unit/<test-file>.test.mjs` — <what to test>
**Test cases:**
- [ ] <test case 1>
- [ ] <test case 2>
- [ ] Coverage check: confirm overall coverage stays ≥75% statements/lines/functions, ≥70% branches (hard rule #9)
### Step N+1: i18n
**Translation keys to add:**
- `<namespace>.<key>` — "<English value>"
### Step N+2: Documentation
- [ ] Update CHANGELOG.md (current release section)
- [ ] Update relevant docs/ files
- [ ] If touching error responses, follow `docs/security/ERROR_SANITIZATION.md`
- [ ] If touching upstream credentials, follow `docs/security/PUBLIC_CREDS.md`
## Verification Plan (Trust-but-Verify — mandatory before declaring done)
1. `git status` + `git diff --stat` — review every changed file; flag anything outside the plan's declared scope
2. `npm run lint` — 0 new errors
3. `npm run typecheck:core` — clean
4. `npm run typecheck:noimplicit:core` — clean
5. `npm run check:cycles` — no new circular deps
6. `npm run build` — must pass
7. `npm run test:coverage` — coverage gate respected
8. `npm run check-docs-sync` (via pre-commit hook) — passes
9. Manual UI verification if the feature touches frontend (start dev server, exercise golden path + 1 edge case)
## Commit Plan
```
feat: <description> (#<NUMBER>)
```
```
### 4.4 Present Plans for Final Approval
Present a summary of all generated plans:
> **Implementation plans generated:**
>
> | # | Feature | Plan File | Steps | Effort | CLAUDE.md recipe |
> | --- | ------- | ---------------------------------------- | ------- | ------ | ---------------------- |
> | 1 | <title> | `_tasks/features-vX.Y.Z/N-title.plan.md` | N steps | Medium | Adding a New Provider |
>
> Reply **"sim"** / **"yes"** to begin implementation of all features.
> Reply with specific issue numbers to implement only certain ones.
---
## Phase 5 — Execute: Implement the Plans (after user says "yes")
> **⚠️ Do NOT enter this phase without explicit user approval from Phase 4.**
### 5.1 Implement Each Feature
For each approved plan, execute it step by step:
1. **Follow the plan** — implement exactly as specified in the `.plan.md` file
2. **Mark progress** — flip checkboxes to `[x]` in the plan as each step completes
### 5.2 Trust-but-Verify Audit (mandatory before commit)
> Aligned with `~/.claude/CLAUDE.md` global rule: never trust a subagent's summary alone.
Run the full audit checklist from the plan's "Verification Plan" section AND inspect the diff yourself:
```bash
git status
git diff --stat
git diff # full diff, scan for out-of-scope changes
npm run lint
npm run typecheck:core
npm run typecheck:noimplicit:core
npm run check:cycles
npm run build
npm run test:coverage
```
**Block-on-failure checklist:**
- [ ] No files changed outside the plan's declared scope (or scope expansion explicitly justified)
- [ ] No deleted symbols/routes/files without a documented replacement (grep to confirm)
- [ ] No weakened or removed test assertions (only additions or alignments with real behavior)
- [ ] Coverage gate green (75/75/75/70)
- [ ] All commands above exit 0
- [ ] If UI was touched: manual smoke test passed and noted
If any item fails, **fix root cause** before committing. Do NOT bypass with `--no-verify` (hard rule #10).
### 5.3 Commit (one feature, one commit)
```bash
git add <only files in the plan>
git commit -m "feat: <description> (#<NUMBER>)"
```
> **No `Co-Authored-By` trailers** (hard rule #16). Commits go solely under `diegosouzapw`.
Then move (do NOT delete yet) the idea file to `_ideia/implemented/`:
```bash
mv _ideia/viable/<NUMBER>-<title>.md _ideia/implemented/
mv _ideia/viable/<NUMBER>-<title>.requirements.md _ideia/implemented/ 2>/dev/null || true
```
> **Why move, not delete?** If the release PR is reverted or rebased, we still have the context. The file is deleted only after the PR merges to `main` (see 5.6).
Continue to the next feature on the same branch — do NOT switch branches between features.
### 5.4 Respond to Authors
For each implemented feature, post a final close-comment **translated into the issue's `reply_lang`**:
```markdown
✅ **Implemented in `release/vX.Y.Z`!**
Hi @<author>! Great news — your feature request has been implemented! 🎉
**What was done:**
- <bullet list of what was built>
**How to try it (after the release PR merges):**
```bash
git fetch origin && git checkout main && git pull
npm install && npm run dev
```
This will be included in the upcoming **vX.Y.Z** release. Feel free to reopen if you spot any issues! 🚀
```
```bash
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<translated comment>"
```
### 5.5 Finalize the Release Branch
After implementing all approved features:
1. **Update CHANGELOG.md** on the release branch with all new feature entries
2. Push: `git push origin release/vX.Y.Z`
3. Hand off to `/generate-release` for the "Tests → Commit → Push → PR to main" stage. Refer to it **by stage name**, not step number, so this command does not break if `/generate-release` renumbers steps.
### 5.6 Post-Merge Cleanup (only after release PR merges to main)
Once the release PR is merged:
```bash
# Now safe to delete — commit history + CHANGELOG are the source of truth
rm _ideia/implemented/<NUMBER>-*.md
```
> If running this command before the merge: STOP at 5.5 and skip 5.6. Re-enter the workflow later just for the cleanup.
### 5.7 Final Summary Report
Present a final summary report to the user:
| Issue | Title | Verdict | Action | Commit |
| ----- | ----- | ---------------- | --------------------------------------------------------------- | --------- |
| #N | Title | ✅ Implemented | Issue closed, idea file in `_ideia/implemented/` (until merge) | `abc1234` |
| #N | Title | ♻️ Reclaimed | Was IN FLIGHT / NEEDS DETAIL, reclaimed after 15d → implemented | `abc1234` |
| #N | Title | ⏭️ Deferred | Issue closed + permanent archive in `_ideia/defer/` | — |
| #N | Title | ❌ Not Fit | Issue closed + permanent archive in `_ideia/notfit/` | — |
| #N | Title | 🔁 Exists | Issue closed + permanent archive in `_ideia/exists/` | — |
| #N | Title | ❓ Needs Detail | Issue OPEN, archive in `_ideia/need_details/` | — |
| #N | Title | 🚧 In Flight | Issue OPEN, archive in `_ideia/in_flight/`, tracked by PR #M | — |
Include:
- Total features harvested
- Total ideas archived per bucket (`need_details/` / `defer/` / `notfit/` / `exists/` / `in_flight/`)
- Total features implemented (idea files in `_ideia/implemented/`, awaiting post-merge cleanup)
- Total reclaimed via Phase 1.7 (stale 15-day rule)
- Total issues closed
- Total issues left open (NEEDS DETAIL + VIABLE-pending + IN FLIGHT)
- Audit results: lint / typecheck / cycles / build / coverage (pass-count per phase)
- Languages used in posted comments (e.g. "3× pt-BR, 5× en, 1× es")

View File

@@ -1,51 +0,0 @@
---
name: issue-triage-ag
description: How to respond to GitHub issues with insufficient information
---
# Issue Triage Workflow
Respond to GitHub issues that need more information before they can be investigated.
## Steps
### 1. Identify issues needing triage
```bash
gh issue list --state open --limit 20
```
### 2. Evaluate each issue
Check if the issue has:
- Clear reproduction steps
- Environment details (OS, Node.js version, OmniRoute version)
- Error logs/screenshots
- Expected vs actual behavior
### 3. Respond with triage template
For issues missing information:
```markdown
Thank you for reporting this issue! To help us investigate, please provide:
1. **OmniRoute version**: (`omniroute --version`)
2. **Node.js version**: (`node --version`)
3. **Operating system**: (e.g., Ubuntu 24.04, macOS 15, Windows 11)
4. **Installation method**: (npm, Docker, source)
5. **Steps to reproduce**: (exact commands/actions that trigger the issue)
6. **Error logs**: (paste relevant logs from the console)
7. **Expected behavior**: (what should happen)
This will help us debug and resolve your issue faster. 🙏
```
### 4. Label the issue
Add appropriate labels: `needs-info`, `bug`, `enhancement`, `question`, etc.
```bash
gh issue edit <NUMBER> --add-label "needs-info"
```

View File

@@ -1,51 +0,0 @@
---
name: issue-triage-cc
description: How to respond to GitHub issues with insufficient information
---
# Issue Triage Workflow
Respond to GitHub issues that need more information before they can be investigated.
## Steps
### 1. Identify issues needing triage
```bash
gh issue list --state open --limit 20
```
### 2. Evaluate each issue
Check if the issue has:
- Clear reproduction steps
- Environment details (OS, Node.js version, OmniRoute version)
- Error logs/screenshots
- Expected vs actual behavior
### 3. Respond with triage template
For issues missing information:
```markdown
Thank you for reporting this issue! To help us investigate, please provide:
1. **OmniRoute version**: (`omniroute --version`)
2. **Node.js version**: (`node --version`)
3. **Operating system**: (e.g., Ubuntu 24.04, macOS 15, Windows 11)
4. **Installation method**: (npm, Docker, source)
5. **Steps to reproduce**: (exact commands/actions that trigger the issue)
6. **Error logs**: (paste relevant logs from the console)
7. **Expected behavior**: (what should happen)
This will help us debug and resolve your issue faster. 🙏
```
### 4. Label the issue
Add appropriate labels: `needs-info`, `bug`, `enhancement`, `question`, etc.
```bash
gh issue edit <NUMBER> --add-label "needs-info"
```

View File

@@ -1,51 +0,0 @@
---
name: issue-triage-cx
description: How to respond to GitHub issues with insufficient information
---
# Issue Triage Workflow
Respond to GitHub issues that need more information before they can be investigated.
## Steps
### 1. Identify issues needing triage
```bash
gh issue list --state open --limit 20
```
### 2. Evaluate each issue
Check if the issue has:
- Clear reproduction steps
- Environment details (OS, Node.js version, OmniRoute version)
- Error logs/screenshots
- Expected vs actual behavior
### 3. Respond with triage template
For issues missing information:
```markdown
Thank you for reporting this issue! To help us investigate, please provide:
1. **OmniRoute version**: (`omniroute --version`)
2. **Node.js version**: (`node --version`)
3. **Operating system**: (e.g., Ubuntu 24.04, macOS 15, Windows 11)
4. **Installation method**: (npm, Docker, source)
5. **Steps to reproduce**: (exact commands/actions that trigger the issue)
6. **Error logs**: (paste relevant logs from the console)
7. **Expected behavior**: (what should happen)
This will help us debug and resolve your issue faster. 🙏
```
### 4. Label the issue
Add appropriate labels: `needs-info`, `bug`, `enhancement`, `question`, etc.
```bash
gh issue edit <NUMBER> --add-label "needs-info"
```

View File

@@ -1,545 +0,0 @@
---
name: port-upstream-features-ag
description: Migrated command port-upstream-features-ag
---
# /port-upstream-features — Port Features from Upstream Projects
## ⚠️ CONFIDENTIAL — This workflow is `.gitignored` and must NEVER be committed.
## Overview
Port features from upstream open-source projects (e.g. [`decolua/9router`](https://github.com/decolua/9router))
into OmniRoute, adapting them for TypeScript and the OmniRoute architecture,
while giving full attribution to the original authors.
The user provides one or more upstream PR identifiers (numbers or URLs).
The agent fetches the source, plans the adaptation, and generates a
structured task file for implementation, then opens a per-port PR on
**`diegosouzapw/OmniRoute`** (never on the upstream tracker).
Companion: `port-upstream-issues-ag.md` (covers upstream **issues**, not PRs).
## Inputs
The user provides:
- One or more **upstream PR identifiers** — bare numbers (`1317 1320`),
full URLs (`https://github.com/decolua/9router/pull/1317`), or a mix.
- Optionally, notes about scope or which strategies to use.
If no input is provided, the agent harvests open upstream PRs and asks
the user which to port before doing anything else.
## Constants (hard-coded — do not infer)
- **Upstream**: `decolua/9router` (JavaScript, Next.js 16)
- **Fork (origin)**: `diegosouzapw/OmniRoute` (TypeScript, Next.js 16)
- **Worktree root**: `.claude/worktrees/`
- **Task notes dir**: `_tasks/features-v${VERSION}/port-tasks/`
- **Dedupe ledger**: `_tasks/features-v${VERSION}/port-tasks/_ported.jsonl`
- **Upstream sources mirror (read-only)**: `_references/9router/`
## Architecture mapping (upstream → OmniRoute)
This table is the single source of truth for where upstream files land in
OmniRoute. OmniRoute has layers that don't exist upstream (a2a, memory,
cloudAgent, guardrails, evals, services bootstrap); when an upstream PR
touches functionality routed through one of those layers downstream, MAP
IT and note it in the task note.
| Upstream (9router, JS) | OmniRoute (TS) | Notes |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `src/app/api/v1/...` | `src/app/api/v1/...` | Public LLM API surface — same shape |
| `src/app/api/...` (dashboard / cli-tools / oauth) | `src/app/api/...` | Internal dashboard API |
| `src/app/(dashboard)/dashboard/...` | `src/app/(dashboard)/dashboard/...` | UI |
| `src/app/landing/` | `src/app/landing/` | Marketing pages |
| `src/sse/handlers/` `src/sse/services/` | `src/sse/handlers/` `src/sse/services/` | Legacy streaming layer (still active in both) |
| `open-sse/handlers/` | `open-sse/handlers/` | Modern handler layer |
| `open-sse/executors/*.js` | `open-sse/executors/*.ts` | One per provider — JS → TS rewrite |
| `open-sse/services/` | `open-sse/services/` | Combo, accountFallback, model, etc. |
| `open-sse/translator/` `open-sse/transformer/` | `open-sse/translator/` `open-sse/transformer/` | Format conversion + Responses API |
| `open-sse/rtk/` (request toolkit) | `open-sse/services/` or `open-sse/utils/` | No 1:1 — fold into nearest service |
| `open-sse/config/` `open-sse/utils/` `open-sse/lib/` | `open-sse/config/` `open-sse/utils/` `open-sse/lib/` | |
| `src/lib/mcp/` | `open-sse/mcp-server/` | MCP moved into open-sse workspace |
| `src/lib/db/` (adapters / helpers / migrations / repos) | `src/lib/db/` (45+ domain modules, 55 migrations) | `localDb.ts` is RE-EXPORT ONLY (hard rule #2) |
| `src/lib/oauth/` | `src/lib/oauth/` | |
| `src/lib/auth/` | `src/server/authz/` + `src/lib/auth*` | OmniRoute splits server-side vs lib helpers |
| `src/lib/network/` | `src/shared/utils/` or `open-sse/utils/` | Fold by purpose |
| `src/lib/tunnel/` `src/lib/updater/` `src/lib/usage/` | `src/lib/services/` (bootstrap) + module per concern | OmniRoute consolidates as embedded services |
| `src/mitm/` | `src/mitm/` | Cert / dns / handlers preserved |
| `src/models/` | `src/models/` | Domain models |
| `src/shared/` | `src/shared/` | Constants, components, hooks, services, utils |
| `src/store/` (Zustand) | `src/store/` | |
| `src/i18n/` + `public/i18n/literals/` | `src/i18n/` + `public/i18n/literals/` | i18n keys MUST be added in ALL locales |
| `skills/9router-*` (top-level spec dirs) | `src/lib/skills/` (framework) + `skills/` (specs) | Different shape — framework vs spec files |
| `cli/` | `bin/` (entry) + `src/lib/services/` modules | OmniRoute folded most CLI into the main app |
| `gitbook/` | `docs/` | Markdown only; no gitbook in OmniRoute |
| (no equivalent upstream) | `src/lib/a2a/` `src/lib/memory/` `src/lib/cloudAgent/` `src/lib/guardrails/` `src/lib/evals/` `electron/` `tests/` | OmniRoute-only — never port AWAY from these |
## Steps
### 1. Sanity + setup
```bash
git -C . remote get-url origin # must end in diegosouzapw/OmniRoute
git branch --show-current # must be release/vX.Y.Z
gh auth status
VERSION=$(node -p "require('./package.json').version")
RELEASE_BRANCH=$(git branch --show-current)
# Idempotent upstream remote for Strategy B (cherry-pick)
git remote get-url upstream 2>/dev/null \
|| git remote add upstream https://github.com/decolua/9router.git
git fetch upstream --quiet
# License gate — confirm once per session, cache the LICENSE blob hash
UPSTREAM_LICENSE_SHA=$(git -C _references/9router rev-parse HEAD:LICENSE 2>/dev/null)
echo "Upstream LICENSE blob: $UPSTREAM_LICENSE_SHA"
# Read _references/9router/LICENSE and confirm permissive (MIT / Apache-2.0 / BSD-style).
# If unsure or the hash changed since last session, ESCALATE TO USER before continuing.
mkdir -p "_tasks/features-v${VERSION}/port-tasks"
touch "_tasks/features-v${VERSION}/port-tasks/_ported.jsonl"
```
The task folder uses the **current development version** (always 1 patch
above the last released). If on `main`, follow `/generate-release` Phase
1 steps 15 to create the next `release/vX.Y.Z` before continuing. All
work BRANCHES off the release branch.
### 2. Discover open upstream PRs (only if no input)
`gh ... --json` can silently truncate large result sets. Use the
numbers-only → batched-metadata pattern:
```bash
TARGETS="_tasks/features-v${VERSION}/port-tasks/_discovery.txt"
# 2a — numbers only, never truncated
gh pr list --repo decolua/9router --state open --limit 500 \
--json number --jq '.[].number' \
> "$TARGETS"
# 2b — full metadata per PR, batched
while read N; do
gh pr view "$N" --repo decolua/9router \
--json number,title,author,createdAt,additions,deletions,labels,mergeable
done < "$TARGETS" > "_tasks/features-v${VERSION}/port-tasks/_discovery.jsonl"
# 2c — open upstream issues for cross-reference (which PR closes which issue)
gh issue list --repo decolua/9router --state open --limit 500 \
--json number,title --jq 'sort_by(.number)' \
> "_tasks/features-v${VERSION}/port-tasks/_open_issues.json"
```
Group results by intent (fix / feat / chore / docs), summarise risk and
size, then ask the user which PRs to port. Wait for explicit selection.
### 3. Read Upstream PR Source Code (per PR)
For each PR — first normalize input (URL → bare number) and run the
dedupe pre-check BEFORE any expensive fetch / diff work:
```bash
# normalize: "https://github.com/decolua/9router/pull/1317" → "1317"
N=$(echo "$arg" | sed -E 's|.*/pull/([0-9]+).*|\1|; s|^#||')
# dedupe — defense in depth (JSONL snapshot + git log as source of truth)
LEDGER="_tasks/features-v${VERSION}/port-tasks/_ported.jsonl"
if grep -q "\"upstream\":${N}\b" "$LEDGER" 2>/dev/null \
|| git log --all --grep "Inspired-by:.*decolua/9router/pull/${N}\b" --oneline | grep -q .; then
echo "PR #${N} already ported — skipping"; continue
fi
```
Then fetch metadata, diff, commits, and author identity for attribution:
```bash
gh pr view "$N" --repo decolua/9router \
--json number,title,author,body,files,additions,deletions,baseRefOid,headRefOid,mergeable,state
gh pr diff "$N" --repo decolua/9router \
> "_tasks/features-v${VERSION}/port-tasks/diff-${N}.patch"
gh api "repos/decolua/9router/pulls/${N}/commits" \
--jq '.[] | {sha, message: .commit.message, author: .commit.author}'
# Author identity used in the Co-authored-by trailer. Prefer the first
# commit's author (PR author may differ — e.g. a maintainer who pushed it).
gh api "repos/decolua/9router/pulls/${N}/commits" \
--jq '.[0].commit.author | "\(.name) <\(.email)>"'
# Cross-ref: upstream issues this PR closes (GraphQL — REST `gh pr view`
# does NOT expose `closingIssuesReferences`).
gh api graphql -f query='
query($owner: String!, $repo: String!, $num: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $num) {
closingIssuesReferences(first: 20) { nodes { number } }
}
}
}' -F owner=decolua -F repo=9router -F num="$N" \
--jq '.data.repository.pullRequest.closingIssuesReferences.nodes[]?.number'
```
### 4. Analyze Compatibility
For each upstream PR, analyse using the **Architecture mapping** table at
the top of this file:
- **Architecture mapping**: which upstream files land in which OmniRoute
files? Read each equivalent OmniRoute file (not just the upstream
copy in `_references/9router/`).
- **Language adaptation**: JS → TS — type signatures, null/undefined,
`unknown` vs `any`, ESM vs CJS quirks.
- **Dependencies**: new npm packages? Check `package.json` of both.
- **Schema changes**: DB migrations required? How do they interact with
the existing 55 migrations?
- **Tests**: which OmniRoute test suite covers this? Default to
`tests/unit/<scope>.test.ts` using `node:test`; MCP via
`vitest.mcp.config.ts`.
- **Security**: any security considerations during adaptation (input
validation, public-cred handling, error sanitization)?
- **i18n**: new UI strings → translation keys in ALL locales
(`src/i18n/` + `public/i18n/literals/`).
- **OmniRoute-only impact**: does this touch a2a / memory / cloudAgent /
guardrails / evals? Note in the task plan.
### 5. Create Task Directory & Generate Task File
```bash
TASK_DIR="_tasks/features-v${VERSION}/port-tasks"
SEQ=$(printf "%02d" $(( $(ls "$TASK_DIR"/*.plan.md 2>/dev/null | wc -l) + 1 )))
```
File naming: `<seq>-<short-kebab-name>.plan.md`, e.g.
`01-provider-quota-grouped-layout.plan.md`. Sequence is zero-padded so
files sort lexicographically.
#### Task file template
```markdown
# Port: <Feature Name>
## Source
| Field | Value |
|-------|-------|
| **Upstream project** | [9router](https://github.com/decolua/9router) |
| **Upstream PR** | [#<number>](https://github.com/decolua/9router/pull/<number>) |
| **PR author** | [@<pr-username>](https://github.com/<pr-username>) |
| **First-commit author** | `<Name> <<email>>` (used in `Co-authored-by` trailer) |
| **Closing upstream issues** | <list from GraphQL `closingIssuesReferences`, or "none"> |
| **Date analyzed** | <YYYY-MM-DD> |
## Summary
<What the feature does in the upstream project.>
## Adaptation plan
### Files to create/modify in OmniRoute
| OmniRoute file | Action | Based on (upstream) |
|----------------|--------|--------------------------------|
| `src/...` | Create | `src/...` (upstream path) |
| `open-sse/...` | Modify | `lib/...` (upstream path) |
### Selected strategy
`A — Manual re-implementation` | `B — Cherry-pick with adaptation` | `C — Direct apply`
### Key adaptations
1. <JS → TS conversion details.>
2. <Architecture differences and how we bridge them.>
3. <OmniRoute-specific integrations (a2a / memory / cloudAgent / guardrails / evals).>
### Dependencies
- [ ] New npm packages: <none / list>
- [ ] DB migration: <none / describe>
- [ ] i18n keys: <none / list — ALL locales>
### Reference files to read during implementation
- `_references/9router/<path1>` (local mirror — preferred)
- `https://github.com/decolua/9router/blob/<branch>/<path1>` (fallback)
## Attribution
When implementing this feature, use these attribution methods:
### 1. Git commit trailer (ONLY place with upstream PR reference)
```
Co-authored-by: <Name> <<email>>
Inspired-by: https://github.com/decolua/9router/pull/<number>
```
> Per CLAUDE.md hard rule #16: `Co-authored-by` is allowed and required
> for human upstream authors; it is forbidden only for AI/bot trailers
> (Claude / GPT / Copilot / etc.).
### 2. CHANGELOG entry (author only — NO upstream link)
```
- **feat(<scope>):** <description>. (thanks @<username>)
```
### 3. PR description block (author only — NO upstream link)
```
## Attribution
Thanks to [@<username>](https://github.com/<username>) for the original implementation.
```
> **Rule**: the upstream PR link is an internal implementation detail.
> It lives ONLY in the commit trailer (`Inspired-by`). The CHANGELOG
> and PR description credit the author naturally, as if they were a
> direct contributor.
## Implementation checklist
- [ ] Read upstream PR diff and reference files
- [ ] Worktree branched off current `release/vX.Y.Z`
- [ ] Files created/modified per adaptation plan
- [ ] TypeScript types added
- [ ] Unit tests added at `tests/unit/<scope>.test.ts`
- [ ] i18n keys added in all locales (if UI-facing)
- [ ] Manual UI smoke on `npm run dev` (if dashboard touched)
- [ ] Commit with `Co-authored-by` + `Inspired-by` trailers
- [ ] CHANGELOG entry inside the PR with `(thanks @<username>)`
- [ ] PR description includes Attribution block (author only)
- [ ] Ledger entry written on PR creation
```
### 6. Present Task to User
After generating the task file(s):
- Show the task file path(s)
- Summarise total LOC, blockers, recommended order
- Explicitly flag:
- New dependencies in `package.json`
- DB migrations
- New i18n keys (all locales)
- Any change to `src/app/api/v1/...` route shapes (public surface)
- Any change to `src/shared/contracts/` (downstream consumers)
- OmniRoute-only layers impacted
- Ask if the user wants to proceed now or save for later
**Do NOT touch code until the user explicitly names which PRs to port.**
### 7. Implementation (one worktree per PR)
#### 7.1 Worktree
```bash
BRANCH="feat/port-pr-${N}-<short-kebab>" # or fix/port-pr-... matching upstream intent
git worktree add ".claude/worktrees/${BRANCH}" -b "$BRANCH" "$RELEASE_BRANCH"
cd ".claude/worktrees/${BRANCH}"
npm install
```
#### 7.2 Strategy decision tree
| Condition | Strategy |
| --------------------------------------------------------------- | ----------------------------------------- |
| Upstream change is JS code → needs TS rewrite (the common case) | **A — Manual re-implementation** (default) |
| Upstream is already TS-compatible AND file paths align 1:1 | **B — Cherry-pick with adaptation** |
| Docs / config / static-asset-only (no executable code) | **C — Direct apply** |
```bash
# Strategy A: re-write upstream change against OmniRoute types & architecture.
# Read _references/9router/<path> for source-of-truth context.
# Attribute upstream author in commit trailer regardless.
# Strategy B: fetch upstream PR head and cherry-pick
git fetch upstream "pull/${N}/head:upstream-pr-${N}"
git cherry-pick upstream-pr-${N} # resolve TS / architecture conflicts manually
# Strategy C: only for docs/config (use 3-way merge so conflicts surface)
git apply --3way "../../_tasks/features-v${VERSION}/port-tasks/diff-${N}.patch"
```
#### 7.3 Implement the feature
Follow the task plan. Keep or port upstream tests, translating them to
OmniRoute conventions:
- Unit: `tests/unit/<scope>.test.ts` with `node:test`
- MCP: via `vitest.mcp.config.ts`
- Integration: `tests/integration/`
- E2E: `tests/e2e/` (Playwright)
#### 7.4 Validate locally — mandatory
```bash
npm run check # lint + test:unit
npm run typecheck:core
npm run typecheck:noimplicit:core
npm run test:vitest # MCP server tests
npm run check:docs-all # docs-sync gates
npm run check:cycles # always — ports often introduce cross-layer imports
```
If contracts / providers / schemas were touched:
```bash
npm run check:route-validation:t06
npm run check:any-budget:t11
```
If end-to-end behaviour is plausibly impacted:
```bash
npm run test:e2e
```
If the diff touches `src/app/(dashboard)/` (UI), manual smoke is
**mandatory** per CLAUDE.md "For UI or frontend changes":
```bash
npm run dev # http://localhost:20128
# Exercise the new/changed UI in a browser. Verify the golden path AND
# at least one edge case. Watch the console for regressions in other tabs.
# Run /capture-release-evidences afterwards if release-evidence is needed.
```
NO `--no-verify`. Do NOT weaken existing tests. Investigate root cause
if anything pre-existing fails.
#### 7.5 Commit with attribution (upstream ref ONLY here)
```bash
git commit -m "$(cat <<'EOF'
<type>(<scope>): <description>
<optional body — root cause / mechanism / user-visible effect>
Co-authored-by: <Name> <<email>>
Inspired-by: https://github.com/decolua/9router/pull/<N>
EOF
)"
```
- The `Inspired-by` link is the ONLY place the upstream PR is referenced.
It MUST NOT appear in the PR body or `CHANGELOG.md`.
- The `Co-authored-by` trailer credits the **human** upstream author.
This is allowed and required by CLAUDE.md hard rule #16 — that rule
bans AI/bot trailers (Claude / GPT / Copilot / etc.), not humans.
- Use lowercase `Co-authored-by:` and `Inspired-by:` (GitHub canonical
render form).
#### 7.6 Update CHANGELOG.md (inside the PR, no upstream link)
In the worktree, append to the current release's section in `CHANGELOG.md`:
```markdown
- **<type>(<scope>):** <description>. (thanks @<upstream-username>)
```
Commit this change in the same PR — either as a separate commit or amended
into the feat/fix commit (operator choice). Credit the upstream author
naturally; **never** reference the upstream PR URL or `decolua/9router`
here.
#### 7.7 Push & open PR (author only, no upstream link)
> **⚠️ FORK-PR GOTCHA**: bare `gh pr create` defaults to the fork's
> PARENT (upstream `decolua/9router`). ALWAYS pass `--repo
> diegosouzapw/OmniRoute`. Verified gotcha (2026-05-23 on ghostty-web).
> Verify with `gh pr view <N> --repo diegosouzapw/OmniRoute` after
> creation.
```bash
git push -u origin "$BRANCH"
OUR_PR_URL=$(gh pr create --repo diegosouzapw/OmniRoute --base "$RELEASE_BRANCH" \
--title "<type>(<scope>): <description>" \
--body "$(cat <<'EOF'
## Summary
<13 bullets>
## Attribution
Thanks to [@<upstream-username>](https://github.com/<upstream-username>) for the original implementation.
## Changes
- <list>
## Test plan
- [ ] npm run check
- [ ] npm run typecheck:core && npm run typecheck:noimplicit:core
- [ ] npm run test:vitest
- [ ] npm run check:docs-all
- [ ] npm run check:cycles
- [ ] npm run test:e2e (if relevant)
- [ ] Manual UI smoke (if dashboard touched)
EOF
)")
```
#### 7.8 Record in dedupe ledger
```bash
echo "{\"upstream\":${N},\"our_pr\":\"${OUR_PR_URL}\",\"branch\":\"${BRANCH}\",\"at\":\"$(date -Iseconds)\"}" \
>> "_tasks/features-v${VERSION}/port-tasks/_ported.jsonl"
```
Step 3's dedupe pre-check reads this on the next run; the `Inspired-by`
trailer in the commit serves as the redundant source of truth.
#### 7.9 Cleanup (after merge / abandonment)
```bash
PR_STATE=$(gh pr view "$OUR_PR_URL" --json state --jq .state)
git worktree remove ".claude/worktrees/${BRANCH}"
if [ "$PR_STATE" = "MERGED" ]; then
git branch -d "$BRANCH"
else
echo "PR not merged (state=$PR_STATE) — keeping branch '$BRANCH'"
fi
```
Task note and ledger entry stay as durable local documentation.
## Hard rules
- All work BRANCHES off `release/vX.Y.Z`. Never off `main`. Never push to
`main` directly.
- One PR per ported upstream PR. Do NOT bundle multiple ports in one PR.
- The upstream PR URL appears ONLY in the commit `Inspired-by` trailer.
Never in PR body, CHANGELOG, or any other surface.
- `Co-authored-by` trailers MUST credit the human upstream author (CLAUDE.md
rule #16 allows humans, bans AI/bot trailers).
- Never widen `src/shared/contracts/` or public route shapes without
explicit user OK.
- Never use `--no-verify`, force-push to release/main, or `--reject` /
`--theirs` / `--ours` to shortcut conflicts.
- Never overwrite a previously-ported PR — the Step 3 dedupe guard
(JSONL + git log on `Inspired-by:`) exists for this; never disable it.
- Verify subagent work yourself per CLAUDE.md: `git status` + `git diff
--stat`, sanity-check scope, and re-run the full validation suite
before accepting any agent-authored change.
- License gate is enforced in Step 1; if the upstream LICENSE blob hash
changes between sessions, re-confirm before continuing.
## Notes
- This workflow is **local-only** and must never be committed to the
repository. The `.md` file is individually listed in `.gitignore`
alongside `port-upstream-issues-ag.md`, and the `_tasks/` directory is
covered by the `/_*/` gitignore rule.
- Task files serve as persistent documentation of what was ported and
from where.
- The dedupe ledger (`_ported.jsonl`) is local-only documentation, NOT
tracked. The git `Inspired-by:` trailer is the authoritative record.
- Companion sibling: `port-upstream-issues-ag.md` for upstream issue
triage and fix porting.

View File

@@ -1,396 +0,0 @@
---
name: port-upstream-features-cc
description: Port one or more open PRs from upstream decolua/9router into OmniRoute, adapt JS→TS, attribute the original author, land via release-branch worktree + per-feature PR.
---
# /port-upstream-features — Port upstream PRs into OmniRoute
## ⚠️ CONFIDENTIAL — this command is `.gitignored` and must NEVER be committed.
Full reference: `.agents/workflows/port-upstream-features-ag.md`.
Sibling command (issue tracker, not PRs): `/port-upstream-issues`.
## Inputs
Arguments: `$ARGUMENTS` (optional). Accepts a space-separated list of
upstream PR identifiers — bare numbers (`1317 1320`), full URLs
(`https://github.com/decolua/9router/pull/1317`), or a mix.
If empty, the command MUST list candidate open upstream PRs first and ask
the user which to port before doing anything else.
## Constants (hard-coded — do not infer)
- Upstream: `decolua/9router` (JavaScript, Next.js 16)
- Fork (origin): `diegosouzapw/OmniRoute` (TypeScript, Next.js 16)
- Worktree root: `.claude/worktrees/`
- Task notes dir: `_tasks/features-v${VERSION}/port-tasks/`
- Dedupe ledger: `_tasks/features-v${VERSION}/port-tasks/_ported.jsonl`
- Upstream sources mirror (read-only): `_references/9router/`
## Architecture mapping (upstream → OmniRoute)
Use this table when planning each port. OmniRoute has layers that don't
exist upstream (a2a, memory, cloudAgent, guardrails, evals, services
bootstrap); when an upstream change touches functionality that lives in
those layers downstream, MAP IT and note it in the task note.
| Upstream (9router, JS) | OmniRoute (TS) | Notes |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `src/app/api/v1/...` | `src/app/api/v1/...` | Public LLM API surface — same shape |
| `src/app/api/...` (dashboard / cli-tools / oauth) | `src/app/api/...` | Internal dashboard API |
| `src/app/(dashboard)/dashboard/...` | `src/app/(dashboard)/dashboard/...` | UI |
| `src/app/landing/` | `src/app/landing/` | Marketing pages |
| `src/sse/handlers/` `src/sse/services/` | `src/sse/handlers/` `src/sse/services/` | Legacy streaming layer (still active in both) |
| `open-sse/handlers/` | `open-sse/handlers/` | Modern handler layer |
| `open-sse/executors/*.js` | `open-sse/executors/*.ts` | One per provider — JS → TS rewrite |
| `open-sse/services/` | `open-sse/services/` | Combo, accountFallback, model, etc. |
| `open-sse/translator/` `open-sse/transformer/` | `open-sse/translator/` `open-sse/transformer/` | Format conversion + Responses API |
| `open-sse/rtk/` (request toolkit) | `open-sse/services/` or `open-sse/utils/` | No 1:1 — fold into nearest service |
| `open-sse/config/` `open-sse/utils/` `open-sse/lib/` | `open-sse/config/` `open-sse/utils/` `open-sse/lib/` | |
| `src/lib/mcp/` | `open-sse/mcp-server/` | MCP moved into open-sse workspace |
| `src/lib/db/` (adapters / helpers / migrations / repos) | `src/lib/db/` (45+ domain modules, 55 migrations) | `localDb.ts` is RE-EXPORT ONLY (hard rule #2) |
| `src/lib/oauth/` | `src/lib/oauth/` | |
| `src/lib/auth/` | `src/server/authz/` + `src/lib/auth*` | OmniRoute splits server-side vs lib helpers |
| `src/lib/network/` | `src/shared/utils/` or `open-sse/utils/` | Fold by purpose |
| `src/lib/tunnel/` `src/lib/updater/` `src/lib/usage/` | `src/lib/services/` (bootstrap) + module per concern | OmniRoute consolidates as embedded services |
| `src/mitm/` | `src/mitm/` | Cert / dns / handlers preserved |
| `src/models/` | `src/models/` | Domain models |
| `src/shared/` | `src/shared/` | Constants, components, hooks, services, utils |
| `src/store/` (Zustand) | `src/store/` | |
| `src/i18n/` + `public/i18n/literals/` | `src/i18n/` + `public/i18n/literals/` | i18n keys MUST be added in ALL locales |
| `skills/9router-*` (top-level spec dirs) | `src/lib/skills/` (framework) + `skills/` (specs) | Different shape — framework vs spec files |
| `cli/` | `bin/` (entry) + `src/lib/services/` modules | OmniRoute folded most CLI into the main app |
| `gitbook/` | `docs/` | Markdown only; no gitbook in OmniRoute |
| (no equivalent upstream) | `src/lib/a2a/` `src/lib/memory/` `src/lib/cloudAgent/` `src/lib/guardrails/` `src/lib/evals/` `electron/` `tests/` | OmniRoute-only — never port AWAY from these |
When a port touches an `(no equivalent)` row downstream, the upstream
change either does not apply, OR you must wire it through one of those
layers. Flag in the task note.
## Execution
### Step 0 — Sanity + setup
```bash
git -C . remote get-url origin # must end in diegosouzapw/OmniRoute
git branch --show-current # must be release/vX.Y.Z (or create one via /generate-release)
gh auth status
VERSION=$(node -p "require('./package.json').version")
RELEASE_BRANCH=$(git branch --show-current)
# Idempotent upstream remote for Strategy B (cherry-pick)
git remote get-url upstream 2>/dev/null \
|| git remote add upstream https://github.com/decolua/9router.git
git fetch upstream --quiet
# License gate — confirm once per session, cache the LICENSE blob hash
UPSTREAM_LICENSE_SHA=$(git -C _references/9router rev-parse HEAD:LICENSE 2>/dev/null)
echo "Upstream LICENSE blob: $UPSTREAM_LICENSE_SHA"
# Read _references/9router/LICENSE and confirm permissive (MIT / Apache-2.0 / BSD-style).
# If unsure or the hash changed since last session, ESCALATE TO USER before continuing.
mkdir -p "_tasks/features-v${VERSION}/port-tasks"
touch "_tasks/features-v${VERSION}/port-tasks/_ported.jsonl"
```
If on `main`, follow `/generate-release` Phase 1 steps 15 to create the
next `release/vX.Y.Z` first.
### Step 1 — Discover (only if no $ARGUMENTS) — two-step harvest
`gh ... --json` can silently truncate large result sets. Use the
numbers-only → batched-metadata pattern:
```bash
TARGETS="_tasks/features-v${VERSION}/port-tasks/_discovery.txt"
# 1a — numbers only, never truncated
gh pr list --repo decolua/9router --state open --limit 500 \
--json number --jq '.[].number' \
> "$TARGETS"
# 1b — full metadata per PR, batched
while read N; do
gh pr view "$N" --repo decolua/9router \
--json number,title,author,createdAt,additions,deletions,labels,mergeable
done < "$TARGETS" > "_tasks/features-v${VERSION}/port-tasks/_discovery.jsonl"
# 1c — open upstream issues for cross-reference (which PR closes which issue)
gh issue list --repo decolua/9router --state open --limit 500 \
--json number,title --jq 'sort_by(.number)' \
> "_tasks/features-v${VERSION}/port-tasks/_open_issues.json"
```
Group results by intent (fix / feat / chore / docs), summarise risk and
size, then ask the user which PRs to port. Wait for explicit selection.
### Step 2 — Per-PR analysis (loop)
For each PR — first normalize input (URL → bare number) and run a dedupe
pre-check BEFORE any expensive fetch / diff work:
```bash
# normalize: "https://github.com/decolua/9router/pull/1317" → "1317"
N=$(echo "$arg" | sed -E 's|.*/pull/([0-9]+).*|\1|; s|^#||')
# dedupe — defense in depth (JSONL snapshot + git log as source of truth)
LEDGER="_tasks/features-v${VERSION}/port-tasks/_ported.jsonl"
if grep -q "\"upstream\":${N}\b" "$LEDGER" 2>/dev/null \
|| git log --all --grep "Inspired-by:.*decolua/9router/pull/${N}\b" --oneline | grep -q .; then
echo "PR #${N} already ported — skipping"; continue
fi
```
Then fetch metadata, diff, commits, author:
```bash
gh pr view "$N" --repo decolua/9router \
--json number,title,author,body,files,additions,deletions,baseRefOid,headRefOid,mergeable,state
gh pr diff "$N" --repo decolua/9router \
> "_tasks/features-v${VERSION}/port-tasks/diff-${N}.patch"
gh api "repos/decolua/9router/pulls/${N}/commits" \
--jq '.[] | {sha, message: .commit.message, author: .commit.author}'
# Author identity used in the Co-authored-by trailer. Prefer the first
# commit's author (PR author may differ — e.g. a maintainer who pushed it).
gh api "repos/decolua/9router/pulls/${N}/commits" \
--jq '.[0].commit.author | "\(.name) <\(.email)>"'
# Cross-ref: upstream issues this PR closes (GraphQL — REST `gh pr view`
# does NOT expose `closingIssuesReferences`).
gh api graphql -f query='
query($owner: String!, $repo: String!, $num: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $num) {
closingIssuesReferences(first: 20) { nodes { number } }
}
}
}' -F owner=decolua -F repo=9router -F num="$N" \
--jq '.data.repository.pullRequest.closingIssuesReferences.nodes[]?.number'
```
Read the diff. Map each upstream file to its OmniRoute equivalent using
the **architecture mapping** table above. Note local commits that overlap
(`git log --oneline -- <our-path>`) and any `_references/9router/<path>`
file you needed to read for source-of-truth context.
Write a task note at
`_tasks/features-v${VERSION}/port-tasks/<seq>-<short-kebab>.plan.md`.
Sequence number = `printf "%02d" $((max_existing + 1))` (zero-padded so
files sort lexicographically). Required fields:
- Upstream source (PR #, title, author, first-commit author identity)
- Files touched (upstream → OmniRoute, per the architecture mapping)
- JS→TS conversion notes
- Dependencies added (npm packages)
- Schema / migration impact
- i18n keys added (with locale coverage checklist)
- OmniRoute-only layers impacted (a2a / memory / cloudAgent / guardrails / evals)
- Selected strategy (A / B / C — see Step 4)
- Closing upstream issues (via GraphQL `closingIssuesReferences`)
- Attribution checklist
### Step 3 — Present plan and wait
Summarise all task notes to the user: total LOC, blockers, recommended
order, and explicitly flag:
- New dependencies in `package.json`
- DB migrations (and how they interact with the 55 existing migrations)
- New i18n keys (MUST be added in ALL locales — `src/i18n/` + `public/i18n/literals/`)
- Any change to `src/app/api/v1/...` route shapes (public surface)
- Any change to `src/shared/contracts/` (downstream consumers)
- OmniRoute-only layers impacted
**Do not touch code until the user names which PRs to port.**
### Step 4 — Implement (one worktree per PR)
For each approved PR:
```bash
BRANCH="feat/port-pr-${N}-<short>" # or fix/port-pr-... matching upstream intent
git worktree add ".claude/worktrees/${BRANCH}" -b "$BRANCH" "$RELEASE_BRANCH"
cd ".claude/worktrees/${BRANCH}"
npm install
```
**Strategy decision tree** (record choice in task note):
| Condition | Strategy |
| ---------------------------------------------------------- | ----------------------------------------- |
| Upstream change is JS code → needs TS rewrite (the common case) | **A — Manual re-implementation** (default) |
| Upstream is already TS-compatible AND file paths align 1:1 | **B — Cherry-pick with adaptation** |
| Docs / config / static-asset-only (no executable code) | **C — Direct apply** |
```bash
# Strategy A: re-write upstream change against OmniRoute types & architecture.
# Read _references/9router/<path> for source-of-truth context.
# Attribute upstream author in commit trailer regardless.
# Strategy B: fetch upstream PR head and cherry-pick
git fetch upstream "pull/${N}/head:upstream-pr-${N}"
git cherry-pick upstream-pr-${N} # resolve TS / architecture conflicts manually
# Strategy C: only for docs/config (use 3-way merge so conflicts surface)
git apply --3way "../../_tasks/features-v${VERSION}/port-tasks/diff-${N}.patch"
```
Keep / port upstream tests. Translate them to OmniRoute test conventions
(`tests/unit/*.test.ts` using `node:test`; MCP via `vitest.mcp.config.ts`).
### Step 5 — Validate (mandatory)
```bash
npm run check # lint + test:unit
npm run typecheck:core
npm run typecheck:noimplicit:core
npm run test:vitest
npm run check:docs-all
npm run check:cycles # always — ports often introduce cross-layer imports
```
If contracts / providers / schemas were touched:
```bash
npm run check:route-validation:t06
npm run check:any-budget:t11
```
If E2E behaviour was plausibly impacted:
```bash
npm run test:e2e
```
If the diff touches `src/app/(dashboard)/` (UI), manual smoke is
**mandatory** per CLAUDE.md "For UI or frontend changes":
```bash
npm run dev # http://localhost:20128
# Exercise the new/changed UI in a browser. Verify the golden path AND
# at least one edge case. Watch the console for regressions in other tabs.
# For release-evidence capture, run /capture-release-evidences afterwards.
```
No `--no-verify`. No weakening of tests. If something fails, fix the root
cause.
### Step 6 — Commit with attribution
```bash
git commit -m "$(cat <<'EOF'
<type>(<scope>): <description>
<optional body — root cause / mechanism / user-visible effect>
Co-authored-by: <Original Author Name> <author@email>
Inspired-by: https://github.com/decolua/9router/pull/<N>
EOF
)"
```
- The `Inspired-by` link is the ONLY place the upstream PR is referenced.
It MUST NOT appear in the PR body or `CHANGELOG.md`.
- The `Co-authored-by` trailer credits the **human** upstream author.
This is allowed and expected by CLAUDE.md hard rule #16 — that rule
bans AI/bot trailers (Claude / GPT / Copilot / etc.), not humans.
- Use lowercase `Co-authored-by:` (GitHub canonical render form).
### Step 7 — Update CHANGELOG.md (inside the PR, no upstream link)
In the worktree, append to the current release's section in `CHANGELOG.md`:
```markdown
- **<type>(<scope>):** <description>. (thanks @<upstream-username>)
```
Commit this change in the same PR — either as a separate commit or amended
into the feat/fix commit (operator choice). Credit the upstream author
naturally as a direct contributor; **never** reference the upstream PR URL
or `decolua/9router` here.
### Step 8 — Push & open PR
> **⚠️ ALWAYS pass `--repo diegosouzapw/OmniRoute`.** Without it,
> `gh pr create` defaults to the **parent** of a GitHub fork — here that
> is upstream `decolua/9router`. Verified gotcha (2026-05-23 on
> ghostty-web): a bare `gh pr create` opened a PR on the upstream
> tracker by accident. Always set `--repo` and verify with
> `gh pr view <N> --repo diegosouzapw/OmniRoute` after creation.
```bash
git push -u origin "$BRANCH"
OUR_PR_URL=$(gh pr create --repo diegosouzapw/OmniRoute --base "$RELEASE_BRANCH" \
--title "<type>(<scope>): <description>" \
--body "$(cat <<'EOF'
## Summary
<13 bullets>
## Attribution
Thanks to [@<upstream-username>](https://github.com/<upstream-username>) for the original implementation.
## Changes
- <list>
## Test plan
- [ ] npm run check
- [ ] npm run typecheck:core && npm run typecheck:noimplicit:core
- [ ] npm run test:vitest
- [ ] npm run check:docs-all
- [ ] npm run check:cycles
- [ ] npm run test:e2e (if relevant)
- [ ] Manual UI smoke (if dashboard touched)
EOF
)")
# Record in dedupe ledger (Step 2 reads this on next run)
echo "{\"upstream\":${N},\"our_pr\":\"${OUR_PR_URL}\",\"branch\":\"${BRANCH}\",\"at\":\"$(date -Iseconds)\"}" \
>> "_tasks/features-v${VERSION}/port-tasks/_ported.jsonl"
```
Return the PR URL to the user.
### Step 9 — Cleanup (after merge / abandonment)
```bash
PR_STATE=$(gh pr view "$OUR_PR_URL" --json state --jq .state)
git worktree remove ".claude/worktrees/${BRANCH}"
if [ "$PR_STATE" = "MERGED" ]; then
git branch -d "$BRANCH"
else
echo "PR not merged (state=$PR_STATE) — keeping branch '$BRANCH'"
fi
```
Task note and ledger entry in `_tasks/features-v${VERSION}/port-tasks/`
stay as durable local documentation.
## Hard rules
- All work BRANCHES off `release/vX.Y.Z`. Never off `main`. Never push to
`main` directly.
- One PR per ported upstream PR. Do NOT bundle multiple ports in one PR.
- The upstream PR URL appears ONLY in the commit `Inspired-by` trailer.
Never in PR body, CHANGELOG, or any other surface.
- `Co-authored-by` trailers MUST credit the human upstream author (CLAUDE.md
rule #16 allows humans, bans AI/bot trailers).
- Never widen `src/shared/contracts/` or public route shapes without
explicit user OK.
- Never use `--no-verify`, force-push to release/main, or `--reject` /
`--theirs` / `--ours` to shortcut conflicts.
- Never overwrite a previously-ported PR — the Step 2 dedupe guard
(JSONL + git log on `Inspired-by:`) exists for this; never disable it.
- Verify subagent work yourself per CLAUDE.md: `git status` + `git diff
--stat`, sanity-check scope, and re-run the full validation suite
before accepting any agent-authored change.
- License gate is enforced in Step 0; if the upstream LICENSE blob hash
changes between sessions, re-confirm before continuing.

View File

@@ -1,521 +0,0 @@
---
name: port-upstream-issues-ag
description: Migrated command port-upstream-issues-ag
---
# /port-upstream-issues — Resolve issues reported on upstream `decolua/9router`
## ⚠️ CONFIDENTIAL — This workflow is `.gitignored` and must NEVER be committed.
## Overview
Companion to `port-upstream-features-ag.md`. While that workflow ports
upstream **PRs**, this one harvests upstream **open issues** (bugs filed on
[`decolua/9router`](https://github.com/decolua/9router)), reproduces them
against OmniRoute, and lands fixes in OmniRoute with full attribution to
the upstream reporter.
This is NOT the same as `/resolve-issues`:
| Workflow | Repo whose issues we read | Issues we close on |
|----------|---------------------------|--------------------|
| `/resolve-issues` | `diegosouzapw/OmniRoute` (our own) | our own |
| `/port-upstream-issues` (this) | `decolua/9router` (upstream, JS) | NONE — we never touch upstream tracker |
> **NEVER comment, close, or react on `decolua/9router`'s issue tracker.**
> Upstream is owned by the original maintainer. Our work is local to
> OmniRoute.
## Inputs
The user provides:
- One or more **upstream issue identifiers** — bare numbers (`1317 1320`),
full URLs (`https://github.com/decolua/9router/issues/1317`), or a mix.
- Optionally, notes about scope or which buckets to skip.
If no input is provided, the agent harvests ALL open upstream issues and
triages before any code change.
## Constants (hard-coded — do not infer)
- **Upstream**: `decolua/9router` (JavaScript, Next.js 16)
- **Fork (origin)**: `diegosouzapw/OmniRoute` (TypeScript, Next.js 16)
- **Worktree root**: `.claude/worktrees/`
- **Task notes**: `_tasks/features-v${VERSION}/port-upstream-issues/`
- **Dedupe ledger**: `_tasks/features-v${VERSION}/port-upstream-issues/_resolved.jsonl`
- **Upstream sources mirror (read-only)**: `_references/9router/`
## Architecture mapping (upstream → OmniRoute)
Single source of truth for where upstream files land in OmniRoute. Use it
when reproducing each bug and planning the fix. OmniRoute has layers that
don't exist upstream (a2a, memory, cloudAgent, guardrails, evals); when
an upstream bug touches functionality routed through one of those layers
downstream, MAP IT and note it in the triage.
| Upstream (9router, JS) | OmniRoute (TS) | Notes |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `src/app/api/v1/...` | `src/app/api/v1/...` | Public LLM API surface — same shape |
| `src/app/api/...` (dashboard / cli-tools / oauth) | `src/app/api/...` | Internal dashboard API |
| `src/app/(dashboard)/dashboard/...` | `src/app/(dashboard)/dashboard/...` | UI |
| `src/app/landing/` | `src/app/landing/` | Marketing pages |
| `src/sse/handlers/` `src/sse/services/` | `src/sse/handlers/` `src/sse/services/` | Legacy streaming layer (still active in both) |
| `open-sse/handlers/` | `open-sse/handlers/` | Modern handler layer |
| `open-sse/executors/*.js` | `open-sse/executors/*.ts` | One per provider — TS in OmniRoute |
| `open-sse/services/` | `open-sse/services/` | Combo, accountFallback, model, etc. |
| `open-sse/translator/` `open-sse/transformer/` | `open-sse/translator/` `open-sse/transformer/` | Format conversion + Responses API |
| `open-sse/rtk/` (request toolkit) | `open-sse/services/` or `open-sse/utils/` | No 1:1 — fold into nearest service |
| `open-sse/config/` `open-sse/utils/` `open-sse/lib/` | `open-sse/config/` `open-sse/utils/` `open-sse/lib/` | |
| `src/lib/mcp/` | `open-sse/mcp-server/` | MCP moved into open-sse workspace |
| `src/lib/db/` (adapters / helpers / migrations / repos) | `src/lib/db/` (45+ domain modules, 55 migrations) | `localDb.ts` is RE-EXPORT ONLY (hard rule #2) |
| `src/lib/oauth/` | `src/lib/oauth/` | |
| `src/lib/auth/` | `src/server/authz/` + `src/lib/auth*` | OmniRoute splits server-side vs lib helpers |
| `src/lib/network/` | `src/shared/utils/` or `open-sse/utils/` | Fold by purpose |
| `src/lib/tunnel/` `src/lib/updater/` `src/lib/usage/` | `src/lib/services/` (bootstrap) + module per concern | OmniRoute consolidates as embedded services |
| `src/mitm/` | `src/mitm/` | Cert / dns / handlers preserved |
| `src/models/` | `src/models/` | Domain models |
| `src/shared/` | `src/shared/` | Constants, components, hooks, services, utils |
| `src/store/` (Zustand) | `src/store/` | |
| `src/i18n/` + `public/i18n/literals/` | `src/i18n/` + `public/i18n/literals/` | i18n keys MUST be added in ALL locales |
| `skills/9router-*` (top-level spec dirs) | `src/lib/skills/` (framework) + `skills/` (specs) | Different shape — framework vs spec files |
| `cli/` | `bin/` (entry) + `src/lib/services/` modules | OmniRoute folded most CLI into the main app |
| `gitbook/` | `docs/` | Markdown only; no gitbook in OmniRoute |
| (no equivalent upstream) | `src/lib/a2a/` `src/lib/memory/` `src/lib/cloudAgent/` `src/lib/guardrails/` `src/lib/evals/` `electron/` `tests/` | OmniRoute-only — bugs here are downstream-specific |
## Steps
### 1. Sanity + setup
```bash
git -C . remote get-url origin # must end in diegosouzapw/OmniRoute
git branch --show-current # must be release/vX.Y.Z
gh auth status
VERSION=$(node -p "require('./package.json').version")
RELEASE_BRANCH=$(git branch --show-current)
# Idempotent upstream remote (may be needed to inspect specific upstream commits when reproducing)
git remote get-url upstream 2>/dev/null \
|| git remote add upstream https://github.com/decolua/9router.git
git fetch upstream --quiet
# License gate — confirm once per session, cache the LICENSE blob hash
UPSTREAM_LICENSE_SHA=$(git -C _references/9router rev-parse HEAD:LICENSE 2>/dev/null)
echo "Upstream LICENSE blob: $UPSTREAM_LICENSE_SHA"
# Read _references/9router/LICENSE and confirm permissive (MIT / Apache-2.0 / BSD-style).
# If unsure or the hash changed since last session, ESCALATE TO USER before continuing.
mkdir -p "_tasks/features-v${VERSION}/port-upstream-issues"
touch "_tasks/features-v${VERSION}/port-upstream-issues/_resolved.jsonl"
```
If on `main`, follow `/generate-release` Phase 1 steps 15 to create the
next `release/vX.Y.Z` before continuing. All work BRANCHES off the release
branch.
### 2. Harvest Open Upstream Issues
⚠️ The JSON output of `gh issue list` can be silently truncated. Use the
two-step approach:
**2a — Numbers only** (small, never truncated):
```bash
HARV="_tasks/features-v${VERSION}/port-upstream-issues"
gh issue list --repo decolua/9router --state open --limit 500 \
--json number --jq '.[].number' \
> "$HARV/_numbers.txt"
wc -l "$HARV/_numbers.txt"
```
**2b — Full metadata per issue** (sequential to avoid rate-limit bursts):
```bash
for N in $(cat "$HARV/_numbers.txt"); do
gh issue view "$N" --repo decolua/9router \
--json number,title,labels,body,comments,createdAt,updatedAt,author,reactionGroups
done > "$HARV/_raw.jsonl"
```
### 3. Cross-Reference Upstream Open PRs
For every issue, check whether an open upstream PR already addresses it
(`fixes #N`, `closes #N`, `for #N`, body mentions). If yes, the canonical
path is **`/port-upstream-features`** with that PR, NOT a re-implementation
here.
```bash
gh pr list --repo decolua/9router --state open --limit 500 \
--json number,title,body \
> "$HARV/_open_prs.json"
```
### 4. Triage Each Issue (NO code yet)
For every issue — first normalize input and run the dedupe pre-check
BEFORE any expensive analysis:
```bash
# normalize: "https://github.com/decolua/9router/issues/1317" → "1317"
N=$(echo "$arg" | sed -E 's|.*/issues/([0-9]+).*|\1|; s|^#||')
# dedupe — defense in depth (JSONL snapshot + git log as source of truth)
LEDGER="$HARV/_resolved.jsonl"
if grep -q "\"upstream\":${N}\b" "$LEDGER" 2>/dev/null \
|| git log --all --grep "Reported-by:.*decolua/9router/issues/${N}\b" --oneline | grep -q .; then
echo "Issue #${N} already resolved here — skipping"; continue
fi
```
Then produce `$HARV/<N>-<short-kebab>.triage.md` using the template at the
bottom of this file. Classify each into ONE bucket:
| Bucket | Meaning | Next action |
|--------|---------|-------------|
| `security` | Security-sensitive (RCE, auth bypass, SSRF, etc.) | Handle FIRST, alone, with its own PR |
| `viable-self` | Bug, reproducible against OmniRoute, fix in scope | Phase 5+ |
| `viable-port` | Already addressed by an open upstream PR | Hand off to `/port-upstream-features` |
| `not-applicable` | Bug specific to 9router internals not mirrored in OmniRoute | Document and skip |
| `needs-repro` | Cannot reproduce locally / not enough info | Document; skip until repro |
| `out-of-scope` | Requires native module changes, new infra, etc. | Document and skip |
| `wontfix` | Conflicts with OmniRoute's direction | Document with reason |
**Reproduction is mandatory before `viable-self`.** OmniRoute is TypeScript
on Next.js; many 9router bugs simply do not exist here because the
implementation is different. If you cannot reproduce against OmniRoute,
the bucket is `not-applicable` or `needs-repro`, never `viable-self`.
Use the architecture mapping above to locate the equivalent OmniRoute
file(s) and read them (NOT just the upstream `_references/9router/` copy)
when deciding reproducibility.
### 5. Analyse Compatibility (for `viable-self`)
For each `viable-self` issue, before writing a fix plan, map:
- **Affected area**: which row of the architecture mapping is hit?
- **Code locality**: read the 9router source files referenced (or implied)
by the issue and the equivalent OmniRoute file(s). Note divergence.
- **JS → TS adaptation**: type signatures, null/undefined handling,
`unknown` vs `any`, ESM vs CJS specifics.
- **DB / schema impact**: any migration needed? How does it interact with
the existing 55 migrations?
- **i18n keys**: any new UI strings → translation keys in ALL locales?
- **OmniRoute-only impact**: does this surface through a2a / memory /
cloudAgent / guardrails / evals?
- **Tests**: which OmniRoute test suite must cover the regression?
Default to `tests/unit/<scope>.test.ts`.
### 6. Present Plan & Wait
Summarise to the user, in this order:
1. **Security findings first** with severity and proposed handling.
2. Counts per bucket and totals.
3. Top `viable-self` ranked by user impact and fix size.
4. Top `viable-port` candidates with upstream PR numbers (hand-off to
`/port-upstream-features`).
5. Open questions for the user (anything ambiguous in `out-of-scope` /
`wontfix` / `not-applicable` that may need re-bucketing).
> **⚠️ Do NOT touch code until the user explicitly names which issues to
> fix in this batch.**
### 7. Implementation (one worktree per fix)
For each approved issue `N`:
```bash
BRANCH="fix/port-issue-${N}-<short-kebab>"
git worktree add ".claude/worktrees/${BRANCH}" -b "$BRANCH" "$RELEASE_BRANCH"
cd ".claude/worktrees/${BRANCH}"
npm install
```
#### 7.1 Write the failing regression test FIRST
Default to `tests/unit/<scope>.test.ts`. For network/E2E-shaped bugs use
`tests/integration/` or `tests/e2e/`. Iterate against the specific file:
```bash
npm run test:unit -- --test tests/unit/<scope>.test.ts
```
#### 7.2 Smallest possible fix
- Do not refactor unrelated code in the same commit.
- Do not change public route shapes unless the issue requires it.
- Match the existing TypeScript style. Run `npm run lint` after editing.
#### 7.3 Validate locally — mandatory
```bash
npm run check # lint + test:unit
npm run typecheck:core
npm run typecheck:noimplicit:core
npm run test:vitest # MCP server tests
npm run check:docs-all # docs-sync gates
npm run check:cycles # always — fixes sometimes add imports
```
If the change touches contracts, providers, or schemas, also:
```bash
npm run check:route-validation:t06
npm run check:any-budget:t11
```
If end-to-end behaviour is plausibly impacted:
```bash
npm run test:e2e
```
If the fix touches `src/app/(dashboard)/` (UI), manual smoke is
**mandatory** per CLAUDE.md "For UI or frontend changes":
```bash
npm run dev # http://localhost:20128
# Reproduce the original bug scenario and verify it's gone.
# Watch the console for regressions in other tabs.
```
NO `--no-verify`. Do NOT weaken existing tests. Investigate root cause if
something pre-existing fails.
#### 7.4 Commit
```bash
git commit -m "$(cat <<'EOF'
fix(<scope>): <description> (port from 9router#<N>)
<short body — root cause and user-visible effect>
Reported-by: <Reporter Name> (https://github.com/decolua/9router/issues/<N>)
EOF
)"
```
- The upstream issue link lives ONLY in this commit trailer. It does NOT
appear in the PR body or in `CHANGELOG.md`.
- If a third party contributed a substantive patch/fix in the upstream
issue comments, add `Co-authored-by: <Name> <email>` as well.
- Per CLAUDE.md hard rule #16: `Co-authored-by` is allowed and required
for human contributors; it is forbidden only for AI/bot trailers
(Claude / GPT / Copilot / etc.).
- Use lowercase `Reported-by:` and `Co-authored-by:` (GitHub canonical
render form).
#### 7.5 Update CHANGELOG.md (inside the PR, no upstream link)
In the worktree, append to the current release's section in `CHANGELOG.md`:
```markdown
- **fix(<scope>):** <description>. (thanks @<upstream-reporter-username>)
```
Commit this change in the same PR — either as a separate commit or amended
into the fix commit (operator choice). Credit the reporter naturally;
**never** reference `decolua/9router` in `CHANGELOG.md`.
#### 7.6 Push & open PR
> **⚠️ FORK-PR GOTCHA**: bare `gh pr create` defaults to the fork's
> PARENT (upstream `decolua/9router`). ALWAYS pass `--repo
> diegosouzapw/OmniRoute`. Verified gotcha (2026-05-23 on ghostty-web).
```bash
git push -u origin "$BRANCH"
OUR_PR_URL=$(gh pr create --repo diegosouzapw/OmniRoute --base "$RELEASE_BRANCH" \
--title "fix(<scope>): <description>" \
--body "$(cat <<'EOF'
## Summary
<bullets>
## Root cause
<what was actually broken>
## Fix
<what changed>
## Attribution
Thanks to [@<reporter-username>](https://github.com/<reporter-username>) for the original report.
## Test plan
- [ ] New regression test at tests/unit/<scope>.test.ts
- [ ] npm run check
- [ ] npm run typecheck:core && npm run typecheck:noimplicit:core
- [ ] npm run test:vitest
- [ ] npm run check:docs-all
- [ ] npm run check:cycles
- [ ] Manual UI smoke (if dashboard touched)
EOF
)")
```
#### 7.7 Record in dedupe ledger
```bash
echo "{\"upstream\":${N},\"our_pr\":\"${OUR_PR_URL}\",\"branch\":\"${BRANCH}\",\"at\":\"$(date -Iseconds)\"}" \
>> "$HARV/_resolved.jsonl"
```
Step 4's dedupe pre-check reads this on the next run; the `Reported-by`
trailer in the commit serves as the redundant source of truth.
Mark the triage note: set `Status: resolved` and record the merged PR URL.
#### 7.8 Cleanup (after merge / abandonment)
```bash
PR_STATE=$(gh pr view "$OUR_PR_URL" --json state --jq .state)
git worktree remove ".claude/worktrees/${BRANCH}"
if [ "$PR_STATE" = "MERGED" ]; then
git branch -d "$BRANCH"
else
echo "PR not merged (state=$PR_STATE) — keeping branch '$BRANCH'"
fi
```
### 8. Roll-up
Once the batch is merged, report to the user:
- Fixed (with our PR URLs on `diegosouzapw/OmniRoute`)
- Handed off to `/port-upstream-features` (with upstream PR numbers)
- Deferred (with reasons)
- New issues opened on **our** fork (`diegosouzapw/OmniRoute`) for any
remaining work worth tracking — **never** open issues on
`decolua/9router`.
---
## Triage Note Template
```markdown
# Upstream Issue #<N>: <Title>
## Source
| Field | Value |
|-------|-------|
| Upstream issue | [decolua/9router#<N>](https://github.com/decolua/9router/issues/<N>) |
| Reporter | [@<username>](https://github.com/<username>) |
| Filed | <YYYY-MM-DD> |
| Last activity | <YYYY-MM-DD> |
| Labels | <list> |
## Bucket
`security` | `viable-self` | `viable-port` | `not-applicable` | `needs-repro` | `out-of-scope` | `wontfix`
## Summary
<24 sentence restatement of the bug, in our words.>
## Reproduction against OmniRoute
- [ ] Reproduced locally on `release/vX.Y.Z`
- Steps:
1. ...
2. ...
- Expected: ...
- Actual: ...
## Architecture mapping
- 9router file(s): `<upstream path>` (also visible in `_references/9router/<path>`)
- OmniRoute file(s): `<our path>` (per the architecture mapping table at the top of this workflow)
- OmniRoute-only layers involved: `<a2a / memory / cloudAgent / guardrails / evals / none>`
## Related upstream PR
<#NNN — if `viable-port`, link here and STOP this workflow for that issue. Otherwise: none.>
## JS → TS notes
<Type signatures, null handling, ESM specifics that differ from 9router.>
## Fix plan
<Bullet plan, OR reason for the chosen non-fix bucket.>
## Risks
- Public API change: no / yes (describe)
- Schema / migration: no / yes (describe)
- i18n keys: no / yes (list — ALL locales)
- Performance: no / yes (describe)
## Validation checklist
- [ ] Failing regression test added first
- [ ] `npm run check`
- [ ] `npm run typecheck:core`
- [ ] `npm run typecheck:noimplicit:core`
- [ ] `npm run test:vitest`
- [ ] `npm run check:docs-all`
- [ ] `npm run check:cycles`
- [ ] `npm run test:e2e` (if E2E impacted)
- [ ] Manual UI smoke on `npm run dev` (if dashboard touched)
## Attribution applied
- [ ] Commit trailer: `Reported-by` (+ `Co-authored-by` if upstream comment patch)
- [ ] CHANGELOG.md inside the PR: `(thanks @<reporter>)` — NO upstream link
- [ ] PR body: thanks block (reporter only, NO upstream link)
- [ ] Ledger entry written on PR creation
## Status
`triaged` | `in-progress` | `resolved` | `deferred` | `wontfix`
## Resolution
<Filled in when status = resolved. Include the merged PR URL on our fork.>
```
---
## Hard rules
- Security first. Always. Alone, on its own worktree, its own PR.
- Reproduce before claiming a fix. No "blind" fixes.
- All work BRANCHES off `release/vX.Y.Z`. Never off `main`. Never push to
`main` directly.
- One PR per fix. Do NOT bundle.
- Never weaken existing tests to go green.
- Never use `--no-verify`, force-push to release/main, or `--reject` /
`--theirs` / `--ours` to shortcut conflicts.
- Never interact with `decolua/9router`'s issue tracker (no comments,
closes, reactions, or referenced fixes from our commits).
- Never widen `src/shared/contracts/` or public route shapes without
explicit user OK.
- Upstream issue URL lives ONLY in the `Reported-by` commit trailer.
Never in PR body, CHANGELOG, or any other surface.
- `Co-authored-by` trailers MUST credit human contributors only (CLAUDE.md
rule #16 allows humans, bans AI/bot trailers).
- Never overwrite a previously-resolved issue — the Step 4 dedupe guard
(JSONL + git log on `Reported-by:`) exists for this; never disable it.
- Verify subagent work yourself per CLAUDE.md: `git status` + `git diff
--stat`, sanity-check scope, full validation suite before accepting.
- License gate is enforced in Step 1; if the upstream LICENSE blob hash
changes between sessions, re-confirm before continuing.
## Notes
- This workflow is **local-only**. The `_tasks/` directory is covered by
the `/_*/` gitignore rule, and this `.md` file is individually listed in
`.gitignore` alongside `port-upstream-features-ag.md`.
- The dedupe ledger (`_resolved.jsonl`) is local-only documentation, NOT
tracked. The git `Reported-by:` trailer is the authoritative record.
- If a downstream consumer (another project of yours) is blocked by a
specific upstream issue, prioritise it regardless of bucket size.
- Companion sibling: `port-upstream-features-ag.md` for upstream PR
porting.

View File

@@ -1,366 +0,0 @@
---
name: port-upstream-issues-cc
description: Triage and fix open issues from upstream decolua/9router against OmniRoute. Reproduce first, security first, one worktree per fix, attribution preserved.
---
# /port-upstream-issues — Resolve upstream-reported bugs in OmniRoute
## ⚠️ CONFIDENTIAL — this command is `.gitignored` and must NEVER be committed.
Full reference: `.agents/workflows/port-upstream-issues-ag.md`.
Sibling command (PR tracker, not issues): `/port-upstream-features`.
> **NOT THE SAME AS `/resolve-issues`.** `/resolve-issues` works on
> **OmniRoute's own** issue tracker. This command reads issues filed on
> **`decolua/9router`** (upstream) and lands fixes here, without ever
> touching the upstream tracker.
## Inputs
Arguments: `$ARGUMENTS` (optional). Accepts a space-separated list of
upstream issue identifiers — bare numbers (`1317 1320`), full URLs
(`https://github.com/decolua/9router/issues/1317`), or a mix. If empty,
the command harvests ALL open upstream issues and triages before any code
change.
## Constants (hard-coded — do not infer)
- Upstream: `decolua/9router` (JavaScript, Next.js 16)
- Fork (origin): `diegosouzapw/OmniRoute` (TypeScript, Next.js 16)
- Worktree root: `.claude/worktrees/`
- Task notes: `_tasks/features-v${VERSION}/port-upstream-issues/`
- Dedupe ledger: `_tasks/features-v${VERSION}/port-upstream-issues/_resolved.jsonl`
- Upstream sources mirror (read-only): `_references/9router/`
- We NEVER comment, close, or react on `decolua/9router`'s issue tracker.
## Architecture mapping (upstream → OmniRoute)
Use this table when reproducing each bug and planning the fix. OmniRoute
has layers that don't exist upstream (a2a, memory, cloudAgent, guardrails,
evals); when an upstream bug touches functionality routed through one of
those layers downstream, MAP IT and note it in the triage.
| Upstream (9router, JS) | OmniRoute (TS) | Notes |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `src/app/api/v1/...` | `src/app/api/v1/...` | Public LLM API surface — same shape |
| `src/app/api/...` (dashboard / cli-tools / oauth) | `src/app/api/...` | Internal dashboard API |
| `src/app/(dashboard)/dashboard/...` | `src/app/(dashboard)/dashboard/...` | UI |
| `src/app/landing/` | `src/app/landing/` | Marketing pages |
| `src/sse/handlers/` `src/sse/services/` | `src/sse/handlers/` `src/sse/services/` | Legacy streaming layer (still active in both) |
| `open-sse/handlers/` | `open-sse/handlers/` | Modern handler layer |
| `open-sse/executors/*.js` | `open-sse/executors/*.ts` | One per provider — TS in OmniRoute |
| `open-sse/services/` | `open-sse/services/` | Combo, accountFallback, model, etc. |
| `open-sse/translator/` `open-sse/transformer/` | `open-sse/translator/` `open-sse/transformer/` | Format conversion + Responses API |
| `open-sse/rtk/` (request toolkit) | `open-sse/services/` or `open-sse/utils/` | No 1:1 — fold into nearest service |
| `open-sse/config/` `open-sse/utils/` `open-sse/lib/` | `open-sse/config/` `open-sse/utils/` `open-sse/lib/` | |
| `src/lib/mcp/` | `open-sse/mcp-server/` | MCP moved into open-sse workspace |
| `src/lib/db/` (adapters / helpers / migrations / repos) | `src/lib/db/` (45+ domain modules, 55 migrations) | `localDb.ts` is RE-EXPORT ONLY (hard rule #2) |
| `src/lib/oauth/` | `src/lib/oauth/` | |
| `src/lib/auth/` | `src/server/authz/` + `src/lib/auth*` | OmniRoute splits server-side vs lib helpers |
| `src/lib/network/` | `src/shared/utils/` or `open-sse/utils/` | Fold by purpose |
| `src/lib/tunnel/` `src/lib/updater/` `src/lib/usage/` | `src/lib/services/` (bootstrap) + module per concern | OmniRoute consolidates as embedded services |
| `src/mitm/` | `src/mitm/` | Cert / dns / handlers preserved |
| `src/models/` | `src/models/` | Domain models |
| `src/shared/` | `src/shared/` | Constants, components, hooks, services, utils |
| `src/store/` (Zustand) | `src/store/` | |
| `src/i18n/` + `public/i18n/literals/` | `src/i18n/` + `public/i18n/literals/` | i18n keys MUST be added in ALL locales |
| `skills/9router-*` (top-level spec dirs) | `src/lib/skills/` (framework) + `skills/` (specs) | Different shape — framework vs spec files |
| `cli/` | `bin/` (entry) + `src/lib/services/` modules | OmniRoute folded most CLI into the main app |
| `gitbook/` | `docs/` | Markdown only; no gitbook in OmniRoute |
| (no equivalent upstream) | `src/lib/a2a/` `src/lib/memory/` `src/lib/cloudAgent/` `src/lib/guardrails/` `src/lib/evals/` `electron/` `tests/` | OmniRoute-only — bugs here are downstream-specific |
## Execution
### Step 0 — Sanity + setup
```bash
git -C . remote get-url origin # must end in diegosouzapw/OmniRoute
git branch --show-current # must be release/vX.Y.Z
gh auth status
VERSION=$(node -p "require('./package.json').version")
RELEASE_BRANCH=$(git branch --show-current)
# Idempotent upstream remote (we may need to inspect specific upstream commits to reproduce)
git remote get-url upstream 2>/dev/null \
|| git remote add upstream https://github.com/decolua/9router.git
git fetch upstream --quiet
# License gate — confirm once per session, cache the LICENSE blob hash
UPSTREAM_LICENSE_SHA=$(git -C _references/9router rev-parse HEAD:LICENSE 2>/dev/null)
echo "Upstream LICENSE blob: $UPSTREAM_LICENSE_SHA"
# Read _references/9router/LICENSE and confirm permissive (MIT / Apache-2.0 / BSD-style).
# If unsure or the hash changed since last session, ESCALATE TO USER before continuing.
mkdir -p "_tasks/features-v${VERSION}/port-upstream-issues"
touch "_tasks/features-v${VERSION}/port-upstream-issues/_resolved.jsonl"
```
If on `main`, follow `/generate-release` Phase 1 steps 15 to create the
next `release/vX.Y.Z` first.
### Step 1 — Harvest (two-step pattern to avoid JSON truncation)
```bash
HARV="_tasks/features-v${VERSION}/port-upstream-issues"
# 1a — numbers only, never truncated
gh issue list --repo decolua/9router --state open --limit 500 \
--json number --jq '.[].number' \
> "$HARV/_numbers.txt"
# 1b — full metadata per issue, batched (sequential to avoid rate-limit bursts)
for N in $(cat "$HARV/_numbers.txt"); do
gh issue view "$N" --repo decolua/9router \
--json number,title,labels,body,comments,createdAt,updatedAt,author,reactionGroups
done > "$HARV/_raw.jsonl"
# 1c — open upstream PRs for cross-reference
gh pr list --repo decolua/9router --state open --limit 500 \
--json number,title,body \
> "$HARV/_open_prs.json"
```
For each issue, scan `_open_prs.json` for `fixes #N`, `closes #N`, `for
#N`. Issues with an open PR are `viable-port` — they belong to
`/port-upstream-features`, not here.
### Step 2 — Triage (no code yet)
For each issue, first normalize input and dedupe-check:
```bash
# normalize: "https://github.com/decolua/9router/issues/1317" → "1317"
N=$(echo "$arg" | sed -E 's|.*/issues/([0-9]+).*|\1|; s|^#||')
# dedupe — defense in depth (JSONL snapshot + git log as source of truth)
LEDGER="_tasks/features-v${VERSION}/port-upstream-issues/_resolved.jsonl"
if grep -q "\"upstream\":${N}\b" "$LEDGER" 2>/dev/null \
|| git log --all --grep "Reported-by:.*decolua/9router/issues/${N}\b" --oneline | grep -q .; then
echo "Issue #${N} already resolved here — skipping"; continue
fi
```
Then write
`_tasks/features-v${VERSION}/port-upstream-issues/<N>-<short-kebab>.triage.md`
using the template in the reference workflow. Buckets:
- `security` — handled FIRST, alone, with its own PR
- `viable-self` — bug, reproducible against OmniRoute, fix in scope
- `viable-port` — already addressed by an open upstream PR → hand-off
- `not-applicable` — 9router-only bug; OmniRoute architecture diverges
- `needs-repro` — cannot reproduce / not enough info
- `out-of-scope` — needs infra change / new module
- `wontfix` — conflicts with OmniRoute direction
**Reproduce before promising a fix.** OmniRoute is TS / Next.js; many
9router bugs don't exist here (different runtime, different layer, fixed
already). If you cannot reproduce, the bucket is `not-applicable` or
`needs-repro`, NEVER `viable-self`.
Use the architecture mapping above to locate the equivalent OmniRoute
file(s) and read them (NOT the upstream `_references/9router/` copy) when
deciding reproducibility.
### Step 3 — Present plan and wait
Summarise in this order:
1. **Security findings first**, with severity.
2. Counts per bucket.
3. Top `viable-self` ranked by impact / fix size.
4. Top `viable-port` with upstream PR numbers (hand-off to
`/port-upstream-features`).
5. `out-of-scope` / `wontfix` items the user might want to re-bucket.
**Do not touch code until the user names which issues to fix in this batch.**
### Step 4 — Implement (one worktree per fix)
For each approved issue `N`:
```bash
BRANCH="fix/port-issue-${N}-<short>"
git worktree add ".claude/worktrees/${BRANCH}" -b "$BRANCH" "$RELEASE_BRANCH"
cd ".claude/worktrees/${BRANCH}"
npm install
```
#### 4.1 Write the failing regression test FIRST
Default suite is `tests/unit/<scope>.test.ts` using `node:test`. For
network-shaped bugs use `tests/integration/`. MCP-shaped issues use
`vitest.mcp.config.ts`. Iterate against the single file:
```bash
npm run test:unit -- --test tests/unit/<scope>.test.ts
```
#### 4.2 Smallest possible fix
- One commit, one concern. No drive-by refactors.
- No public route / contract shape changes unless the issue demands it
(flag first).
- Match the existing TS style. Run `npm run lint` after editing.
#### 4.3 Validate locally — mandatory
```bash
npm run check # lint + test:unit
npm run typecheck:core
npm run typecheck:noimplicit:core
npm run test:vitest
npm run check:docs-all
npm run check:cycles # always — fixes sometimes add imports
```
If contracts / providers / schemas were touched:
```bash
npm run check:route-validation:t06
npm run check:any-budget:t11
```
If E2E behaviour was plausibly impacted:
```bash
npm run test:e2e
```
If the fix touches `src/app/(dashboard)/` (UI), manual smoke is
**mandatory** per CLAUDE.md "For UI or frontend changes":
```bash
npm run dev # http://localhost:20128
# Reproduce the original bug scenario and verify it's gone.
# Watch the console for regressions in other tabs.
```
NO `--no-verify`. Do not weaken pre-existing tests. Debug root cause.
#### 4.4 Commit
```bash
git commit -m "$(cat <<'EOF'
fix(<scope>): <description> (port from 9router#<N>)
<short body — root cause and user-visible effect>
Reported-by: <Reporter Name> (https://github.com/decolua/9router/issues/<N>)
EOF
)"
```
- The upstream issue URL lives ONLY in this trailer.
- Add `Co-authored-by: <Name> <email>` ONLY if a third party contributed
a substantive patch in the upstream issue comments — not for the report
alone. Per CLAUDE.md rule #16, human co-authors are allowed; AI/bot
trailers (Claude / GPT / Copilot / etc.) are not.
- Use lowercase `Co-authored-by:` / `Reported-by:` (GitHub canonical
render form).
#### 4.5 Update CHANGELOG.md (inside the PR, no upstream link)
In the worktree, append to the current release's section in `CHANGELOG.md`:
```markdown
- **fix(<scope>):** <description>. (thanks @<reporter-username>)
```
Commit this change in the same PR — either as a separate commit or amended
into the fix commit (operator choice). Credit the reporter naturally;
**never** reference `decolua/9router` in `CHANGELOG.md`.
#### 4.6 Push & open PR
> **⚠️ CRITICAL**: pass `--repo diegosouzapw/OmniRoute`. Bare `gh pr
> create` defaults to the fork's PARENT (upstream `decolua/9router`).
> Verified gotcha (2026-05-23 on ghostty-web).
```bash
git push -u origin "$BRANCH"
OUR_PR_URL=$(gh pr create --repo diegosouzapw/OmniRoute --base "$RELEASE_BRANCH" \
--title "fix(<scope>): <description>" \
--body "$(cat <<'EOF'
## Summary
<bullets>
## Root cause
<what was actually broken>
## Fix
<what changed>
## Attribution
Thanks to [@<reporter-username>](https://github.com/<reporter-username>) for the original report.
## Test plan
- [ ] New regression test at tests/unit/<scope>.test.ts
- [ ] npm run check
- [ ] npm run typecheck:core && npm run typecheck:noimplicit:core
- [ ] npm run test:vitest
- [ ] npm run check:docs-all
- [ ] npm run check:cycles
- [ ] Manual UI smoke (if dashboard touched)
EOF
)")
# Record in dedupe ledger (Step 2 reads this on next run)
echo "{\"upstream\":${N},\"our_pr\":\"${OUR_PR_URL}\",\"branch\":\"${BRANCH}\",\"at\":\"$(date -Iseconds)\"}" \
>> "_tasks/features-v${VERSION}/port-upstream-issues/_resolved.jsonl"
```
Return the PR URL to the user. Update the triage note: `Status: resolved`
+ merged PR URL.
#### 4.7 Cleanup (after merge / abandonment)
```bash
PR_STATE=$(gh pr view "$OUR_PR_URL" --json state --jq .state)
git worktree remove ".claude/worktrees/${BRANCH}"
if [ "$PR_STATE" = "MERGED" ]; then
git branch -d "$BRANCH"
else
echo "PR not merged (state=$PR_STATE) — keeping branch '$BRANCH'"
fi
```
### Step 5 — Roll-up
Once the batch is merged, report:
- Fixed (with PR URLs on `diegosouzapw/OmniRoute`)
- Handed off to `/port-upstream-features` (with upstream PR numbers)
- Deferred (with reasons)
- New issues opened on **our** fork for remaining work — NEVER on
`decolua/9router`.
## Hard rules
- Security first. Always. Alone, on its own worktree, its own PR.
- Reproduce before claiming a fix. No "blind" fixes.
- All work BRANCHES off `release/vX.Y.Z`. Never off `main`. Never push to
`main` directly.
- One PR per fix. Do NOT bundle.
- Never weaken existing tests to go green.
- Never use `--no-verify`, force-push to release/main, or `--reject` /
`--theirs` / `--ours` to shortcut conflicts.
- Never interact with `decolua/9router`'s issue tracker (no comments,
closes, reactions, or referenced fixes from our commits).
- Never widen `src/shared/contracts/` or public route shapes without
explicit user OK.
- Upstream issue URL lives ONLY in the `Reported-by` commit trailer.
Never in PR body, CHANGELOG, or any other surface.
- `Co-authored-by` trailers MUST credit human contributors only (CLAUDE.md
rule #16 allows humans, bans AI/bot trailers).
- Never overwrite a previously-resolved issue — the Step 2 dedupe guard
(JSONL + git log on `Reported-by:`) exists for this; never disable it.
- Verify subagent work yourself per CLAUDE.md: `git status` + `git diff
--stat`, sanity-check scope, full validation suite before accepting.
- License gate is enforced in Step 0; if the upstream LICENSE blob hash
changes between sessions, re-confirm before continuing.

View File

@@ -1,262 +0,0 @@
---
name: resolve-issues-ag
description: Fetch all open GitHub issues, analyze bugs, resolve up to 30 per batch via per-issue worktrees + PRs into the release branch, triage the rest, wait for user validation
---
# /resolve-issues — Automated Issue Resolution Workflow
## Overview
This workflow fetches all open issues from the project's GitHub repository, classifies them, analyzes bugs, proposes a resolution plan, waits for user validation, and ONLY THEN implements fixes. The current `release/vX.Y.Z` branch is the integration target — each individual fix is implemented on its own short-lived `fix/<issue>-<short>` branch inside its own git worktree, merged into the release branch via PR, then the worktree and local branch are deleted. The release branch is later merged to `main` via `/generate-release`.
> **BRANCH RULE**: The current `release/vX.Y.Z` branch is the integration target. Each fix MUST live on its own `fix/<ISSUE>-<short>` branch cut from the release branch, inside its own worktree under `.worktrees/`. After the per-issue PR is merged into the release branch, the worktree and local branch are deleted. Never commit fixes directly to the release branch. If no release branch exists yet, create one first using `/generate-release` Phase 1 steps 15.
> **⛔ PR PROHIBITION**: If a fix is associated with a contributor's PR, you MUST merge their PR — NEVER close it and re-implement the fix yourself. See `/review-prs` workflow for the full policy. The `gh pr close` command is FORBIDDEN unless the repository owner explicitly requests it.
> **🌐 REPLY LANGUAGE**: All comments posted to issues (close messages, RESPOND comments, PR descriptions visible to the reporter) MUST match the reporter's language. When in doubt, default to **English**. The reporter's language is detected from the issue body and prior comments by that author.
## Steps
### 1. Identify the GitHub Repository
// turbo
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
- Parse the owner and repo name from the URL
### 2. Ensure Release Branch Exists
// turbo
Before doing any work, ensure a `release/vX.Y.Z` branch exists. If you are currently on `main`, create one:
```bash
git branch --show-current
# If on main, determine next version and create the release branch
VERSION=$(node -p "require('./package.json').version")
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=999?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
git checkout -b release/v$NEXT
npm version patch --no-git-tag-version
npm install
```
> Threshold: patches climb to `.999` before rolling. Example: `3.4.999` → `3.5.0`.
If already on a `release/vX.Y.Z` branch, continue working there.
### 3. Fetch All Open Issues (cap 30 per batch)
// turbo-all
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. Use the two-step approach below.
**Step 3a — Get Issue numbers only** (small output, never truncated):
- Run: `gh issue list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
- Count them and remember the total.
**Step 3b — Fetch full metadata for each Issue** (parallel, validated against 3a):
- For each issue number from step 3a, run:
`gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author,url`
- Batch in parallel (812 concurrent calls). After completion, assert `fetched_count == count_from_3a`; if mismatch, retry the missing IDs.
- Sort by oldest first (FIFO).
**Step 3c — Cap at 30 per run**:
- If more than 30 open issues qualify as bugs after step 4, ask the user which subset of up to 30 to handle now. The remainder is deferred to the next run.
### 4. Classify Each Issue
For each issue, determine its type:
- **Bug** — Has `bug` label, or body contains error messages, stack traces, "doesn't work", "broken", "crash", "error"
- **Feature Request** — Has `enhancement`/`feature` label, or body describes new functionality
- **Question** — Has `question` label, or is asking "how to" something
- **Other** — Anything else
Focus ONLY on **Bugs** for resolution. Feature requests and questions are skipped with a note in the final report.
#### 4.5. PR-Linked Check (mandatory)
For every bug, query linked PRs:
```bash
gh issue view <NUMBER> --repo <owner>/<repo> --json closedByPullRequestsReferences,body
```
If the issue is referenced by an **open** contributor PR (or the body links to one), do NOT plan a self-implemented fix. Mark the issue as `🤝 PR-LINKED — redirect to /review-prs` in the report and stop deeper analysis for it. **NEVER close the contributor PR.**
### 5. Deep-Read Each Bug Issue (One-by-One Analysis)
Read each bug issue thoroughly, one at a time. Each issue gets focused attention.
#### 5a. Understand the Problem
1. **Read the entire body** — Description, Steps to Reproduce, Expected/Actual Behavior, Error Logs, Screenshots
2. **Read ALL comments** — bot triage (Kilo, etc.) and owner/community responses. Look for:
- Someone already responded with a fix
- Community member confirmed it is resolved
- Bot duplicate flag. **DO NOT blindly trust bot labels (e.g., `kilo-duplicate`).** Re-verify independently from current source + web research.
3. **Identify the claimed error** — exact error message, status code, provider/model, OS, Node version.
#### 5b. Check Information Sufficiency
Verify the issue contains:
- [ ] Clear description of the problem
- [ ] Steps to reproduce OR error logs
- [ ] Provider/model/version information
- [ ] Expected vs actual behavior
**If ANY item is missing → auto-classify as `📝 RESPOND — Needs Info` and skip 5d.** Do not attempt root-cause analysis on under-specified issues.
#### 5c. Determine Issue Disposition
| Disposition | When to Apply | Action |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| **✅ CLOSE — Already Fixed** | Owner responded with fix + no user follow-up, OR community confirmed fix | Close with comment citing which version fixed it |
| **✅ CLOSE — Duplicate** | You have independently verified the issue is a duplicate (do NOT rely solely on bot flags) + user provides no new info | Close referencing the original issue |
| **✅ CLOSE — Stale** | We requested logs/info > 7 days ago with no reply | Close thanking the user, invite to reopen if needed |
| **📝 RESPOND — Needs Info** | Issue is real but missing critical reproduction details (also triggered by 5b) | Comment asking for specifics per `/issue-triage` |
| **📝 RESPOND — User Config** | Error is caused by unsupported env (Node version, wrong model path, missing API enablement) | Comment explaining the user-side fix |
| **🤝 PR-LINKED** | An open contributor PR already targets this issue (from step 4.5) | Redirect to `/review-prs`; do not re-implement |
| **🔧 FIX — Code Change** | Root cause is confirmed in the codebase | Research, propose solution in report, wait for approval |
#### 5d. For "FIX — Code Change" Issues
Before coding, perform deep source analysis:
1. **Search the codebase** — grep for error strings, function names, affected files
2. **Search the web** — upstream API changes, SDK updates, breaking changes
3. **Read the full source file** — don't rely on grep snippets
4. **Verify the root cause** is in our code, not user misconfiguration
5. **Formulate a proposed solution** — exact files/lines/logic
6. **Create an Implementation Plan file** at `_tasks/fixes-vX.Y.Z/<ISSUE>-<short-description>.plan.md` (`vX.Y.Z` = current release branch version). Create the directory first: `mkdir -p _tasks/fixes-vX.Y.Z`. The plan contains: Overview, Reproduction Steps, Regression Test Outline, Implementation Steps (files/changes), Rollout Notes.
7. **DO NOT modify the codebase yet** — wait for user approval.
#### 5e. For "RESPOND" Issues
Post a substantive comment that:
- Acknowledges the specific error reported
- Explains the likely root cause
- Provides concrete steps (version upgrade, env var fix, model path correction)
- Asks for follow-up info if needed
**No generic templates.** Every comment references the user's specific error and environment, and is written in the reporter's language (English default).
### 6. Generate Report & Wait for Validation
Present a summary report. For FIX bugs, explicitly explain the proposed solution (files to change + logic) and confirm it will land via per-issue worktree → PR → release branch after approval. Include the reporter's detected language per row so the user can verify.
| Issue | Title | Status | Reply Lang | Proposed Action / Version |
| ----- | ----- | -------------- | ---------- | ------------------------------------------ |
| #N | Title | ✅ Close | en | Already fixed / duplicate (explain why) |
| #N | Title | 🔧 Propose | pt-BR | Code fix plan summary + worktree branch |
| #N | Title | 📝 Respond | en | Guidance comment to be posted |
| #N | Title | ❓ Needs Info | en | Triage comment to be posted |
| #N | Title | 🤝 PR-Linked | en | Redirect to /review-prs (PR #M) |
| #N | Title | ⏭️ Skip | — | Feature request / not a bug |
> **⚠️ IMPORTANT**: Do NOT implement code changes, commit, push, or close issues at this step.
> Wait for the user to review the proposed fixes and respond with **OK** before proceeding.
- If the user says **OK** → Proceed to step 7
- If the user requests changes → Adjust and re-present the report
- If the user rejects → Revert any accidental changes and stop
### 7. Implement Fixes via Per-Issue Worktrees + PRs (only after user approval)
For each approved FIX issue (up to 30 per batch), repeat the following sequence. Issues can be processed sequentially or in parallel (one worktree each — never two fixes in the same worktree).
#### 7.1. Spin up an isolated worktree on a fresh fix branch
```bash
ISSUE=<NUMBER>
SHORT=<short-kebab-desc>
RELEASE_BRANCH=$(git -C <project_root> branch --show-current) # release/vX.Y.Z
WT_DIR=".worktrees/fix-${ISSUE}-${SHORT}"
BRANCH="fix/${ISSUE}-${SHORT}"
git fetch origin "$RELEASE_BRANCH"
git worktree add "$WT_DIR" -b "$BRANCH" "origin/$RELEASE_BRANCH"
cd "$WT_DIR"
```
#### 7.2. Write the regression test first (TDD)
- Author a unit/integration test that reproduces the bug. **It must fail on the unfixed code.** Run it and confirm the failure.
- Hard rule #8: any production change must ship with tests in the same PR. The regression test is non-negotiable.
#### 7.3. Implement the fix
- Apply the approved plan from `_tasks/fixes-vX.Y.Z/<ISSUE>-<short>.plan.md`.
- Keep the diff scoped to this issue. No drive-by refactors.
#### 7.4. Run the test suite
- `npm run test:all` (or the appropriate suite for the touched area; the regression test MUST be included).
- All tests must pass before commit. Also run the relevant `lint` / `typecheck` per CLAUDE.md trust-but-verify checklist.
#### 7.5. Update CHANGELOG.md and commit (single commit, same diff)
- Add the new bug-fix entry under the current `vX.Y.Z` section of CHANGELOG.md.
- CHANGELOG entry + code + test go in **one** commit on the fix branch:
```bash
git add <changed files> CHANGELOG.md
git commit -m "fix: <description> (#${ISSUE})"
```
#### 7.6. Push and open a PR into the release branch
```bash
git push -u origin "$BRANCH"
gh pr create \
--base "$RELEASE_BRANCH" \
--head "$BRANCH" \
--title "fix: <description> (#${ISSUE})" \
--body "Closes #${ISSUE}\n\n<short summary, plan link, regression test reference>"
```
#### 7.7. Merge the PR into the release branch
- Wait for CI green, then merge with the project's default merge strategy.
- The PR title becomes the release-branch commit.
#### 7.8. Clean up worktree and local branch
```bash
cd <project_root>
git worktree remove "$WT_DIR"
git branch -D "$BRANCH"
```
#### 7.9. Close the issue with a localized comment
Match the reporter's language (English default). Template:
> **EN**: Thanks for reporting! Fixed in `release/vX.Y.Z` (already merged into the active development branch — feel free to pull and test it now). It will ship in the next release (vX.Y.Z).
>
> **pt-BR**: Obrigado pelo report! Corrigido em `release/vX.Y.Z` (já mergeado na branch de desenvolvimento atual — pode dar pull e testar). Vai sair na próxima release (vX.Y.Z).
```bash
gh issue close "$ISSUE" --repo <owner>/<repo> --comment "<localized message above>"
```
#### 7.10. Close non-FIX dispositions
After all FIX issues are merged:
- `Duplicate`: close referencing the original issue (localized).
- `Stale`: close thanking the user and inviting reopen (localized).
- `RESPOND — Needs Info` / `RESPOND — User Config`: post the substantive comment from 5e (localized).
- `PR-LINKED`: leave the issue open; comment redirecting to the contributor PR if not already linked.
#### 7.11. Hand off to release flow (optional)
If a release PR to `main` is desired now, run `/generate-release` Phase 1 steps 710 (tests → commit version bump → push → open PR to main → wait for user).
If NO fixes were committed, skip 7.77.11 and just conclude the workflow.

View File

@@ -1,263 +0,0 @@
---
name: resolve-issues-cc
description: Fetch all open GitHub issues, analyze bugs, resolve up to 30 per batch via per-issue worktrees + PRs into the release branch, triage the rest, wait for user validation
allowed-tools: Bash, Read, Edit, Write, Grep, Glob, WebFetch, WebSearch, AskUserQuestion, Agent
---
# /resolve-issues — Automated Issue Resolution Workflow
## Overview
This workflow fetches all open issues from the project's GitHub repository, classifies them, analyzes bugs, proposes a resolution plan, waits for user validation, and ONLY THEN implements fixes. The current `release/vX.Y.Z` branch is the integration target — each individual fix is implemented on its own short-lived `fix/<issue>-<short>` branch inside its own git worktree, merged into the release branch via PR, then the worktree and local branch are deleted. The release branch is later merged to `main` via `/generate-release`.
> **BRANCH RULE**: The current `release/vX.Y.Z` branch is the integration target. Each fix MUST live on its own `fix/<ISSUE>-<short>` branch cut from the release branch, inside its own worktree under `.worktrees/`. After the per-issue PR is merged into the release branch, the worktree and local branch are deleted. Never commit fixes directly to the release branch. If no release branch exists yet, create one first using `/generate-release` Phase 1 steps 15.
> **⛔ PR PROHIBITION**: If a fix is associated with a contributor's PR, you MUST merge their PR — NEVER close it and re-implement the fix yourself. See `/review-prs` workflow for the full policy. The `gh pr close` command is FORBIDDEN unless the repository owner explicitly requests it.
> **🌐 REPLY LANGUAGE**: All comments posted to issues (close messages, RESPOND comments, PR descriptions visible to the reporter) MUST match the reporter's language. When in doubt, default to **English**. The reporter's language is detected from the issue body and prior comments by that author.
## Steps
### 1. Identify the GitHub Repository
// turbo
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
- Parse the owner and repo name from the URL
### 2. Ensure Release Branch Exists
// turbo
Before doing any work, ensure a `release/vX.Y.Z` branch exists. If you are currently on `main`, create one:
```bash
git branch --show-current
# If on main, determine next version and create the release branch
VERSION=$(node -p "require('./package.json').version")
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=999?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
git checkout -b release/v$NEXT
npm version patch --no-git-tag-version
npm install
```
> Threshold: patches climb to `.999` before rolling. Example: `3.4.999` → `3.5.0`.
If already on a `release/vX.Y.Z` branch, continue working there.
### 3. Fetch All Open Issues (cap 30 per batch)
// turbo-all
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. Use the two-step approach below.
**Step 3a — Get Issue numbers only** (small output, never truncated):
- Run: `gh issue list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
- Count them and remember the total.
**Step 3b — Fetch full metadata for each Issue** (parallel, validated against 3a):
- For each issue number from step 3a, run:
`gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author,url`
- Batch in parallel (812 concurrent calls). After completion, assert `fetched_count == count_from_3a`; if mismatch, retry the missing IDs.
- Sort by oldest first (FIFO).
**Step 3c — Cap at 30 per run**:
- If more than 30 open issues qualify as bugs after step 4, ask the user (via AskUserQuestion) which subset of up to 30 to handle now. The remainder is deferred to the next run.
### 4. Classify Each Issue
For each issue, determine its type:
- **Bug** — Has `bug` label, or body contains error messages, stack traces, "doesn't work", "broken", "crash", "error"
- **Feature Request** — Has `enhancement`/`feature` label, or body describes new functionality
- **Question** — Has `question` label, or is asking "how to" something
- **Other** — Anything else
Focus ONLY on **Bugs** for resolution. Feature requests and questions are skipped with a note in the final report.
#### 4.5. PR-Linked Check (mandatory)
For every bug, query linked PRs:
```bash
gh issue view <NUMBER> --repo <owner>/<repo> --json closedByPullRequestsReferences,body
```
If the issue is referenced by an **open** contributor PR (or the body links to one), do NOT plan a self-implemented fix. Mark the issue as `🤝 PR-LINKED — redirect to /review-prs` in the report and stop deeper analysis for it. **NEVER close the contributor PR.**
### 5. Deep-Read Each Bug Issue (One-by-One Analysis)
Read each bug issue thoroughly, one at a time. Each issue gets focused attention.
#### 5a. Understand the Problem
1. **Read the entire body** — Description, Steps to Reproduce, Expected/Actual Behavior, Error Logs, Screenshots
2. **Read ALL comments** — bot triage (Kilo, etc.) and owner/community responses. Look for:
- Someone already responded with a fix
- Community member confirmed it is resolved
- Bot duplicate flag. **DO NOT blindly trust bot labels (e.g., `kilo-duplicate`).** Re-verify independently from current source + web research.
3. **Identify the claimed error** — exact error message, status code, provider/model, OS, Node version.
#### 5b. Check Information Sufficiency
Verify the issue contains:
- [ ] Clear description of the problem
- [ ] Steps to reproduce OR error logs
- [ ] Provider/model/version information
- [ ] Expected vs actual behavior
**If ANY item is missing → auto-classify as `📝 RESPOND — Needs Info` and skip 5d.** Do not attempt root-cause analysis on under-specified issues.
#### 5c. Determine Issue Disposition
| Disposition | When to Apply | Action |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| **✅ CLOSE — Already Fixed** | Owner responded with fix + no user follow-up, OR community confirmed fix | Close with comment citing which version fixed it |
| **✅ CLOSE — Duplicate** | You have independently verified the issue is a duplicate (do NOT rely solely on bot flags) + user provides no new info | Close referencing the original issue |
| **✅ CLOSE — Stale** | We requested logs/info > 7 days ago with no reply | Close thanking the user, invite to reopen if needed |
| **📝 RESPOND — Needs Info** | Issue is real but missing critical reproduction details (also triggered by 5b) | Comment asking for specifics per `/issue-triage` |
| **📝 RESPOND — User Config** | Error is caused by unsupported env (Node version, wrong model path, missing API enablement) | Comment explaining the user-side fix |
| **🤝 PR-LINKED** | An open contributor PR already targets this issue (from step 4.5) | Redirect to `/review-prs`; do not re-implement |
| **🔧 FIX — Code Change** | Root cause is confirmed in the codebase | Research, propose solution in report, wait for approval |
#### 5d. For "FIX — Code Change" Issues
Before coding, perform deep source analysis:
1. **Search the codebase**`grep`/`Grep` for error strings, function names, affected files
2. **Search the web** — upstream API changes, SDK updates, breaking changes
3. **Read the full source file** — don't rely on grep snippets
4. **Verify the root cause** is in our code, not user misconfiguration
5. **Formulate a proposed solution** — exact files/lines/logic
6. **Create an Implementation Plan file** at `_tasks/fixes-vX.Y.Z/<ISSUE>-<short-description>.plan.md` (`vX.Y.Z` = current release branch version). Create the directory first: `mkdir -p _tasks/fixes-vX.Y.Z`. The plan contains: Overview, Reproduction Steps, Regression Test Outline, Implementation Steps (files/changes), Rollout Notes.
7. **DO NOT modify the codebase yet** — wait for user approval.
#### 5e. For "RESPOND" Issues
Post a substantive comment that:
- Acknowledges the specific error reported
- Explains the likely root cause
- Provides concrete steps (version upgrade, env var fix, model path correction)
- Asks for follow-up info if needed
**No generic templates.** Every comment references the user's specific error and environment, and is written in the reporter's language (English default).
### 6. Generate Report & Wait for Validation
Present a summary report. For FIX bugs, explicitly explain the proposed solution (files to change + logic) and confirm it will land via per-issue worktree → PR → release branch after approval. Include the reporter's detected language per row so the user can verify.
| Issue | Title | Status | Reply Lang | Proposed Action / Version |
| ----- | ----- | -------------- | ---------- | ------------------------------------------ |
| #N | Title | ✅ Close | en | Already fixed / duplicate (explain why) |
| #N | Title | 🔧 Propose | pt-BR | Code fix plan summary + worktree branch |
| #N | Title | 📝 Respond | en | Guidance comment to be posted |
| #N | Title | ❓ Needs Info | en | Triage comment to be posted |
| #N | Title | 🤝 PR-Linked | en | Redirect to /review-prs (PR #M) |
| #N | Title | ⏭️ Skip | — | Feature request / not a bug |
> **⚠️ IMPORTANT**: Do NOT implement code changes, commit, push, or close issues at this step.
> Wait for the user to review the proposed fixes and respond with **OK** before proceeding.
- If the user says **OK** → Proceed to step 7
- If the user requests changes → Adjust and re-present the report
- If the user rejects → Revert any accidental changes and stop
### 7. Implement Fixes via Per-Issue Worktrees + PRs (only after user approval)
For each approved FIX issue (up to 30 per batch), repeat the following sequence. Issues can be processed sequentially or in parallel (one worktree each — never two fixes in the same worktree).
#### 7.1. Spin up an isolated worktree on a fresh fix branch
```bash
ISSUE=<NUMBER>
SHORT=<short-kebab-desc>
RELEASE_BRANCH=$(git -C <project_root> branch --show-current) # release/vX.Y.Z
WT_DIR=".worktrees/fix-${ISSUE}-${SHORT}"
BRANCH="fix/${ISSUE}-${SHORT}"
git fetch origin "$RELEASE_BRANCH"
git worktree add "$WT_DIR" -b "$BRANCH" "origin/$RELEASE_BRANCH"
cd "$WT_DIR"
```
#### 7.2. Write the regression test first (TDD)
- Author a unit/integration test that reproduces the bug. **It must fail on the unfixed code.** Run it and confirm the failure.
- Hard rule #8: any production change must ship with tests in the same PR. The regression test is non-negotiable.
#### 7.3. Implement the fix
- Apply the approved plan from `_tasks/fixes-vX.Y.Z/<ISSUE>-<short>.plan.md`.
- Keep the diff scoped to this issue. No drive-by refactors.
#### 7.4. Run the test suite
- `npm run test:all` (or the appropriate suite for the touched area; the regression test MUST be included).
- All tests must pass before commit. Also run the relevant `lint` / `typecheck` per CLAUDE.md trust-but-verify checklist.
#### 7.5. Update CHANGELOG.md and commit (single commit, same diff)
- Add the new bug-fix entry under the current `vX.Y.Z` section of CHANGELOG.md.
- CHANGELOG entry + code + test go in **one** commit on the fix branch:
```bash
git add <changed files> CHANGELOG.md
git commit -m "fix: <description> (#${ISSUE})"
```
#### 7.6. Push and open a PR into the release branch
```bash
git push -u origin "$BRANCH"
gh pr create \
--base "$RELEASE_BRANCH" \
--head "$BRANCH" \
--title "fix: <description> (#${ISSUE})" \
--body "Closes #${ISSUE}\n\n<short summary, plan link, regression test reference>"
```
#### 7.7. Merge the PR into the release branch
- Wait for CI green, then merge with the project's default merge strategy.
- The PR title becomes the release-branch commit.
#### 7.8. Clean up worktree and local branch
```bash
cd <project_root>
git worktree remove "$WT_DIR"
git branch -D "$BRANCH"
```
#### 7.9. Close the issue with a localized comment
Match the reporter's language (English default). Template:
> **EN**: Thanks for reporting! Fixed in `release/vX.Y.Z` (already merged into the active development branch — feel free to pull and test it now). It will ship in the next release (vX.Y.Z).
>
> **pt-BR**: Obrigado pelo report! Corrigido em `release/vX.Y.Z` (já mergeado na branch de desenvolvimento atual — pode dar pull e testar). Vai sair na próxima release (vX.Y.Z).
```bash
gh issue close "$ISSUE" --repo <owner>/<repo> --comment "<localized message above>"
```
#### 7.10. Close non-FIX dispositions
After all FIX issues are merged:
- `Duplicate`: close referencing the original issue (localized).
- `Stale`: close thanking the user and inviting reopen (localized).
- `RESPOND — Needs Info` / `RESPOND — User Config`: post the substantive comment from 5e (localized).
- `PR-LINKED`: leave the issue open; comment redirecting to the contributor PR if not already linked.
#### 7.11. Hand off to release flow (optional)
If a release PR to `main` is desired now, run `/generate-release` Phase 1 steps 710 (tests → commit version bump → push → open PR to main → wait for user).
If NO fixes were committed, skip 7.77.11 and just conclude the workflow.

View File

@@ -1,269 +0,0 @@
---
name: resolve-issues-cx
description: Fetch all open GitHub issues, analyze bugs, resolve up to 30 per batch via per-issue worktrees + PRs into the release branch, triage the rest, wait for user validation
---
# /resolve-issues — Automated Issue Resolution Workflow
## Overview
This workflow fetches all open issues from the project's GitHub repository, classifies them, analyzes bugs, proposes a resolution plan, waits for user validation, and ONLY THEN implements fixes. The current `release/vX.Y.Z` branch is the integration target — each individual fix is implemented on its own short-lived `fix/<issue>-<short>` branch inside its own git worktree, merged into the release branch via PR, then the worktree and local branch are deleted. The release branch is later merged to `main` via `/generate-release`.
## Codex Execution Notes
- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` for independent reads, checks, and GitHub calls.
- The initial report/plan is a hard stop. Do not edit code, close issues, or commit until the user explicitly approves the report.
- Keep classification and bug analysis bounded enough to produce the user-facing report before deep implementation work.
- One worktree per fix — never reuse a worktree for two different issues, even sequentially in the same session.
> **BRANCH RULE**: The current `release/vX.Y.Z` branch is the integration target. Each fix MUST live on its own `fix/<ISSUE>-<short>` branch cut from the release branch, inside its own worktree under `.worktrees/`. After the per-issue PR is merged into the release branch, the worktree and local branch are deleted. Never commit fixes directly to the release branch. If no release branch exists yet, create one first using `/generate-release` Phase 1 steps 15.
> **⛔ PR PROHIBITION**: If a fix is associated with a contributor's PR, you MUST merge their PR — NEVER close it and re-implement the fix yourself. See `/review-prs` workflow for the full policy. The `gh pr close` command is FORBIDDEN unless the repository owner explicitly requests it.
> **🌐 REPLY LANGUAGE**: All comments posted to issues (close messages, RESPOND comments, PR descriptions visible to the reporter) MUST match the reporter's language. When in doubt, default to **English**. The reporter's language is detected from the issue body and prior comments by that author.
## Steps
### 1. Identify the GitHub Repository
// turbo
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
- Parse the owner and repo name from the URL
### 2. Ensure Release Branch Exists
// turbo
Before doing any work, ensure a `release/vX.Y.Z` branch exists. If you are currently on `main`, create one:
```bash
git branch --show-current
# If on main, determine next version and create the release branch
VERSION=$(node -p "require('./package.json').version")
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=999?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
git checkout -b release/v$NEXT
npm version patch --no-git-tag-version
npm install
```
> Threshold: patches climb to `.999` before rolling. Example: `3.4.999` → `3.5.0`.
If already on a `release/vX.Y.Z` branch, continue working there.
### 3. Fetch All Open Issues (cap 30 per batch)
// turbo-all
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. Use the two-step approach below.
**Step 3a — Get Issue numbers only** (small output, never truncated):
- Run: `gh issue list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
- Count them and remember the total.
**Step 3b — Fetch full metadata for each Issue** (parallel, validated against 3a):
- For each issue number from step 3a, run:
`gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author,url`
- Batch in parallel (812 concurrent calls). After completion, assert `fetched_count == count_from_3a`; if mismatch, retry the missing IDs.
- Sort by oldest first (FIFO).
**Step 3c — Cap at 30 per run**:
- If more than 30 open issues qualify as bugs after step 4, ask the user which subset of up to 30 to handle now. The remainder is deferred to the next run.
### 4. Classify Each Issue
For each issue, determine its type:
- **Bug** — Has `bug` label, or body contains error messages, stack traces, "doesn't work", "broken", "crash", "error"
- **Feature Request** — Has `enhancement`/`feature` label, or body describes new functionality
- **Question** — Has `question` label, or is asking "how to" something
- **Other** — Anything else
Focus ONLY on **Bugs** for resolution. Feature requests and questions are skipped with a note in the final report.
#### 4.5. PR-Linked Check (mandatory)
For every bug, query linked PRs:
```bash
gh issue view <NUMBER> --repo <owner>/<repo> --json closedByPullRequestsReferences,body
```
If the issue is referenced by an **open** contributor PR (or the body links to one), do NOT plan a self-implemented fix. Mark the issue as `🤝 PR-LINKED — redirect to /review-prs` in the report and stop deeper analysis for it. **NEVER close the contributor PR.**
### 5. Deep-Read Each Bug Issue (One-by-One Analysis)
Read each bug issue thoroughly, one at a time. Each issue gets focused attention.
#### 5a. Understand the Problem
1. **Read the entire body** — Description, Steps to Reproduce, Expected/Actual Behavior, Error Logs, Screenshots
2. **Read ALL comments** — bot triage (Kilo, etc.) and owner/community responses. Look for:
- Someone already responded with a fix
- Community member confirmed it is resolved
- Bot duplicate flag. **DO NOT blindly trust bot labels (e.g., `kilo-duplicate`).** Re-verify independently from current source + web research.
3. **Identify the claimed error** — exact error message, status code, provider/model, OS, Node version.
#### 5b. Check Information Sufficiency
Verify the issue contains:
- [ ] Clear description of the problem
- [ ] Steps to reproduce OR error logs
- [ ] Provider/model/version information
- [ ] Expected vs actual behavior
**If ANY item is missing → auto-classify as `📝 RESPOND — Needs Info` and skip 5d.** Do not attempt root-cause analysis on under-specified issues.
#### 5c. Determine Issue Disposition
| Disposition | When to Apply | Action |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| **✅ CLOSE — Already Fixed** | Owner responded with fix + no user follow-up, OR community confirmed fix | Close with comment citing which version fixed it |
| **✅ CLOSE — Duplicate** | You have independently verified the issue is a duplicate (do NOT rely solely on bot flags) + user provides no new info | Close referencing the original issue |
| **✅ CLOSE — Stale** | We requested logs/info > 7 days ago with no reply | Close thanking the user, invite to reopen if needed |
| **📝 RESPOND — Needs Info** | Issue is real but missing critical reproduction details (also triggered by 5b) | Comment asking for specifics per `/issue-triage` |
| **📝 RESPOND — User Config** | Error is caused by unsupported env (Node version, wrong model path, missing API enablement) | Comment explaining the user-side fix |
| **🤝 PR-LINKED** | An open contributor PR already targets this issue (from step 4.5) | Redirect to `/review-prs`; do not re-implement |
| **🔧 FIX — Code Change** | Root cause is confirmed in the codebase | Research, propose solution in report, wait for approval |
#### 5d. For "FIX — Code Change" Issues
Before coding, perform deep source analysis:
1. **Search the codebase** — grep for error strings, function names, affected files
2. **Search the web** — upstream API changes, SDK updates, breaking changes
3. **Read the full source file** — don't rely on grep snippets
4. **Verify the root cause** is in our code, not user misconfiguration
5. **Formulate a proposed solution** — exact files/lines/logic
6. **Create an Implementation Plan file** at `_tasks/fixes-vX.Y.Z/<ISSUE>-<short-description>.plan.md` (`vX.Y.Z` = current release branch version). Create the directory first: `mkdir -p _tasks/fixes-vX.Y.Z`. The plan contains: Overview, Reproduction Steps, Regression Test Outline, Implementation Steps (files/changes), Rollout Notes.
7. **DO NOT modify the codebase yet** — wait for user approval.
#### 5e. For "RESPOND" Issues
Post a substantive comment that:
- Acknowledges the specific error reported
- Explains the likely root cause
- Provides concrete steps (version upgrade, env var fix, model path correction)
- Asks for follow-up info if needed
**No generic templates.** Every comment references the user's specific error and environment, and is written in the reporter's language (English default).
### 6. Generate Report & Wait for Validation
Present a summary report. For FIX bugs, explicitly explain the proposed solution (files to change + logic) and confirm it will land via per-issue worktree → PR → release branch after approval. Include the reporter's detected language per row so the user can verify.
| Issue | Title | Status | Reply Lang | Proposed Action / Version |
| ----- | ----- | -------------- | ---------- | ------------------------------------------ |
| #N | Title | ✅ Close | en | Already fixed / duplicate (explain why) |
| #N | Title | 🔧 Propose | pt-BR | Code fix plan summary + worktree branch |
| #N | Title | 📝 Respond | en | Guidance comment to be posted |
| #N | Title | ❓ Needs Info | en | Triage comment to be posted |
| #N | Title | 🤝 PR-Linked | en | Redirect to /review-prs (PR #M) |
| #N | Title | ⏭️ Skip | — | Feature request / not a bug |
> **⚠️ IMPORTANT**: Do NOT implement code changes, commit, push, or close issues at this step.
> Wait for the user to review the proposed fixes and respond with **OK** before proceeding.
- If the user says **OK** → Proceed to step 7
- If the user requests changes → Adjust and re-present the report
- If the user rejects → Revert any accidental changes and stop
### 7. Implement Fixes via Per-Issue Worktrees + PRs (only after user approval)
For each approved FIX issue (up to 30 per batch), repeat the following sequence. Issues can be processed sequentially or in parallel (one worktree each — never two fixes in the same worktree).
#### 7.1. Spin up an isolated worktree on a fresh fix branch
```bash
ISSUE=<NUMBER>
SHORT=<short-kebab-desc>
RELEASE_BRANCH=$(git -C <project_root> branch --show-current) # release/vX.Y.Z
WT_DIR=".worktrees/fix-${ISSUE}-${SHORT}"
BRANCH="fix/${ISSUE}-${SHORT}"
git fetch origin "$RELEASE_BRANCH"
git worktree add "$WT_DIR" -b "$BRANCH" "origin/$RELEASE_BRANCH"
cd "$WT_DIR"
```
#### 7.2. Write the regression test first (TDD)
- Author a unit/integration test that reproduces the bug. **It must fail on the unfixed code.** Run it and confirm the failure.
- Hard rule #8: any production change must ship with tests in the same PR. The regression test is non-negotiable.
#### 7.3. Implement the fix
- Apply the approved plan from `_tasks/fixes-vX.Y.Z/<ISSUE>-<short>.plan.md`.
- Keep the diff scoped to this issue. No drive-by refactors.
#### 7.4. Run the test suite
- `npm run test:all` (or the appropriate suite for the touched area; the regression test MUST be included).
- All tests must pass before commit. Also run the relevant `lint` / `typecheck` per CLAUDE.md trust-but-verify checklist.
#### 7.5. Update CHANGELOG.md and commit (single commit, same diff)
- Add the new bug-fix entry under the current `vX.Y.Z` section of CHANGELOG.md.
- CHANGELOG entry + code + test go in **one** commit on the fix branch:
```bash
git add <changed files> CHANGELOG.md
git commit -m "fix: <description> (#${ISSUE})"
```
#### 7.6. Push and open a PR into the release branch
```bash
git push -u origin "$BRANCH"
gh pr create \
--base "$RELEASE_BRANCH" \
--head "$BRANCH" \
--title "fix: <description> (#${ISSUE})" \
--body "Closes #${ISSUE}\n\n<short summary, plan link, regression test reference>"
```
#### 7.7. Merge the PR into the release branch
- Wait for CI green, then merge with the project's default merge strategy.
- The PR title becomes the release-branch commit.
#### 7.8. Clean up worktree and local branch
```bash
cd <project_root>
git worktree remove "$WT_DIR"
git branch -D "$BRANCH"
```
#### 7.9. Close the issue with a localized comment
Match the reporter's language (English default). Template:
> **EN**: Thanks for reporting! Fixed in `release/vX.Y.Z` (already merged into the active development branch — feel free to pull and test it now). It will ship in the next release (vX.Y.Z).
>
> **pt-BR**: Obrigado pelo report! Corrigido em `release/vX.Y.Z` (já mergeado na branch de desenvolvimento atual — pode dar pull e testar). Vai sair na próxima release (vX.Y.Z).
```bash
gh issue close "$ISSUE" --repo <owner>/<repo> --comment "<localized message above>"
```
#### 7.10. Close non-FIX dispositions
After all FIX issues are merged:
- `Duplicate`: close referencing the original issue (localized).
- `Stale`: close thanking the user and inviting reopen (localized).
- `RESPOND — Needs Info` / `RESPOND — User Config`: post the substantive comment from 5e (localized).
- `PR-LINKED`: leave the issue open; comment redirecting to the contributor PR if not already linked.
#### 7.11. Hand off to release flow (optional)
If a release PR to `main` is desired now, run `/generate-release` Phase 1 steps 710 (tests → commit version bump → push → open PR to main → wait for user).
If NO fixes were committed, skip 7.77.11 and just conclude the workflow.

View File

@@ -1,271 +0,0 @@
---
name: review-discussions-ag
description: Read all open GitHub Discussions, summarize them, respond to pending ones, create issues from actionable feature requests, and triage stale threads for closure
---
# /review-discussions — GitHub Discussions Review & Response Workflow
## Overview
This workflow reads all open GitHub Discussions, generates a categorized summary, identifies which ones need a response, drafts and posts replies, optionally creates issues from actionable feature requests, and triages stale threads for closure.
**Modern tooling (replaces deprecated `browser_subagent` flow):**
- Reads use `gh api graphql` — one query returns 50 discussions with full bodies, comments, replies, IDs, and `updatedAt`.
- Writes (post comment, create issue, close discussion) use `gh api graphql` mutations or `gh issue create`.
- Pace at ~1s between writes to avoid abuse-detection throttling.
- `WebFetch` is acceptable only for read-only HTML scraping when GraphQL is unavailable — never for write actions.
// turbo-all
## Steps
### 1. Identify the GitHub Repository
- Run: `git -C <project_root> remote get-url origin` to extract `owner/repo`.
- Parse owner and repo name from the URL (https or ssh form).
### 2. Fetch All Open Discussions (paginated GraphQL)
GraphQL caps each `discussions` query at 50 nodes — repos with more than 50 open discussions **must paginate**. Loop with `first: 50, after: $cursor` until `pageInfo.hasNextPage` is `false`. Skipping pagination silently drops the older half of the backlog, which is exactly where most stale-candidates and unanswered follow-ups live (regression observed 2026-05-28: page-1-only fetch missed 5 follow-ups and 4 stale candidates ranging from 23d to 56d).
Each page request must return the **same field set** — easy mistake is to fetch page 2 without `body` (because the cursor query was hand-edited). Define one query string with `body` on both the discussion and every comment/reply, and reuse it across pages.
Critical fields per discussion: `id` (node ID, **not** the visible `number`), `number`, `title`, `url`, `createdAt`, `updatedAt`, `author.login`, `category.name`, `body`, `answerChosenAt`, `labels(first: 10) { nodes { name } }`, plus nested `comments(first: 50) { totalCount, nodes { id, author.login, body, createdAt, replies(first: 20) { nodes { author.login, body, createdAt } } } }`. Must also include `pageInfo { hasNextPage endCursor }` on the discussions connection.
Persist the **merged** result (all pages concatenated) to `/tmp/discussions-<repo>-<date>.json` so re-runs in the same session avoid a re-fetch. Build an `id → number` map for the post phase — the GraphQL `addDiscussionComment` mutation requires the node ID, not the number.
Capture **image attachments** present in body or comments (`<img src="...">` or markdown `![...](...)`). Surface their count in the per-discussion summary (e.g., `📷 3 screenshots`) so the user can decide if visual context matters before approving a draft.
### 3. Summarize All Discussions
For each discussion, extract:
- **Title** and **#Number**
- **Author** (GitHub username)
- **Category** (Announcements, General, Ideas, Q&A, Show and tell)
- **Created** + **Last updated** (ISO date)
- **Summary** of original post (1-2 sentences)
- **Comment count** + **last commenter** + **last comment date** — determine these by **chronological `createdAt`**, not iteration order. Comments and their nested replies must be merged into a single sorted timeline before picking the latest event (otherwise a recent top-level reply gets shadowed by an older nested reply of an earlier comment, and the discussion is misclassified).
- **Maintainer involvement**: whether the repo owner already replied, and how many times
- **Pending action** — derived state, see categories below
- **Attachments**: count of screenshots / videos / pastebin links
- **Detected language** of the reporter (for reply-language matching)
### 4. Present Summary Report to User
Group by **pending action**, not by category, so the human sees triage buckets at a glance:
| State | Meaning |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
| ⚠️ Needs first response | Zero comments, or all comments are from non-maintainers |
| 🔄 Follow-up pending | Maintainer replied, but reporter or third party added a new comment maintainer has not addressed |
| 🕒 Stale (>15d) | Maintainer was last to comment, no activity for 15+ days — candidate for soft-close or reporter-ping (see step 8)|
| ✅ Answered | Maintainer already replied AND last commenter is the maintainer AND age < 15d |
| 🏁 Resolved | `answerChosenAt` is set |
Within each bucket, present a table:
| # | Category | Title | Author | Updated | Notes |
| --- | -------- | ------------------ | ------ | ------- | ---------------------- |
| #N | Q&A | short title (60ch) | @user | YYYY-MM-DD | 📷2 · 🐛bug · 💡FR |
Tag rows with content hints when detected: `🐛bug` (`[BUG]` / `error` / stacktrace in body), `💡FR` (`feature request` / `add support for`), `❓support` (config/usage question), `🙏thanks` (short ack-only follow-up).
### 5. Draft & Post Responses
#### Reply templates by intent
Pick the template that matches the discussion intent — do NOT use a single generic format.
**A. Bug confirmed** — ack + root cause + tracking + workaround
```
Hey @user! Confirmed -- {root cause in one sentence}. I traced it to `path/to/file.ts:line`.
{Why it happens: 2-4 sentences of technical detail}
I have opened {issue #N} to track the fix. Workaround until it ships: {concrete steps}. Will update here when the patch lands.
```
**B. Feature Request** — ack + status + scope + commit
```
Hey @user! {Status: "Already exists" / "Tracked in #N" / "Reasonable, opening an issue"}.
{If already exists: pointer to dashboard page or doc}
{If tracked: link to umbrella, summarize order/priority}
{If new: open issue + post link back}
{Optional: short technical note on feasibility / trade-offs}
```
**C. Support / config question** — direct answer + reference + offer to dig deeper
```
Hey @user! {One-sentence answer}.
Steps:
1. ...
2. ...
3. ...
Reference: `docs/<path>.md`. If it still fails after that, paste {specific thing} and I will trace it.
```
**D. Thank-you / short follow-up** — 1-2 sentences
```
Glad it helps, @user! {Concrete next marker — when patch ships / when to expect next update}.
```
**E. Stale / closing** — see step 8
#### Posting via gh (replaces deprecated browser flow)
```bash
gh api graphql -f query='
mutation($id: ID!, $body: String!) {
addDiscussionComment(input: {discussionId: $id, body: $body}) {
comment { id url }
}
}' -f id="$NODE_ID" -f body="$BODY"
```
For **threaded replies** (recommended when responding to a specific comment in a long thread), add `replyToId: $parentCommentId` to the input.
**Output hygiene** (still applies even via API — the comment renders in GitHub UI):
- ASCII-safe punctuation: regular hyphens `-`, `->` for arrows
- Markdown OK: `**bold**`, fenced code blocks, `[text](url)` links
- No bare error messages with stack traces from internal logs — sanitize
- Match reporter's language (pt-BR reporter → pt-BR reply; ru reporter → ru reply); default to English when uncertain
**Pacing**: `sleep 1` between mutations. GitHub abuse-detection trips around 10/sec for the same actor.
**Verification**: capture the returned `comment.url` from each mutation. Failed posts (returncode != 0 or `errors` in response) get logged separately and retried once after a 5s pause.
### 6. Create Issues from Actionable Feature Requests
For discussions that contain concrete, actionable feature requests:
1. **Deduplicate FIRST** — before drafting, search existing issues:
```bash
gh issue list --repo $OWNER/$REPO --search "<keywords from FR>" --state open --json number,title,labels
```
If a matching issue (or umbrella) already exists, reuse it — never create a duplicate. Post a comment in the discussion linking to the existing issue.
2. **Ask the user which to create** — even after dedup, the human approves the final list.
3. **Create the issue** with `gh issue create`:
```bash
gh issue create --repo $OWNER/$REPO \
--title "[feature] <short imperative>" \
--label enhancement \
--body @/tmp/issue-body.md
```
Body template:
```markdown
## Feature Request
**Source:** Discussion #N by @author
## Problem
What limitation the user hit (in their words, paraphrased)
## Proposed Solution
How it could work
### Implementation Ideas
- File paths likely to touch
- Related modules / patterns already in the codebase
### Current Workarounds
What users can do today
## Additional Context
- Discussion: #N
- Related issues/PRs: #X, #Y
- Upstream references: link to similar implementations in `_references/` if applicable
```
4. **Generate task file in `_ideia/`** when the feature needs deeper investigation before implementation:
```
_ideia/<short-kebab-slug>.md
```
Contains: problem statement, current OmniRoute state, how upstream (`_references/9router`, `_references/CLIProxyAPI`, etc.) handles it, proposed implementation levels (short/medium/long term), acceptance criteria.
5. **Link back to discussion** with the real URL:
```
Follow-up @reporter — I've opened issue #N to track this. {1-line summary of what the issue covers}.
```
### 7. Final Report
| Discussion | Action Taken |
| ---------- | ------------------------------------------------------------- |
| #N — Title | Responded (bug confirmed, tracking #M) |
| #N — Title | Responded + created issue #M + task file `_ideia/X.md` |
| #N — Title | Responded (support answered with workaround) |
| #N — Title | Responded to follow-up comment |
| #N — Title | Closed (stale 15+d, no reply from reporter) |
| #N — Title | Ping sent (stale 15+d, will close in 7d if no response) |
Include totals: comments posted, issues created, discussions closed, discussions pinged. Capture median response time for the batch.
### 8. Stale Discussion Triage (auto-close candidates)
Identify discussions matching **all** of:
- `updatedAt > 15 days ago`
- Maintainer already replied at least once
- Last commenter is the maintainer (the ball is on the reporter's side)
- `answerChosenAt` is null (not formally resolved)
- Category in `{Q&A, General}` — skip `Ideas` / `Show and tell` / `Announcements` (those serve as community references and shouldn't be closed)
- `comments.totalCount >= 2` — there was actual conversation, not a drive-by post
- No label named `keep-open` (escape hatch)
For each candidate, present to the user with a recommended action:
| Action | When |
| --------------- | ----------------------------------------------------------------------------- |
| **Soft-close** | Default — maintainer answered concretely and reporter went silent |
| **Ping reporter** | Maintainer asked for more info (log dump, screenshot) and never got it |
| **Keep open** | Conversation is mid-debug and closing would lose context — operator override |
**Soft-close mutation:**
```bash
gh api graphql -f query='
mutation($id: ID!) {
closeDiscussion(input: {discussionId: $id, reason: RESOLVED}) {
discussion { id closed }
}
}' -f id="$NODE_ID"
```
Valid `reason` values: `RESOLVED`, `OUTDATED`, `DUPLICATE`. Default to `OUTDATED` for "no response" closures, `RESOLVED` for answered-but-not-confirmed.
Before closing, post a closing comment:
```
Closing for inactivity -- feel free to reopen if you still hit this, or open a fresh issue with a current log. Thanks!
```
**Ping flow** (alternative):
```
@reporter -- still happening on the latest version? Otherwise I'll close this in 7 days for inactivity.
```
Persist the ping in `_cache/discussions-pinged-<date>.json` so the next run knows to close discussions that were pinged 7+ days ago without a reply.
## Notes
- This workflow is **interactive** — always present the summary and wait for user approval before posting responses, creating issues, or closing discussions.
- Gather batched approval — separate consents for "reply scope", "create issues for?", "close stale?". Stale handling is a distinct consent from reply posting.
- For discussions in non-English languages (`pt-BR`, `ru`, `zh`, `es`), respond in the same language as the original post. Default to English when uncertain.
- Always reference specific dashboard paths, config options, doc files, or code locations (`file:line`) when explaining existing features — never wave hands.
- When a discussion reveals a bug, separate it from feature requests in the report. Bugs need a tracking issue + workaround; FRs need scoping.
- Before recommending a workaround that mentions a file/flag/setting, verify it exists in the **current** codebase (the previous turn's memory may be stale).
- Trust-but-verify: after a batch post, spot-check 2-3 random `comment.url` returns in the browser to confirm the comments rendered cleanly (no Unicode mojibake, no broken markdown).
## Anti-patterns to avoid
- ❌ Posting via `browser_subagent` clicks — slow, flaky, and obsolete since `gh api graphql` mutations exist.
- ❌ N+1 fetches (one per discussion) — use one GraphQL query for all 50.
- ❌ Creating an issue without checking for an existing umbrella / similar one first.
- ❌ Generic "thanks, I'll look into it!" responses — every reply must reference a file, doc, or concrete action.
- ❌ Closing a stale discussion without posting a closing comment first.
- ❌ Skipping the user approval gate ("turbo-all" never bypasses interactive consent for writes).

View File

@@ -1,272 +0,0 @@
---
name: review-discussions-cc
description: Read all open GitHub Discussions, summarize them, respond to pending ones, create issues from actionable feature requests, and triage stale threads for closure
---
# /review-discussions — GitHub Discussions Review & Response Workflow
## Overview
This workflow reads all open GitHub Discussions, generates a categorized summary, identifies which ones need a response, drafts and posts replies, optionally creates issues from actionable feature requests, and triages stale threads for closure.
**Modern tooling (replaces deprecated `browser_subagent` flow):**
- Reads use `gh api graphql` — one query returns 50 discussions with full bodies, comments, replies, IDs, and `updatedAt`.
- Writes (post comment, create issue, close discussion) use `gh api graphql` mutations or `gh issue create`.
- Pace at ~1s between writes to avoid abuse-detection throttling.
- `WebFetch` is acceptable only for read-only HTML scraping when GraphQL is unavailable — never for write actions.
// turbo-all
## Steps
### 1. Identify the GitHub Repository
- Run: `git -C <project_root> remote get-url origin` to extract `owner/repo`.
- Parse owner and repo name from the URL (https or ssh form).
### 2. Fetch All Open Discussions (paginated GraphQL)
GraphQL caps each `discussions` query at 50 nodes — repos with more than 50 open discussions **must paginate**. Loop with `first: 50, after: $cursor` until `pageInfo.hasNextPage` is `false`. Skipping pagination silently drops the older half of the backlog, which is exactly where most stale-candidates and unanswered follow-ups live (regression observed 2026-05-28: page-1-only fetch missed 5 follow-ups and 4 stale candidates ranging from 23d to 56d).
Each page request must return the **same field set** — easy mistake is to fetch page 2 without `body` (because the cursor query was hand-edited). Define one query string with `body` on both the discussion and every comment/reply, and reuse it across pages.
Critical fields per discussion: `id` (node ID, **not** the visible `number`), `number`, `title`, `url`, `createdAt`, `updatedAt`, `author.login`, `category.name`, `body`, `answerChosenAt`, `labels(first: 10) { nodes { name } }`, plus nested `comments(first: 50) { totalCount, nodes { id, author.login, body, createdAt, replies(first: 20) { nodes { author.login, body, createdAt } } } }`. Must also include `pageInfo { hasNextPage endCursor }` on the discussions connection.
Persist the **merged** result (all pages concatenated) to `/tmp/discussions-<repo>-<date>.json` so re-runs in the same session avoid a re-fetch. Build an `id → number` map for the post phase — the GraphQL `addDiscussionComment` mutation requires the node ID, not the number.
Capture **image attachments** present in body or comments (`<img src="...">` or markdown `![...](...)`). Surface their count in the per-discussion summary (e.g., `📷 3 screenshots`) so the user can decide if visual context matters before approving a draft.
### 3. Summarize All Discussions
For each discussion, extract:
- **Title** and **#Number**
- **Author** (GitHub username)
- **Category** (Announcements, General, Ideas, Q&A, Show and tell)
- **Created** + **Last updated** (ISO date)
- **Summary** of original post (1-2 sentences)
- **Comment count** + **last commenter** + **last comment date** — determine these by **chronological `createdAt`**, not iteration order. Comments and their nested replies must be merged into a single sorted timeline before picking the latest event (otherwise a recent top-level reply gets shadowed by an older nested reply of an earlier comment, and the discussion is misclassified).
- **Maintainer involvement**: whether the repo owner already replied, and how many times
- **Pending action** — derived state, see categories below
- **Attachments**: count of screenshots / videos / pastebin links
- **Detected language** of the reporter (for reply-language matching)
### 4. Present Summary Report to User
Group by **pending action**, not by category, so the human sees triage buckets at a glance:
| State | Meaning |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
| ⚠️ Needs first response | Zero comments, or all comments are from non-maintainers |
| 🔄 Follow-up pending | Maintainer replied, but reporter or third party added a new comment maintainer has not addressed |
| 🕒 Stale (>15d) | Maintainer was last to comment, no activity for 15+ days — candidate for soft-close or reporter-ping (see step 8)|
| ✅ Answered | Maintainer already replied AND last commenter is the maintainer AND age < 15d |
| 🏁 Resolved | `answerChosenAt` is set |
Within each bucket, present a table:
| # | Category | Title | Author | Updated | Notes |
| --- | -------- | ------------------ | ------ | ------- | ---------------------- |
| #N | Q&A | short title (60ch) | @user | YYYY-MM-DD | 📷2 · 🐛bug · 💡FR |
Tag rows with content hints when detected: `🐛bug` (`[BUG]` / `error` / stacktrace in body), `💡FR` (`feature request` / `add support for`), `❓support` (config/usage question), `🙏thanks` (short ack-only follow-up).
### 5. Draft & Post Responses
#### Reply templates by intent
Pick the template that matches the discussion intent — do NOT use a single generic format.
**A. Bug confirmed** — ack + root cause + tracking + workaround
```
Hey @user! Confirmed -- {root cause in one sentence}. I traced it to `path/to/file.ts:line`.
{Why it happens: 2-4 sentences of technical detail}
I have opened {issue #N} to track the fix. Workaround until it ships: {concrete steps}. Will update here when the patch lands.
```
**B. Feature Request** — ack + status + scope + commit
```
Hey @user! {Status: "Already exists" / "Tracked in #N" / "Reasonable, opening an issue"}.
{If already exists: pointer to dashboard page or doc}
{If tracked: link to umbrella, summarize order/priority}
{If new: open issue + post link back}
{Optional: short technical note on feasibility / trade-offs}
```
**C. Support / config question** — direct answer + reference + offer to dig deeper
```
Hey @user! {One-sentence answer}.
Steps:
1. ...
2. ...
3. ...
Reference: `docs/<path>.md`. If it still fails after that, paste {specific thing} and I will trace it.
```
**D. Thank-you / short follow-up** — 1-2 sentences
```
Glad it helps, @user! {Concrete next marker — when patch ships / when to expect next update}.
```
**E. Stale / closing** — see step 8
#### Posting via gh (replaces deprecated browser flow)
```bash
gh api graphql -f query='
mutation($id: ID!, $body: String!) {
addDiscussionComment(input: {discussionId: $id, body: $body}) {
comment { id url }
}
}' -f id="$NODE_ID" -f body="$BODY"
```
For **threaded replies** (recommended when responding to a specific comment in a long thread), add `replyToId: $parentCommentId` to the input.
**Output hygiene** (still applies even via API — the comment renders in GitHub UI):
- ASCII-safe punctuation: regular hyphens `-`, `->` for arrows
- Markdown OK: `**bold**`, fenced code blocks, `[text](url)` links
- No bare error messages with stack traces from internal logs — sanitize
- Match reporter's language (pt-BR reporter → pt-BR reply; ru reporter → ru reply); default to English when uncertain
**Pacing**: `sleep 1` between mutations. GitHub abuse-detection trips around 10/sec for the same actor.
**Verification**: capture the returned `comment.url` from each mutation. Failed posts (returncode != 0 or `errors` in response) get logged separately and retried once after a 5s pause.
### 6. Create Issues from Actionable Feature Requests
For discussions that contain concrete, actionable feature requests:
1. **Deduplicate FIRST** — before drafting, search existing issues:
```bash
gh issue list --repo $OWNER/$REPO --search "<keywords from FR>" --state open --json number,title,labels
```
If a matching issue (or umbrella) already exists, reuse it — never create a duplicate. Post a comment in the discussion linking to the existing issue.
2. **Ask the user which to create** — even after dedup, the human approves the final list.
3. **Create the issue** with `gh issue create`:
```bash
gh issue create --repo $OWNER/$REPO \
--title "[feature] <short imperative>" \
--label enhancement \
--body @/tmp/issue-body.md
```
Body template:
```markdown
## Feature Request
**Source:** Discussion #N by @author
## Problem
What limitation the user hit (in their words, paraphrased)
## Proposed Solution
How it could work
### Implementation Ideas
- File paths likely to touch (use `Grep` if needed to confirm)
- Related modules / patterns already in the codebase
### Current Workarounds
What users can do today
## Additional Context
- Discussion: #N
- Related issues/PRs: #X, #Y
- Upstream references: link to similar implementations in `_references/` if applicable
```
4. **Generate task file in `_ideia/`** when the feature needs deeper investigation before implementation:
```
_ideia/<short-kebab-slug>.md
```
Contains: problem statement, current OmniRoute state, how upstream (`_references/9router`, `_references/CLIProxyAPI`, etc.) handles it, proposed implementation levels (short/medium/long term), acceptance criteria.
5. **Link back to discussion** with the real URL:
```
Follow-up @reporter — I've opened issue #N to track this. {1-line summary of what the issue covers}.
```
### 7. Final Report
| Discussion | Action Taken |
| ---------- | ------------------------------------------------------------- |
| #N — Title | Responded (bug confirmed, tracking #M) |
| #N — Title | Responded + created issue #M + task file `_ideia/X.md` |
| #N — Title | Responded (support answered with workaround) |
| #N — Title | Responded to follow-up comment |
| #N — Title | Closed (stale 15+d, no reply from reporter) |
| #N — Title | Ping sent (stale 15+d, will close in 7d if no response) |
Include totals: comments posted, issues created, discussions closed, discussions pinged. Capture median response time for the batch.
### 8. Stale Discussion Triage (auto-close candidates)
Identify discussions matching **all** of:
- `updatedAt > 15 days ago`
- Maintainer already replied at least once
- Last commenter is the maintainer (the ball is on the reporter's side)
- `answerChosenAt` is null (not formally resolved)
- Category in `{Q&A, General}` — skip `Ideas` / `Show and tell` / `Announcements` (those serve as community references and shouldn't be closed)
- `comments.totalCount >= 2` — there was actual conversation, not a drive-by post
- No label named `keep-open` (escape hatch)
For each candidate, present to the user with a recommended action:
| Action | When |
| --------------- | ----------------------------------------------------------------------------- |
| **Soft-close** | Default — maintainer answered concretely and reporter went silent |
| **Ping reporter** | Maintainer asked for more info (log dump, screenshot) and never got it |
| **Keep open** | Conversation is mid-debug and closing would lose context — operator override |
**Soft-close mutation:**
```bash
gh api graphql -f query='
mutation($id: ID!) {
closeDiscussion(input: {discussionId: $id, reason: RESOLVED}) {
discussion { id closed }
}
}' -f id="$NODE_ID"
```
Valid `reason` values: `RESOLVED`, `OUTDATED`, `DUPLICATE`. Default to `OUTDATED` for "no response" closures, `RESOLVED` for answered-but-not-confirmed.
Before closing, post a closing comment:
```
Closing for inactivity -- feel free to reopen if you still hit this, or open a fresh issue with a current log. Thanks!
```
**Ping flow** (alternative):
```
@reporter -- still happening on the latest version? Otherwise I'll close this in 7 days for inactivity.
```
Persist the ping in `_cache/discussions-pinged-<date>.json` so the next run knows to close discussions that were pinged 7+ days ago without a reply.
## Notes
- This workflow is **interactive** — always present the summary and wait for user approval before posting responses, creating issues, or closing discussions.
- Use `AskUserQuestion` to gather batched approval — separate questions for "reply scope", "create issues for?", "close stale?". Stale handling is a separate consent from reply posting.
- For discussions in non-English languages (`pt-BR`, `ru`, `zh`, `es`), respond in the same language as the original post. Default to English when uncertain.
- Always reference specific dashboard paths, config options, doc files, or code locations (`file:line`) when explaining existing features — never wave hands.
- When a discussion reveals a bug, separate it from feature requests in the report. Bugs need a tracking issue + workaround; FRs need scoping.
- Before recommending a workaround that mentions a file/flag/setting, verify it exists in the **current** codebase (the previous turn's memory may be stale).
- Trust-but-verify: after a batch post, spot-check 2-3 random `comment.url` returns in the browser to confirm the comments rendered cleanly (no Unicode mojibake, no broken markdown).
- **Secure-by-default guidance** ([tldrsec/awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults)): when responses recommend security-relevant code (auth, crypto, SSRF, XSS sanitization), prefer well-tested libraries (Helmet.js, DOMPurify, Google Tink, ssrf-req-filter, safe-regex) over hand-rolled solutions.
## Anti-patterns to avoid
- ❌ Posting via `browser_subagent` clicks — slow, flaky, and obsolete since `gh api graphql` mutations exist.
- ❌ N+1 fetches (one per discussion) — use one GraphQL query for all 50.
- ❌ Creating an issue without checking for an existing umbrella / similar one first.
- ❌ Generic "thanks, I'll look into it!" responses — every reply must reference a file, doc, or concrete action.
- ❌ Closing a stale discussion without posting a closing comment first.
- ❌ Skipping the user approval gate ("turbo-all" never bypasses interactive consent for writes).

View File

@@ -1,278 +0,0 @@
---
name: review-discussions-cx
description: Read all open GitHub Discussions, summarize them, respond to pending ones, create issues from actionable feature requests, and triage stale threads for closure
---
# /review-discussions — GitHub Discussions Review & Response Workflow
## Overview
This workflow reads all open GitHub Discussions, generates a categorized summary, identifies which ones need a response, drafts and posts replies, optionally creates issues from actionable feature requests, and triages stale threads for closure.
**Modern tooling (replaces deprecated `browser_subagent` flow):**
- Reads use `gh api graphql` — one query returns 50 discussions with full bodies, comments, replies, IDs, and `updatedAt`.
- Writes (post comment, create issue, close discussion) use `gh api graphql` mutations or `gh issue create`.
- Pace at ~1s between writes to avoid abuse-detection throttling.
- `WebFetch` is acceptable only for read-only HTML scraping when GraphQL is unavailable — never for write actions.
## Codex Execution Notes
- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` for independent reads (e.g., parallel `gh issue list` dedup searches across multiple FRs) — never for write actions.
- The summary report is a hard stop. Do not post discussion replies, create issues, or close discussions until the user explicitly approves each phase.
- Use the `apply_patch` tool to write reply bodies to `/tmp/reply-<num>.md` before invoking `gh api graphql -F body=@/tmp/reply-<num>.md` if the body contains tricky shell-escape characters.
- Stop after step 4 (summary), step 6 (issue creation), and step 8 (stale triage). Three explicit consents per run.
// turbo-all
## Steps
### 1. Identify the GitHub Repository
- Run: `git -C <project_root> remote get-url origin` to extract `owner/repo`.
- Parse owner and repo name from the URL (https or ssh form).
### 2. Fetch All Open Discussions (paginated GraphQL)
GraphQL caps each `discussions` query at 50 nodes — repos with more than 50 open discussions **must paginate**. Loop with `first: 50, after: $cursor` until `pageInfo.hasNextPage` is `false`. Skipping pagination silently drops the older half of the backlog, which is exactly where most stale-candidates and unanswered follow-ups live (regression observed 2026-05-28: page-1-only fetch missed 5 follow-ups and 4 stale candidates ranging from 23d to 56d).
Each page request must return the **same field set** — easy mistake is to fetch page 2 without `body` (because the cursor query was hand-edited). Define one query string with `body` on both the discussion and every comment/reply, and reuse it across pages.
Critical fields per discussion: `id` (node ID, **not** the visible `number`), `number`, `title`, `url`, `createdAt`, `updatedAt`, `author.login`, `category.name`, `body`, `answerChosenAt`, `labels(first: 10) { nodes { name } }`, plus nested `comments(first: 50) { totalCount, nodes { id, author.login, body, createdAt, replies(first: 20) { nodes { author.login, body, createdAt } } } }`. Must also include `pageInfo { hasNextPage endCursor }` on the discussions connection.
Persist the **merged** result (all pages concatenated) to `/tmp/discussions-<repo>-<date>.json` so re-runs in the same session avoid a re-fetch. Build an `id → number` map for the post phase — the GraphQL `addDiscussionComment` mutation requires the node ID, not the number.
Capture **image attachments** present in body or comments (`<img src="...">` or markdown `![...](...)`). Surface their count in the per-discussion summary (e.g., `📷 3 screenshots`) so the user can decide if visual context matters before approving a draft.
### 3. Summarize All Discussions
For each discussion, extract:
- **Title** and **#Number**
- **Author** (GitHub username)
- **Category** (Announcements, General, Ideas, Q&A, Show and tell)
- **Created** + **Last updated** (ISO date)
- **Summary** of original post (1-2 sentences)
- **Comment count** + **last commenter** + **last comment date** — determine these by **chronological `createdAt`**, not iteration order. Comments and their nested replies must be merged into a single sorted timeline before picking the latest event (otherwise a recent top-level reply gets shadowed by an older nested reply of an earlier comment, and the discussion is misclassified).
- **Maintainer involvement**: whether the repo owner already replied, and how many times
- **Pending action** — derived state, see categories below
- **Attachments**: count of screenshots / videos / pastebin links
- **Detected language** of the reporter (for reply-language matching)
### 4. Present Summary Report to User
Group by **pending action**, not by category, so the human sees triage buckets at a glance:
| State | Meaning |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
| ⚠️ Needs first response | Zero comments, or all comments are from non-maintainers |
| 🔄 Follow-up pending | Maintainer replied, but reporter or third party added a new comment maintainer has not addressed |
| 🕒 Stale (>15d) | Maintainer was last to comment, no activity for 15+ days — candidate for soft-close or reporter-ping (see step 8)|
| ✅ Answered | Maintainer already replied AND last commenter is the maintainer AND age < 15d |
| 🏁 Resolved | `answerChosenAt` is set |
Within each bucket, present a table:
| # | Category | Title | Author | Updated | Notes |
| --- | -------- | ------------------ | ------ | ------- | ---------------------- |
| #N | Q&A | short title (60ch) | @user | YYYY-MM-DD | 📷2 · 🐛bug · 💡FR |
Tag rows with content hints when detected: `🐛bug` (`[BUG]` / `error` / stacktrace in body), `💡FR` (`feature request` / `add support for`), `❓support` (config/usage question), `🙏thanks` (short ack-only follow-up).
### 5. Draft & Post Responses
#### Reply templates by intent
Pick the template that matches the discussion intent — do NOT use a single generic format.
**A. Bug confirmed** — ack + root cause + tracking + workaround
```
Hey @user! Confirmed -- {root cause in one sentence}. I traced it to `path/to/file.ts:line`.
{Why it happens: 2-4 sentences of technical detail}
I have opened {issue #N} to track the fix. Workaround until it ships: {concrete steps}. Will update here when the patch lands.
```
**B. Feature Request** — ack + status + scope + commit
```
Hey @user! {Status: "Already exists" / "Tracked in #N" / "Reasonable, opening an issue"}.
{If already exists: pointer to dashboard page or doc}
{If tracked: link to umbrella, summarize order/priority}
{If new: open issue + post link back}
{Optional: short technical note on feasibility / trade-offs}
```
**C. Support / config question** — direct answer + reference + offer to dig deeper
```
Hey @user! {One-sentence answer}.
Steps:
1. ...
2. ...
3. ...
Reference: `docs/<path>.md`. If it still fails after that, paste {specific thing} and I will trace it.
```
**D. Thank-you / short follow-up** — 1-2 sentences
```
Glad it helps, @user! {Concrete next marker — when patch ships / when to expect next update}.
```
**E. Stale / closing** — see step 8
#### Posting via gh (replaces deprecated browser flow)
```bash
gh api graphql -f query='
mutation($id: ID!, $body: String!) {
addDiscussionComment(input: {discussionId: $id, body: $body}) {
comment { id url }
}
}' -f id="$NODE_ID" -f body="$BODY"
```
For **threaded replies** (recommended when responding to a specific comment in a long thread), add `replyToId: $parentCommentId` to the input.
**Output hygiene** (still applies even via API — the comment renders in GitHub UI):
- ASCII-safe punctuation: regular hyphens `-`, `->` for arrows
- Markdown OK: `**bold**`, fenced code blocks, `[text](url)` links
- No bare error messages with stack traces from internal logs — sanitize
- Match reporter's language (pt-BR reporter → pt-BR reply; ru reporter → ru reply); default to English when uncertain
**Pacing**: `sleep 1` between mutations. GitHub abuse-detection trips around 10/sec for the same actor.
**Verification**: capture the returned `comment.url` from each mutation. Failed posts (returncode != 0 or `errors` in response) get logged separately and retried once after a 5s pause.
### 6. Create Issues from Actionable Feature Requests
For discussions that contain concrete, actionable feature requests:
1. **Deduplicate FIRST** — before drafting, search existing issues:
```bash
gh issue list --repo $OWNER/$REPO --search "<keywords from FR>" --state open --json number,title,labels
```
If a matching issue (or umbrella) already exists, reuse it — never create a duplicate. Post a comment in the discussion linking to the existing issue.
2. **Ask the user which to create** — even after dedup, the human approves the final list.
3. **Create the issue** with `gh issue create`:
```bash
gh issue create --repo $OWNER/$REPO \
--title "[feature] <short imperative>" \
--label enhancement \
--body @/tmp/issue-body.md
```
Body template:
```markdown
## Feature Request
**Source:** Discussion #N by @author
## Problem
What limitation the user hit (in their words, paraphrased)
## Proposed Solution
How it could work
### Implementation Ideas
- File paths likely to touch
- Related modules / patterns already in the codebase
### Current Workarounds
What users can do today
## Additional Context
- Discussion: #N
- Related issues/PRs: #X, #Y
- Upstream references: link to similar implementations in `_references/` if applicable
```
4. **Generate task file in `_ideia/`** when the feature needs deeper investigation before implementation:
```
_ideia/<short-kebab-slug>.md
```
Contains: problem statement, current OmniRoute state, how upstream (`_references/9router`, `_references/CLIProxyAPI`, etc.) handles it, proposed implementation levels (short/medium/long term), acceptance criteria.
5. **Link back to discussion** with the real URL:
```
Follow-up @reporter — I've opened issue #N to track this. {1-line summary of what the issue covers}.
```
### 7. Final Report
| Discussion | Action Taken |
| ---------- | ------------------------------------------------------------- |
| #N — Title | Responded (bug confirmed, tracking #M) |
| #N — Title | Responded + created issue #M + task file `_ideia/X.md` |
| #N — Title | Responded (support answered with workaround) |
| #N — Title | Responded to follow-up comment |
| #N — Title | Closed (stale 15+d, no reply from reporter) |
| #N — Title | Ping sent (stale 15+d, will close in 7d if no response) |
Include totals: comments posted, issues created, discussions closed, discussions pinged. Capture median response time for the batch.
### 8. Stale Discussion Triage (auto-close candidates)
Identify discussions matching **all** of:
- `updatedAt > 15 days ago`
- Maintainer already replied at least once
- Last commenter is the maintainer (the ball is on the reporter's side)
- `answerChosenAt` is null (not formally resolved)
- Category in `{Q&A, General}` — skip `Ideas` / `Show and tell` / `Announcements` (those serve as community references and shouldn't be closed)
- `comments.totalCount >= 2` — there was actual conversation, not a drive-by post
- No label named `keep-open` (escape hatch)
For each candidate, present to the user with a recommended action:
| Action | When |
| --------------- | ----------------------------------------------------------------------------- |
| **Soft-close** | Default — maintainer answered concretely and reporter went silent |
| **Ping reporter** | Maintainer asked for more info (log dump, screenshot) and never got it |
| **Keep open** | Conversation is mid-debug and closing would lose context — operator override |
**Soft-close mutation:**
```bash
gh api graphql -f query='
mutation($id: ID!) {
closeDiscussion(input: {discussionId: $id, reason: RESOLVED}) {
discussion { id closed }
}
}' -f id="$NODE_ID"
```
Valid `reason` values: `RESOLVED`, `OUTDATED`, `DUPLICATE`. Default to `OUTDATED` for "no response" closures, `RESOLVED` for answered-but-not-confirmed.
Before closing, post a closing comment:
```
Closing for inactivity -- feel free to reopen if you still hit this, or open a fresh issue with a current log. Thanks!
```
**Ping flow** (alternative):
```
@reporter -- still happening on the latest version? Otherwise I'll close this in 7 days for inactivity.
```
Persist the ping in `_cache/discussions-pinged-<date>.json` so the next run knows to close discussions that were pinged 7+ days ago without a reply.
## Notes
- This workflow is **interactive** — always present the summary and wait for user approval before posting responses, creating issues, or closing discussions.
- Three explicit consents per run: reply scope (after step 4), issue creation list (in step 6), stale-close list (in step 8).
- For discussions in non-English languages (`pt-BR`, `ru`, `zh`, `es`), respond in the same language as the original post. Default to English when uncertain.
- Always reference specific dashboard paths, config options, doc files, or code locations (`file:line`) when explaining existing features — never wave hands.
- When a discussion reveals a bug, separate it from feature requests in the report. Bugs need a tracking issue + workaround; FRs need scoping.
- Before recommending a workaround that mentions a file/flag/setting, verify it exists in the **current** codebase (the previous turn's memory may be stale).
- Trust-but-verify: after a batch post, spot-check 2-3 random `comment.url` returns to confirm the comments rendered cleanly (no Unicode mojibake, no broken markdown).
## Anti-patterns to avoid
- ❌ Posting via `browser_subagent` clicks — slow, flaky, and obsolete since `gh api graphql` mutations exist.
- ❌ N+1 fetches (one per discussion) — use one GraphQL query for all 50.
- ❌ Creating an issue without checking for an existing umbrella / similar one first.
- ❌ Generic "thanks, I'll look into it!" responses — every reply must reference a file, doc, or concrete action.
- ❌ Closing a stale discussion without posting a closing comment first.
- ❌ Skipping the user approval gate ("turbo-all" never bypasses interactive consent for writes).

View File

@@ -1,257 +0,0 @@
---
name: review-prs-ag
description: Analyze open Pull Requests from the project's GitHub repository, generate a critical report, and optionally implement approved changes
---
# /review-prs — PR Review & Analysis Workflow
## ⛔ ABSOLUTE PROHIBITION — Read Before Anything Else
> **NEVER close a contributor's PR if you intend to use ANY of their code, ideas, or fixes.**
>
> **NEVER manually integrate contributor code into a release branch and then close their PR.**
>
> These actions are **STRICTLY FORBIDDEN** under all circumstances:
>
> 1. ❌ Closing a PR and cherry-picking/copying its code into a release branch
> 2. ❌ Closing a PR "because of conflicts" and re-implementing the same fix yourself
> 3. ❌ Closing a PR and committing a "similar" solution inspired by it
> 4. ❌ Using `gh pr close` on any PR whose content was or will be used
>
> **Why**: Closing a PR after taking the contributor's work means they get ZERO credit on GitHub — no "Merged" badge, no contribution graph entry, no public record. This is effectively stealing their contribution. An audit found this happened to **37 PRs** in the past.
>
> **The ONLY acceptable flow**: Resolve conflicts IN the contributor's branch, push fixes TO their branch, then merge THEIR PR via `gh pr merge`. See Step 7 and Step 8 for the exact procedure.
>
> **When to close a PR**: ONLY when the user (repository owner) explicitly requests it, OR when the PR is clearly spam/malicious, OR when the author themselves asks to close it. In ALL other cases, leave it open.
## Overview
This workflow fetches all open PRs from the project's GitHub repository, performs a critical analysis of each one, generates a detailed report, and waits for user approval before proceeding with implementation. **All improvements are committed on the current release branch** (`release/vX.Y.Z`).
> **BRANCH RULE**: PRs are ALWAYS merged into the current `release/vX.Y.Z` branch, NEVER directly into `main`. The release branch acts as a staging area — only after all PRs are integrated and tests pass does the release branch get merged into `main` via the `/generate-release` workflow.
## Steps
### 1. Identify the GitHub Repository
- Read `package.json` to get the repository URL, or use the git remote origin URL
// turbo
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
### 2. Ensure Release Branch Exists
// turbo
Before doing any work, ensure you are on the current release branch:
```bash
# Check current branch
git branch --show-current
# If on main, determine next version and create the release branch
VERSION=$(node -p "require('./package.json').version")
# Bump patch: e.g. 3.3.11 → 3.3.12
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=999?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
git checkout -b release/v$NEXT
npm version patch --no-git-tag-version
npm install
```
If already on a `release/vX.Y.Z` branch, continue working there.
### 3. Fetch Open Pull Requests
// turbo-all
**⚠️ CRITICAL**: The JSON output of `gh pr list` can be truncated by the tool, silently hiding PRs. You MUST use the two-step approach below to guarantee **all** PRs are fetched.
**Step 3a — Get PR numbers only** (small output, never truncated):
- Run: `gh pr list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
- This outputs one PR number per line. Count them and confirm total.
**Step 3b — Fetch full metadata for each PR** (one call per PR):
- For each PR number from step 3a, run:
`gh pr view <NUMBER> --repo <owner>/<repo> --json number,title,author,headRefName,baseRefName,body,createdAt,additions,deletions,files`
- You may batch these into parallel calls (up to 4 at a time).
**Step 3c — Fetch diffs for each PR** (one call per PR, saved to /tmp):
- For each PR number, run:
`gh pr diff <NUMBER> --repo <owner>/<repo> > /tmp/pr<NUMBER>.diff`
- Then read each diff file with the appropriate file-read tool (`Read` in Claude Code; equivalent in your agent runtime).
- For each open PR, collect:
- PR number, title, author, branch, number of commits, date
- PR description/body
- Files changed (diff)
- Existing review comments (from bots or humans)
**Verification**: Confirm the count of PRs analyzed matches the count from step 3a before proceeding.
### 3.5 Redirect PR Base Branches to Release Branch
// turbo-all
**⚠️ CRITICAL**: Contributors typically open PRs targeting `main`. Before analyzing or merging, redirect ALL open PRs to target the current release branch instead.
```bash
# Get the current release branch name
RELEASE_BRANCH=$(git branch --show-current) # e.g. release/v3.5.4
# For each open PR that targets main, change its base to the release branch
for PR_NUM in $(gh pr list --repo <owner>/<repo> --state open --json number,baseRefName --jq '.[] | select(.baseRefName == "main") | .number'); do
echo "Redirecting PR #$PR_NUM$RELEASE_BRANCH"
gh pr edit "$PR_NUM" --repo <owner>/<repo> --base "$RELEASE_BRANCH"
done
```
This ensures:
1. PRs merge into the release branch, not directly into `main`
2. Merge conflict detection is accurate against the release branch
3. The release branch accumulates all changes before the final merge to `main`
4. If the release branch doesn't exist on remote yet, push it first: `git push origin $RELEASE_BRANCH`
### 4. Analyze Each PR — For each open PR, perform the following analysis:
#### 4a. Feature Assessment
- **Does it make sense?** Evaluate if the feature fills a real gap or solves a valid problem
- **Alignment** — Check if it aligns with the project's architecture and roadmap
- **Complexity** — Assess if the scope is reasonable or if it should be split
#### 4b. Code Quality Review
- Check for code duplication
- Evaluate error handling patterns (consistent with existing codebase?)
- Check naming conventions and code style
- Verify TypeScript types (any `any` usage, missing types?)
#### 4c. Security Review
- Check for missing authentication/authorization on new endpoints
- Check for injection vulnerabilities (URL params, SQL, XSS)
- Verify input validation on all user-controlled data
- Check for hardcoded secrets or credentials
#### 4d. Architecture Review
- Does the change follow existing patterns?
- Are there any breaking changes to public APIs?
- Is the database schema affected? Migration needed?
- Impact on performance (N+1 queries, missing indexes?)
#### 4e. Test Coverage
- Does the PR include tests?
- Are edge cases covered?
- Would existing tests break?
#### 4f. Cross-Layer (Global) Analysis
Perform a **global impact assessment** to verify whether the PR changes are complete across all layers of the application:
- **Backend → Frontend check**: If the PR adds or modifies backend-only resources (new endpoints, services, data models), evaluate whether corresponding frontend changes are missing:
- Does a new endpoint require a new screen/page in the dashboard?
- Should there be a new action button, menu item, or navigation link?
- Are there new data fields that should be displayed or editable in the UI?
- Does a new feature need a toggle, configuration panel, or status indicator?
- **Frontend → Backend check**: If the PR adds frontend elements, verify the backend support exists:
- Are the required API endpoints implemented?
- Is the data model sufficient for the new UI components?
- **Cross-cutting concerns**: Check shared layers (types, DTOs, validation schemas, routes, middleware) for completeness
- **Document gaps** — If missing layers are detected, list them as **IMPORTANT** issues in the report with concrete suggestions for what should be added
### 5. Generate Report — Create a markdown report for each PR including:
- **PR Summary** — What it does, files affected, commit count
- **Improvements/Benefits** — Numbered list with impact level (HIGH/MEDIUM/LOW)
- **Risks & Issues** — Categorized as CRITICAL / IMPORTANT / MINOR
- **Scoring Table** — Rate across: Feature Relevance, Code Quality, Security, Robustness, Tests
- **Verdict** — Ready to merge? With mandatory vs optional fixes
- **Next Steps** — What will happen if approved
### 6. Present to User
- Show the report in the final response and stop. Mark this as a blocking checkpoint awaiting explicit user approval.
- Wait for user decision:
- **Approved** → Proceed to step 7
- **Approved with changes** → Implement the fixes and corrections before merging
- **Rejected** → Close the PR or leave a review comment
### 7. Pre-Merge Fixes & CI Green-Lighting (if approved)
> **⚠️ Fixes and Conflict Resolutions MUST be pushed back to the PR branch before merging.** We want the PR itself to be green and fully valid before it integrates.
- **Sync latest fixes & Resolve Conflicts:** Merge the current `release` branch into the PR branch. If there are merge conflicts, you MUST resolve them inside the author's PR branch. NEVER resolve conflicts by closing their PR and doing the work in a separate branch, as this steals credit from the original author.
- **Implement improvements:** Apply the required fixes identified in the analysis directly on the PR branch (e.g., adding missing API routes, fixing SSRF, applying comments from other agents).
- **Pushing changes to PR branches:**
```bash
# Checkout the PR locally
gh pr checkout <NUMBER>
# Apply fixes, commit your changes
git commit -m "chore: apply review suggestions and missing layers"
# Attempt to push directly to the PR branch
git push
```
- **Fallback (ONLY for external forks without maintainer edit access):**
Using `cherry-pick` instead of fixing the contributor's PR directly is a **LAST RESORT**. You MUST ALWAYS attempt to `git push` your fixes to their branch first.
**ONLY if `git push` explicitly fails with a permission/access error** (meaning the contributor unchecked "Allow edits from maintainers" or it's a locked fork), you may use `git cherry-pick` to bring their changes into the release branch and fix the issues locally.
Even then, ensure you preserve the contributor's authorship (`git commit --author="Contributor Name <email>"` if creating new commits).
Once you have integrated their work into the release branch, **DO NOT close their PR**. Leave it open so the contributor retains credit. Under NO CIRCUMSTANCES should you use `gh pr close`.
- Run the project's test suite locally to verify nothing breaks:
// turbo
- Run: `npm test` or equivalent test command
### 8. Merge into Release Branch (NEVER CLOSE!)
> **⚠️ CRITICAL**: NEVER use `gh pr close` for a PR whose idea or code was accepted. Closing a PR in a contributor's face after taking their idea—or closing it just because it had conflicts—is unacceptable.
> You MUST ALWAYS resolve conflicts and apply fixes ON THE AUTHOR'S PR BRANCH (unless explicitly locked from edits), and then merge the PR using GitHub so the contributor gets the official "Merged" badge and proper credit on their profile. **Do not use cherry-pick just because it is "easier" than resolving conflicts on their branch.**
Even if the PR had severe conflicts or required significant architectural adjustments, you MUST:
1. Resolve any conflicts and apply the fixes directly to their PR branch (as detailed in step 7) or use cherry-picking into the release branch.
2. If you managed to fix their branch, merge it into the release branch using the GitHub CLI:
`gh pr merge <NUMBER> --repo <owner>/<repo> --squash --body "Integrated into release/vX.Y.Z"`
3. If you had to use cherry-picking because you couldn't push to their branch, DO NOT close the PR. GitHub will sometimes auto-detect the cherry-picked commits and mark it as Merged. If it doesn't, leave it open. The repository owner will handle it. NEVER run `gh pr close`.
In ALL cases:
- Post a **thank-you comment** on the PR via the GitHub API before or immediately after merging.
- The message should:
- Thank the author by name/username for their contribution.
- Explain what was adjusted or improved (if we pushed fixes to their branch or cherry-picked).
- Note it will be included in the upcoming release.
- Be friendly, professional, and encouraging.
> **⚠️ MANDATORY CHANGELOG CREDIT**: When cherry-picking is used (because the PR branch couldn't be pushed to or `gh pr merge` failed), the contributor does NOT get the automatic GitHub "Merged" badge. In this case, you MUST compensate by adding an explicit entry to `CHANGELOG.md` in the `[Unreleased]` section with `(#PR_NUMBER — thanks @username)` format. This ensures the contributor gets public credit in the release notes even if GitHub doesn't auto-detect the cherry-pick. This is NOT optional — skipping it effectively erases the contributor's work from the release record.
### 9. Sync Local Release Branch
After merging PRs, sync the local release branch to include the new changes:
```bash
git fetch origin
git pull origin release/vX.Y.Z
```
### 10. Continue or Finalize
After processing all approved PRs:
- If more PRs remain, go back to step 7
- When all PRs are processed, **update CHANGELOG.md** on the release branch with all new entries
- Run **test coverage** to verify the gate (≥75% statements/lines/functions, ≥70% branches — measured ~82%):
```bash
npm run test:coverage
```
- Fix any test regressions introduced by merged PRs
- Run `/generate-release` workflow Phase 1 steps 710 (tests → commit → push → open PR to main → wait for user)
- The `/generate-release` workflow handles the final merge from `release/vX.Y.Z` → `main`

View File

@@ -1,257 +0,0 @@
---
name: review-prs-cc
description: Analyze open Pull Requests from the project's GitHub repository, generate a critical report, and optionally implement approved changes
---
# /review-prs — PR Review & Analysis Workflow
## ⛔ ABSOLUTE PROHIBITION — Read Before Anything Else
> **NEVER close a contributor's PR if you intend to use ANY of their code, ideas, or fixes.**
>
> **NEVER manually integrate contributor code into a release branch and then close their PR.**
>
> These actions are **STRICTLY FORBIDDEN** under all circumstances:
>
> 1. ❌ Closing a PR and cherry-picking/copying its code into a release branch
> 2. ❌ Closing a PR "because of conflicts" and re-implementing the same fix yourself
> 3. ❌ Closing a PR and committing a "similar" solution inspired by it
> 4. ❌ Using `gh pr close` on any PR whose content was or will be used
>
> **Why**: Closing a PR after taking the contributor's work means they get ZERO credit on GitHub — no "Merged" badge, no contribution graph entry, no public record. This is effectively stealing their contribution. An audit found this happened to **37 PRs** in the past.
>
> **The ONLY acceptable flow**: Resolve conflicts IN the contributor's branch, push fixes TO their branch, then merge THEIR PR via `gh pr merge`. See Step 7 and Step 8 for the exact procedure.
>
> **When to close a PR**: ONLY when the user (repository owner) explicitly requests it, OR when the PR is clearly spam/malicious, OR when the author themselves asks to close it. In ALL other cases, leave it open.
## Overview
This workflow fetches all open PRs from the project's GitHub repository, performs a critical analysis of each one, generates a detailed report, and waits for user approval before proceeding with implementation. **All improvements are committed on the current release branch** (`release/vX.Y.Z`).
> **BRANCH RULE**: PRs are ALWAYS merged into the current `release/vX.Y.Z` branch, NEVER directly into `main`. The release branch acts as a staging area — only after all PRs are integrated and tests pass does the release branch get merged into `main` via the `/generate-release` workflow.
## Steps
### 1. Identify the GitHub Repository
- Read `package.json` to get the repository URL, or use the git remote origin URL
// turbo
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
### 2. Ensure Release Branch Exists
// turbo
Before doing any work, ensure you are on the current release branch:
```bash
# Check current branch
git branch --show-current
# If on main, determine next version and create the release branch
VERSION=$(node -p "require('./package.json').version")
# Bump patch: e.g. 3.3.11 → 3.3.12
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=999?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
git checkout -b release/v$NEXT
npm version patch --no-git-tag-version
npm install
```
If already on a `release/vX.Y.Z` branch, continue working there.
### 3. Fetch Open Pull Requests
// turbo-all
**⚠️ CRITICAL**: The JSON output of `gh pr list` can be truncated by the tool, silently hiding PRs. You MUST use the two-step approach below to guarantee **all** PRs are fetched.
**Step 3a — Get PR numbers only** (small output, never truncated):
- Run: `gh pr list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
- This outputs one PR number per line. Count them and confirm total.
**Step 3b — Fetch full metadata for each PR** (one call per PR):
- For each PR number from step 3a, run:
`gh pr view <NUMBER> --repo <owner>/<repo> --json number,title,author,headRefName,baseRefName,body,createdAt,additions,deletions,files`
- You may batch these into parallel calls (up to 4 at a time).
**Step 3c — Fetch diffs for each PR** (one call per PR, saved to /tmp):
- For each PR number, run:
`gh pr diff <NUMBER> --repo <owner>/<repo> > /tmp/pr<NUMBER>.diff`
- Then read each diff file with the `Read` tool.
- For each open PR, collect:
- PR number, title, author, branch, number of commits, date
- PR description/body
- Files changed (diff)
- Existing review comments (from bots or humans)
**Verification**: Confirm the count of PRs analyzed matches the count from step 3a before proceeding.
### 3.5 Redirect PR Base Branches to Release Branch
// turbo-all
**⚠️ CRITICAL**: Contributors typically open PRs targeting `main`. Before analyzing or merging, redirect ALL open PRs to target the current release branch instead.
```bash
# Get the current release branch name
RELEASE_BRANCH=$(git branch --show-current) # e.g. release/v3.5.4
# For each open PR that targets main, change its base to the release branch
for PR_NUM in $(gh pr list --repo <owner>/<repo> --state open --json number,baseRefName --jq '.[] | select(.baseRefName == "main") | .number'); do
echo "Redirecting PR #$PR_NUM$RELEASE_BRANCH"
gh pr edit "$PR_NUM" --repo <owner>/<repo> --base "$RELEASE_BRANCH"
done
```
This ensures:
1. PRs merge into the release branch, not directly into `main`
2. Merge conflict detection is accurate against the release branch
3. The release branch accumulates all changes before the final merge to `main`
4. If the release branch doesn't exist on remote yet, push it first: `git push origin $RELEASE_BRANCH`
### 4. Analyze Each PR — For each open PR, perform the following analysis:
#### 4a. Feature Assessment
- **Does it make sense?** Evaluate if the feature fills a real gap or solves a valid problem
- **Alignment** — Check if it aligns with the project's architecture and roadmap
- **Complexity** — Assess if the scope is reasonable or if it should be split
#### 4b. Code Quality Review
- Check for code duplication
- Evaluate error handling patterns (consistent with existing codebase?)
- Check naming conventions and code style
- Verify TypeScript types (any `any` usage, missing types?)
#### 4c. Security Review
- Check for missing authentication/authorization on new endpoints
- Check for injection vulnerabilities (URL params, SQL, XSS)
- Verify input validation on all user-controlled data
- Check for hardcoded secrets or credentials
#### 4d. Architecture Review
- Does the change follow existing patterns?
- Are there any breaking changes to public APIs?
- Is the database schema affected? Migration needed?
- Impact on performance (N+1 queries, missing indexes?)
#### 4e. Test Coverage
- Does the PR include tests?
- Are edge cases covered?
- Would existing tests break?
#### 4f. Cross-Layer (Global) Analysis
Perform a **global impact assessment** to verify whether the PR changes are complete across all layers of the application:
- **Backend → Frontend check**: If the PR adds or modifies backend-only resources (new endpoints, services, data models), evaluate whether corresponding frontend changes are missing:
- Does a new endpoint require a new screen/page in the dashboard?
- Should there be a new action button, menu item, or navigation link?
- Are there new data fields that should be displayed or editable in the UI?
- Does a new feature need a toggle, configuration panel, or status indicator?
- **Frontend → Backend check**: If the PR adds frontend elements, verify the backend support exists:
- Are the required API endpoints implemented?
- Is the data model sufficient for the new UI components?
- **Cross-cutting concerns**: Check shared layers (types, DTOs, validation schemas, routes, middleware) for completeness
- **Document gaps** — If missing layers are detected, list them as **IMPORTANT** issues in the report with concrete suggestions for what should be added
### 5. Generate Report — Create a markdown report for each PR including:
- **PR Summary** — What it does, files affected, commit count
- **Improvements/Benefits** — Numbered list with impact level (HIGH/MEDIUM/LOW)
- **Risks & Issues** — Categorized as CRITICAL / IMPORTANT / MINOR
- **Scoring Table** — Rate across: Feature Relevance, Code Quality, Security, Robustness, Tests
- **Verdict** — Ready to merge? With mandatory vs optional fixes
- **Next Steps** — What will happen if approved
### 6. Present to User
- Show the report in the final response and stop. This is a mandatory checkpoint awaiting explicit user approval before continuing.
- Wait for user decision:
- **Approved** → Proceed to step 7
- **Approved with changes** → Implement the fixes and corrections before merging
- **Rejected** → Close the PR or leave a review comment
### 7. Pre-Merge Fixes & CI Green-Lighting (if approved)
> **⚠️ Fixes and Conflict Resolutions MUST be pushed back to the PR branch before merging.** We want the PR itself to be green and fully valid before it integrates.
- **Sync latest fixes & Resolve Conflicts:** Merge the current `release` branch into the PR branch. If there are merge conflicts, you MUST resolve them inside the author's PR branch. NEVER resolve conflicts by closing their PR and doing the work in a separate branch, as this steals credit from the original author.
- **Implement improvements:** Apply the required fixes identified in the analysis directly on the PR branch (e.g., adding missing API routes, fixing SSRF, applying comments from other agents).
- **Pushing changes to PR branches:**
```bash
# Checkout the PR locally
gh pr checkout <NUMBER>
# Apply fixes, commit your changes
git commit -m "chore: apply review suggestions and missing layers"
# Attempt to push directly to the PR branch
git push
```
- **Fallback (ONLY for external forks without maintainer edit access):**
Using `cherry-pick` instead of fixing the contributor's PR directly is a **LAST RESORT**. You MUST ALWAYS attempt to `git push` your fixes to their branch first.
**ONLY if `git push` explicitly fails with a permission/access error** (meaning the contributor unchecked "Allow edits from maintainers" or it's a locked fork), you may use `git cherry-pick` to bring their changes into the release branch and fix the issues locally.
Even then, ensure you preserve the contributor's authorship (`git commit --author="Contributor Name <email>"` if creating new commits).
Once you have integrated their work into the release branch, **DO NOT close their PR**. Leave it open so the contributor retains credit. Under NO CIRCUMSTANCES should you use `gh pr close`.
- Run the project's test suite locally to verify nothing breaks:
// turbo
- Run: `npm test` or equivalent test command
### 8. Merge into Release Branch (NEVER CLOSE!)
> **⚠️ CRITICAL**: NEVER use `gh pr close` for a PR whose idea or code was accepted. Closing a PR in a contributor's face after taking their idea—or closing it just because it had conflicts—is unacceptable.
> You MUST ALWAYS resolve conflicts and apply fixes ON THE AUTHOR'S PR BRANCH (unless explicitly locked from edits), and then merge the PR using GitHub so the contributor gets the official "Merged" badge and proper credit on their profile. **Do not use cherry-pick just because it is "easier" than resolving conflicts on their branch.**
Even if the PR had severe conflicts or required significant architectural adjustments, you MUST:
1. Resolve any conflicts and apply the fixes directly to their PR branch (as detailed in step 7) or use cherry-picking into the release branch.
2. If you managed to fix their branch, merge it into the release branch using the GitHub CLI:
`gh pr merge <NUMBER> --repo <owner>/<repo> --squash --body "Integrated into release/vX.Y.Z"`
3. If you had to use cherry-picking because you couldn't push to their branch, DO NOT close the PR. GitHub will sometimes auto-detect the cherry-picked commits and mark it as Merged. If it doesn't, leave it open. The repository owner will handle it. NEVER run `gh pr close`.
In ALL cases:
- Post a **thank-you comment** on the PR via the GitHub API before or immediately after merging.
- The message should:
- Thank the author by name/username for their contribution.
- Explain what was adjusted or improved (if we pushed fixes to their branch or cherry-picked).
- Note it will be included in the upcoming release.
- Be friendly, professional, and encouraging.
> **⚠️ MANDATORY CHANGELOG CREDIT**: When cherry-picking is used (because the PR branch couldn't be pushed to or `gh pr merge` failed), the contributor does NOT get the automatic GitHub "Merged" badge. In this case, you MUST compensate by adding an explicit entry to `CHANGELOG.md` in the `[Unreleased]` section with `(#PR_NUMBER — thanks @username)` format. This ensures the contributor gets public credit in the release notes even if GitHub doesn't auto-detect the cherry-pick. This is NOT optional — skipping it effectively erases the contributor's work from the release record.
### 9. Sync Local Release Branch
After merging PRs, sync the local release branch to include the new changes:
```bash
git fetch origin
git pull origin release/vX.Y.Z
```
### 10. Continue or Finalize
After processing all approved PRs:
- If more PRs remain, go back to step 7
- When all PRs are processed, **update CHANGELOG.md** on the release branch with all new entries
- Run **test coverage** to verify the gate (≥75% statements/lines/functions, ≥70% branches — measured ~82%):
```bash
npm run test:coverage
```
- Fix any test regressions introduced by merged PRs
- Run `/generate-release` workflow Phase 1 steps 710 (tests → commit → push → open PR to main → wait for user)
- The `/generate-release` workflow handles the final merge from `release/vX.Y.Z` → `main`

View File

@@ -1,268 +0,0 @@
---
name: review-prs-cx
description: Analyze open Pull Requests from the project's GitHub repository, generate a critical report, and optionally implement approved changes
---
# /review-prs — PR Review & Analysis Workflow
## ⛔ ABSOLUTE PROHIBITION — Read Before Anything Else
> **NEVER close a contributor's PR if you intend to use ANY of their code, ideas, or fixes.**
>
> **NEVER manually integrate contributor code into a release branch and then close their PR.**
>
> These actions are **STRICTLY FORBIDDEN** under all circumstances:
>
> 1. ❌ Closing a PR and cherry-picking/copying its code into a release branch
> 2. ❌ Closing a PR "because of conflicts" and re-implementing the same fix yourself
> 3. ❌ Closing a PR and committing a "similar" solution inspired by it
> 4. ❌ Using `gh pr close` on any PR whose content was or will be used
>
> **Why**: Closing a PR after taking the contributor's work means they get ZERO credit on GitHub — no "Merged" badge, no contribution graph entry, no public record. This is effectively stealing their contribution. An audit found this happened to **37 PRs** in the past.
>
> **The ONLY acceptable flow**: Resolve conflicts IN the contributor's branch, push fixes TO their branch, then merge THEIR PR via `gh pr merge`. See Step 7 and Step 8 for the exact procedure.
>
> **When to close a PR**: ONLY when the user (repository owner) explicitly requests it, OR when the PR is clearly spam/malicious, OR when the author themselves asks to close it. In ALL other cases, leave it open.
## Overview
This workflow fetches all open PRs from the project's GitHub repository, performs a critical analysis of each one, generates a detailed report, and waits for user approval before proceeding with implementation. **All improvements are committed on the current release branch** (`release/vX.Y.Z`).
> **BRANCH RULE**: PRs are ALWAYS merged into the current `release/vX.Y.Z` branch, NEVER directly into `main`. The release branch acts as a staging area — only after all PRs are integrated and tests pass does the release branch get merged into `main` via the `/generate-release` workflow.
## Codex Execution Notes
The source Claude command uses `// turbo` and `// turbo-all` as execution hints. In Codex, treat them explicitly as follows:
- `// turbo`: batch independent local reads and small `gh`/`git` calls with `multi_tool_use.parallel`.
- `// turbo-all`: fan out independent per-PR/per-issue calls in practical batches, usually up to 4 GitHub calls at a time.
- Do not expand Step 4 into exhaustive CI-log debugging before Step 6. Fetch numbers, metadata, diffs/review comments, quick merge/conflict status, and only inspect extra logs when they directly affect the verdict.
- Step 6 is a hard stop. In Codex, present the report in the final response and wait for the user before Step 7/8.
- Do not checkout PR branches, edit files, post PR comments, close PRs, merge, cherry-pick, or run broad fix/test loops until the user explicitly approves the report.
- If `gh pr diff` is too large, record the limit and use `gh pr view --json files` plus `git fetch refs/pull/...` with `git diff --stat` / `git diff --name-status`; only read targeted hunks needed for confirmed findings.
## Steps
### 1. Identify the GitHub Repository
- Read `package.json` to get the repository URL, or use the git remote origin URL
// turbo
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
### 2. Ensure Release Branch Exists
// turbo
Before doing any work, ensure you are on the current release branch:
```bash
# Check current branch
git branch --show-current
# If on main, determine next version and create the release branch
VERSION=$(node -p "require('./package.json').version")
# Bump patch: e.g. 3.3.11 → 3.3.12
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=999?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
git checkout -b release/v$NEXT
npm version patch --no-git-tag-version
npm install
```
If already on a `release/vX.Y.Z` branch, continue working there.
### 3. Fetch Open Pull Requests
// turbo-all
**⚠️ CRITICAL**: The JSON output of `gh pr list` can be truncated by the tool, silently hiding PRs. You MUST use the two-step approach below to guarantee **all** PRs are fetched.
**Step 3a — Get PR numbers only** (small output, never truncated):
- Run: `gh pr list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
- This outputs one PR number per line. Count them and confirm total.
**Step 3b — Fetch full metadata for each PR** (one call per PR):
- For each PR number from step 3a, run:
`gh pr view <NUMBER> --repo <owner>/<repo> --json number,title,author,headRefName,baseRefName,body,createdAt,additions,deletions,files`
- You may batch these into parallel calls (up to 4 at a time).
**Step 3c — Fetch diffs for each PR** (one call per PR, saved to /tmp):
- For each PR number, run:
`gh pr diff <NUMBER> --repo <owner>/<repo> > /tmp/pr<NUMBER>.diff`
- Then read each diff file with the appropriate file-read tool (`Read` in Claude Code; equivalent in your agent runtime).
- For each open PR, collect:
- PR number, title, author, branch, number of commits, date
- PR description/body
- Files changed (diff)
- Existing review comments (from bots or humans)
**Verification**: Confirm the count of PRs analyzed matches the count from step 3a before proceeding.
### 3.5 Redirect PR Base Branches to Release Branch
// turbo-all
**⚠️ CRITICAL**: Contributors typically open PRs targeting `main`. Before analyzing or merging, redirect ALL open PRs to target the current release branch instead.
```bash
# Get the current release branch name
RELEASE_BRANCH=$(git branch --show-current) # e.g. release/v3.5.4
# For each open PR that targets main, change its base to the release branch
for PR_NUM in $(gh pr list --repo <owner>/<repo> --state open --json number,baseRefName --jq '.[] | select(.baseRefName == "main") | .number'); do
echo "Redirecting PR #$PR_NUM$RELEASE_BRANCH"
gh pr edit "$PR_NUM" --repo <owner>/<repo> --base "$RELEASE_BRANCH"
done
```
This ensures:
1. PRs merge into the release branch, not directly into `main`
2. Merge conflict detection is accurate against the release branch
3. The release branch accumulates all changes before the final merge to `main`
4. If the release branch doesn't exist on remote yet, push it first: `git push origin $RELEASE_BRANCH`
### 4. Analyze Each PR — For each open PR, perform the following analysis:
#### 4a. Feature Assessment
- **Does it make sense?** Evaluate if the feature fills a real gap or solves a valid problem
- **Alignment** — Check if it aligns with the project's architecture and roadmap
- **Complexity** — Assess if the scope is reasonable or if it should be split
#### 4b. Code Quality Review
- Check for code duplication
- Evaluate error handling patterns (consistent with existing codebase?)
- Check naming conventions and code style
- Verify TypeScript types (any `any` usage, missing types?)
#### 4c. Security Review
- Check for missing authentication/authorization on new endpoints
- Check for injection vulnerabilities (URL params, SQL, XSS)
- Verify input validation on all user-controlled data
- Check for hardcoded secrets or credentials
#### 4d. Architecture Review
- Does the change follow existing patterns?
- Are there any breaking changes to public APIs?
- Is the database schema affected? Migration needed?
- Impact on performance (N+1 queries, missing indexes?)
#### 4e. Test Coverage
- Does the PR include tests?
- Are edge cases covered?
- Would existing tests break?
#### 4f. Cross-Layer (Global) Analysis
Perform a **global impact assessment** to verify whether the PR changes are complete across all layers of the application:
- **Backend → Frontend check**: If the PR adds or modifies backend-only resources (new endpoints, services, data models), evaluate whether corresponding frontend changes are missing:
- Does a new endpoint require a new screen/page in the dashboard?
- Should there be a new action button, menu item, or navigation link?
- Are there new data fields that should be displayed or editable in the UI?
- Does a new feature need a toggle, configuration panel, or status indicator?
- **Frontend → Backend check**: If the PR adds frontend elements, verify the backend support exists:
- Are the required API endpoints implemented?
- Is the data model sufficient for the new UI components?
- **Cross-cutting concerns**: Check shared layers (types, DTOs, validation schemas, routes, middleware) for completeness
- **Document gaps** — If missing layers are detected, list them as **IMPORTANT** issues in the report with concrete suggestions for what should be added
### 5. Generate Report — Create a markdown report for each PR including:
- **PR Summary** — What it does, files affected, commit count
- **Improvements/Benefits** — Numbered list with impact level (HIGH/MEDIUM/LOW)
- **Risks & Issues** — Categorized as CRITICAL / IMPORTANT / MINOR
- **Scoring Table** — Rate across: Feature Relevance, Code Quality, Security, Robustness, Tests
- **Verdict** — Ready to merge? With mandatory vs optional fixes
- **Next Steps** — What will happen if approved
### 6. Present to User
- Show the report in the final response and stop. Mark this as a blocking checkpoint awaiting explicit user approval before continuing.
- Wait for user decision:
- **Approved** → Proceed to step 7
- **Approved with changes** → Implement the fixes and corrections before merging
- **Rejected** → Close the PR or leave a review comment
### 7. Pre-Merge Fixes & CI Green-Lighting (if approved)
> **⚠️ Fixes and Conflict Resolutions MUST be pushed back to the PR branch before merging.** We want the PR itself to be green and fully valid before it integrates.
- **Sync latest fixes & Resolve Conflicts:** Merge the current `release` branch into the PR branch. If there are merge conflicts, you MUST resolve them inside the author's PR branch. NEVER resolve conflicts by closing their PR and doing the work in a separate branch, as this steals credit from the original author.
- **Implement improvements:** Apply the required fixes identified in the analysis directly on the PR branch (e.g., adding missing API routes, fixing SSRF, applying comments from other agents).
- **Pushing changes to PR branches:**
```bash
# Checkout the PR locally
gh pr checkout <NUMBER>
# Apply fixes, commit your changes
git commit -m "chore: apply review suggestions and missing layers"
# Attempt to push directly to the PR branch
git push
```
- **Fallback (ONLY for external forks without maintainer edit access):**
Using `cherry-pick` instead of fixing the contributor's PR directly is a **LAST RESORT**. You MUST ALWAYS attempt to `git push` your fixes to their branch first.
**ONLY if `git push` explicitly fails with a permission/access error** (meaning the contributor unchecked "Allow edits from maintainers" or it's a locked fork), you may use `git cherry-pick` to bring their changes into the release branch and fix the issues locally.
Even then, ensure you preserve the contributor's authorship (`git commit --author="Contributor Name <email>"` if creating new commits).
Once you have integrated their work into the release branch, **DO NOT close their PR**. Leave it open so the contributor retains credit. Under NO CIRCUMSTANCES should you use `gh pr close`.
- Run the project's test suite locally to verify nothing breaks:
// turbo
- Run: `npm test` or equivalent test command
### 8. Merge into Release Branch (NEVER CLOSE!)
> **⚠️ CRITICAL**: NEVER use `gh pr close` for a PR whose idea or code was accepted. Closing a PR in a contributor's face after taking their idea—or closing it just because it had conflicts—is unacceptable.
> You MUST ALWAYS resolve conflicts and apply fixes ON THE AUTHOR'S PR BRANCH (unless explicitly locked from edits), and then merge the PR using GitHub so the contributor gets the official "Merged" badge and proper credit on their profile. **Do not use cherry-pick just because it is "easier" than resolving conflicts on their branch.**
Even if the PR had severe conflicts or required significant architectural adjustments, you MUST:
1. Resolve any conflicts and apply the fixes directly to their PR branch (as detailed in step 7) or use cherry-picking into the release branch.
2. If you managed to fix their branch, merge it into the release branch using the GitHub CLI:
`gh pr merge <NUMBER> --repo <owner>/<repo> --squash --body "Integrated into release/vX.Y.Z"`
3. If you had to use cherry-picking because you couldn't push to their branch, DO NOT close the PR. GitHub will sometimes auto-detect the cherry-picked commits and mark it as Merged. If it doesn't, leave it open. The repository owner will handle it. NEVER run `gh pr close`.
In ALL cases:
- Post a **thank-you comment** on the PR via the GitHub API before or immediately after merging.
- The message should:
- Thank the author by name/username for their contribution.
- Explain what was adjusted or improved (if we pushed fixes to their branch or cherry-picked).
- Note it will be included in the upcoming release.
- Be friendly, professional, and encouraging.
> **⚠️ MANDATORY CHANGELOG CREDIT**: When cherry-picking is used (because the PR branch couldn't be pushed to or `gh pr merge` failed), the contributor does NOT get the automatic GitHub "Merged" badge. In this case, you MUST compensate by adding an explicit entry to `CHANGELOG.md` in the `[Unreleased]` section with `(#PR_NUMBER — thanks @username)` format. This ensures the contributor gets public credit in the release notes even if GitHub doesn't auto-detect the cherry-pick. This is NOT optional — skipping it effectively erases the contributor's work from the release record.
### 9. Sync Local Release Branch
After merging PRs, sync the local release branch to include the new changes:
```bash
git fetch origin
git pull origin release/vX.Y.Z
```
### 10. Continue or Finalize
After processing all approved PRs:
- If more PRs remain, go back to step 7
- When all PRs are processed, **update CHANGELOG.md** on the release branch with all new entries
- Run **test coverage** to verify the gate (≥75% statements/lines/functions, ≥70% branches — measured ~82%):
```bash
npm run test:coverage
```
- Fix any test regressions introduced by merged PRs
- Run `/generate-release` workflow Phase 1 steps 710 (tests → commit → push → open PR to main → wait for user)
- The `/generate-release` workflow handles the final merge from `release/vX.Y.Z` → `main`

View File

@@ -1,342 +0,0 @@
---
name: version-bump-ag
description: Bump version, auto-generate CHANGELOG from git commits, update all versioned files, and refresh root + docs/ documentation to reflect the current project state
---
# Version Bump Workflow
Automatically bump the project version, generate CHANGELOG entries from git history since the last tag, update every file that references the version, and refresh project documentation to reflect the current state.
> **VERSION RULE: Always use PATCH bumps (3.x.y → 3.x.y+1)**
> NEVER use `npm version minor` or `npm version major`.
> Always use: `npm version patch --no-git-tag-version`
> The threshold rule: when `y` reaches 1000, bump to `3.(x+1).0` — e.g. `3.4.999` → `3.5.0`.
---
## Phase 1: Determine Version
### 1. Read current version and last tag
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
CURRENT_VERSION=$(node -p "require('./package.json').version")
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
CURRENT_BRANCH=$(git branch --show-current)
echo "Current version: $CURRENT_VERSION"
echo "Last tag: $LAST_TAG"
echo "Current branch: $CURRENT_BRANCH"
```
### 2. Calculate new version
Apply the patch bump rule:
- If the current patch number is `9`, the new version is `3.(minor+1).0`
- Otherwise, increment patch: `3.x.y``3.x.(y+1)`
If the version was ALREADY bumped (e.g. you are on a release branch and package.json already has the new version), **skip the npm version bump** and use the existing version.
### 3. Bump package.json (if needed)
// turbo
```bash
# Only if version hasn't been bumped yet
npm version patch --no-git-tag-version
```
Or for threshold (y=10):
```bash
# Manual threshold bump
VERSION="3.X.0" # compute manually
npm version "$VERSION" --no-git-tag-version
```
---
## Phase 2: Generate CHANGELOG from Git History
### 4. Collect commits since last tag
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
echo "=== Commits since $LAST_TAG ==="
git log "$LAST_TAG"..HEAD --pretty=format:"%h %s" --no-merges | head -100
echo ""
echo "=== Merge commits ==="
git log "$LAST_TAG"..HEAD --merges --pretty=format:"%h %s" | head -50
```
### 5. Classify commits and generate CHANGELOG section
Analyze each commit message and classify into categories based on the conventional-commit prefix and content:
| Category | Patterns |
| ------------------- | ------------------------------------------------ |
| ✨ New Features | `feat:`, `feat(*):` |
| 🐛 Bug Fixes | `fix:`, `fix(*):` |
| ⚠️ Breaking Changes | `BREAKING CHANGE`, `!:` suffix |
| 🛠️ Maintenance | `chore:`, `refactor:`, `perf:`, `build:` |
| 🧪 Tests | `test:`, `tests:` |
| 📝 Documentation | `docs:` |
| 🔒 Security | `security:`, CVE references, vulnerability fixes |
| 🌍 i18n | translation updates, locale changes |
For each category with entries, create a markdown section with descriptive bullet points. Use the commit messages but rewrite them to be human-readable and descriptive (not raw commit messages).
**If a commit references a PR number** (e.g. `#880`, `PR #885`), include it in the description.
### 6. Update CHANGELOG.md
Replace the `## [Unreleased]` section content with the generated entries, then add the new versioned section:
```markdown
## [Unreleased]
---
## [NEW_VERSION] — YYYY-MM-DD
### ✨ New Features
- **Feature name:** Description (#PR)
### 🐛 Bug Fixes
- **Fix name:** Description (#PR)
### 🛠️ Maintenance
- **Item:** Description
---
## [PREVIOUS_VERSION] — YYYY-MM-DD
...
```
The date must be today's date in `YYYY-MM-DD` format.
---
## Phase 3: Sync Version Across All Files
### 7. Update workspace package.json files and openapi.yaml
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
VERSION=$(node -p "require('./package.json').version")
# Update docs/reference/openapi.yaml version
sed -i "s/ version: .*/ version: $VERSION/" docs/reference/openapi.yaml
echo "✓ docs/reference/openapi.yaml → $VERSION"
# Update workspace packages (open-sse, electron)
for dir in electron open-sse; do
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
(cd "$dir" && npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null)
echo "$dir/package.json → $VERSION"
fi
done
echo "✓ All workspace packages synced to $VERSION"
```
### 8. Update llm.txt version references
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
VERSION=$(node -p "require('./package.json').version")
OLD_VERSION_PATTERN='[0-9]\+\.[0-9]\+\.[0-9]\+'
# Update "Current version:" line
sed -i "s/\*\*Current version:\*\* $OLD_VERSION_PATTERN/**Current version:** $VERSION/" llm.txt
# Update "Key Features (vX.Y.Z)" header
sed -i "s/## Key Features (v$OLD_VERSION_PATTERN)/## Key Features (v$VERSION)/" llm.txt
echo "✓ llm.txt → $VERSION"
```
### 9. Regenerate lock file
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
npm install
echo "✓ Lock file regenerated"
```
---
## Phase 4: Update Root Documentation
Based on the CHANGELOG entries generated in Phase 2, review and update these root-level files if relevant changes warrant updates:
### 10. Review and update root documentation files
For each file below, read the current content and determine if the CHANGELOG entries require any updates. Only modify files where substantive changes have occurred:
| File | When to update |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `README.md` | New providers, major features, stats changes (test count, provider count), badges, installation instructions, feature table |
| `AGENTS.md` | Architecture changes, new modules, new commands, new providers, new services/handlers/executors |
| `CONTRIBUTING.md` | Dev workflow changes, new tooling, test infrastructure changes |
| `SECURITY.md` | Security fixes, new auth mechanisms, vulnerability disclosures |
| `llm.txt` | Provider count changes, new features, architecture changes |
**Update rules:**
- **README.md**: Update provider count, test count, feature highlights table, badges if any numbers changed. If a new provider was added, add it to the provider table. If a major feature was added, add it to the features section.
- **AGENTS.md**: If new architecture components (handlers, executors, services, DB modules) were added, update the Architecture section. If new commands were added, update the Build/Test table.
- **SECURITY.md**: Add new vulnerability fixes or security improvements to the relevant section.
- **llm.txt**: Update provider count, feature list, version references.
### 11. Review and update docs/ files (excluding i18n/)
For each file in `docs/` (excluding `docs/i18n/`), review if CHANGELOG changes affect it:
| File | When to update |
| --------------------------------------------- | ------------------------------------------------------------------ |
| `docs/reference/API_REFERENCE.md` | New API endpoints, changed request/response formats |
| `docs/architecture/ARCHITECTURE.md` | New modules, new services, changed data flow |
| `docs/architecture/CODEBASE_DOCUMENTATION.md` | New files, architectural changes, module reorganization |
| `docs/architecture/REPOSITORY_MAP.md` | New folders / files / one-line descriptions |
| `docs/reference/CLI-TOOLS.md` | New CLI tool integrations, config format changes |
| `docs/guides/USER_GUIDE.md` | UX changes, new dashboard pages, settings changes |
| `docs/reference/PROVIDER_REFERENCE.md` | New providers (regenerate via `scripts/gen-provider-reference.ts`) |
| `docs/frameworks/MCP-SERVER.md` | New MCP tools, changed tool signatures, scope changes |
| `docs/frameworks/A2A-SERVER.md` | New A2A skills, protocol changes |
| `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` | New external agent protocols supported |
| `docs/frameworks/CLOUD_AGENT.md` | Cloud agent additions (codex-cloud, devin, jules) or API changes |
| `docs/architecture/AUTHZ_GUIDE.md` | New route classifications, policy changes |
| `docs/security/GUARDRAILS.md` | New guardrails registered, priority/order changes |
| `docs/security/COMPLIANCE.md` | Audit log / retention / no-log policy changes |
| `docs/frameworks/SKILLS.md` | Skill framework / registry / built-in skill changes |
| `docs/frameworks/MEMORY.md` | Memory pipeline / extraction / injection / Qdrant changes |
| `docs/frameworks/EVALS.md` | Evaluation framework changes, new evaluators |
| `docs/frameworks/WEBHOOKS.md` | New webhook events, payload schema changes |
| `docs/routing/REASONING_REPLAY.md` | Reasoning capture/replay pipeline changes |
| `docs/routing/AUTO-COMBO.md` | Routing changes, new strategies, scoring weight changes |
| `docs/architecture/RESILIENCE_GUIDE.md` | Circuit breaker / cooldown / lockout behavior changes |
| `docs/security/STEALTH_GUIDE.md` | TLS / CLI fingerprint changes |
| `docs/ops/TUNNELS_GUIDE.md` | Cloudflare tunnel feature changes |
| `docs/guides/ELECTRON_GUIDE.md` | Electron build / signing / packaging changes |
| `docs/guides/TROUBLESHOOTING.md` | New known issues, resolved problems |
| `docs/ops/RELEASE_CHECKLIST.md` | Process changes |
| `docs/ops/COVERAGE_PLAN.md` | Coverage gate adjustments, target metrics |
| `docs/reference/openapi.yaml` | Already updated in step 7 |
**Only update files where the CHANGELOG entries directly affect the documented content.** Do NOT update files just to bump a version number — only when the documented behavior, features, or architecture has actually changed.
---
## Phase 5: Verify
### 12. Run lint check
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
npm run lint
```
### 13. Run tests
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
npm test
```
### 14. Verify version sync across all files
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
VERSION=$(node -p "require('./package.json').version")
echo "Expected version: $VERSION"
echo ""
echo "--- package.json ---"
grep '"version"' package.json | head -1
echo "--- open-sse/package.json ---"
grep '"version"' open-sse/package.json | head -1
echo "--- electron/package.json ---"
[ -f electron/package.json ] && grep '"version"' electron/package.json | head -1
echo "--- docs/reference/openapi.yaml ---"
grep " version:" docs/reference/openapi.yaml | head -1
echo "--- llm.txt ---"
grep "Current version:" llm.txt
echo "--- CHANGELOG.md (first versioned entry) ---"
grep "^## \[" CHANGELOG.md | head -2
```
### 15. 🛑 STOP — Present Summary to User
**STOP** and present a summary to the user including:
- Old version → New version
- CHANGELOG entries generated
- Files modified
- Test results
- Any documentation updates made
**Wait for the user to confirm before committing.**
---
## Phase 6: Commit (only after user approval)
### 16. Stage and commit
// turbo-all
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
git add -A
VERSION=$(node -p "require('./package.json').version")
git commit -m "chore(release): bump to v$VERSION — changelog, docs, version sync"
```
---
## Notes
- This workflow does **NOT** create tags, releases, or deploy. Use `/generate-release` for the full release cycle after this.
- This workflow does **NOT** update `docs/i18n/` translations. Translation updates are handled manually or via release tooling — there is no `/update-i18n` workflow shipped in this repo.
- The CHANGELOG generation is based on git commits since the last tag. If there are no new commits, the workflow should inform the user and stop.
- Always verify the generated CHANGELOG entries make sense — raw commit messages may need rewriting for clarity.
- If the version was already bumped (e.g. you're on a `release/vX.Y.Z` branch), skip the `npm version` step and use the existing version.
## Version Touchpoints Checklist
| File | Field/Pattern |
| ----------------------------- | ----------------------------------------------------------- |
| `package.json` | `"version": "X.Y.Z"` |
| `open-sse/package.json` | `"version": "X.Y.Z"` |
| `electron/package.json` | `"version": "X.Y.Z"` |
| `docs/reference/openapi.yaml` | `version: X.Y.Z` |
| `llm.txt` | `**Current version:** X.Y.Z` and `## Key Features (vX.Y.Z)` |
| `CHANGELOG.md` | `## [X.Y.Z] — YYYY-MM-DD` |

View File

@@ -1,342 +0,0 @@
---
name: version-bump-cc
description: Bump version, auto-generate CHANGELOG from git commits, update all versioned files, and refresh root + docs/ documentation to reflect the current project state
---
# Version Bump Workflow
Automatically bump the project version, generate CHANGELOG entries from git history since the last tag, update every file that references the version, and refresh project documentation to reflect the current state.
> **VERSION RULE: Always use PATCH bumps (3.x.y → 3.x.y+1)**
> NEVER use `npm version minor` or `npm version major`.
> Always use: `npm version patch --no-git-tag-version`
> The threshold rule: when `y` reaches 1000, bump to `3.(x+1).0` — e.g. `3.4.999` → `3.5.0`.
---
## Phase 1: Determine Version
### 1. Read current version and last tag
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
CURRENT_VERSION=$(node -p "require('./package.json').version")
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
CURRENT_BRANCH=$(git branch --show-current)
echo "Current version: $CURRENT_VERSION"
echo "Last tag: $LAST_TAG"
echo "Current branch: $CURRENT_BRANCH"
```
### 2. Calculate new version
Apply the patch bump rule:
- If the current patch number is `9`, the new version is `3.(minor+1).0`
- Otherwise, increment patch: `3.x.y``3.x.(y+1)`
If the version was ALREADY bumped (e.g. you are on a release branch and package.json already has the new version), **skip the npm version bump** and use the existing version.
### 3. Bump package.json (if needed)
// turbo
```bash
# Only if version hasn't been bumped yet
npm version patch --no-git-tag-version
```
Or for threshold (y=10):
```bash
# Manual threshold bump
VERSION="3.X.0" # compute manually
npm version "$VERSION" --no-git-tag-version
```
---
## Phase 2: Generate CHANGELOG from Git History
### 4. Collect commits since last tag
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
echo "=== Commits since $LAST_TAG ==="
git log "$LAST_TAG"..HEAD --pretty=format:"%h %s" --no-merges | head -100
echo ""
echo "=== Merge commits ==="
git log "$LAST_TAG"..HEAD --merges --pretty=format:"%h %s" | head -50
```
### 5. Classify commits and generate CHANGELOG section
Analyze each commit message and classify into categories based on the conventional-commit prefix and content:
| Category | Patterns |
| ------------------- | ------------------------------------------------ |
| ✨ New Features | `feat:`, `feat(*):` |
| 🐛 Bug Fixes | `fix:`, `fix(*):` |
| ⚠️ Breaking Changes | `BREAKING CHANGE`, `!:` suffix |
| 🛠️ Maintenance | `chore:`, `refactor:`, `perf:`, `build:` |
| 🧪 Tests | `test:`, `tests:` |
| 📝 Documentation | `docs:` |
| 🔒 Security | `security:`, CVE references, vulnerability fixes |
| 🌍 i18n | translation updates, locale changes |
For each category with entries, create a markdown section with descriptive bullet points. Use the commit messages but rewrite them to be human-readable and descriptive (not raw commit messages).
**If a commit references a PR number** (e.g. `#880`, `PR #885`), include it in the description.
### 6. Update CHANGELOG.md
Replace the `## [Unreleased]` section content with the generated entries, then add the new versioned section:
```markdown
## [Unreleased]
---
## [NEW_VERSION] — YYYY-MM-DD
### ✨ New Features
- **Feature name:** Description (#PR)
### 🐛 Bug Fixes
- **Fix name:** Description (#PR)
### 🛠️ Maintenance
- **Item:** Description
---
## [PREVIOUS_VERSION] — YYYY-MM-DD
...
```
The date must be today's date in `YYYY-MM-DD` format.
---
## Phase 3: Sync Version Across All Files
### 7. Update workspace package.json files and openapi.yaml
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
VERSION=$(node -p "require('./package.json').version")
# Update docs/reference/openapi.yaml version
sed -i "s/ version: .*/ version: $VERSION/" docs/reference/openapi.yaml
echo "✓ docs/reference/openapi.yaml → $VERSION"
# Update workspace packages (open-sse, electron)
for dir in electron open-sse; do
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
(cd "$dir" && npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null)
echo "$dir/package.json → $VERSION"
fi
done
echo "✓ All workspace packages synced to $VERSION"
```
### 8. Update llm.txt version references
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
VERSION=$(node -p "require('./package.json').version")
OLD_VERSION_PATTERN='[0-9]\+\.[0-9]\+\.[0-9]\+'
# Update "Current version:" line
sed -i "s/\*\*Current version:\*\* $OLD_VERSION_PATTERN/**Current version:** $VERSION/" llm.txt
# Update "Key Features (vX.Y.Z)" header
sed -i "s/## Key Features (v$OLD_VERSION_PATTERN)/## Key Features (v$VERSION)/" llm.txt
echo "✓ llm.txt → $VERSION"
```
### 9. Regenerate lock file
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
npm install
echo "✓ Lock file regenerated"
```
---
## Phase 4: Update Root Documentation
Based on the CHANGELOG entries generated in Phase 2, review and update these root-level files if relevant changes warrant updates:
### 10. Review and update root documentation files
For each file below, read the current content and determine if the CHANGELOG entries require any updates. Only modify files where substantive changes have occurred:
| File | When to update |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `README.md` | New providers, major features, stats changes (test count, provider count), badges, installation instructions, feature table |
| `AGENTS.md` | Architecture changes, new modules, new commands, new providers, new services/handlers/executors |
| `CONTRIBUTING.md` | Dev workflow changes, new tooling, test infrastructure changes |
| `SECURITY.md` | Security fixes, new auth mechanisms, vulnerability disclosures |
| `llm.txt` | Provider count changes, new features, architecture changes |
**Update rules:**
- **README.md**: Update provider count, test count, feature highlights table, badges if any numbers changed. If a new provider was added, add it to the provider table. If a major feature was added, add it to the features section.
- **AGENTS.md**: If new architecture components (handlers, executors, services, DB modules) were added, update the Architecture section. If new commands were added, update the Build/Test table.
- **SECURITY.md**: Add new vulnerability fixes or security improvements to the relevant section.
- **llm.txt**: Update provider count, feature list, version references.
### 11. Review and update docs/ files (excluding i18n/)
For each file in `docs/` (excluding `docs/i18n/`), review if CHANGELOG changes affect it:
| File | When to update |
| --------------------------------------------- | ------------------------------------------------------------------ |
| `docs/reference/API_REFERENCE.md` | New API endpoints, changed request/response formats |
| `docs/architecture/ARCHITECTURE.md` | New modules, new services, changed data flow |
| `docs/architecture/CODEBASE_DOCUMENTATION.md` | New files, architectural changes, module reorganization |
| `docs/architecture/REPOSITORY_MAP.md` | New folders / files / one-line descriptions |
| `docs/reference/CLI-TOOLS.md` | New CLI tool integrations, config format changes |
| `docs/guides/USER_GUIDE.md` | UX changes, new dashboard pages, settings changes |
| `docs/reference/PROVIDER_REFERENCE.md` | New providers (regenerate via `scripts/gen-provider-reference.ts`) |
| `docs/frameworks/MCP-SERVER.md` | New MCP tools, changed tool signatures, scope changes |
| `docs/frameworks/A2A-SERVER.md` | New A2A skills, protocol changes |
| `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` | New external agent protocols supported |
| `docs/frameworks/CLOUD_AGENT.md` | Cloud agent additions (codex-cloud, devin, jules) or API changes |
| `docs/architecture/AUTHZ_GUIDE.md` | New route classifications, policy changes |
| `docs/security/GUARDRAILS.md` | New guardrails registered, priority/order changes |
| `docs/security/COMPLIANCE.md` | Audit log / retention / no-log policy changes |
| `docs/frameworks/SKILLS.md` | Skill framework / registry / built-in skill changes |
| `docs/frameworks/MEMORY.md` | Memory pipeline / extraction / injection / Qdrant changes |
| `docs/frameworks/EVALS.md` | Evaluation framework changes, new evaluators |
| `docs/frameworks/WEBHOOKS.md` | New webhook events, payload schema changes |
| `docs/routing/REASONING_REPLAY.md` | Reasoning capture/replay pipeline changes |
| `docs/routing/AUTO-COMBO.md` | Routing changes, new strategies, scoring weight changes |
| `docs/architecture/RESILIENCE_GUIDE.md` | Circuit breaker / cooldown / lockout behavior changes |
| `docs/security/STEALTH_GUIDE.md` | TLS / CLI fingerprint changes |
| `docs/ops/TUNNELS_GUIDE.md` | Cloudflare tunnel feature changes |
| `docs/guides/ELECTRON_GUIDE.md` | Electron build / signing / packaging changes |
| `docs/guides/TROUBLESHOOTING.md` | New known issues, resolved problems |
| `docs/ops/RELEASE_CHECKLIST.md` | Process changes |
| `docs/ops/COVERAGE_PLAN.md` | Coverage gate adjustments, target metrics |
| `docs/reference/openapi.yaml` | Already updated in step 7 |
**Only update files where the CHANGELOG entries directly affect the documented content.** Do NOT update files just to bump a version number — only when the documented behavior, features, or architecture has actually changed.
---
## Phase 5: Verify
### 12. Run lint check
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
npm run lint
```
### 13. Run tests
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
npm test
```
### 14. Verify version sync across all files
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
VERSION=$(node -p "require('./package.json').version")
echo "Expected version: $VERSION"
echo ""
echo "--- package.json ---"
grep '"version"' package.json | head -1
echo "--- open-sse/package.json ---"
grep '"version"' open-sse/package.json | head -1
echo "--- electron/package.json ---"
[ -f electron/package.json ] && grep '"version"' electron/package.json | head -1
echo "--- docs/reference/openapi.yaml ---"
grep " version:" docs/reference/openapi.yaml | head -1
echo "--- llm.txt ---"
grep "Current version:" llm.txt
echo "--- CHANGELOG.md (first versioned entry) ---"
grep "^## \[" CHANGELOG.md | head -2
```
### 15. 🛑 STOP — Present Summary to User
**STOP** and present a summary to the user including:
- Old version → New version
- CHANGELOG entries generated
- Files modified
- Test results
- Any documentation updates made
**Wait for the user to confirm before committing.**
---
## Phase 6: Commit (only after user approval)
### 16. Stage and commit
// turbo-all
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
git add -A
VERSION=$(node -p "require('./package.json').version")
git commit -m "chore(release): bump to v$VERSION — changelog, docs, version sync"
```
---
## Notes
- This workflow does **NOT** create tags, releases, or deploy. Use `/generate-release` for the full release cycle after this.
- This workflow does **NOT** update `docs/i18n/` translations. Translation updates are handled manually or via release tooling — the `/update-i18n` command does not currently exist as a Claude Code slash command.
- The CHANGELOG generation is based on git commits since the last tag. If there are no new commits, the workflow should inform the user and stop.
- Always verify the generated CHANGELOG entries make sense — raw commit messages may need rewriting for clarity.
- If the version was already bumped (e.g. you're on a `release/vX.Y.Z` branch), skip the `npm version` step and use the existing version.
## Version Touchpoints Checklist
| File | Field/Pattern |
| ----------------------------- | ----------------------------------------------------------- |
| `package.json` | `"version": "X.Y.Z"` |
| `open-sse/package.json` | `"version": "X.Y.Z"` |
| `electron/package.json` | `"version": "X.Y.Z"` |
| `docs/reference/openapi.yaml` | `version: X.Y.Z` |
| `llm.txt` | `**Current version:** X.Y.Z` and `## Key Features (vX.Y.Z)` |
| `CHANGELOG.md` | `## [X.Y.Z] — YYYY-MM-DD` |

View File

@@ -1,347 +0,0 @@
---
name: version-bump-cx
description: Bump version, auto-generate CHANGELOG from git commits, update all versioned files, and refresh root + docs/ documentation to reflect the current project state
---
# Version Bump Workflow
Automatically bump the project version, generate CHANGELOG entries from git history since the last tag, update every file that references the version, and refresh project documentation to reflect the current state.
## Codex Execution Notes
- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` for independent reads, checks, and GitHub calls.
- Any user-approval phase is a hard stop: present the report/status in the final response and wait before committing, pushing, tagging, publishing, or deploying.
> **VERSION RULE: Always use PATCH bumps (3.x.y → 3.x.y+1)**
> NEVER use `npm version minor` or `npm version major`.
> Always use: `npm version patch --no-git-tag-version`
> The threshold rule: when `y` reaches 1000, bump to `3.(x+1).0` — e.g. `3.4.999` → `3.5.0`.
---
## Phase 1: Determine Version
### 1. Read current version and last tag
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
CURRENT_VERSION=$(node -p "require('./package.json').version")
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
CURRENT_BRANCH=$(git branch --show-current)
echo "Current version: $CURRENT_VERSION"
echo "Last tag: $LAST_TAG"
echo "Current branch: $CURRENT_BRANCH"
```
### 2. Calculate new version
Apply the patch bump rule:
- If the current patch number is `9`, the new version is `3.(minor+1).0`
- Otherwise, increment patch: `3.x.y``3.x.(y+1)`
If the version was ALREADY bumped (e.g. you are on a release branch and package.json already has the new version), **skip the npm version bump** and use the existing version.
### 3. Bump package.json (if needed)
// turbo
```bash
# Only if version hasn't been bumped yet
npm version patch --no-git-tag-version
```
Or for threshold (y=10):
```bash
# Manual threshold bump
VERSION="3.X.0" # compute manually
npm version "$VERSION" --no-git-tag-version
```
---
## Phase 2: Generate CHANGELOG from Git History
### 4. Collect commits since last tag
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
echo "=== Commits since $LAST_TAG ==="
git log "$LAST_TAG"..HEAD --pretty=format:"%h %s" --no-merges | head -100
echo ""
echo "=== Merge commits ==="
git log "$LAST_TAG"..HEAD --merges --pretty=format:"%h %s" | head -50
```
### 5. Classify commits and generate CHANGELOG section
Analyze each commit message and classify into categories based on the conventional-commit prefix and content:
| Category | Patterns |
| ------------------- | ------------------------------------------------ |
| ✨ New Features | `feat:`, `feat(*):` |
| 🐛 Bug Fixes | `fix:`, `fix(*):` |
| ⚠️ Breaking Changes | `BREAKING CHANGE`, `!:` suffix |
| 🛠️ Maintenance | `chore:`, `refactor:`, `perf:`, `build:` |
| 🧪 Tests | `test:`, `tests:` |
| 📝 Documentation | `docs:` |
| 🔒 Security | `security:`, CVE references, vulnerability fixes |
| 🌍 i18n | translation updates, locale changes |
For each category with entries, create a markdown section with descriptive bullet points. Use the commit messages but rewrite them to be human-readable and descriptive (not raw commit messages).
**If a commit references a PR number** (e.g. `#880`, `PR #885`), include it in the description.
### 6. Update CHANGELOG.md
Replace the `## [Unreleased]` section content with the generated entries, then add the new versioned section:
```markdown
## [Unreleased]
---
## [NEW_VERSION] — YYYY-MM-DD
### ✨ New Features
- **Feature name:** Description (#PR)
### 🐛 Bug Fixes
- **Fix name:** Description (#PR)
### 🛠️ Maintenance
- **Item:** Description
---
## [PREVIOUS_VERSION] — YYYY-MM-DD
...
```
The date must be today's date in `YYYY-MM-DD` format.
---
## Phase 3: Sync Version Across All Files
### 7. Update workspace package.json files and openapi.yaml
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
VERSION=$(node -p "require('./package.json').version")
# Update docs/reference/openapi.yaml version
sed -i "s/ version: .*/ version: $VERSION/" docs/reference/openapi.yaml
echo "✓ docs/reference/openapi.yaml → $VERSION"
# Update workspace packages (open-sse, electron)
for dir in electron open-sse; do
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
(cd "$dir" && npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null)
echo "$dir/package.json → $VERSION"
fi
done
echo "✓ All workspace packages synced to $VERSION"
```
### 8. Update llm.txt version references
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
VERSION=$(node -p "require('./package.json').version")
OLD_VERSION_PATTERN='[0-9]\+\.[0-9]\+\.[0-9]\+'
# Update "Current version:" line
sed -i "s/\*\*Current version:\*\* $OLD_VERSION_PATTERN/**Current version:** $VERSION/" llm.txt
# Update "Key Features (vX.Y.Z)" header
sed -i "s/## Key Features (v$OLD_VERSION_PATTERN)/## Key Features (v$VERSION)/" llm.txt
echo "✓ llm.txt → $VERSION"
```
### 9. Regenerate lock file
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
npm install
echo "✓ Lock file regenerated"
```
---
## Phase 4: Update Root Documentation
Based on the CHANGELOG entries generated in Phase 2, review and update these root-level files if relevant changes warrant updates:
### 10. Review and update root documentation files
For each file below, read the current content and determine if the CHANGELOG entries require any updates. Only modify files where substantive changes have occurred:
| File | When to update |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `README.md` | New providers, major features, stats changes (test count, provider count), badges, installation instructions, feature table |
| `AGENTS.md` | Architecture changes, new modules, new commands, new providers, new services/handlers/executors |
| `CONTRIBUTING.md` | Dev workflow changes, new tooling, test infrastructure changes |
| `SECURITY.md` | Security fixes, new auth mechanisms, vulnerability disclosures |
| `llm.txt` | Provider count changes, new features, architecture changes |
**Update rules:**
- **README.md**: Update provider count, test count, feature highlights table, badges if any numbers changed. If a new provider was added, add it to the provider table. If a major feature was added, add it to the features section.
- **AGENTS.md**: If new architecture components (handlers, executors, services, DB modules) were added, update the Architecture section. If new commands were added, update the Build/Test table.
- **SECURITY.md**: Add new vulnerability fixes or security improvements to the relevant section.
- **llm.txt**: Update provider count, feature list, version references.
### 11. Review and update docs/ files (excluding i18n/)
For each file in `docs/` (excluding `docs/i18n/`), review if CHANGELOG changes affect it:
| File | When to update |
| --------------------------------------------- | ------------------------------------------------------------------ |
| `docs/reference/API_REFERENCE.md` | New API endpoints, changed request/response formats |
| `docs/architecture/ARCHITECTURE.md` | New modules, new services, changed data flow |
| `docs/architecture/CODEBASE_DOCUMENTATION.md` | New files, architectural changes, module reorganization |
| `docs/architecture/REPOSITORY_MAP.md` | New folders / files / one-line descriptions |
| `docs/reference/CLI-TOOLS.md` | New CLI tool integrations, config format changes |
| `docs/guides/USER_GUIDE.md` | UX changes, new dashboard pages, settings changes |
| `docs/reference/PROVIDER_REFERENCE.md` | New providers (regenerate via `scripts/gen-provider-reference.ts`) |
| `docs/frameworks/MCP-SERVER.md` | New MCP tools, changed tool signatures, scope changes |
| `docs/frameworks/A2A-SERVER.md` | New A2A skills, protocol changes |
| `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` | New external agent protocols supported |
| `docs/frameworks/CLOUD_AGENT.md` | Cloud agent additions (codex-cloud, devin, jules) or API changes |
| `docs/architecture/AUTHZ_GUIDE.md` | New route classifications, policy changes |
| `docs/security/GUARDRAILS.md` | New guardrails registered, priority/order changes |
| `docs/security/COMPLIANCE.md` | Audit log / retention / no-log policy changes |
| `docs/frameworks/SKILLS.md` | Skill framework / registry / built-in skill changes |
| `docs/frameworks/MEMORY.md` | Memory pipeline / extraction / injection / Qdrant changes |
| `docs/frameworks/EVALS.md` | Evaluation framework changes, new evaluators |
| `docs/frameworks/WEBHOOKS.md` | New webhook events, payload schema changes |
| `docs/routing/REASONING_REPLAY.md` | Reasoning capture/replay pipeline changes |
| `docs/routing/AUTO-COMBO.md` | Routing changes, new strategies, scoring weight changes |
| `docs/architecture/RESILIENCE_GUIDE.md` | Circuit breaker / cooldown / lockout behavior changes |
| `docs/security/STEALTH_GUIDE.md` | TLS / CLI fingerprint changes |
| `docs/ops/TUNNELS_GUIDE.md` | Cloudflare tunnel feature changes |
| `docs/guides/ELECTRON_GUIDE.md` | Electron build / signing / packaging changes |
| `docs/guides/TROUBLESHOOTING.md` | New known issues, resolved problems |
| `docs/ops/RELEASE_CHECKLIST.md` | Process changes |
| `docs/ops/COVERAGE_PLAN.md` | Coverage gate adjustments, target metrics |
| `docs/reference/openapi.yaml` | Already updated in step 7 |
**Only update files where the CHANGELOG entries directly affect the documented content.** Do NOT update files just to bump a version number — only when the documented behavior, features, or architecture has actually changed.
---
## Phase 5: Verify
### 12. Run lint check
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
npm run lint
```
### 13. Run tests
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
npm test
```
### 14. Verify version sync across all files
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
VERSION=$(node -p "require('./package.json').version")
echo "Expected version: $VERSION"
echo ""
echo "--- package.json ---"
grep '"version"' package.json | head -1
echo "--- open-sse/package.json ---"
grep '"version"' open-sse/package.json | head -1
echo "--- electron/package.json ---"
[ -f electron/package.json ] && grep '"version"' electron/package.json | head -1
echo "--- docs/reference/openapi.yaml ---"
grep " version:" docs/reference/openapi.yaml | head -1
echo "--- llm.txt ---"
grep "Current version:" llm.txt
echo "--- CHANGELOG.md (first versioned entry) ---"
grep "^## \[" CHANGELOG.md | head -2
```
### 15. 🛑 STOP — Present Summary to User
**STOP** and present a summary to the user including:
- Old version → New version
- CHANGELOG entries generated
- Files modified
- Test results
- Any documentation updates made
**Wait for the user to confirm before committing.**
---
## Phase 6: Commit (only after user approval)
### 16. Stage and commit
// turbo-all
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute
git add -A
VERSION=$(node -p "require('./package.json').version")
git commit -m "chore(release): bump to v$VERSION — changelog, docs, version sync"
```
---
## Notes
- This workflow does **NOT** create tags, releases, or deploy. Use `/generate-release` for the full release cycle after this.
- This workflow does **NOT** update `docs/i18n/` translations. Translation updates are handled manually or via release tooling — there is no `/update-i18n` workflow shipped in this repo.
- The CHANGELOG generation is based on git commits since the last tag. If there are no new commits, the workflow should inform the user and stop.
- Always verify the generated CHANGELOG entries make sense — raw commit messages may need rewriting for clarity.
- If the version was already bumped (e.g. you're on a `release/vX.Y.Z` branch), skip the `npm version` step and use the existing version.
## Version Touchpoints Checklist
| File | Field/Pattern |
| ----------------------------- | ----------------------------------------------------------- |
| `package.json` | `"version": "X.Y.Z"` |
| `open-sse/package.json` | `"version": "X.Y.Z"` |
| `electron/package.json` | `"version": "X.Y.Z"` |
| `docs/reference/openapi.yaml` | `version: X.Y.Z` |
| `llm.txt` | `**Current version:** X.Y.Z` and `## Key Features (vX.Y.Z)` |
| `CHANGELOG.md` | `## [X.Y.Z] — YYYY-MM-DD` |

View File

@@ -1 +0,0 @@
/home/diegosouzapw/.gemini/config/projects/0db0ca8e-3c51-48d9-83e2-da62c6f0a02b.json

View File

@@ -9,6 +9,7 @@
# Dependencies and build output
node_modules
.next
.build
out
build
dist
@@ -38,10 +39,15 @@ playwright-report
blob-report
# Documentation
# Translations (~51 MB) are excluded — the Docs viewer reads English sources.
# Screenshots (~1.7 MB) and SVGs (~250 KB) are needed at build time for MDX
# image resolution (fumadocs-mdx bundles them).
# Raster sources under docs/diagrams/ only (exported SVGs are required).
# Issue #2348: The Dashboard Docs viewer reads markdown from `/app/docs` at
# runtime. The previous `docs/*` block hid every file except openapi.yaml,
# so the in-product help screen failed with ENOENT for every page.
# We now keep the English markdown tree plus the docs assets imported by MDX
# during `next build`, while still dropping the bulky translated docs and
# extra raster diagram sources that account for most of the docs footprint
# of the ~50 MB docs directory. The Docs viewer reads the default-locale
# (English) sources at runtime, so translations are not required in the
# container image.
docs/i18n/**
docs/diagrams/**/*.png
docs/diagrams/**/*.jpg

View File

@@ -6,7 +6,6 @@
# │ Reference: docs/ENVIRONMENT.md for full details and usage scenarios. │
# └─────────────────────────────────────────────────────────────────────────────┘
# ═══════════════════════════════════════════════════════════════════════════════
# 1. REQUIRED SECRETS — Must be set before first run!
# ═══════════════════════════════════════════════════════════════════════════════
@@ -62,7 +61,6 @@ DISABLE_SQLITE_AUTO_BACKUP=false
# Default: redis://localhost:6379 (or redis://redis:6379 in Docker)
REDIS_URL=redis://localhost:6379
# ═══════════════════════════════════════════════════════════════════════════════
# 3. NETWORK & PORTS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -111,9 +109,13 @@ PORT=20128
# Used by: src/app/api/v1/relay/chat/completions/route.ts
# RELAY_IP_PER_MINUTE=30
# Use Turbopack in local dev. Next 16.2.4 can fail to compile next/font/google
# through the custom dev runner without this on Windows.
OMNIROUTE_USE_TURBOPACK=1
# Bundler selection for `npm run dev`. Set to 1 to opt into Turbopack.
# Default is 0 (webpack) because Turbopack 16.2.x panics on the OmniRoute
# module graph with "internal error: entered unreachable code: there must be
# a path to a root" (turbopack-core/module_graph/mod.rs:662). Same bug class
# the production Docker build worked around in PR #4052. Webpack starts
# slower but compiles cleanly. Re-enable once upstream Turbopack ships a fix.
OMNIROUTE_USE_TURBOPACK=0
# Skip the SQLite integrity health check on startup (faster boot on large DBs).
# Used by: src/lib/db/core.ts, src/lib/db/healthCheck.ts. Set to 1 to skip.
@@ -159,6 +161,11 @@ OMNIROUTE_USE_TURBOPACK=1
# Values: production | development | Default: production
NODE_ENV=production
# Container runtime — controls startup script behavior (permissions, advice).
# Values: docker | podman | Default: docker
# Set to "podman" when running under rootless Podman so the entrypoint
# gives the correct fix instructions (podman unshare chown vs sudo chown).
CONTAINER_HOST=docker
# ═══════════════════════════════════════════════════════════════════════════════
# 4. SECURITY & AUTHENTICATION
@@ -186,7 +193,8 @@ AUTH_COOKIE_SECURE=false
REQUIRE_API_KEY=false
# Allow revealing full API key values in the Dashboard UI.
# Used by: Dashboard providers page — controls show/hide of key values.
# Used by: src/shared/constants/featureFlagDefinitions.ts — controls show/hide of key values.
# Also configurable from Dashboard > Settings > Feature Flags.
# Default: false | Security risk if enabled on shared instances.
ALLOW_API_KEY_REVEAL=false
@@ -238,7 +246,6 @@ ALLOW_API_KEY_REVEAL=false
# When unset, OmniRoute uses the per-feature defaults. Set to "false"/"0" to disable.
# OUTBOUND_SSRF_GUARD_ENABLED=true
# ═══════════════════════════════════════════════════════════════════════════════
# 5. INPUT SANITIZATION & PII PROTECTION (FASE-01)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -271,6 +278,14 @@ ALLOW_API_KEY_REVEAL=false
# PII_RESPONSE_SANITIZATION=false
# PII_RESPONSE_SANITIZATION_MODE=redact # redact = mask PII | warn = log only | block = drop response
# ── VS Code Tokenized-Route Context Sanitizer ──
# Strips implicit active-editor context (editorContext/activeEditor/currentFile/
# selection/openTabs…) from requests on the /v1/vscode/[token]/* routes before
# forwarding upstream, and redacts the content of explicitly-attached sensitive
# files (.env, private keys, kubeconfig, credentials/secrets). Explicit
# attachments otherwise pass through. Secure-by-default: ON unless set to 0.
# Used by: src/app/api/v1/vscode/contextSanitizer.ts
# OMNIROUTE_VSCODE_SANITIZE_CONTEXT=1 # set to 0 to disable
# ═══════════════════════════════════════════════════════════════════════════════
# 6. TOOL & ROUTING POLICIES
@@ -291,6 +306,11 @@ ALLOW_API_KEY_REVEAL=false
# Default: 5000 | Minimum: 1000
# OMNIROUTE_PAYLOAD_RULES_RELOAD_MS=5000
# Prefer Claude Code OAuth for unprefixed Claude-family model IDs such as
# claude-sonnet-4-6 or newly released IDs like claude-fable-5.
# Used by: open-sse/services/model.ts. Explicit provider prefixes still win.
# Default: false
# OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS=false
# ═══════════════════════════════════════════════════════════════════════════════
# 7. URLS & CLOUD SYNC
@@ -365,7 +385,6 @@ NEXT_PUBLIC_CLOUD_URL=
#OMNIROUTE_OPENCODE_QUOTA_URL=https://opencode.ai/zen/go/v1/quota
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=https://api.z.ai/api/monitor/usage/quota/limit
# ═══════════════════════════════════════════════════════════════════════════════
# 8. OUTBOUND PROXY (Upstream Provider Calls)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -383,6 +402,19 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# ALL_PROXY=socks5://127.0.0.1:7890
# NO_PROXY=localhost,127.0.0.1
# Max concurrent sockets per cached HTTP/SOCKS proxy dispatcher.
# Long-lived SSE streams such as Codex /v1/responses need more than one
# connection when multiple requests share the same account-level proxy.
# Set to 1 only for legacy diagnostics. Values above 256 are capped.
# OMNIROUTE_PROXY_DISPATCHER_CONNECTIONS=32
# Proxy fail-open mode (default: false = fail-closed).
# When false, a request whose assigned proxy fails to resolve is REFUSED rather than
# falling back to a direct connection — prevents real-IP leaks in egress-controlled
# deployments. Set true to restore the legacy DIRECT fallback (legacy behaviour).
# Used by: src/sse/handlers/chatHelpers.ts
# PROXY_FAIL_OPEN=false
# TLS fingerprint spoofing (opt-in) — mimics Chrome 124 TLS handshake via wreq-js.
# Reduces risk of JA3/JA4 fingerprint-based blocking by providers (e.g., Google).
# Used by: open-sse/executors — replaces Node.js default TLS fingerprint.
@@ -393,7 +425,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# Used by: open-sse/services/claudeTurnstileSolver.ts
# OMNIROUTE_TURNSTILE_IGNORE_TLS_ERRORS=false
# ═══════════════════════════════════════════════════════════════════════════════
# 9. CLI TOOL INTEGRATION
# ═══════════════════════════════════════════════════════════════════════════════
@@ -423,6 +454,11 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# CLI_QODER_BIN=qoder
# CLI_QWEN_BIN=qwen
# Override the Hermes Agent home directory (where OmniRoute reads/writes the
# Hermes CLI config). Matches the env var the Hermes PowerShell installer sets
# on Windows (%LOCALAPPDATA%\hermes); defaults to ~/.hermes when unset.
# Used by: src/lib/cli-helper/config-generator/hermesHome.ts
# HERMES_HOME=~/.hermes
# ═══════════════════════════════════════════════════════════════════════════════
# 10. INTERNAL AGENT & MCP INTEGRATIONS
@@ -444,6 +480,11 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# Legacy alias for OMNIROUTE_API_KEY.
# ROUTER_API_KEY=
# CLI remote-mode context/profile for `omniroute` commands (overrides the active
# context in the local contexts store). Equivalent to the `--context <name>` flag.
# Used by: bin/cli/program.mjs, bin/cli/api.mjs (remote mode).
# OMNIROUTE_CONTEXT=
# Enforce scope-based access control on MCP tool calls.
# Used by: open-sse/mcp-server/server.ts — rejects calls outside allowed scopes.
# OMNIROUTE_MCP_ENFORCE_SCOPES=false
@@ -473,6 +514,17 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# Default: 70
PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
# Gap (ms) between consecutive OAuth quota fetches in a bulk provider-limits sync.
# OAuth providers are fetched one at a time with this spacing so a single host
# never bursts simultaneous usage/refresh requests to the same upstream. Set to 0
# to opt out (restores fully concurrent fetches). Default: 1500
PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Delay (ms) before refreshing provider limits after a real usage event (e.g. a
# completed request). Gives the upstream quota API time to register the consumption
# before the dashboard polls. Default: 5000
#PROVIDER_LIMITS_POST_USAGE_REFRESH_DELAY_MS=5000
# Disable all background services (sync, pricing, model refresh).
# Used by: src/instrumentation-node.ts, src/lib/initCloudSync.ts
# Useful for: CI builds, test environments, or resource-constrained containers.
@@ -487,6 +539,11 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
# Used by: src/lib/jobs/budgetResetJob.ts. Floor: 10000.
#OMNIROUTE_BUDGET_RESET_JOB_INTERVAL_MS=600000
# Emergency budget-exhaustion fallback (set false or 0 to disable the reroute to
# nvidia/openai/gpt-oss-120b when a request fails with a 402 budget error).
# Used by: open-sse/services/emergencyFallback.ts. Default: enabled.
#OMNIROUTE_EMERGENCY_FALLBACK=true
# Reasoning cache cleanup cadence (ms). Default: 1800000 (30m). Floor: 60000.
# Used by: src/lib/jobs/reasoningCacheCleanupJob.ts.
#OMNIROUTE_REASONING_CACHE_CLEANUP_INTERVAL_MS=1800000
@@ -511,6 +568,12 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
# Default: <repo>/src/lib/db/migrations.
#OMNIROUTE_MIGRATIONS_DIR=
# Mass-pending-migrations safety threshold (#3416). If more than this many
# migrations are pending on an existing DB, startup aborts (a wiped tracking
# table could cause data loss). Raise it to restore an older backup; set to 0
# to disable the check. Used by: src/lib/db/migrationRunner.ts. Default: 50.
#OMNIROUTE_MAX_PENDING_MIGRATIONS=50
# Trust user-managed RTK project filter rules without strict signature checks.
# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0.
#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0
@@ -554,7 +617,6 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
# Default: ~/.gemini/antigravity-cli/antigravity-oauth-token
#AGY_TOKEN_FILE=
# ═══════════════════════════════════════════════════════════════════════════════
# 11. OAUTH PROVIDER CREDENTIALS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -583,6 +645,11 @@ CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
# Trae OAuth token override. Used by: open-sse/executors/trae.ts.
# TRAE_TOKEN=
# ── The Old LLM (theoldllm) ──
# Playwright navigation timeout (ms) for the browser-backed token capture.
# Used by: open-sse/executors/theoldllm.ts. Default: 30000 (30s).
# THEOLDLLM_NAV_TIMEOUT_MS=30000
# ── Gemini / Gemini CLI / Antigravity / Windsurf (all Google-based) ──
# These providers ship public OAuth client_id/secret values (or Firebase Web
# keys) embedded in their public CLIs/binaries. Defaults are baked into the
@@ -701,7 +768,6 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# CLI_USER_ID= # legacy alias for OMNIROUTE_USER_ID
# SERVER_URL= # legacy alias for OMNIROUTE_SERVER
# ═══════════════════════════════════════════════════════════════════════════════
# 12. PROVIDER USER-AGENT OVERRIDES
# ═══════════════════════════════════════════════════════════════════════════════
@@ -737,7 +803,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
# CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts.
# CODEX_CLIENT_VERSION=0.132.0
# ═══════════════════════════════════════════════════════════════════════════════
# 13. CLI FINGERPRINT COMPATIBILITY (Anti-Detection)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -766,7 +831,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
#KIMI_CLI_VERSION=1.36.0
#KIMI_CODING_DEVICE_ID=
# ═══════════════════════════════════════════════════════════════════════════════
# 14. API KEY PROVIDERS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -789,7 +853,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
# OpenAI/Mistral/Together/Fireworks/NVIDIA configured via Dashboard → Providers
# also work for embeddings.
# ═══════════════════════════════════════════════════════════════════════════════
# 15. TIMEOUT SETTINGS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -797,7 +860,8 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
# Used by: src/shared/utils/runtimeTimeouts.ts — centralized timeout resolution.
#
# Hierarchy: REQUEST_TIMEOUT_MS acts as a global override.
# If set, it becomes the default for FETCH_TIMEOUT_MS and STREAM_IDLE_TIMEOUT_MS.
# If set, it becomes the default for FETCH_TIMEOUT_MS, STREAM_IDLE_TIMEOUT_MS,
# and STREAM_READINESS_TIMEOUT_MS.
# The fine-grained variables below override their respective defaults only when set.
# ── Global shortcut ──
@@ -836,6 +900,22 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
# OMNIROUTE_PPLX_TLS_TIMEOUT_MS=30000
# OMNIROUTE_PPLX_TLS_GRACE_MS=10000
# ── Grok web TLS sidecar (Chrome-fingerprinted client) ──
# Used by: open-sse/services/grokTlsClient.ts — wire-level timeout for the
# bogdanfinn/tls-client koffi binding and the JS-side grace window layered on
# top of it when the native library is wedged.
# OMNIROUTE_GROK_TLS_TIMEOUT_MS=60000
# OMNIROUTE_GROK_TLS_GRACE_MS=10000
# ── Browser-backed web-cookie chat (Playwright shared pool) ──
# Used by: open-sse/services/browserPool.ts + browserBackedChat.ts. The shared
# browser pool warms a headless context for web-cookie providers (e.g. claude-web)
# that need a real browser to satisfy anti-bot challenges. Set OMNIROUTE_BROWSER_POOL=off
# to fully disable the pool; set WEB_COOKIE_USE_BROWSER=1 to opt a web-cookie chat
# request into the browser-backed path.
# OMNIROUTE_BROWSER_POOL=on
# WEB_COOKIE_USE_BROWSER=0
# ── Circuit breaker thresholds and reset windows ──
# Used by: open-sse/config/constants.ts → src/lib/resilience/settings.ts.
# Defaults match historical PROVIDER_PROFILES values (post-scaling for
@@ -851,6 +931,7 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
# ── Stream idle detection ──
# STREAM_IDLE_TIMEOUT_MS=600000 # Max silence between SSE chunks (default: 600000)
# # Extended-thinking models rarely pause >90s.
# STREAM_READINESS_TIMEOUT_MS=80000 # Time to receive the first non-ping SSE event
# ── TLS client (wreq-js fingerprint proxy) ──
# TLS_CLIENT_TIMEOUT_MS=600000 # Inherits from FETCH_TIMEOUT_MS by default
@@ -868,7 +949,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
# Default: 30000 (30 seconds)
# SHUTDOWN_TIMEOUT_MS=30000
# ═══════════════════════════════════════════════════════════════════════════════
# 16. LOGGING
# ═══════════════════════════════════════════════════════════════════════════════
@@ -942,7 +1022,6 @@ APP_LOG_TO_FILE=true
# Default: 100000
# PROXY_LOGS_TABLE_MAX_ROWS=100000
# ═══════════════════════════════════════════════════════════════════════════════
# 17. MEMORY OPTIMIZATION (Low-RAM / Docker)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1022,7 +1101,6 @@ APP_LOG_TO_FILE=true
# Used by: open-sse/utils/usageTracking.ts
# USAGE_TOKEN_BUFFER=100
# ═══════════════════════════════════════════════════════════════════════════════
# 18. PRICING SYNC
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1038,6 +1116,18 @@ APP_LOG_TO_FILE=true
# Comma-separated data sources. Default: litellm
# PRICING_SYNC_SOURCES=litellm
# ═══════════════════════════════════════════════════════════════════════════════
# 18b. ARENA ELO SYNC
# ═══════════════════════════════════════════════════════════════════════════════
# Auto-update model intelligence from Arena AI leaderboard ELO scores (powers the
# Free Provider Rankings page). ON by default — fetches from api.wulong.dev on startup
# (non-blocking, never fatal). Set to false to opt out of the outbound sync.
# Also configurable from Dashboard > Settings > Feature Flags.
# Used by: src/shared/constants/featureFlagDefinitions.ts, src/lib/arenaEloSync.ts
# ARENA_ELO_SYNC_ENABLED=true
# Sync interval in seconds. Default: 86400 (24 hours).
# ARENA_ELO_SYNC_INTERVAL=86400
# ═══════════════════════════════════════════════════════════════════════════════
# 19. MODEL SYNC (Dev)
@@ -1047,7 +1137,6 @@ APP_LOG_TO_FILE=true
# Default: 86400 (24 hours)
# MODELS_DEV_SYNC_INTERVAL=86400
# ═══════════════════════════════════════════════════════════════════════════════
# 20. PROVIDER-SPECIFIC SETTINGS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1058,6 +1147,14 @@ APP_LOG_TO_FILE=true
# Default: 86400000 (24 hours)
# OPENROUTER_CATALOG_TTL_MS=86400000
# ── Model catalog response shape ──
# Include display-friendly name fields in /v1/models responses.
# Disable for clients that expect model IDs only.
# Defined in: src/shared/constants/featureFlagDefinitions.ts
# Used by: src/app/api/v1/models/catalog.ts
# Default: true
# MODEL_CATALOG_INCLUDE_NAMES=true
# ── NanoBanana (Image Generation) ──
# Polling config for async image generation jobs.
# Used by: open-sse/handlers/imageGeneration.ts
@@ -1128,7 +1225,6 @@ APP_LOG_TO_FILE=true
# Used by: open-sse/config/providerRegistry.ts — allows Docker service names.
# LOCAL_HOSTNAMES=omlx,mlx-audio
# ═══════════════════════════════════════════════════════════════════════════════
# 21. PROXY HEALTH
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1141,6 +1237,11 @@ APP_LOG_TO_FILE=true
# Health check result cache TTL (ms). Default: 30000 (30s)
# PROXY_HEALTH_CACHE_TTL_MS=30000
# Allow OAuth and provider validation flows to bypass a pinned proxy and connect
# directly when proxy reachability pre-checks fail. Default: false.
# Also configurable from Dashboard > Settings > Feature Flags.
# OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK=false
# Rate limit maximum wait time before failing a request (ms). Default: 120000 (2 min)
# Used by: open-sse/services/rateLimitManager.ts
# RATE_LIMIT_MAX_WAIT_MS=120000
@@ -1150,11 +1251,48 @@ APP_LOG_TO_FILE=true
# Accepted values: true|1|on (force on), false|0|off (force off), unset (use Dashboard).
# RATE_LIMIT_AUTO_ENABLE=
# Provider cooldown tracking: minimum time (ms) before a failed provider/connection
# can be retried. Prevents subsequent requests from re-walking failing providers.
# Scaled exponentially: minCooldown * 2^(failures-1), capped at maxRetryCooldownMs.
# Used by: open-sse/services/providerCooldownTracker.ts
# PROVIDER_COOLDOWN_MIN_MS=5000
# Provider cooldown tracking: maximum time (ms) before a failed provider/connection
# is retried regardless. Hard cap to prevent providers from being skipped indefinitely.
# Used by: open-sse/services/providerCooldownTracker.ts
# PROVIDER_COOLDOWN_MAX_MS=300000
# Enable/disable global provider cooldown tracking. Opt-in: this global
# cross-request cooldown overlaps the existing Connection Cooldown / Provider
# Circuit Breaker layers, so it is OFF by default. When disabled, only the
# existing per-request/per-connection cooldown state is used (previous behavior).
# Used by: open-sse/services/providerCooldownTracker.ts
# Accepted values: true|1|on (enable). Unset or anything else = disabled (default).
# PROVIDER_COOLDOWN_ENABLED=true
# Transparent stream recovery (free-claude-code port). When enabled, the opening SSE
# window is briefly held (up to STREAM_RECOVERY.HOLDBACK_MS) so an upstream truncation
# before any byte reaches the client can be retried invisibly. Opt-in: holding the
# window adds up to that much time-to-first-token latency on every stream, so it is
# OFF by default. Seeds ResilienceSettings.streamRecovery.enabled.
# Used by: open-sse/services/streamRecovery.ts, open-sse/handlers/chatCore.ts
# Accepted values: true|1|on (enable). Unset or anything else = disabled (default).
# STREAM_RECOVERY_ENABLED=true
# Mid-stream continuation (Fase 4.4): when an upstream stream truncates AFTER bytes
# already reached the client, re-request with the partial text as an assistant prefill
# and stitch the missing suffix (plain-text OpenAI-compatible streams only; never with a
# tool call in flight). OFF by default — the recovered tail arrives as one burst, not
# token-by-token. Independent of STREAM_RECOVERY_ENABLED (different risk profile).
# Seeds ResilienceSettings.streamRecovery.continueMidStream.
# Used by: open-sse/services/streamRecovery.ts, open-sse/handlers/chatCore.ts
# Accepted values: true|1|on (enable). Unset or anything else = disabled (default).
# STREAM_RECOVERY_MIDSTREAM_ENABLED=true
# Stagger interval (ms) between provider token healthchecks at startup.
# Used by: src/lib/tokenHealthCheck.ts. Default: 3000.
# HEALTHCHECK_STAGGER_MS=3000
# ═══════════════════════════════════════════════════════════════════════════════
# 22. DEBUGGING
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1167,9 +1305,9 @@ APP_LOG_TO_FILE=true
# Enable verbose trace logging for OmniRoute internals.
# Used by: open-sse/handlers/chatCore.ts.
# OMNIRROUTE_TRACE=true
# OMNIROUTE_TRACE=true
# Standard DEBUG flag (same effect as OMNIRROUTE_TRACE).
# Standard DEBUG flag (same effect as OMNIROUTE_TRACE).
# DEBUG=true
# CURSOR_STREAM_DEBUG=1
@@ -1180,6 +1318,16 @@ APP_LOG_TO_FILE=true
# Used by: open-sse/executors/cursor.ts.
# CURSOR_STREAM_TIMEOUT_MS=300000
# Cursor tool-commit directive toggle. Default-on: when a request declares
# tools, a directive is prepended so composer-2.5 reliably issues tool calls
# instead of narrating intent. Set to 0 to disable.
# Used by: open-sse/executors/cursor.ts.
# CURSOR_TOOL_DIRECTIVE=1
# Per-image fetch timeout (ms) for remote image_url vision input. Default: 15000.
# Used by: open-sse/utils/cursorImages.ts.
# CURSOR_IMAGE_FETCH_TIMEOUT_MS=15000
# Cursor state DB path override (for cursor version detection).
# Used by: open-sse/utils/cursorVersionDetector.ts. Default: probed automatically.
# CURSOR_STATE_DB_PATH=
@@ -1204,7 +1352,6 @@ APP_LOG_TO_FILE=true
# Enable E2E test mode — relaxes auth and enables test harness hooks.
# NEXT_PUBLIC_OMNIROUTE_E2E_MODE=true
# ═══════════════════════════════════════════════════════════════════════════════
# 23. GITHUB INTEGRATION (Issue Reporting)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1222,7 +1369,6 @@ APP_LOG_TO_FILE=true
# GITHUB_ISSUES_TOKEN when unset.
# GITHUB_TOKEN=
# ═══════════════════════════════════════════════════════════════════════════════
# 24. PROVIDER QUOTAS, TUNNELS & SANDBOXED SKILLS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1255,10 +1401,23 @@ APP_LOG_TO_FILE=true
# Used by: src/app/api/providers/command-code/auth/shared.ts.
# COMMAND_CODE_CALLBACK_PORT=
# ── Command Code CLI version header ──
# Value sent as the x-command-code-version header to the Command Code upstream.
# Overrides the built-in default; bump if the upstream requires a newer CLI version.
# Used by: open-sse/executors/commandCode.ts
# Default: 0.33.2
# COMMAND_CODE_VERSION=0.33.2
# ── MITM debug proxy (development only) ──
# Used by: src/mitm/server.cjs — captures upstream traffic for inspection.
# MITM_LOCAL_PORT=443
# MITM_DISABLE_TLS_VERIFY=0
# Idle socket timeout (ms) for proxied connections; sockets idle past this are torn
# down to avoid leaking half-open tunnels (src/mitm/socketTimeouts.ts, server.cjs).
# MITM_IDLE_TIMEOUT_MS=60000
# Routing-decision log verbosity: 0 silences, higher values log more bypass/route
# decisions (src/mitm/server.cjs, _internal/bypass.cjs).
# MITM_VERBOSE=1
# ── 1Proxy egress pool ──
# Used by: src/lib/oneproxySync.ts — fetches proxy nodes from the OmniRoute
@@ -1302,6 +1461,10 @@ APP_LOG_TO_FILE=true
# dashboard's tunnel manager. Used by: src/lib/tailscaleTunnel.ts.
# TAILSCALE_BIN=/usr/local/bin/tailscale
# TAILSCALED_BIN=/usr/local/bin/tailscaled
# Pre-shared Tailscale auth key for non-interactive / headless `tailscale up`
# (passed via --auth-key=). When unset, login falls back to the interactive
# browser auth URL. Used by: src/lib/tailscaleTunnel.ts.
# TAILSCALE_AUTHKEY=
# ── Ngrok tunnel ──
# Used by: src/lib/ngrokTunnel.ts — authenticates outbound tunnels.
@@ -1327,7 +1490,6 @@ APP_LOG_TO_FILE=true
# SKILLS_SANDBOX_NETWORK_ENABLED=0
# SKILLS_ALLOWED_SANDBOX_IMAGES=
# ═══════════════════════════════════════════════════════════════════════════════
# 25. TEST & E2E
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1349,6 +1511,12 @@ APP_LOG_TO_FILE=true
# Disable the OAuth token healthcheck loop during tests (default: true).
# OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK=true
# Exclude specific providers from the PROACTIVE token-refresh sweep (comma-separated,
# case-insensitive). Targeted alternative to OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK: keeps
# rotating-cascade providers (Codex/OpenAI share one Auth0 family) on the reactive 401
# path only, while short-TTL providers like Kimi-coding keep being refreshed proactively.
# OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS=codex,openai
# Silence healthcheck noise in Playwright stdout (default: true).
# OMNIROUTE_HIDE_HEALTHCHECK_LOGS=true
@@ -1438,8 +1606,98 @@ INSPECTOR_MASK_SECRETS=true
INSPECTOR_LLM_HOSTS_EXTRA=
INSPECTOR_INTERNAL_INGEST_TOKEN=
# Quota Sharing (Group B — planos 16+22)
QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# QUOTA_STORE_REDIS_URL= # ex.: redis://localhost:6379 (apenas quando driver=redis)
# QUOTA_SATURATION_THRESHOLD=0.5 # 0..1; >= threshold ativa modo strict (sem empréstimo)
# QUOTA_SOFT_DEPRIORITIZE_FACTOR=0.7 # 0..1; multiplicador do score quando soft policy ativa
# QUOTA_CONSUMPTION_RETENTION_DAYS=14 # GC de buckets quota_consumption.updated_at antigos
QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# QUOTA_STORE_REDIS_URL= # ex.: redis://localhost:6379 (apenas quando driver=redis)
# QUOTA_SATURATION_THRESHOLD=0.5 # 0..1; >= threshold ativa modo strict (sem empréstimo)
# QUOTA_SOFT_DEPRIORITIZE_FACTOR=0.7 # 0..1; multiplicador do score quando soft policy ativa
# QUOTA_CONSUMPTION_RETENTION_DAYS=14 # GC de buckets quota_consumption.updated_at antigos
# ─── OpenCode config regeneration (scripts/ad-hoc/regen-opencode-config.ts) ───
# Base URL of the OmniRoute instance to query for /v1/models when regenerating
# an opencode.json with accurate limit.context values. Used by:
# scripts/ad-hoc/regen-opencode-config.ts. Default: http://localhost:20128
# OMNIROUTE_URL=
# API key to authenticate against the OmniRoute /v1/models endpoint. Falls back
# to OPENCODE_API_KEY when unset. Used by: scripts/ad-hoc/regen-opencode-config.ts.
# OMNIROUTE_KEY=
# OpenCode-style API key (sk-...) for the regenerated opencode.json. Used by:
# scripts/ad-hoc/regen-opencode-config.ts. Falls back to OMNIROUTE_KEY.
# OPENCODE_API_KEY=
# ─── Bifrost Go sidecar (PR-4 in #3932) ──────────────────────────────────────
# Master kill switch for the bifrost sidecar proxy. When set to 0, the
# /api/v1/relay/chat/completions/bifrost route returns 503 with the
# X-Bifrost-Killswitch header and the operator is bounced to the TS path.
# Use this to disable the sidecar without redeploying (e.g. during a
# tier-1 router incident or a key rotation). Default: 1 (sidecar active).
# BIFROST_ENABLED=1
# When BIFROST_BASE_URL is set, /api/v1/relay/chat/completions/bifrost routes
# traffic to the Go gateway instead of the TS relay handler, removing TS from
# the hot path. Auth/rate-limit/injection-guard stay in the route (security not
# duplicated). Falls back to TS path via X-Bifrost-Fallback header on
# timeout/failure. See bin/omniroute for the local-redis companion.
# BIFROST_BASE_URL=
# API key for the Bifrost gateway (sent as Authorization: Bearer ...). If
# unset, the route expects the request to carry a valid OmniRoute API key;
# this key is for gateway-side auth only.
# BIFROST_API_KEY=
# When true, the Bifrost sidecar route streams responses back via SSE through
# the gateway rather than the TS streaming executor. Default: true (when
# BIFROST_BASE_URL is set).
# BIFROST_STREAMING_ENABLED=
# Per-request timeout when proxying to the Bifrost gateway. Default: 30000 (30s).
# BIFROST_TIMEOUT_MS=
# Alias for BIFROST_API_KEY (used by scripts that read the env via
# OMNIROUTE_*). Falls back to BIFROST_API_KEY when unset.
# OMNIROUTE_BIFROST_KEY=
# ─── 1-click local service launchers (PR-3 in #3932) ────────────────────────
# Master switch for /api/local/* routes. When unset or "0", all /api/local/*
# routes return 503 in production. Default: 0. Must be "1" in non-loopback
# deploys to enable the Redis launcher and similar 1-click local service
# starters. Belt-and-suspenders with the isLocalOnlyPath() route-guard
# classification (LOCAL_ONLY_API_PREFIXES in src/server/authz/routeGuard.ts).
# OMNIROUTE_LOCAL_ENDPOINTS_ENABLED=
# Bearer token for /api/local/* callers that aren't on loopback (e.g. the
# desktop app). When set, requests from non-loopback IPs must carry
# Authorization: Bearer <token>. Required when
# OMNIROUTE_LOCAL_ENDPOINTS_ENABLED=1 in non-loopback deployments. Default:
# unset (loopback-only).
# OMNIROUTE_LOCAL_ENDPOINTS_TOKEN=
# Container name for the 1-click Redis launcher (`omniroute redis up`).
# Default: omniroute-redis. Used by bin/cli/commands/redis.mjs and the
# RedisLauncherPanel.
# OMNIROUTE_REDIS_CONTAINER_NAME=
# Host port for the 1-click Redis launcher. Default: 6379. Bump if the host
# already binds 6379. The container's internal port stays 6379.
# OMNIROUTE_REDIS_HOST_PORT=
# Redis image used by the 1-click Redis launcher. Default: redis:7-alpine.
# Override to redis:8-alpine or a private registry mirror as needed.
# OMNIROUTE_REDIS_IMAGE=
# ── Cluster Profile: Qdrant Vector Memory (opt-in via `docker compose --profile memory up`) ──
# Qdrant is an OPTIONAL sidecar for deployments that need cosine-distance vector
# search at >1M embeddings. The default vector store is sqlite-vec
# (src/lib/memory/vectorStore.ts:108); flip this profile on only if you hit the
# sqlite-vec ceiling or want persistent cross-replica vector state. See
# docs/architecture/cluster-decisions.md § "Qdrant (memory profile)".
# QDRANT_HOST=qdrant
# QDRANT_PORT=6333
# QDRANT_GRPC_PORT=6334
# QDRANT_API_KEY=
# QDRANT_COLLECTION=omniroute-memory
# QDRANT_EMBEDDING_MODEL=text-embedding-3-small
# QDRANT_VECTOR_SIZE=1536
# QDRANT_HNSW_EF_CONSTRUCT=128
# ── Cluster Profile: Bifrost Tier-1 Router (opt-in via `docker compose --profile bifrost up`) ──
# Bifrost is an OPTIONAL Go-based Tier-1 router that handles the upstream-provider
# multiplexing layer. Default: OmniRoute's open-sse/executors/bifrost.ts in-process
# executor handles routing directly. Flip this profile on only if you want the
# gateway as a separate sidecar (helps in 3+ replica deployments where you want
# provider rotation centralised). See docs/architecture/cluster-decisions.md §
# "Bifrost (bifrost profile)".
# BIFROST_BASE_URL=http://bifrost:8080
# BIFROST_API_KEY=
# BIFROST_STREAMING_ENABLED=true
# BIFROST_TIMEOUT_MS=30000

5
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
# Funding links for OmniRoute — rendered as the "Sponsor" button on GitHub.
# Docs: https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
github: diegosouzapw
# Additional platforms (uncomment and fill in before enabling):
# custom: ["https://omniroute.online/donate"]

29
.github/actions/npm-ci-retry/action.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: npm ci with retry
description: Run npm ci with retries for transient registry/network failures.
runs:
using: composite
steps:
- shell: bash
run: |
set -euo pipefail
max_attempts=3
delay_seconds=20
for attempt in $(seq 1 "$max_attempts"); do
if [ "$attempt" -gt 1 ]; then
echo "npm ci attempt $attempt/$max_attempts after transient failure"
fi
if npm ci; then
exit 0
fi
exit_code=$?
if [ "$attempt" -eq "$max_attempts" ]; then
exit "$exit_code"
fi
sleep "$delay_seconds"
delay_seconds=$((delay_seconds * 2))
done

View File

@@ -24,6 +24,20 @@ updates:
update-types: ["version-update:semver-major"]
- dependency-name: "eslint-config-next"
update-types: ["version-update:semver-major"]
# jscpd v5 is a Rust rewrite (native binary, no Node.js programmatic API).
# scripts/check/check-duplication.mjs is deliberately pinned to jscpd@4 (it
# parses jscpd-report.json against a frozen baseline). A v5 major would break
# the duplication gate — migrate the gate intentionally, not via dependabot.
- dependency-name: "jscpd"
update-types: ["version-update:semver-major"]
# @huggingface/transformers is HARD-PINNED at 3.5.2 (exact, no caret) — FROZEN.
# It is load-bearing for the LLMLingua ONNX compression engine (open-sse/services/
# compression/engines/llmlingua/ — worker.ts pins @huggingface/transformers@3.5.2)
# and for local memory embeddings (src/lib/memory/embedding/transformersLocal.ts),
# and was VPS-validated at 3.5.2 (#4014). 4.x breaks both, and even 3.x minors must
# be re-validated on the VPS — so freeze ALL auto-bumps (no update-types = ignore
# every version). Migrate it intentionally, not via dependabot (#4050).
- dependency-name: "@huggingface/transformers"
- package-ecosystem: "github-actions"
directory: "/"

View File

@@ -7,9 +7,10 @@ on:
- "v*"
workflow_dispatch:
# Least-privilege default: read-only at the top level; the build job that pushes to
# GHCR grants packages: write itself (Scorecard TokenPermissions).
permissions:
contents: read
packages: write
env:
IMAGE_NAME: ghcr.io/kang-heewon/omniroute
@@ -19,9 +20,14 @@ jobs:
name: Build and Push Fork Image
if: github.repository == 'kang-heewon/OmniRoute'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v4

View File

@@ -21,23 +21,114 @@ env:
CI_NODE_26_VERSION: "26"
jobs:
changes:
name: Change Classification
runs-on: ubuntu-latest
outputs:
code: ${{ steps.classify.outputs.code }}
docs: ${{ steps.classify.outputs.docs }}
i18n: ${{ steps.classify.outputs.i18n }}
workflow: ${{ steps.classify.outputs.workflow }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
fetch-depth: 0
- id: classify
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [ "$EVENT_NAME" != "pull_request" ]; then
{
echo "code=true"
echo "docs=true"
echo "i18n=true"
echo "workflow=true"
} >> "$GITHUB_OUTPUT"
exit 0
fi
code=false
docs=false
i18n=false
workflow=false
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
while IFS= read -r file; do
case "$file" in
.github/workflows/*|.zizmor.yml)
workflow=true
code=true
;;
docs/*|*.md)
docs=true
;;
src/i18n/*|src/i18n/messages/*|scripts/i18n/*|config/i18n.json)
i18n=true
code=true
;;
src/*|open-sse/*|bin/*|electron/*|tests/*|scripts/*|package.json|package-lock.json|tsconfig*.json|next.config.*|vitest*.config.*|playwright.config.*)
code=true
;;
db/*|config/*)
code=true
;;
*)
code=true
;;
esac
done < changed-files.txt
{
echo "code=$code"
echo "docs=$docs"
echo "i18n=$i18n"
echo "workflow=$workflow"
} >> "$GITHUB_OUTPUT"
lint:
name: Lint
runs-on: ubuntu-latest
env:
# tsx gates below (known-symbols, route-guard-membership) import modules that
# open SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run audit:deps
- run: npm run lint
- run: npm run check:cycles
- run: npm run check:route-validation:t06
- run: npm run check:any-budget:t11
- run: npm run check:docs-sync
- run: npm run check:provider-consistency
- run: npm run check:fetch-targets
- run: npm run check:deps
- run: npm run check:file-size
- run: npm run check:error-helper
- run: npm run check:migration-numbering
- run: npm run check:public-creds
- run: npm run check:db-rules
- run: npm run check:known-symbols
- run: npm run check:route-guard-membership
- run: npm run check:test-discovery
- run: npm run check:tracked-artifacts
- run: npm run check:lockfile
- run: npm run check:licenses
# check:docs-sync is run by the docs-sync-strict job (via check:docs-all) and the
# husky pre-commit hook; the standalone copy here was redundant (ROI dedup).
- run: npm run typecheck:core
# typecheck:noimplicit:core is a forward-looking gate (noImplicitAny).
# Run informationally for now — many pre-existing call sites still need
@@ -45,30 +136,266 @@ jobs:
- run: npm run typecheck:noimplicit:core
continue-on-error: true
docs-sync-strict:
name: Docs Sync (Strict)
quality-gate:
name: Quality Ratchet
runs-on: ubuntu-latest
needs: test-coverage
if: ${{ !cancelled() && needs.test-coverage.result == 'success' }}
# security-events: read lets the CodeQL ratchet read open code-scanning alerts
# via `gh api .../code-scanning/alerts`. contents: read keeps checkout working.
permissions:
contents: read
security-events: read
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
# Coverage mergeada (coverage-summary.json) p/ o ratchet de cobertura.
- uses: actions/download-artifact@v8
with:
name: coverage-report
path: coverage/
- run: npm run quality:collect
# Catraca: falha se qualquer métrica regredir vs quality-baseline.json (commitado).
# Hoje: contagem de warnings do ESLint. Fase 4 estende com cobertura (lida do
# coverage mergeado). Tamanho de arquivo e duplicação têm gates dedicados.
- name: Ratchet check
run: node scripts/quality/check-quality-ratchet.mjs --summary .artifacts/quality-ratchet.md
# Fase 6A.5: require-tighten — BLOQUEANTE (promovido de advisory no fim do ciclo
# v3.8.27). Falha quando uma métrica MELHOROU sem o baseline ter sido apertado no
# mesmo PR (força capturar ganhos permanentes). As métricas coverage.* carregam
# tightenSlack para o gap anti-flake (CI mergeado > baseline) não disparar falso-
# positivo. Verificado limpo (exit 0) no tip de release/v3.8.27 com a cobertura
# mergeada == baseline (ver a nota _require_tighten_flip_blocking em
# config/quality/quality-baseline.json).
- name: Require-tighten (blocking)
run: node scripts/quality/check-quality-ratchet.mjs --require-tighten
# Catraca de duplicação (jscpd@4 sobre src+open-sse). Roda neste job (paralelo)
# para não pesar no caminho crítico do lint.
- name: Duplication ratchet
run: npm run check:duplication
- name: Complexity ratchet
run: npm run check:complexity
# Fase 7 INT: dead-code, cognitive-complexity, type-coverage promovidos de
# advisory (quality-extended) para BLOQUEANTES aqui. Os 3 leem seus baseline
# de quality-baseline.json e saem 1 em regressão.
- name: Dead-code ratchet (knip)
run: npm run check:dead-code
- name: Cognitive complexity ratchet (sonarjs)
run: npm run check:cognitive-complexity
- name: Type coverage ratchet
run: npm run check:type-coverage
- name: Compression budget ratchet (F2.4 / N4)
run: npm run check:compression-budget
# CodeQL alerts ratchet — BLOQUEANTE (promovido de advisory na v3.8.26).
# Lê metrics.codeqlAlerts.value de quality-baseline.json e sai 1 SOMENTE numa
# regressão real (alertas abertos > baseline). Falha de medição (gh/auth/api)
# é skip gracioso com exit 0 — security-events:read no job-level permissions.
- name: CodeQL alerts ratchet (blocking)
run: npm run check:codeql-ratchet
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Append summary
if: always()
run: cat .artifacts/quality-ratchet.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload ratchet report
if: always()
uses: actions/upload-artifact@v7
with:
name: quality-ratchet
path: .artifacts/quality-ratchet.md
if-no-files-found: warn
# Phase 7/8 extended quality gates — MIXED (Etapa 2, v3.8.26). The job no longer
# carries a job-level continue-on-error: the three ratchet-blocking steps below
# (Secret scan / Workflow lint / Bundle size, all passing --ratchet) FAIL the job
# on a measured regression vs config/quality/quality-baseline.json. The remaining
# steps stay ADVISORY via step-level continue-on-error (scanner install,
# vuln/osv ratchet, OpenAPI/oasdiff breaking-change, circular-deps/dpdm): they
# depend on external binaries/state that may legitimately self-skip, so they must
# never block. The blocking gates themselves SKIP (exit 0) when their binary/plugin
# is absent — only a measured regression on the SAME metric the baseline froze
# blocks. The CodeQL ratchet was PROMOTED to the quality-gate job (v3.8.26).
# SonarQube needs SONAR_TOKEN/SONAR_HOST_URL secrets.
quality-extended:
name: Quality Gates (Extended)
runs-on: ubuntu-latest
steps:
# fetch-depth: 0 — the OpenAPI breaking-change gate (oasdiff) reads the base
# spec via `git show <base_ref>:docs/reference/openapi.yaml`; a shallow clone
# would lack the base ref and the gate would self-skip (base-unresolved).
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
# Dead-code, cognitive-complexity, type-coverage foram promovidos ao job
# quality-gate (bloqueante) na Fase 7 INT — não rodam aqui para evitar duplo custo.
- name: Circular deps (dpdm; advisory)
continue-on-error: true
run: npm run check:circular-deps
# BLOCKING ratchet (Etapa 2): bundleSize must not regress vs the baseline
# (gzip via @size-limit/file, installed by `npm ci`). --ratchet exits 1 on a
# measured regression; it SKIPs (exit 0) when the size-limit plugin/build is
# absent (a non-comparable measurement never blocks).
- name: Bundle size (ratchet, blocking)
run: npm run check:bundle-size -- --ratchet
# CodeQL ratchet foi PROMOVIDO a BLOQUEANTE no job quality-gate (v3.8.26) —
# não roda aqui para evitar duplo run/duplo report.
# Install the advisory security scanners so the gates below actually run
# (they self-skip when the binaries are absent). Robustness lessons baked in:
# • `go install …/gitleaks/v8@latest` produces a binary WITHOUT the version
# ldflags gitleaks needs (and often fails) — avoided.
# • `curl …api.github.com/…/releases/latest` is UNAUTHENTICATED and
# rate-limited to 60 req/hr/IP; when throttled it returns an empty body,
# so the asset URL resolves to nothing and the install silently no-ops —
# every gate then self-skips and the metric is never produced. We instead
# use `gh release download`, which is preinstalled on GitHub runners and
# authenticated via GITHUB_TOKEN (5000 req/hr) — robust under load.
# • actionlint keeps its official download script; zizmor stays on pipx.
# We `set +e` (no single failure aborts the step), ALWAYS export $GITHUB_PATH
# at the end, and print diagnostics so the next CI run proves exactly what
# installed. The job is continue-on-error too, so an install hiccup never
# blocks the build.
- name: Install advisory security scanners (gitleaks/osv/actionlint/zizmor)
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set +e
mkdir -p "$HOME/.local/bin"
# gitleaks — download latest linux x64 tarball via gh (authed), extract binary
rm -rf /tmp/gl && mkdir -p /tmp/gl
gh release download --repo gitleaks/gitleaks --pattern '*linux_x64.tar.gz' --dir /tmp/gl
tar -xzf /tmp/gl/*linux_x64.tar.gz -C "$HOME/.local/bin" gitleaks
# osv-scanner — download latest linux amd64 bare binary via gh (authed)
rm -rf /tmp/osv && mkdir -p /tmp/osv
gh release download --repo google/osv-scanner --pattern '*linux_amd64' --dir /tmp/osv
install -m 0755 /tmp/osv/*linux_amd64 "$HOME/.local/bin/osv-scanner"
# actionlint — official download script
bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) latest "$HOME/.local/bin"
# zizmor — PyPI (pipx preferred, pip --user fallback); lands in ~/.local/bin
pipx install zizmor || pip install --user zizmor
# oasdiff — download latest linux amd64 tarball via gh (authed), extract binary
rm -rf /tmp/oasd && mkdir -p /tmp/oasd
gh release download --repo oasdiff/oasdiff --pattern '*linux_amd64.tar.gz' --dir /tmp/oasd
tar -xzf /tmp/oasd/*linux_amd64.tar.gz -C "$HOME/.local/bin" oasdiff
# ALWAYS export the bin dir (even if any step above failed)
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# diagnostics — prove what installed on the next CI run
ls -la "$HOME/.local/bin"
"$HOME/.local/bin/gitleaks" version || true
"$HOME/.local/bin/actionlint" -version || true
"$HOME/.local/bin/osv-scanner" --version || true
"$HOME/.local/bin/oasdiff" --version || true
zizmor --version || true
# BLOCKING ratchet (Etapa 2): secretFindings must not regress vs the baseline.
# --ratchet exits 1 on a measured regression; it SKIPs (exit 0) when gitleaks
# is absent (a missing binary never blocks).
- name: Secret scan (gitleaks, ratchet, blocking)
run: npm run check:secrets -- --ratchet
# BLOCKING ratchet (v3.8.27 cycle-end): vulnCount must not regress vs the
# baseline. --ratchet exits 1 on a measured regression (measured > baseline);
# it SKIPs (exit 0) when osv-scanner is absent or osv.dev is unreachable (a
# missing/failed measurement never blocks). See the CVE-variance note in
# docs/security/SUPPLY_CHAIN.md — a newly-disclosed CVE on an unchanged dep can
# red this gate; the fix is to bump the dep or re-baseline metrics.vulnCount.
- name: Vulnerability ratchet (osv-scanner, ratchet, blocking)
run: npm run check:vuln-ratchet -- --ratchet
# BLOCKING ratchet (Etapa 2): zizmorFindings must not regress vs the baseline.
# ONLY zizmor is ratcheted — actionlint findings are reported, not blocking.
# --ratchet exits 1 on a measured zizmor regression; it SKIPs (exit 0) when
# zizmor is absent (a missing binary never blocks).
- name: Workflow lint (actionlint+zizmor, ratchet, blocking)
run: npm run check:workflows -- --ratchet
# OpenAPI breaking-change detection (oasdiff). Diffs the PR's public API
# contract (docs/reference/openapi.yaml) against the base branch's spec.
# BLOCKING ratchet (Fase 9 Onda 0): reads metrics.openapiBreaking.value and
# exits 1 ONLY on a measured regression (count > baseline). It SKIPs (exit 0)
# when oasdiff is absent or the base spec can't be resolved — a missing
# measurement never blocks. BASE_REF is read by the script from the env
# (never interpolated into a shell body) — workflow-injection-safe.
- name: OpenAPI breaking-change (oasdiff, ratchet, blocking)
env:
BASE_REF: ${{ github.base_ref }}
run: npm run check:openapi-breaking -- --ratchet
docs-sync-strict:
name: Docs Sync (Strict)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:docs-all
# Previously-orphaned contract gates (existed as files, never wired anywhere).
# All exit 0 today: cli-i18n is a hard gate, openapi-coverage is a ratchet
# (floor ~36), openapi-security-tiers is advisory (Hard Rules #15/#17).
- name: CLI i18n consistency
run: npm run check:cli-i18n
- name: OpenAPI route coverage (ratchet)
run: npm run check:openapi-coverage
- name: OpenAPI security-tier consistency (advisory)
run: npm run check:openapi-security-tiers
- name: OpenAPI spec paths resolve to real routes (anti-hallucination)
run: npm run check:openapi-routes
- name: Doc /api refs resolve to real routes (anti-hallucination)
run: npm run check:docs-symbols
- name: i18n translation drift (warn)
run: node scripts/i18n/check-translation-drift.mjs --warn
docs-lint:
name: Docs Lint (prose — advisory)
runs-on: ubuntu-latest
# Advisory (warning-first): prose/markdown style must not block merges while the
# existing doc corpus is brought up to style. Promote to blocking once it converges.
continue-on-error: true
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- name: markdownlint (docs + root, advisory)
run: npx --yes markdownlint-cli2 "docs/**/*.md" "*.md" "!docs/i18n" "!docs/research" || true
- name: Vale prose lint (Microsoft style, advisory)
# Non-fatal: a Vale/reviewdog setup error must not turn this advisory job red.
continue-on-error: true
uses: errata-ai/vale-action@reviewdog
with:
files: docs
fail_on_error: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
i18n-ui-coverage:
name: i18n UI Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65
i18n-matrix:
@@ -77,7 +404,9 @@ jobs:
outputs:
langs: ${{ steps.langs.outputs.langs }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- id: langs
run: |
LANG_DIR="src/i18n/messages"
@@ -94,14 +423,22 @@ jobs:
lang: ${{ fromJson(needs.i18n-matrix.outputs.langs) }}
needs: i18n-matrix
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Validate ${{ matrix.lang }}
env:
# Pass the matrix value via env (never interpolate ${{ ... }} straight
# into the run: script body) so the shell receives a variable, not
# inlined text — zizmor template-injection mitigation. Named MATRIX_LANG
# to avoid clobbering the POSIX `LANG` locale variable.
MATRIX_LANG: ${{ matrix.lang }}
run: |
python3 scripts/i18n/validate_translation.py quick -l '${{ matrix.lang }}' > result.txt
python3 scripts/i18n/validate_translation.py quick -l "$MATRIX_LANG" > result.txt
- name: Upload result
if: always()
@@ -115,8 +452,9 @@ jobs:
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v6
with:
@@ -125,6 +463,14 @@ jobs:
run: git fetch --no-tags origin "${GITHUB_BASE_REF}" --depth=1
- name: Validate source changes include tests
run: node scripts/check/check-pr-test-policy.mjs --summary-file .artifacts/pr-test-policy.md
# Anti test-masking: flag net assert removal / new assert.ok(true) in changed tests.
- name: Detect test-masking (weakened assertions)
run: npm run check:test-masking
# Evidence-in-PR-body (Hard Rule #18 mechanized): claims of "tests pass" must carry output.
- name: Require evidence in PR body
run: npm run check:pr-evidence
env:
PR_BODY: ${{ github.event.pull_request.body }}
- name: Publish PR test policy summary
if: always()
run: |
@@ -135,15 +481,42 @@ jobs:
build:
name: Build
runs-on: ubuntu-latest
needs: changes
if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.code == 'true' }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Cache Next.js build cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae
with:
path: .build/next/cache
key: nextjs-${{ runner.os }}-node-${{ env.CI_NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*', 'open-sse/**/*', 'db/**/*', 'next.config.mjs', 'tsconfig*.json', 'postcss.config.*', 'tailwind.config.*') }}
restore-keys: |
nextjs-${{ runner.os }}-node-${{ env.CI_NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-
- run: npm run build
- name: Archive Next.js build for downstream jobs
# Use tar so the archive preserves paths relative to CWD (.build/next/...).
# upload-artifact path-stripping is ambiguous when exclude patterns are used;
# an explicit tar avoids the double-nesting issue (.build/next/next/...).
# Keep standalone/node_modules intact: package/electron jobs consume the
# Next-traced standalone tree and must not replace it with root node_modules.
run: |
tar -czf /tmp/e2e-build.tar.gz \
--exclude='.build/next/cache' \
.build/next
- name: Upload Next.js build for downstream jobs
uses: actions/upload-artifact@v7
with:
name: next-build
path: /tmp/e2e-build.tar.gz
retention-days: 1
package-artifact:
name: Package Artifact
@@ -152,14 +525,28 @@ jobs:
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Download Next.js build artifact
uses: actions/download-artifact@v8
with:
name: next-build
path: /tmp/
- name: Extract Next.js build artifact
run: |
tar -xzf /tmp/e2e-build.tar.gz
# build:cli consumes the downloaded .build/next standalone artifact and assembles dist/;
# it only rebuilds if the downloaded standalone artifact is missing.
- run: npm run build:cli
- name: Assert dist/server.js exists
run: test -f dist/server.js || (echo "dist/server.js missing — build:cli did not assemble correctly" && exit 1)
- run: npm run check:pack-artifact
electron-package-smoke:
@@ -171,14 +558,23 @@ jobs:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
CSC_IDENTITY_AUTO_DISCOVERY: "false"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run build
- name: Download Next.js build artifact
uses: actions/download-artifact@v8
with:
name: next-build
path: /tmp/
- name: Extract Next.js build artifact
run: |
tar -xzf /tmp/e2e-build.tar.gz
- name: Install Electron dependencies
working-directory: electron
run: npm install --no-audit --no-fund
@@ -204,62 +600,121 @@ jobs:
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: node --max-old-space-size=4096 --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
node-24-compat:
name: Node 24 Compatibility (${{ matrix.shard }}/2)
test-vitest:
name: Vitest (MCP / autoCombo / UI components)
runs-on: ubuntu-latest
timeout-minutes: 15
needs: build
strategy:
fail-fast: false
matrix:
shard: [1, 2]
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
# The second test runner (CLAUDE.md: "Both test runners must pass") — was never
# wired into CI until the 2026-06-09 quality audit (Fase 6A.2).
- run: npm run test:vitest
# vitest:ui is RED today (14 fails — UI component drift accumulated while the
# suite never ran in CI). Informational until the Fase 6A triage (2026-06-16+)
# fixes the components/tests; then drop continue-on-error to make it blocking.
- run: npm run test:vitest:ui
continue-on-error: true
node-24-compat:
name: Node 24 Compatibility Tests (${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: build
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_24_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run build
- run: node --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
node-26-compat:
name: Node 26 Compatibility (${{ matrix.shard }}/2)
node-26-compat-build:
name: Node 26 Compatibility Build
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 25
needs: build
strategy:
fail-fast: false
matrix:
shard: [1, 2]
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_26_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Cache Next.js build cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae
with:
path: .build/next/cache
key: nextjs-${{ runner.os }}-node-${{ env.CI_NODE_26_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*', 'open-sse/**/*', 'db/**/*', 'next.config.mjs', 'tsconfig*.json', 'postcss.config.*', 'tailwind.config.*') }}
restore-keys: |
nextjs-${{ runner.os }}-node-${{ env.CI_NODE_26_VERSION }}-${{ hashFiles('package-lock.json') }}-
- run: npm run build
- run: node --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts
node-26-compat:
name: Node 26 Compatibility Tests (${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: node-26-compat-build
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_26_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
test-coverage-shard:
name: Coverage Shard (${{ matrix.shard }}/8)
@@ -275,12 +730,14 @@ jobs:
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Run c8 over shard ${{ matrix.shard }}/8
run: |
@@ -297,8 +754,8 @@ jobs:
--reporter=json \
--exclude=tests/** \
--exclude=**/*.test.* \
node --max-old-space-size=4096 --import tsx --test --test-force-exit --test-concurrency=4 \
--test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts
node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \
--test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
- name: Upload raw shard coverage
if: always()
uses: actions/upload-artifact@v7
@@ -312,17 +769,19 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
needs: test-coverage-shard
if: ${{ always() && needs.test-coverage-shard.result == 'success' }}
if: ${{ !cancelled() && needs.test-coverage-shard.result == 'success' }}
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- name: Download all shard coverage
uses: actions/download-artifact@v8
with:
@@ -330,32 +789,38 @@ jobs:
path: coverage-shards/
merge-multiple: true
- name: Merge + report + gate
# Merging 8 shards of raw v8 coverage is memory-heavy; the default Node
# heap OOMs (exit 134). Raise it for the c8 merge/report step.
# Merging 8 shards of raw v8 coverage is memory-heavy. `--merge-async`
# keeps the V8 coverage merge incremental instead of loading every raw
# JSON blob into one in-memory merge, which avoids Node heap OOMs.
env:
NODE_OPTIONS: --max-old-space-size=6144
NODE_OPTIONS: --max-old-space-size=8192
run: |
mkdir -p coverage
if [ ! -d coverage-shards ] || ! find coverage-shards -maxdepth 1 -type f -name '*.json' | grep -q .; then
first_coverage_file=""
if [ -d coverage-shards ]; then
first_coverage_file="$(find coverage-shards -maxdepth 1 -type f -name '*.json' -print -quit)"
fi
if [ -z "$first_coverage_file" ]; then
echo "::error::No raw coverage shard data was downloaded."
find . -maxdepth 3 -type f | sort
exit 1
fi
# Gate aligned to the project's local coverage bar (npm run test:coverage
# uses 40/40/40/40). The previous 75/70 gate never ran on main (the
# coverage shards always failed → this job was skipped), so it was never
# actually enforced and is inconsistent with the repo's real standard.
# Gate aligned to the project's local coverage bar: `npm run test:coverage`
# gates at 60/60/60/60, so CI must match it (the previous CI floor of 40
# silently undershot the local bar — a real drift). Real merged coverage is
# ~79/79/82/75, so 60 is a conservative floor with headroom; the Fase-4
# coverage ratchet (quality-baseline.json) layers "must not drop vs baseline"
# on top of this floor.
npx c8 report \
--temp-directory coverage-shards \
--reports-dir coverage \
--merge-async \
--reporter=text-summary \
--reporter=html \
--reporter=json-summary \
--reporter=lcov \
--exclude=tests/** \
--exclude=**/*.test.* \
--check-coverage \
--statements 40 --lines 40 --functions 40 --branches 40
--statements 60 --lines 60 --functions 60 --branches 60
- name: Build coverage summary
if: always()
run: |
@@ -380,7 +845,6 @@ jobs:
name: coverage-report
path: |
coverage/coverage-summary.json
coverage/lcov.info
coverage/coverage-report.md
if-no-files-found: warn
@@ -388,13 +852,14 @@ jobs:
name: SonarQube
runs-on: ubuntu-latest
needs: test-coverage
if: ${{ always() && needs.test-coverage.result == 'success' }}
if: ${{ !cancelled() && needs.test-coverage.result == 'success' }}
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/download-artifact@v8
with:
@@ -418,8 +883,9 @@ jobs:
coverage-pr-comment:
name: PR Coverage Comment
runs-on: ubuntu-latest
if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && needs.changes.outputs.code == 'true' }}
needs:
- changes
- pr-test-policy
- test-coverage
permissions:
@@ -494,30 +960,49 @@ jobs:
}
test-e2e:
name: E2E Tests (${{ matrix.shard }}/6)
name: E2E Tests (${{ matrix.shard }}/9)
runs-on: ubuntu-latest
timeout-minutes: 20
# Build artifact from the `build` job is downloaded instead of rebuilding
# (~5min saved per shard). 9 shards (up from 6) reduces tests per shard by
# ~33%. Playwright browser is cached across runs (~1.5min saved per shard).
# Heavy shard target: ≤20min (was ~40min). Timeout 45min to cover slow runners.
timeout-minutes: 45
needs: build
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6]
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9]
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
OMNIROUTE_PLAYWRIGHT_SKIP_BUILD: "1"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Cache Playwright browsers
uses: actions/cache@v5.0.5
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: playwright-chromium-${{ runner.os }}-
- run: npx playwright install --with-deps chromium
- run: npm run build
- run: npx playwright test tests/e2e/*.spec.ts --shard=${{ matrix.shard }}/6
- name: Download Next.js build artifact
uses: actions/download-artifact@v8
with:
name: next-build
path: /tmp/
- name: Extract Next.js build artifact
run: |
tar -xzf /tmp/e2e-build.tar.gz
- run: npx playwright test tests/e2e/*.spec.ts --shard=${{ matrix.shard }}/9
test-integration:
name: Integration Tests (${{ matrix.shard }}/2)
@@ -535,14 +1020,16 @@ jobs:
DATA_DIR: /tmp/omniroute-ci-${{ matrix.shard }}
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: node --import tsx --test --test-force-exit --test-concurrency=1 --test-shard=${{ matrix.shard }}/2 tests/integration/*.test.ts
- run: node --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=${{ matrix.shard }}/2 tests/integration/*.test.ts
test-security:
name: Security Tests
@@ -553,31 +1040,34 @@ jobs:
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run test:security
ci-summary:
name: CI Dashboard
runs-on: ubuntu-latest
if: always()
if: ${{ !cancelled() }}
needs:
- changes
- lint
- docs-sync-strict
- i18n-ui-coverage
- i18n
- pr-test-policy
- build
- package-artifact
- electron-package-smoke
- test-unit
- node-24-compat
- node-26-compat-build
- node-26-compat
- test-coverage
- sonarqube
@@ -614,11 +1104,11 @@ jobs:
echo "## 🧱 Core Checks" >> "$GITHUB_STEP_SUMMARY"
echo "| Job | Status |" >> "$GITHUB_STEP_SUMMARY"
echo "|-----|--------|" >> "$GITHUB_STEP_SUMMARY"
echo "| Change Classification | $(status '${{ needs.changes.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Lint | $(status '${{ needs.lint.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Docs Sync (Strict) | $(status '${{ needs.docs-sync-strict.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| i18n UI Coverage | $(status '${{ needs.i18n-ui-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| PR Test Policy | $(status '${{ needs.pr-test-policy.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| SonarQube | $(status '${{ needs.sonarqube.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
@@ -628,14 +1118,15 @@ jobs:
echo "| Build Matrix | $(status '${{ needs.build.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Package Artifact | $(status '${{ needs.package-artifact.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Electron Package Smoke | $(status '${{ needs.electron-package-smoke.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Node 26 Compatibility Build | $(status '${{ needs.node-26-compat-build.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "## 🧪 Tests" >> "$GITHUB_STEP_SUMMARY"
echo "| Suite | Status |" >> "$GITHUB_STEP_SUMMARY"
echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY"
echo "| Unit | $(status '${{ needs.test-unit.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Node 24 Compatibility | $(status '${{ needs.node-24-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Node 26 Compatibility | $(status '${{ needs.node-26-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Node 24 Compatibility Tests | $(status '${{ needs.node-24-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Node 26 Compatibility Tests | $(status '${{ needs.node-26-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Coverage | $(status '${{ needs.test-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| PR Coverage Comment | $(status '${{ needs.coverage-pr-comment.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| E2E | $(status '${{ needs.test-e2e.result }}') |" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -10,6 +10,10 @@ on:
pull_request_review:
types: [submitted]
# Least-privilege default: no token permissions at the top level; the `claude` job
# grants exactly what it needs below (Scorecard TokenPermissions).
permissions: {}
jobs:
claude:
if: |
@@ -26,8 +30,9 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 1
- name: Run Claude Code

31
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: CodeQL
# OWNER ACTION REQUIRED before enabling auto-triggers: advanced CodeQL conflicts with
# GitHub "default setup" — the analyze step fails with "CodeQL analyses from advanced
# configurations cannot be processed when the default setup is enabled". Switch repo
# Settings → Code security → CodeQL from Default to Advanced, THEN restore the
# push/pull_request/schedule triggers below. Until then this only runs on manual dispatch
# so it never produces a red check on PRs. (The codeqlAlerts ratchet keeps working via the
# default setup's alerts in the meantime.)
on:
workflow_dispatch:
permissions:
contents: read
jobs:
analyze:
name: Analyze (javascript-typescript)
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: javascript-typescript
queries: security-extended
- uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:javascript-typescript"

62
.github/workflows/dast-smoke.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: DAST smoke (PR)
on:
pull_request:
branches: ["main", "release/**"]
permissions:
contents: read
jobs:
dast-smoke:
runs-on: ubuntu-latest
# ADVISORY while this new gate matures (repo convention: advisory -> blocking).
# Flip to blocking (remove continue-on-error) once it's proven stable across a few PRs.
continue-on-error: true
timeout-minutes: 12
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-api-key-secret-with-sufficient-length-aaaa
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Build CLI bundle
run: npm run build:cli
- name: Start OmniRoute
env:
PORT: "20128"
INJECTION_GUARD_MODE: block
run: |
node dist/server.js > server.log 2>&1 &
echo $! > server.pid
for _ in $(seq 1 30); do
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo up; break; fi
sleep 2
done
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- run: pip install schemathesis
- name: Schemathesis smoke (high-risk endpoints, blocking)
run: |
schemathesis run docs/reference/openapi.yaml --url http://localhost:20128 \
--include-path-regex '^/v1/(chat/completions|models)$|^/api/(auth|keys)' \
--max-examples 8 --workers 4 --checks all --max-response-time 30 \
--request-timeout 20 --suppress-health-check all --no-color
- name: promptfoo injection-guard (blocking)
env:
OMNIROUTE_URL: http://localhost:20128
OMNIROUTE_API_KEY: not-needed-blocked-before-upstream
run: npx --yes promptfoo@latest eval -c promptfooconfig.yaml --no-cache
- name: Stop server
if: always()
run: kill "$(cat server.pid)" || true
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: dast-smoke-logs
path: server.log
retention-days: 7

View File

@@ -17,27 +17,82 @@ jobs:
name: Deploy OmniRoute to VPS
runs-on: ubuntu-latest
steps:
- name: Check VPS SSH reachability from runner
id: reach
env:
# Pass the host via env (never interpolate a secret straight into the
# script body) so /dev/tcp gets a shell variable, not inlined text.
VPS_HOST: ${{ secrets.VPS_HOST }}
run: |
set -uo pipefail
# A GitHub-hosted runner can only deploy when it can actually open a TCP
# connection to the VPS SSH port. The Local VPS lives on a private LAN and
# the Akamai host firewalls :22 to known IPs, so the runner is routinely
# unable to reach it (`dial tcp ***:22: i/o timeout`). Treat "unreachable
# from the runner" as a SKIP — the real deploys are run manually from an
# allowed network via the deploy-vps-local / deploy-vps-akamai skills — so
# an unreachable host no longer red-fails every release/push pipeline.
# When the host IS reachable, the deploy step below still runs in full and
# its health gate surfaces any genuine deploy failure.
if timeout 15 bash -c 'exec 3<>"/dev/tcp/${VPS_HOST}/22"' 2>/dev/null; then
echo "reachable=true" >> "$GITHUB_OUTPUT"
echo "✅ VPS_HOST:22 reachable from the runner — proceeding with deploy."
else
echo "reachable=false" >> "$GITHUB_OUTPUT"
echo "::warning title=Auto-deploy skipped::VPS_HOST:22 is not reachable from this GitHub runner (private LAN / firewalled). Deploy manually with the deploy-vps-local or deploy-vps-akamai skill."
fi
- name: Deploy via SSH
if: steps.reach.outputs.reachable == 'true'
uses: appleboy/ssh-action@v1
continue-on-error: true
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: 22
timeout: 30s
command_timeout: 5m
timeout: 60s
command_timeout: 15m
script: |
echo "=== Updating OmniRoute ==="
npm install -g omniroute@latest 2>&1
INSTALLED_VERSION=$(omniroute --version 2>/dev/null || echo "unknown")
echo "Installed version: $INSTALLED_VERSION"
set -euo pipefail
echo "=== Restarting PM2 ==="
pm2 restart omniroute || pm2 start omniroute --name omniroute -- --port 20128
echo "=== Updating OmniRoute ==="
npm install -g omniroute@latest
INSTALLED_VERSION=$(omniroute --version 2>/dev/null | tr -d '[:space:]' || echo "unknown")
echo "Installed CLI version: $INSTALLED_VERSION"
# Recreate the PM2 process instead of `pm2 restart`. A bare restart
# re-runs whatever script path was saved earlier; after the build-output
# reorg (app/ -> dist/, .next -> .build/next) a process pinned to the old
# app/server-ws.mjs path can no longer start, and the node process dies
# while PM2 still reports "online" — so the box never binds :20128.
# Always launch via the `omniroute` bin so .env is loaded and the dist/
# layout is resolved correctly.
echo "=== (Re)creating PM2 process via bin ==="
pm2 delete omniroute 2>/dev/null || true
pm2 start omniroute --name omniroute -- --port 20128
pm2 save
echo "=== Health Check ==="
sleep 3
curl -sf http://localhost:20128/api/settings > /dev/null && echo "✅ OmniRoute is healthy" || echo "❌ Health check failed"
# Health gate: fail the deploy unless the box actually reports healthy.
# Poll /api/monitoring/health for "status":"healthy" (a deeper signal than
# a static page 200 — it confirms the app booted, not just that a port is
# bound). Boot can take a while after a native-module/build-layout change,
# so poll up to ~3min before giving up.
echo "=== Health Check (gates the deploy) ==="
ok=0
for i in $(seq 1 36); do
BODY=$(curl -sf -m 5 http://localhost:20128/api/monitoring/health 2>/dev/null || true)
if printf '%s' "$BODY" | grep -q '"status":"healthy"'; then
ok=1
echo "✅ /api/monitoring/health -> healthy (attempt $i) — version $INSTALLED_VERSION"
break
fi
echo "… not healthy yet (attempt $i/36), retrying in 5s"
sleep 5
done
if [ "$ok" != "1" ]; then
echo "❌ Health check failed — /api/monitoring/health never reported healthy after ~3min"
echo "--- recent PM2 logs ---"
pm2 logs omniroute --lines 40 --nostream || true
exit 1
fi
echo "=== Deploy complete ==="

View File

@@ -25,9 +25,10 @@ on:
type: boolean
default: false
# Least-privilege default: read-only at the top level; the build and merge jobs that
# push to GHCR grant packages: write themselves (Scorecard TokenPermissions).
permissions:
contents: read
packages: write
jobs:
prepare:
@@ -41,8 +42,9 @@ jobs:
IMAGE_NAME: diegosouzapw/omniroute
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
# Need full tag history for semver comparison when deciding :latest.
fetch-depth: 0
@@ -123,6 +125,9 @@ jobs:
needs: prepare
if: needs.prepare.outputs.skip != 'true'
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
@@ -138,8 +143,9 @@ jobs:
GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
fetch-depth: 0
@@ -176,20 +182,46 @@ jobs:
env:
DOCKER_BUILDKIT_INLINE_CACHE: 1
- name: Export digest
- name: Build and push WEB platform image by digest
id: build-web
uses: docker/build-push-action@v7
with:
context: .
target: runner-web
platforms: ${{ matrix.platform }}
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
tags: |
${{ env.IMAGE_NAME }}
${{ env.GHCR_IMAGE_NAME }}
cache-from: type=gha,scope=docker-web-${{ matrix.arch }}
cache-to: type=gha,scope=docker-web-${{ matrix.arch }},mode=max
no-cache: false
env:
DIGEST: ${{ steps.build.outputs.digest }}
DOCKER_BUILDKIT_INLINE_CACHE: 1
- name: Export digests
env:
DIGEST_BASE: ${{ steps.build.outputs.digest }}
DIGEST_WEB: ${{ steps.build-web.outputs.digest }}
run: |
set -euo pipefail
mkdir -p /tmp/digests
digest="${DIGEST#sha256:}"
touch "/tmp/digests/${digest}"
mkdir -p /tmp/digests/base /tmp/digests/web
touch "/tmp/digests/base/${DIGEST_BASE#sha256:}"
touch "/tmp/digests/web/${DIGEST_WEB#sha256:}"
- name: Upload digest
- name: Upload base digests
uses: actions/upload-artifact@v7
with:
name: digests-${{ matrix.arch }}
path: /tmp/digests/*
name: digests-base-${{ matrix.arch }}
path: /tmp/digests/base/*
if-no-files-found: error
retention-days: 1
- name: Upload web digests
uses: actions/upload-artifact@v7
with:
name: digests-web-${{ matrix.arch }}
path: /tmp/digests/web/*
if-no-files-found: error
retention-days: 1
@@ -200,6 +232,10 @@ jobs:
- build
if: needs.prepare.outputs.skip != 'true'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
env:
IMAGE_NAME: diegosouzapw/omniroute
GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute
@@ -207,8 +243,9 @@ jobs:
PROMOTE_LATEST: ${{ needs.prepare.outputs.promote_latest }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
fetch-depth: 0
@@ -228,60 +265,123 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download digests
- name: Download base digests
uses: actions/download-artifact@v8
with:
pattern: digests-*
path: /tmp/digests
pattern: digests-base-*
path: /tmp/digests/base
merge-multiple: true
- name: Download web digests
uses: actions/download-artifact@v8
with:
pattern: digests-web-*
path: /tmp/digests/web
merge-multiple: true
- name: Create Docker Hub manifest
run: |
set -euo pipefail
tags=(-t "${IMAGE_NAME}:${VERSION}")
if [ "$PROMOTE_LATEST" = "true" ]; then
tags+=(-t "${IMAGE_NAME}:latest")
fi
create_manifest() {
local image="$1" suffix="$2" dir="$3"
local tags=(-t "${image}:${VERSION}${suffix}")
if [ "$PROMOTE_LATEST" = "true" ]; then
tags+=(-t "${image}:latest${suffix}")
fi
local refs=()
while IFS= read -r digest_file; do
refs+=("${image}@sha256:$(basename "$digest_file")")
done < <(find "$dir" -type f | sort)
if [ "${#refs[@]}" -eq 0 ]; then
echo "No image digests in $dir" >&2
exit 1
fi
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
}
refs=()
while IFS= read -r digest_file; do
refs+=("${IMAGE_NAME}@sha256:$(basename "$digest_file")")
done < <(find /tmp/digests -type f | sort)
if [ "${#refs[@]}" -eq 0 ]; then
echo "No image digests were downloaded." >&2
exit 1
fi
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
create_manifest "${IMAGE_NAME}" "" /tmp/digests/base
create_manifest "${IMAGE_NAME}" "-web" /tmp/digests/web
- name: Create GHCR manifest
run: |
set -euo pipefail
tags=(-t "${GHCR_IMAGE_NAME}:${VERSION}")
if [ "$PROMOTE_LATEST" = "true" ]; then
tags+=(-t "${GHCR_IMAGE_NAME}:latest")
fi
create_manifest() {
local image="$1" suffix="$2" dir="$3"
local tags=(-t "${image}:${VERSION}${suffix}")
if [ "$PROMOTE_LATEST" = "true" ]; then
tags+=(-t "${image}:latest${suffix}")
fi
local refs=()
while IFS= read -r digest_file; do
refs+=("${image}@sha256:$(basename "$digest_file")")
done < <(find "$dir" -type f | sort)
if [ "${#refs[@]}" -eq 0 ]; then
echo "No image digests in $dir" >&2
exit 1
fi
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
}
refs=()
while IFS= read -r digest_file; do
refs+=("${GHCR_IMAGE_NAME}@sha256:$(basename "$digest_file")")
done < <(find /tmp/digests -type f | sort)
if [ "${#refs[@]}" -eq 0 ]; then
echo "No image digests were downloaded." >&2
exit 1
fi
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
create_manifest "${GHCR_IMAGE_NAME}" "" /tmp/digests/base
create_manifest "${GHCR_IMAGE_NAME}" "-web" /tmp/digests/web
- name: Inspect image
if: needs.prepare.outputs.version != 'main'
run: |
docker buildx imagetools inspect "${IMAGE_NAME}:${VERSION}"
- name: Generate CycloneDX SBOM (image, advisory)
if: needs.prepare.outputs.version != 'main'
continue-on-error: true
uses: anchore/sbom-action@v0
with:
image: ${{ env.GHCR_IMAGE_NAME }}:${{ env.VERSION }}
format: cyclonedx-json
output-file: sbom-image.cdx.json
artifact-name: sbom-image.cdx.json
# Visibility scan: reports HIGH + CRITICAL into the SARIF (Security tab) but
# never blocks (exit-code 0). The blocking gate below narrows to CRITICAL.
- name: Trivy image scan (SARIF, advisory)
if: needs.prepare.outputs.version != 'main'
continue-on-error: true
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ env.GHCR_IMAGE_NAME }}:${{ env.VERSION }}
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL
exit-code: "0"
# BLOCKING gate (v3.8.27 cycle-end): fail the release on a CRITICAL CVE in the
# published image. Narrowed to severity CRITICAL (HIGH stays visible in the
# SARIF step above, not blocking). ignore-unfixed:true so an unfixable base-image
# CVE with no upstream patch does not red the release (reduces false-blocks);
# a fixable CRITICAL still blocks. Per docs/security/SUPPLY_CHAIN.md. NB: Trivy
# scans against a CVE DB that grows continuously — a newly-disclosed CRITICAL on
# an unchanged base image can red this gate; the fix is to rebuild on a patched
# base, bump the dep, or add a justified .trivyignore entry (see the CVE-variance
# note in docs/security/SUPPLY_CHAIN.md).
- name: Trivy CRITICAL gate (blocking)
if: needs.prepare.outputs.version != 'main'
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ env.GHCR_IMAGE_NAME }}:${{ env.VERSION }}
format: table
severity: CRITICAL
ignore-unfixed: true
exit-code: "1"
- name: Upload Trivy SARIF to Security tab
if: needs.prepare.outputs.version != 'main'
continue-on-error: true
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: trivy-results.sarif
category: trivy-image
- name: Update Docker Hub description
# Only refresh README/description when we actually promote :latest
# (avoids overwriting from main pushes or back-fill builds).

View File

@@ -11,43 +11,56 @@ on:
required: true
type: string
# Least-privilege default: read-only at the top level; each job grants the writes it
# needs (build/release upload assets, publish-npm forwards npm provenance / packages
# to the reusable workflow) — Scorecard TokenPermissions.
permissions:
contents: write
id-token: write
packages: write
contents: read
jobs:
validate:
name: Validate version
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.validate.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- name: Validate version format
id: validate
env:
# Pass workflow context via env (never interpolate ${{ ... }} straight
# into the run: script body) so the shell receives variables, not
# inlined text — zizmor template-injection mitigation. INPUT_VERSION is
# the operator-supplied value and is regex-validated below before use.
EVENT_NAME: ${{ github.event_name }}
INPUT_VERSION: ${{ inputs.version }}
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
if [[ "$EVENT_NAME" == "push" ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
else
VERSION="${{ inputs.version }}"
VERSION="$INPUT_VERSION"
fi
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid version format. Expected: v1.6.8"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "✓ Valid version: $VERSION"
build:
name: Build Electron (${{ matrix.platform }})
needs: validate
runs-on: ${{ matrix.runner }}
permissions:
contents: write # electron-builder may publish artifacts with GH_TOKEN
strategy:
fail-fast: false
matrix:
@@ -71,7 +84,9 @@ jobs:
deb_ext: .deb
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@v6
with:
@@ -79,7 +94,7 @@ jobs:
cache: npm
- name: Cache node_modules
uses: actions/cache@v5
uses: actions/cache@v5.0.5
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
@@ -99,7 +114,7 @@ jobs:
# that cause EPERM errors during Next.js standalone build glob scans.
# Create a clean temp profile directory to avoid this.
mkdir -p "$RUNNER_TEMP/home"
echo "USERPROFILE=$RUNNER_TEMP/home" >> $GITHUB_ENV
echo "USERPROFILE=$RUNNER_TEMP/home" >> "$GITHUB_ENV"
- name: Build Next.js standalone
env:
@@ -109,8 +124,13 @@ jobs:
- name: Sync version in electron/package.json
shell: bash
env:
# Pass the validated version via env (never interpolate ${{ ... }}
# straight into the run: script body) — zizmor template-injection
# mitigation. Already regex-validated (^v[0-9]+\.[0-9]+\.[0-9]+$) in
# the `validate` job, so it cannot carry shell metacharacters.
VERSION: ${{ needs.validate.outputs.version }}
run: |
VERSION="${{ needs.validate.outputs.version }}"
VERSION_NO_V="${VERSION#v}"
node -e "
const fs = require('fs');
@@ -136,9 +156,16 @@ jobs:
- name: Smoke packaged Electron app
if: matrix.platform != 'linux'
# Windows CI: requestSingleInstanceLock() fails due to USERPROFILE
# sanitization needed for the build step. Smoke is best-effort there.
continue-on-error: ${{ matrix.platform == 'windows' }}
# Best-effort smoke on Windows + macos-arm64:
# - Windows: requestSingleInstanceLock() fails due to USERPROFILE
# sanitization needed for the build step.
# - macos-arm64: the headless GitHub arm64 runner crashes Electron's GPU
# process (gpu_process_host exit_code=15 → network service crash →
# "No rendezvous client, terminating process"), so the app can't bind
# 127.0.0.1:20128 in time. The identical bundle is smoke-gated on
# macos-intel + linux, so packaging is still verified per-OS; we don't
# let the arm64 runner's GPU flakiness block the desktop release.
continue-on-error: ${{ matrix.platform == 'windows' || matrix.platform == 'macos-arm64' }}
env:
ELECTRON_SMOKE_TIMEOUT_MS: 60000
ELECTRON_SMOKE_STREAM_LOGS: "1"
@@ -185,10 +212,13 @@ jobs:
name: Create Release
needs: [validate, build]
runs-on: ubuntu-latest
permissions:
contents: write # softprops/action-gh-release creates the GitHub Release
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- name: Download all artifacts
@@ -198,14 +228,20 @@ jobs:
merge-multiple: true
- name: Create source archives
env:
# Pass the validated version via env (never interpolate ${{ ... }}
# straight into the run: script body) — zizmor template-injection
# mitigation. Already regex-validated (^v[0-9]+\.[0-9]+\.[0-9]+$) in
# the `validate` job, so it cannot carry shell metacharacters.
VERSION: ${{ needs.validate.outputs.version }}
run: |
# Create source code archives (excluding dev dependencies and build artifacts)
export TARBALL="OmniRoute-${{ needs.validate.outputs.version }}.source.tar.gz"
export ZIPBALL="OmniRoute-${{ needs.validate.outputs.version }}.source.zip"
export TARBALL="OmniRoute-${VERSION}.source.tar.gz"
export ZIPBALL="OmniRoute-${VERSION}.source.zip"
# Use git archive for clean source export
git archive --format=tar.gz --prefix=OmniRoute-${{ needs.validate.outputs.version }}/ HEAD -o "release-assets/$TARBALL"
git archive --format=zip --prefix=OmniRoute-${{ needs.validate.outputs.version }}/ HEAD -o "release-assets/$ZIPBALL"
git archive --format=tar.gz --prefix="OmniRoute-${VERSION}/" HEAD -o "release-assets/$TARBALL"
git archive --format=zip --prefix="OmniRoute-${VERSION}/" HEAD -o "release-assets/$ZIPBALL"
echo "✓ Created source archives:"
ls -lh "release-assets/$TARBALL" "release-assets/$ZIPBALL"
@@ -235,6 +271,14 @@ jobs:
publish-npm:
name: Publish to npm
needs: [validate, release]
permissions:
# Must be `write`, not `read`: this job calls the reusable npm-publish.yml whose
# `publish` job needs `contents: write` (gh release upload — attach the SBOM, #3874).
# A reusable workflow's job cannot request more permission than the caller grants,
# so a `read` here makes GitHub reject the run at startup (startup_failure).
contents: write
id-token: write # npm provenance (forwarded to the reusable workflow)
packages: write # publish to npm.pkg.github.com
uses: ./.github/workflows/npm-publish.yml
with:
version: ${{ needs.validate.outputs.version }}

View File

@@ -0,0 +1,63 @@
name: Mutation Redundancy (disableBail, on-demand)
# One-off measurement to UNBLOCK R1 (test-redundancy prune). The nightly mutation run
# (nightly-mutation.yml) bails on the first kill, so `killedBy` lists only the FIRST
# killer — 🟠 redundant is understated and 🟢 unique overstated (see the caveat in
# scripts/quality/mutation-radiography.mjs). This workflow re-runs the SAME combo +
# chatCore leaf batches with stryker.disablebail.json (disableBail:true, incremental:false)
# so `killedBy` lists EVERY killer. Feed the uploaded reports to
# `node scripts/quality/mutation-radiography.mjs --candidates mutation-nobail-*/mutation.json`
# to get the accurate R1 prune-candidate list (🔴 empty 🟠 redundant) for human review.
#
# Batches mirror the nightly's leaf decomposition (d/e/f/g/h/i) rather than 2 mega-batches:
# disableBail is MORE expensive than bail (it never stops early), and Stryker only writes
# mutation.json on a SUCCESSFUL finish — a batch cancelled at the cap produces NO data — so
# smaller batches each fit the 300min headroom and run in parallel. auth/accountFallback and
# the security quartet are out of scope: R1 targets the combo/chatCore leaves.
on:
workflow_dispatch:
permissions:
contents: read
jobs:
stryker-nobail:
name: Stryker disableBail (batch ${{ matrix.batch.name }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
batch:
- name: d
mutate: "open-sse/services/combo/comboStructure.ts,open-sse/services/combo/autoStrategy.ts,open-sse/services/combo/validateQuality.ts"
- name: e
mutate: "open-sse/services/combo/shadowRouting.ts,open-sse/services/combo/targetSorters.ts,open-sse/services/combo/comboPredicates.ts,open-sse/services/combo/rrState.ts,open-sse/services/combo/comboData.ts"
- name: f
mutate: "open-sse/services/combo/quotaScoring.ts,open-sse/services/combo/quotaStrategies.ts"
- name: g
mutate: "open-sse/handlers/chatCore/comboContextCache.ts,open-sse/handlers/chatCore/idempotency.ts,open-sse/handlers/chatCore/passthroughHelpers.ts,open-sse/handlers/chatCore/responseHeaders.ts,open-sse/handlers/chatCore/sanitization.ts,open-sse/handlers/chatCore/upstreamTimeouts.ts"
- name: h
mutate: "open-sse/handlers/chatCore/headers.ts,open-sse/handlers/chatCore/logTruncation.ts,open-sse/handlers/chatCore/memoryExtraction.ts,open-sse/handlers/chatCore/nonStreamingSse.ts,open-sse/handlers/chatCore/passthroughToolNames.ts,open-sse/handlers/chatCore/executorHelpers.ts"
- name: i
mutate: "open-sse/handlers/chatCore/telemetryHelpers.ts,open-sse/handlers/chatCore/memorySkillsInjection.ts,open-sse/handlers/chatCore/semanticCache.ts"
timeout-minutes: 300
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Run Stryker (disableBail)
env:
BATCH_MUTATE: ${{ matrix.batch.mutate }}
run: npx stryker run --config-file stryker.disablebail.json --mutate "$BATCH_MUTATE"
- name: Upload mutation report
if: always()
uses: actions/upload-artifact@v7
with:
name: mutation-nobail-${{ matrix.batch.name }}
path: reports/mutation/
if-no-files-found: warn
retention-days: 14

View File

@@ -0,0 +1,102 @@
name: Nightly LLM Security
on:
schedule:
- cron: "53 5 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
promptfoo-guard:
name: promptfoo — injection guard (block mode, no secret)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with: { node-version: "24", cache: npm }
- run: npm ci
- name: Build CLI bundle
env: { JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation }
run: npm run build:cli
- name: Start OmniRoute (block mode)
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
PORT: "20128"
INJECTION_GUARD_MODE: block
run: |
node dist/server.js > server.log 2>&1 &
echo $! > server.pid
for i in $(seq 1 30); do
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo up; break; fi
sleep 2
done
- name: promptfoo guard-validation
run: npx --yes promptfoo@latest eval -c promptfooconfig.yaml --no-cache
env:
OMNIROUTE_URL: http://localhost:20128
OMNIROUTE_API_KEY: not-needed-blocked-before-upstream
- name: Stop server
if: always()
run: kill "$(cat server.pid)" || true
garak:
name: garak probes (skip without provider secret)
runs-on: ubuntu-latest
# NOTE: the `secrets` context is NOT available in a job-level `if:` — referencing
# it there makes GitHub reject the file on push (startup_failure on every push).
# Map the secret into a job-level env and gate each step on a presence check, so
# the job stays green and simply skips the probes when the secret is absent.
env:
PROMPTFOO_PROVIDER_KEY: ${{ secrets.PROMPTFOO_PROVIDER_KEY }}
steps:
- name: Gate on provider secret
id: gate
run: |
if [ -n "$PROMPTFOO_PROVIDER_KEY" ]; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "run=false" >> "$GITHUB_OUTPUT"
echo "::notice::PROMPTFOO_PROVIDER_KEY not set — skipping garak probes (advisory)."
fi
- uses: actions/checkout@v7
with:
persist-credentials: false
if: steps.gate.outputs.run == 'true'
- uses: actions/setup-node@v6
if: steps.gate.outputs.run == 'true'
with: { node-version: "24", cache: npm }
- run: npm ci
if: steps.gate.outputs.run == 'true'
- name: Build CLI bundle
if: steps.gate.outputs.run == 'true'
env: { JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation }
run: npm run build:cli
- name: Start OmniRoute
if: steps.gate.outputs.run == 'true'
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
PORT: "20128"
run: |
node dist/server.js > server.log 2>&1 &
echo $! > server.pid
for i in $(seq 1 30); do
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo up; break; fi
sleep 2
done
- uses: actions/setup-python@v6
if: steps.gate.outputs.run == 'true'
with: { python-version: "3.12" }
- run: pip install garak
if: steps.gate.outputs.run == 'true'
- name: garak limited probes
if: steps.gate.outputs.run == 'true'
env:
OPENAI_API_KEY: ${{ secrets.PROMPTFOO_PROVIDER_KEY }}
OPENAI_BASE_URL: http://localhost:20128/v1
run: garak --model_type openai --model_name gpt-4o-mini --probes promptinject,dan,leakreplay --report_prefix garak-omniroute || true
- name: Stop server
if: always() && steps.gate.outputs.run == 'true'
run: kill "$(cat server.pid)" || true

163
.github/workflows/nightly-mutation.yml vendored Normal file
View File

@@ -0,0 +1,163 @@
name: Nightly Mutation
on:
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
stryker:
name: Stryker mutation (batch ${{ matrix.batch.name }} — advisory)
runs-on: ubuntu-latest
# Mutation testing is expensive. History of the budget:
# - Full 8-module set TIMED OUT at the 180min cap (run 27705123780 = exactly 180min).
# The two god-files chatCore.ts/combo.ts dominated ~2/3 of the mutants and were
# removed from stryker.conf.json `mutate`.
# - The remaining 6 modules in 3 batches: auth.ts and accountFallback.ts are large; the
# a (auth+publicCreds) and b (accountFallback+error) batches still ran near the 180min
# cap (the perTest dry-run over ~130 covering test files is itself costly), so the two
# big modules are now ISOLATED into their own batches (a=auth, b=accountFallback).
# - Onda 3 / Fase 9 T5 re-add: combo.ts was split into 11 leaves; the 8 well-covered
# combo/* leaves are back in `mutate` (covered by the 24 combo-*.test.ts), grouped into
# 2 batches (d=heavy, e=light). After #4204 (D7b) merged, the reset-aware quota pair
# quotaScoring/quotaStrategies was added as batch f. A covering-test audit then added the
# 6 chatCore/* leaves with direct unit coverage as batch g. A follow-up then wrote dedicated
# unit tests for 6 more leaves and added them as batch h. A final follow-up added dedicated
# tests (no mock.module — fetch-override + crafted inputs + temp-DATA_DIR) for telemetryHelpers
# + memorySkillsInjection + semanticCache (its cache-HIT block now has a setCachedResponse
# fixture) as batch i — ALL 15/15 chatCore leaves are now mutated. See
# _mutate_godfiles_excluded_comment in stryker.conf.json.
# 9 PARALLEL batches, each overriding the mutate set via `--mutate` (Stryker 9 CLI:
# `-m, --mutate <comma-list>`; the conf's `mutate[]` remains the local-run default/union).
# - Cold-seeding budget (per-batch `timeout-minutes: ${{ matrix.batch.timeout || 180 }}`):
# a COLD run must COMPLETE once to write stryker-incremental.json (Stryker writes it only on
# a successful finish); a job cancelled at the cap writes nothing, so the next run is cold
# again — an infinite never-seeds loop. actions/cache is also branch-scoped, so each branch
# (incl. release) must seed its OWN cache via a run with enough headroom. Measured cold runs
# Measured cold totals (run 27801802713, extrapolated from the % at the 180/350 cancel point):
# auth ~375min (2301 mutants — EXCEEDS the 360min job max even isolated), accountFallback
# ~358min (1441), the c security quartet ~348min (1163), d ~197min (1316), g=142, h=132, e=66,
# f=45, i=33. The widely-covered modules blow the budget because the tap-runner re-runs every
# covering test file per mutant (a perTest fixed cost over ~138 test files, times thousands of
# mutants). A flat timeout bump cannot rescue auth (>360min max) — so the three over-budget
# batches are SPLIT so each half fits: auth->a1/a2 and accountFallback->b1/b2 by mutation range
# (`file:startLine-endLine`), the c quartet->c1/c2 by module pair. Splitting also seeds each
# sub-batch's own incremental cache, after which nightlies re-test only changed mutants.
# Full coverage every night in parallel; wall-clock = the slowest batch's cold run until seeded.
# Runs at stryker concurrency=4 with per-process DATA_DIR isolation
# (tests/_setup/isolateDataDir.ts) — see _concurrency_comment in stryker.conf.json.
strategy:
fail-fast: false
matrix:
batch:
# Per-batch `timeout` (minutes) tiers the cold-seeding budget by measured cost; batches
# without the key default to 180. Cold-run profiling (run 27801802713) showed the
# widely-covered modules need FAR more than the 180 cap because the tap-runner re-runs every
# covering test file per mutant: auth ~375min (2301 mutants, EXCEEDS the 360min GitHub job
# max even isolated), accountFallback ~358min, the c security quartet ~348min — none fit a
# single job. So auth/accountFallback are split by MUTATION RANGE (`file:startLine-endLine`,
# ~half the mutants each) into a1/a2, b1/b2; the c quartet is split by MODULE pair into
# c1/c2. d (3 combo modules, ~197min) stays whole. Split-heavy batches get 300min headroom
# for their cold seeding run; once each batch completes once and writes
# stryker-incremental.json, later runs re-test only changed mutants and finish far faster.
# g/h (142/132min cold) keep a 240 buffer; e/f/i (33-66min) keep the 180 default.
- name: a1
mutate: "src/sse/services/auth.ts:1-1109"
timeout: 300
- name: a2
mutate: "src/sse/services/auth.ts:1110-2218"
timeout: 300
- name: b1
mutate: "open-sse/services/accountFallback.ts:1-863"
timeout: 300
- name: b2
mutate: "open-sse/services/accountFallback.ts:864-1726"
timeout: 300
- name: c1
mutate: "src/server/authz/routeGuard.ts,src/shared/utils/circuitBreaker.ts"
timeout: 300
- name: c2
mutate: "open-sse/utils/error.ts,open-sse/utils/publicCreds.ts"
timeout: 300
- name: d
mutate: "open-sse/services/combo/comboStructure.ts,open-sse/services/combo/autoStrategy.ts,open-sse/services/combo/validateQuality.ts"
timeout: 300
- name: e
mutate: "open-sse/services/combo/shadowRouting.ts,open-sse/services/combo/targetSorters.ts,open-sse/services/combo/comboPredicates.ts,open-sse/services/combo/rrState.ts,open-sse/services/combo/comboData.ts"
- name: f
mutate: "open-sse/services/combo/quotaScoring.ts,open-sse/services/combo/quotaStrategies.ts"
- name: g
mutate: "open-sse/handlers/chatCore/comboContextCache.ts,open-sse/handlers/chatCore/idempotency.ts,open-sse/handlers/chatCore/passthroughHelpers.ts,open-sse/handlers/chatCore/responseHeaders.ts,open-sse/handlers/chatCore/sanitization.ts,open-sse/handlers/chatCore/upstreamTimeouts.ts"
timeout: 240
- name: h
mutate: "open-sse/handlers/chatCore/headers.ts,open-sse/handlers/chatCore/logTruncation.ts,open-sse/handlers/chatCore/memoryExtraction.ts,open-sse/handlers/chatCore/nonStreamingSse.ts,open-sse/handlers/chatCore/passthroughToolNames.ts,open-sse/handlers/chatCore/executorHelpers.ts"
timeout: 240
- name: i
mutate: "open-sse/handlers/chatCore/telemetryHelpers.ts,open-sse/handlers/chatCore/memorySkillsInjection.ts,open-sse/handlers/chatCore/semanticCache.ts"
# Per-batch budget: split-heavy batches (a1/a2/b1/b2/c1/c2/d) override to 300min, g/h to 240min;
# the rest default to 180min. `matrix.batch.timeout` is null for batches without the key -> `|| 180`.
# NOTE: a1+a2 both mutate auth.ts (disjoint line ranges) and b1+b2 both mutate accountFallback.ts;
# when merging the per-batch mutation.json for radiography/scores, same-file mutants from sibling
# ranges must be UNIONED (scripts/check/check-mutation-ratchet.mjs::measureMutationScores and
# scripts/quality/mutation-radiography.mjs both merge per file).
timeout-minutes: ${{ matrix.batch.timeout || 180 }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Restore Stryker incremental cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: reports/mutation/stryker-incremental.json
key: stryker-incremental-${{ matrix.batch.name }}-${{ github.run_id }}
restore-keys: stryker-incremental-${{ matrix.batch.name }}-
- name: Run Stryker (advisory)
id: stryker
continue-on-error: true
env:
BATCH_MUTATE: ${{ matrix.batch.mutate }}
run: npx stryker run --mutate "$BATCH_MUTATE"
- name: Upload mutation report
if: always()
uses: actions/upload-artifact@v7
with:
name: mutation-report-${{ matrix.batch.name }}
path: reports/mutation/
if-no-files-found: warn
retention-days: 14
# Aggregation gate (T3): each split batch emits a PARTIAL view of a mutated file
# (auth.ts lives in a1+a2, accountFallback in b1+b2), so a PER-BATCH ratchet would
# only ever see half a file vs the whole-file baseline. This job runs AFTER every
# batch, downloads all reports, and ratchets the MERGED per-module scores
# (check-mutation-ratchet UNIONS same-file mutants across reports) against the
# dedicatedGate `mutationScore.*` floors in quality-baseline.json (seeded ~2pt below
# the first full measurement). Blocking: a module dropping below its floor fails the
# run. Missing reports (e.g. an artifact-upload flake) are skipped, never failed.
mutation-ratchet:
name: Mutation score ratchet (blocking)
needs: stryker
if: always()
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
- name: Download all mutation reports
uses: actions/download-artifact@v8
with:
pattern: mutation-report-*
path: reports/all
- name: Ratchet merged per-module mutation scores
run: node scripts/check/check-mutation-ratchet.mjs reports/all/*/mutation.json --ratchet

35
.github/workflows/nightly-property.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Nightly Property Discovery
on:
schedule:
- cron: "0 6 * * *"
workflow_dispatch:
permissions:
contents: read
issues: write
jobs:
property-random-seed:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- name: fast-check random seed (high runs)
id: prop
run: FC_SEED=random FC_NUM_RUNS=2000 npm run test:property
- name: Open issue on failure
if: failure()
uses: actions/github-script@v9
with:
script: |
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: "Nightly property-test failure (Fase 8 B)",
body: "fast-check found a counterexample with a random seed. Check the run logs for the reproducible seed + minimal case, then add it as a fixture.\n\nRun: " + context.serverUrl + "/" + context.repo.owner + "/" + context.repo.repo + "/actions/runs/" + context.runId,
labels: ["quality-gate-finding"],
});

110
.github/workflows/nightly-resilience.yml vendored Normal file
View File

@@ -0,0 +1,110 @@
name: Nightly Resilience
on:
schedule:
- cron: "41 4 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
heap:
name: Heap-growth gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- run: npm run test:heap
chaos:
name: Resilience chaos (fault injection)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- run: npm run test:chaos
k6-soak:
name: k6 load/soak
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Build CLI bundle
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
run: npm run build:cli
- name: Start OmniRoute (background)
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
PORT: "20128"
run: |
node dist/server.js > server.log 2>&1 &
echo $! > server.pid
for i in $(seq 1 30); do
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo "server up"; break; fi
sleep 2
done
- name: Install k6
uses: grafana/setup-k6-action@v1
- name: Run k6 soak
run: k6 run tests/load/k6-soak.js
env:
BASE_URL: http://localhost:20128
SOAK_DURATION: "3m"
SOAK_VUS: "10"
- name: Stop server
if: always()
run: kill "$(cat server.pid)" || true
a11y:
name: A11y axe (nightly, freeze-and-alert)
runs-on: ubuntu-latest
# The Playwright webServer (`start` mode) builds Next via build-next-isolated.mjs and
# boots the standalone server itself (waits on /api/monitoring/health, 15min webServer
# timeout). Unlike the per-PR test-e2e job, this nightly job has no pre-built artifact,
# so it self-builds — hence the generous job timeout. REQUIRE_AXE=1 makes the suite run
# the real axe analysis (the 4 page tests are gated to nightly so per-PR e2e stays fast)
# and makes the meta-test fail loudly if @axe-core/playwright ever goes missing.
timeout-minutes: 30
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
REQUIRE_AXE: "1"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v5.0.5
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: playwright-chromium-${{ runner.os }}-
- run: npx playwright install --with-deps chromium
- name: Run axe a11y suite (self-building webServer)
run: npx playwright test tests/e2e/a11y.spec.ts

View File

@@ -0,0 +1,72 @@
name: Nightly Schemathesis
on:
schedule:
- cron: "23 4 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
schemathesis:
name: Schemathesis — OpenAPI contract fuzz (advisory)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with: { node-version: "24", cache: npm }
- run: npm ci
- name: Build CLI bundle
env: { JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation }
run: npm run build:cli
- name: Start OmniRoute (background)
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
PORT: "20128"
run: |
node dist/server.js > server.log 2>&1 &
echo $! > server.pid
for i in $(seq 1 30); do
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo "server up"; break; fi
sleep 2
done
- uses: actions/setup-python@v6
with: { python-version: "3.12" }
- name: Install schemathesis
run: pip install schemathesis
- name: Schemathesis contract fuzz (advisory)
# Advisory gate: never fails the job. `continue-on-error` covers a crash of the
# step itself; `|| true` covers schemathesis exiting non-zero when it finds spec
# violations / upstream 500s — both are expected here (most /v1 endpoints proxy an
# upstream that has no provider configured in CI). The point of the nightly is to
# PROVE the contract is fuzzable and surface regressions, not to gate the build.
continue-on-error: true
run: |
schemathesis run docs/reference/openapi.yaml \
--url http://localhost:20128 \
--max-examples 20 \
--workers 4 \
--checks all \
--max-response-time 30 \
--request-timeout 30 \
--suppress-health-check all \
--report junit \
--report-junit-path schemathesis-report/junit.xml \
--no-color \
|| true
- name: Stop server
if: always()
run: kill "$(cat server.pid)" || true
- name: Upload schemathesis report
if: always()
uses: actions/upload-artifact@v7
with:
name: schemathesis-report
path: |
schemathesis-report/
server.log
if-no-files-found: warn
retention-days: 14

View File

@@ -37,10 +37,11 @@ on:
NPM_TOKEN:
required: true
# Least-privilege default: read-only at the top level; each publish job grants the
# id-token (npm provenance) / packages (GitHub Packages) writes it needs (Scorecard
# TokenPermissions).
permissions:
contents: read
id-token: write
packages: write
env:
NPM_PUBLISH_NODE_VERSION: "24"
@@ -48,10 +49,15 @@ env:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write # gh release upload (attach SBOM to the GitHub Release)
id-token: write # npm provenance
packages: write # publish to npm.pkg.github.com
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
# Need full tag history to compare against highest semver when
# deciding whether this release should claim dist-tag `latest`.
fetch-depth: 0
@@ -141,6 +147,25 @@ jobs:
if: steps.resolve.outputs.skip != 'true'
run: npm run check:pack-artifact
- name: Generate CycloneDX SBOM (npm)
if: steps.resolve.outputs.skip != 'true'
run: npx @cyclonedx/cyclonedx-npm --ignore-npm-errors --output-format JSON --output-file sbom-npm.cdx.json
- name: Upload SBOM (npm) as workflow artifact
if: steps.resolve.outputs.skip != 'true'
uses: actions/upload-artifact@v7
with:
name: sbom-npm
path: sbom-npm.cdx.json
if-no-files-found: error
- name: Attach SBOM to GitHub Release
if: steps.resolve.outputs.skip != 'true' && github.event_name == 'release'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: gh release upload "$TAG" sbom-npm.cdx.json --clobber
- name: Publish to npm
if: steps.resolve.outputs.skip != 'true'
env:
@@ -152,7 +177,7 @@ jobs:
# Always pass --tag explicitly. Defense in depth: even if VERSION is
# accidentally an older release, `npm publish --tag historic` will
# NOT promote it to `@latest`.
npm publish --access public --tag "$TAG"
npm publish --provenance --access public --tag "$TAG"
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG)"
- name: Publish to GitHub Packages
@@ -172,9 +197,14 @@ jobs:
publish-opencode-plugin:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # npm provenance
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -208,5 +238,5 @@ jobs:
echo "⚠️ ${PKG_NAME}@${PKG_VERSION} is already published on npm — skipping."
exit 0
fi
npm publish --access public --ignore-scripts
npm publish --provenance --access public --ignore-scripts
echo "✅ Published ${PKG_NAME}@${PKG_VERSION}"

View File

@@ -32,7 +32,9 @@ jobs:
matrix:
node: ["22", "24"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
@@ -47,7 +49,9 @@ jobs:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "22"

View File

@@ -32,7 +32,9 @@ jobs:
matrix:
node: ["20", "22", "24"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
@@ -46,7 +48,9 @@ jobs:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "20"

84
.github/workflows/quality.yml vendored Normal file
View File

@@ -0,0 +1,84 @@
name: Quality Gates
on:
pull_request:
branches: ["release/**"]
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
CI_NODE_VERSION: "24"
jobs:
fast-gates:
name: Fast Quality Gates
runs-on: ubuntu-latest
# tsx gates (known-symbols, route-guard-membership) import modules that open
# SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- run: npm run check:provider-consistency
- run: npm run check:fetch-targets
- run: npm run check:openapi-routes
- run: npm run check:docs-symbols
- run: npm run check:deps
- run: npm run check:file-size
- run: npm run check:error-helper
- run: npm run check:migration-numbering
- run: npm run check:public-creds
- run: npm run check:db-rules
- run: npm run check:known-symbols
- run: npm run check:route-guard-membership
- run: npm run check:test-discovery
- run: npm run check:any-budget:t11
- name: Typecheck (core)
run: npm run typecheck:core
# TIA: build the impact map at runtime (gitignored, ~21MB) and run only the
# unit tests impacted by this PR's changed files. Fail-safe runs the FULL
# unit suite on hub/unmapped changes — TIA accelerates, never replaces, the net.
#
# BLOCKING (flipped 2026-06-17). The pre-existing release unit test-debt that kept
# this advisory was cleared: #4030 (16 Zod/registry reds, lossless restore) and
# #4063 (the last red — the LiveWS boot test — root-caused as a real event-loop
# stall in the WS sidecar, fixed + relocated to the integration suite). A full
# ci.yml run on release/v3.8.28 then showed all 8 unit shards green, so PR->release
# now blocks on unit-test regressions in the impacted set (typecheck:core already
# blocked above). Fail-safe still runs the FULL unit suite on hub/unmapped changes.
- name: Impacted unit tests (TIA, fail-safe full; blocking)
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
git fetch --no-tags origin "$GITHUB_BASE_REF" || true
node scripts/quality/build-test-impact-map.mjs
SEL="$(node scripts/quality/select-impacted-tests.mjs)"
if [ -z "$SEL" ]; then echo "No source/test changes — skipping unit tests"; exit 0; fi
# CI runners are 4-vCPU; run at --test-concurrency=4 (matching the ci.yml unit
# job) rather than test:unit's local-tuned concurrency=20. Oversubscribing the
# runner makes timing-sensitive tests (db-backup, upstream-timeout, ...) flake,
# which must not happen on a blocking gate. DATA_DIR isolation keeps the parallel
# run race-free regardless of concurrency.
if echo "$SEL" | grep -q "__RUN_ALL__"; then
echo "Fail-safe: running FULL unit suite (CI concurrency)"; npm run test:unit:ci; exit $?
fi
echo "Running impacted tests:"; echo "$SEL"
mapfile -t FILES <<< "$SEL"
node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${FILES[@]}"

40
.github/workflows/scorecard.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: OpenSSF Scorecard
on:
branch_protection_rule:
schedule:
- cron: "27 7 * * 1"
push:
branches: ["main"]
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
# security-events: write removed — Scorecard findings are advisory and no longer
# uploaded to the code-scanning Security tab (they are supply-chain/posture scores,
# not code vulnerabilities, and drowned out real CodeQL alerts). The run still
# produces the OpenSSF badge (publish_results) and a downloadable SARIF artifact.
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Run analysis
uses: ossf/scorecard-action@v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: true
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: SARIF file
path: results.sarif
retention-days: 5

29
.github/workflows/semgrep.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: semgrep
on:
pull_request:
branches: ["main", "release/**"]
push:
branches: ["main"]
permissions:
contents: read
jobs:
semgrep:
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run semgrep (advisory)
continue-on-error: true
run: |
semgrep scan --config p/owasp-top-ten --config p/secrets \
--sarif --output semgrep.sarif --metrics off || true
python -c "import json; d=json.load(open('semgrep.sarif')); print('semgrepFindings=%d' % len(d['runs'][0]['results']))" || echo "semgrepFindings=SKIP"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: semgrep-sarif
path: semgrep.sarif
retention-days: 14

69
.github/workflows/wiki-sync.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: Wiki Sync
# Keeps the GitHub wiki in sync with docs/ on every release that lands on main.
# The wiki has no native generator and historically drifts (it sat at "212+ providers /
# 14 strategies / 37 MCP tools" while code was at 226 / 15 / 87, and new docs like
# SUPPLY_CHAIN never appeared). This runs scripts/docs/sync-wiki.mjs, which:
# - ADDS any docs/ page missing from the wiki (curated; internal reports excluded),
# - syncs the four cover-page counts on Home.md.
# It does NOT overwrite existing wiki pages by default: several docs sources still carry
# stale counts (e.g. ARCHITECTURE.md says "177 providers" while the wiki cover is 226),
# so blind overwrite would regress the wiki. Full content parity (--update-existing) is
# gated on regenerating those sources — see docs/ops/DOCUMENTATION_AUDIT_REPORT.md.
on:
push:
branches: [main]
paths:
- "docs/**"
- "README.md"
- "AGENTS.md"
- "src/shared/constants/routingStrategies.ts"
- "config/i18n.json"
- "open-sse/mcp-server/server.ts"
- "scripts/docs/sync-wiki.mjs"
workflow_dispatch:
permissions:
contents: write
concurrency:
group: wiki-sync
cancel-in-progress: false
jobs:
sync-wiki:
name: Sync wiki with docs
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "24"
- name: Clone wiki
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
git clone "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.wiki.git" wiki
- name: Sync wiki (add missing pages + cover counts)
run: node scripts/docs/sync-wiki.mjs --wiki-dir wiki
- name: Commit & push if changed
run: |
cd wiki
if [ -n "$(git status --porcelain)" ]; then
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "docs(wiki): auto-sync pages + cover counts with docs"
git push
echo "Wiki updated."
else
echo "Wiki already in sync — nothing to push."
fi

100
.gitignore vendored
View File

@@ -9,6 +9,24 @@ _ideia/
_mono_repo/
_references/
_tasks/
.agents/**
.claude/**
.gemini/**
.config/**
.data/**
.logs/**
.tests/**
.coverage/**
coverage/
.dist/**
.next/**
.build/**
.out/**
# Stryker mutation testing — ephemeral sandbox + generated reports (never commit)
.stryker-tmp/
reports/mutation/
stryker-output-*.json
# Memory Bank and Cursor rules (local-only AI agent context)
@@ -31,38 +49,19 @@ docs/new-features/
# dependencies
node_modules/
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
.data/
.next-playwright/
.agents/
workflows/
.omo/
# devbox
.devbox/
# testing
coverage/
coverage**
# next.js
.next/
/out/
# production
/build
/app
cloud/*
# misc
# Also ignore a root node_modules SYMLINK (worktree setups symlink it from the main
# checkout). The trailing-slash pattern above only matches a directory, so without this
# a symlink named node_modules could be staged by `git add -A` and committed.
/node_modules
*.map
.DS_Store
*.pem
# Obsidian sync plugin — committed for community distribution
!obsidian-plugin/
obsidian-plugin/node_modules/
# Serena AI assistant config (local-only tool, not project code)
.serena/
# debug
npm-debug.log*
@@ -73,6 +72,9 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# Provider API keys (never commit)
*.api-key
.nvidia-api-key
# vercel
.vercel
@@ -124,10 +126,13 @@ app.__qa_backup/
.app-build-backup-*/
backup/
# Production standalone build (created by scripts/prepublish.mjs)
# Conflicts with Next.js App Router detection in dev (root app/ shadows src/app/)
# npm publish still includes it via package.json "files" field
/app/
# Build intermediates (.build/) and shippable standalone (dist/).
# These are fully reproducible from source; never committed.
# Layer 1: Next.js now writes to .build/next (was .next); assembled bundle → dist/
# (Previously /app/ was the standalone output; renamed to /dist/ in Layer 1.)
/.build/
/dist/
/.next/
# Electron
electron/dist-electron/
@@ -142,9 +147,6 @@ vscode-extension/
*.sqlite-wal
*.sqlite-journal
# Compiled npm-package build artifact (not source, should not be in git)
/app
# IDEA
.idea/
@@ -160,6 +162,9 @@ typescript
# Superpowers plans/specs (internal tooling, not project code)
docs/superpowers/
# TIA test-impact map — generated at runtime in CI (build-test-impact-map.mjs), never committed (~21MB)
config/quality/test-impact-map.json
# GitNexus local index
.gitnexus
.worktrees
@@ -203,3 +208,22 @@ scripts/i18n/_pending-keys.json
# PR Reviews and local feedback files
pr_reviews*.json
#hidden local data directories (never commit)
.local-data/
.data-dev/
/.junie/
# internal setup prompts with personal credentials — never commit
CODEX-SETUP-PROMPT.md
# Quality ratchet — métricas efêmeras (baseline commitado em config/quality/; métricas não)
config/quality/quality-metrics.json
# Runtime logs (diretório local, nunca versionado)
/logs/
-home-diegosouzapw-dev-automações-bots-yt-downloader-20260504 .txt
-home-diegosouzapw-dev-automações-bots-yt-downloader-20260410 .txt
docs/prompts/AGENT-OWNERSHIP-PROTOCOL.omniroute.md
docs/prompts/AGENT-OWNERSHIP-PROTOCOL.md
docs/prompts/AGENT-OWNERSHIP-PROTOCOL.omniroute-mim.md
docs/prompts/AGENT-OWNERSHIP-PROTOCOL.omniroute-mid.md
omniroute.md

76
.gitleaks.toml Normal file
View File

@@ -0,0 +1,76 @@
# .gitleaks.toml — Configuração do gitleaks para OmniRoute
# Task 7.18 — PLANO-QUALITY-GATES-FASE7.md
#
# Estende as regras padrão do gitleaks com allowlists específicas do projeto.
#
# INSTRUÇÕES para allowlists:
# Findings legítimos (fixtures de teste, creds OAuth públicas já cobertas pelo
# check-public-creds.mjs, valores de exemplo em docs) devem ser registrados abaixo
# em [[allowlist]] com um comentário explicativo obrigatório.
#
# NÃO adicione uma entrada de allowlist sem justificativa. Cada entrada é revisada
# a cada release (stale-enforcement). Regra: se o finding é um valor real que o
# sistema usa em produção, é um verdadeiro positivo — não allowliste, corrija.
#
# Referência: docs/security/PUBLIC_CREDS.md (credenciais OAuth públicas conhecidas)
# CLAUDE.md Hard Rule #11 (resolvePublicCred obrigatório)
# Herdar TODAS as regras padrão do gitleaks. ATENÇÃO: um config customizado
# SEM [extend].useDefault = true (e sem [[rules]] próprias) resulta em ZERO
# regras — o gitleaks SUBSTITUI o ruleset padrão pelo arquivo, não o estende
# automaticamente. Sem esta seção, `gitleaks --config .gitleaks.toml` nunca
# detecta nada (todo finding vira 0), tornando o gate inerte. Com useDefault,
# a allowlist abaixo é aplicada POR CIMA das ~170 regras padrão.
[extend]
useDefault = true
# Para desabilitar uma regra específica, usar:
# [[rules]]
# id = "rule-id"
# [rules.allowlist]
# description = "..."
# ---------------------------------------------------------------------------
# Allowlist global do projeto
# Entradas aqui são ignoradas em TODAS as varreduras.
# ---------------------------------------------------------------------------
[allowlist]
description = "OmniRoute project-level allowlist — fixtures, test vectors, public OAuth creds"
# Paths a ignorar completamente (node_modules, builds, etc.)
paths = [
'''node_modules''',
'''\.next''',
'''dist''',
'''\.git''',
'''coverage''',
'''\.nyc_output''',
]
# Commits específicos a ignorar (ex: commit que introduziu fixtures de teste)
# commits = []
# Regexes de stopwords — linhas que contêm estes padrões são ignoradas.
# Usar apenas para falsos positivos comprovados com justificativa abaixo.
# stopwords = []
# Regexes de targets (paths de arquivos) que podem ser allowlistados por regra.
# Ver [[allowlist]] por-regra abaixo para granularidade.
# ---------------------------------------------------------------------------
# Allowlist por-regra (adicionar conforme necessário durante o stale review)
# ---------------------------------------------------------------------------
#
# Exemplo (REMOVER / SUBSTITUIR por entradas reais quando necessário):
#
# [[rules]]
# # Allowlistar fixtures de teste que contêm tokens OAuth de exemplo/inválidos
# # Adicionado: 2026-06-13 | Revisar em: v3.9.0 | Justificativa: valores não-reais de teste
# id = "github-fine-grained-pat"
# [rules.allowlist]
# description = "Test fixture PATs — valores sintéticos, não funcionais"
# paths = [
# '''tests/fixtures/''',
# '''tests/unit/''',
# ]
#

View File

@@ -1,31 +1,13 @@
# #!/usr/bin/env sh
# if ! command -v npx >/dev/null 2>&1; then
# echo "⚠️ npx not found in PATH — skipping pre-commit hooks"
# echo " Run 'npm run lint && npm run check:any-budget:t11' manually before pushing."
# exit 0
# fi
#!/usr/bin/env sh
if ! command -v npx >/dev/null 2>&1; then
echo "⚠️ npx not found in PATH — skipping pre-commit hooks"
echo " Run 'npm run lint && npm run check:any-budget:t11' manually before pushing."
exit 0
fi
# npx lint-staged
# node scripts/check/check-docs-sync.mjs
# npm run check:any-budget:t11
# # Strict env-doc sync (FASE 2)
# node scripts/check/check-env-doc-sync.mjs
# # CLI i18n consistency check — all t() keys must exist in en.json (FASE 8.3)
# node scripts/check/check-cli-i18n.mjs
# # i18n docs drift advisory (FASE 5) — warn-only on pre-commit; CI enforces strict.
# node scripts/i18n/check-translation-drift.mjs --warn || \
# echo "⚠️ i18n drift detected. Run 'npm run i18n:run' to update locale mirrors."
# # i18n UI coverage advisory (FASE 6) — pre-commit warns; CI enforces strict.
# node scripts/i18n/check-ui-keys-coverage.mjs --threshold=80 || \
# echo "⚠️ UI i18n coverage below 80% for at least one locale."
# # OpenAPI coverage check — fails if coverage < 99% (FASE 08 content audit)
# node scripts/check/check-openapi-coverage.mjs
# # OpenAPI security tier consistency check — fails if x-loopback-only / x-always-protected
# # annotations diverge from routeGuard.ts compile-time constants (FASE 08 content audit)
# node scripts/check/check-openapi-security-tiers.mjs
# Cheap, deterministic local gates (re-enabled). Slower checks (i18n drift,
# openapi coverage/security-tiers, env-doc sync) run in CI to keep commits fast.
npx lint-staged
node scripts/check/check-docs-sync.mjs
npm run check:any-budget:t11
node scripts/check/check-tracked-artifacts.mjs

View File

@@ -1,8 +1,12 @@
#!/usr/bin/env sh
#if ! command -v npm >/dev/null 2>&1; then
# echo "⚠️ npm not found in PATH — skipping pre-push hooks"
# echo " Run 'npm test' manually before pushing."
# exit 0
#fi
# .husky/pre-push — fast deterministic gates (<10s total)
# Intentionally excludes test:unit (slow; covered by CI pre-push remote run).
# Activated: 2026-06-13 (6A.12 — replaced commented-out test:unit stub)
#npm run test:unit
if ! command -v npm >/dev/null 2>&1; then
echo "⚠️ npm not found in PATH — skipping pre-push hooks"
echo " Run 'npm run check:any-budget:t11 && npm run check:tracked-artifacts' manually before pushing."
exit 0
fi
npm run check:any-budget:t11 && npm run check:tracked-artifacts

View File

@@ -1,219 +0,0 @@
{
"sources": {
"CLAUDE.md": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"locales": {
"pt-BR": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "301b997e936b1d476e6094042666b96b33f42a4372fd2d9ccf904aacbfd7f023",
"updated_at": "2026-05-22T20:13:39.165Z"
},
"az": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "c26844ec50b2abfbb002767fb5c6c9c3982ec65789435bbc134bf1a7b50bf84a",
"updated_at": "2026-05-22T20:13:39.166Z"
},
"bn": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "0b1416ec3b5af8cc6415d12a9ff12ce078acca4a7bb52a7422ebde3b6ae22832",
"updated_at": "2026-05-22T20:13:39.166Z"
},
"ar": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "bc747c5ea2a387dd37dea9dc1337d9734f2ec0da38a7134573d9743e3f4d2aef",
"updated_at": "2026-05-22T20:13:39.167Z"
},
"cs": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "e37dce45820b53be9d3d5ead08cadb8d13e4c77597b3009bb0be4e485917f5c6",
"updated_at": "2026-05-22T20:13:39.167Z"
},
"da": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "645c382bebe0931eaad6747e33667fd094b92b3f4042549b953db798de6b1f46",
"updated_at": "2026-05-22T20:13:39.168Z"
},
"de": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "b6558f82eb67676baf4d78946a8d1e1b808f7763e5ebaf9f57948cbd1641dc0f",
"updated_at": "2026-05-22T20:13:39.168Z"
},
"es": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "afb2a2dfa74a74c3105b0ac70181d33f5eefd201efe7edb6aba0740864639fe5",
"updated_at": "2026-05-22T20:13:39.169Z"
},
"fa": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "58966769aeb71d23c55cf4aeb452f2bbe32036df8fba1d7596f3e861897d507b",
"updated_at": "2026-05-22T20:13:39.169Z"
},
"fi": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "04cf72a3b370edcb3d54361c6cc250b3af227ba183376827006c7998839740aa",
"updated_at": "2026-05-22T20:13:39.169Z"
},
"fr": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "8dca6de87c88e04396f1139ac8b6328d188defaa5abc99eeb4026f53de772ba1",
"updated_at": "2026-05-22T20:13:39.170Z"
},
"he": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "0308a2c8c5a6b6261474a76c160f3c0658c75f56a633a57bd7300b83ccb32c9d",
"updated_at": "2026-05-22T20:13:39.170Z"
},
"hi": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "a3e902c3b1812a41ccb14c9f16d2cf2e42b8c005a5d557043e582d48eda024c4",
"updated_at": "2026-05-22T20:13:39.170Z"
},
"gu": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "1bc2419db39c7c7960b065222a3e3990e9e53c4d3427ec02422e3e506aa53733",
"updated_at": "2026-05-22T20:13:39.171Z"
},
"hu": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "ec30f54810f8b5b84e3ac8bf7bc8d05acc8616d71801b672ec02f0bc225cf607",
"updated_at": "2026-05-22T20:13:39.171Z"
},
"id": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "aebd8fdad7ec4857aa1b958cda37f59681846525207d71d98d729775031afb94",
"updated_at": "2026-05-22T20:13:39.172Z"
},
"in": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "ac70a1282877f8c4f792e9235f7a6fbce3cbec4a424ce4947074bb210fe12d67",
"updated_at": "2026-05-22T20:13:39.172Z"
},
"it": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "260f10dd121441d08e15a8d511789d8f8f7a788995305ab5e8dc6d0c1a3db06e",
"updated_at": "2026-05-22T20:13:39.173Z"
},
"ja": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "83871c13e99d13d7928409b2aef182f55fe36fbb2425f8fa4bd0df0466afed28",
"updated_at": "2026-05-22T20:13:39.173Z"
},
"mr": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "0c35c5261dd3b6c6b729d14653f95cc112f3ed9829d2d41b61e5a960abc68556",
"updated_at": "2026-05-22T20:13:39.173Z"
},
"ko": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "4ea4079b37d90e90e9a32d0da290e04e38bcd6426512b50ca7be7139354bc329",
"updated_at": "2026-05-22T20:13:39.174Z"
},
"ms": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "0e8031cd987766a69afc7aaf7c3560a57736d37e7238ddccfb848d6fbeb3f212",
"updated_at": "2026-05-22T20:13:39.174Z"
},
"nl": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "27e554c3a9b86db1f04539fcac18d3e75e6599d4de9f5ac0cf789a136b5737d4",
"updated_at": "2026-05-22T20:13:39.174Z"
},
"phi": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "f955af44cc0e8e87a2a7b12313f0dfad9aae1a50d7855e74c8155df3601279ad",
"updated_at": "2026-05-22T20:13:39.175Z"
},
"no": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "de4e3d940ae485c85da13db4471fcc68926ec53e660d92633f01293c5db0a04d",
"updated_at": "2026-05-22T20:13:39.175Z"
},
"pl": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "de5be8961e5184431404d0021e31c3ef0857f7439fbd1c71ddc0c6828bed86bb",
"updated_at": "2026-05-22T20:13:39.175Z"
},
"ro": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "19d98b22bc77c1870b749fad6e62a41c6ef53f3cad303d1c471fba4bf7012797",
"updated_at": "2026-05-22T20:13:39.175Z"
},
"ru": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "25e0c70ec25bd24b073b9d693299c3c3d32d66a410569362719e9c9ecacd759d",
"updated_at": "2026-05-22T20:13:39.176Z"
},
"pt": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "0d31647b21af967d8d4e9ecbcfc4901a66db377a6853d7b59296a2d73f0cab12",
"updated_at": "2026-05-22T20:13:39.176Z"
},
"sk": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "9d5d0ce1c51da4959d1c306969bff0bf36d3a3492ccdbdbd82e4f4d951f32c8b",
"updated_at": "2026-05-22T20:13:39.176Z"
},
"sw": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "11cd3dc7a605eebddf4a34c13de66c713d67adb32e4242962a65c71e5a034c7e",
"updated_at": "2026-05-22T20:13:39.177Z"
},
"sv": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "93b679feb6415315ab96e08298217e66be84265a277e267ecefd25b2cef04026",
"updated_at": "2026-05-22T20:13:39.177Z"
},
"ta": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "55f3df128513a1b4a8cc3f58eb84769814589d80e1c8b8f03ab0635750a76d2d",
"updated_at": "2026-05-22T20:13:39.177Z"
},
"te": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "81019604dd1fb38dfda55c1b8cf5cf8243347be08221a5e1eb11e8c76e095fab",
"updated_at": "2026-05-22T20:13:39.178Z"
},
"tr": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "b67efd07b179be016b388dff4b8979597c0a7b2db602629cef539dbec14913ab",
"updated_at": "2026-05-22T20:13:39.178Z"
},
"th": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "a793a12dc0cba5e3641cd544f63ab23e9cfe1b80568a08770a81ffd890287eda",
"updated_at": "2026-05-22T20:13:39.179Z"
},
"ur": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "6c03049aee6d85b6fd4a3bf9b89a6204e461d3f05e9cb767e36c80af796452df",
"updated_at": "2026-05-22T20:13:39.179Z"
},
"vi": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "4d35c6898f98913a02551dcfbf4d3748b2461e2d8206d292f292acacf0fc7133",
"updated_at": "2026-05-22T20:13:39.180Z"
},
"zh-CN": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "d3f574c244d157c1fe1b9fc86192d1565df2dc6343ac9ef63c0fd3f91d97f492",
"updated_at": "2026-05-22T20:13:39.180Z"
},
"uk-UA": {
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
"target_hash": "ffe6357e38d56ba2595e8fb8e21db4ae91bf03d10fca50a2b722e0d0bb6c01d9",
"updated_at": "2026-05-22T20:13:39.181Z"
}
}
},
"docs/architecture/ARCHITECTURE.md": {
"source_hash": "f9b4f17a1b0331fb5500768943438411ed88a38278381680caacc37c90bfb869",
"locales": {
"pt-BR": {
"source_hash": "f9b4f17a1b0331fb5500768943438411ed88a38278381680caacc37c90bfb869",
"target_hash": "e320c6172a88a0f3b698ba1a2e9e938424ece66625765395837bcf55cc29454e",
"updated_at": "2026-05-22T20:13:39.183Z"
}
}
}
}
}

14
.markdownlint.json Normal file
View File

@@ -0,0 +1,14 @@
{
"_comment": "Advisory markdown lint for docs/ + root *.md. Rules that conflict with the existing doc style (heavy inline HTML, long lines, centered headings) are disabled so the gate stays signal-not-noise. Run: npm run lint:md",
"default": true,
"MD013": false,
"MD033": false,
"MD041": false,
"MD024": { "siblings_only": true },
"MD026": false,
"MD036": false,
"MD040": false,
"MD029": false,
"MD007": { "indent": 2 },
"MD046": false
}

17
.mcp.json.example Normal file
View File

@@ -0,0 +1,17 @@
{
"$comment_purpose": "OPT-IN agent-lsp / LSP-in-the-loop (Quality Gates Fase 7 Task 15). Copy this file to `.mcp.json` to enable. It exposes a TypeScript language server to coding agents (Claude Code, etc.) so they get diagnostics / hover / go-to-definition / blast-radius BEFORE writing code — turning 'invented symbol' review-catches into impossible-at-edit-time. Pairs with `npm run typecheck:core` as a compile-before-claim check.",
"$comment_safety": "Shipped as `.example` (NOT `.mcp.json`) on purpose so it never auto-loads an unvetted server into everyone's session. Pick an MCP<->LSP bridge you trust and have verified locally, then drop in its package + args below. A broken MCP entry only logs a connection error; it does not break agent sessions. The underlying language server is `typescript-language-server` (npm, mature) — install via `npm i -g typescript-language-server typescript` or rely on npx.",
"mcpServers": {
"typescript-lsp": {
"command": "npx",
"args": [
"-y",
"<your-mcp-lsp-bridge>",
"--lsp",
"typescript-language-server",
"--stdio"
],
"$note": "Replace <your-mcp-lsp-bridge> with the concrete MCP<->LSP adapter you chose. It must speak MCP on stdio and proxy to `typescript-language-server --stdio`. Scope it to this repo's tsconfig (open-sse/tsconfig.json / tsconfig.json) for accurate diagnostics."
}
}
}

View File

@@ -9,6 +9,16 @@ app/vscode-extension/
**/db.json
# Source code (pre-built app/ is published instead)
#
# NOTE (#3578 / #3821-review): package.json "files" is the source of truth for what
# ships. It now allowlists the backend source closure the MCP server needs at runtime
# (open-sse/, src/lib, src/server, ...) and OVERRIDES the broad src/ + open-sse/ excludes
# below — npm honors files[] over .npmignore for inclusion. These lines are kept only as
# intent/back-stop: if files[] is ever trimmed back to specific paths, they must NOT be
# allowed to re-hide the MCP closure (that would silently reintroduce the --mcp
# ERR_MODULE_NOT_FOUND #3578 fixed). The closure gate in
# tests/unit/mcp-published-files-closure-3578.test.ts asserts the real `npm pack` output
# in both directions (closure present + zero test files), catching such a regression.
src/
open-sse/
docs/
@@ -18,6 +28,17 @@ images/
logs/
scripts/
# Co-located tests must never ship even when their parent dir is allowlisted by files[].
# (Primary guard is the "!**/*.test.*" negations in package.json files[]; this is defense
# in depth for any nested dir the allowlist pulls in.)
**/__tests__/
**/*.test.ts
**/*.test.tsx
**/*.test.js
**/*.test.mjs
**/*.spec.ts
**/*.spec.tsx
# Config/dev files
*.md
!README.md

View File

@@ -1,31 +0,0 @@
rules:
- id: cli-no-sqlite-direct
patterns:
- pattern: new Database(...)
paths:
include:
- "/bin/**"
exclude:
- "/bin/cli/sqlite.mjs"
message: >
Direct SQLite access in bin/ is banned. Use src/lib/db/* helpers or
withRuntime() from bin/cli/runtime.mjs. See CLAUDE.md hard rule #5 and
bin/cli/CONVENTIONS.md.
languages: [js]
severity: ERROR
- id: cli-no-raw-sql
patterns:
- pattern: $DB.prepare("INSERT INTO ...")
- pattern: $DB.prepare("DELETE FROM ...")
- pattern: $DB.prepare("UPDATE $TABLE SET ...")
paths:
include:
- "/bin/**"
exclude:
- "/bin/cli/sqlite.mjs"
message: >
Raw SQL in bin/ is banned. Use src/lib/db/* helpers. See CLAUDE.md
hard rule #5 and bin/cli/CONVENTIONS.md.
languages: [js]
severity: ERROR

22
.size-limit.json Normal file
View File

@@ -0,0 +1,22 @@
[
{
"name": "CLI entry (omniroute.mjs)",
"path": "bin/omniroute.mjs",
"limit": "15 KB"
},
{
"name": "MCP server entry (mcp-server.mjs)",
"path": "bin/mcp-server.mjs",
"limit": "5 KB"
},
{
"name": "Node runtime support (nodeRuntimeSupport.mjs)",
"path": "bin/nodeRuntimeSupport.mjs",
"limit": "8 KB"
},
{
"name": "Reset password entry (reset-password.mjs)",
"path": "bin/reset-password.mjs",
"limit": "6 KB"
}
]

15
.vale.ini Normal file
View File

@@ -0,0 +1,15 @@
# Advisory prose lint for OmniRoute docs (Vale — https://vale.sh).
# Warning-first: surfaces vague language, passive voice, and terminology drift without
# blocking CI. The Microsoft style is pulled by the CI step (vale sync / vale-action).
# Project terms live in the OmniRoute vocabulary so they never read as misspellings.
StylesPath = .vale/styles
MinAlertLevel = warning
Packages = Microsoft
Vocab = OmniRoute
[*.md]
BasedOnStyles = Vale, Microsoft
# Code-heavy reference docs and auto-generated catalogs are noisy under prose rules.
[docs/reference/PROVIDER_REFERENCE.md]
BasedOnStyles = Vale

View File

@@ -0,0 +1,50 @@
OmniRoute
combo
combos
[Cc]ombo
[Aa]uto-[Cc]ombo
MCP
A2A
ACP
RTK
Caveman
Troglodita
Qdrant
FTS5
SQLite
LowDB
OAuth
PKCE
SSE
JSON-RPC
LKGP
P2C
Antigravity
Codex
Kiro
Qoder
Pollinations
LongCat
Cerebras
DeepSeek
Groq
Cline
OpenCode
Termux
Electron
Fumadocs
Notion
Obsidian
WebDAV
IPv4
IPv6
SOCKS5
loopback
backoff
changelog
i18n
locale
locales
roadmap
[Ww]ildcard
upstream

31
.vscode/launch.json vendored
View File

@@ -1,31 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Dev Server",
"type": "node",
"request": "launch",
"runtimeExecutable": "${env:HOME}/.nvm/versions/node/v22.22.2/bin/node",
"program": "${workspaceFolder}/scripts/run-next.mjs",
"args": ["dev"],
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**", "node_modules/**"]
},
{
"name": "Debug Prod Server",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/scripts/run-next.mjs",
"args": ["start"],
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**", "node_modules/**"]
},
{
"name": "Attach to Running Server",
"type": "node",
"request": "attach",
"port": 9229,
"skipFiles": ["<node_internals>/**", "node_modules/**"]
}
]
}

112
.vscode/settings.json vendored
View File

@@ -19,32 +19,116 @@
}
},
"git.ignoreLimitWarning": true,
// ─── Git: não adicionar os ~44 repos aninhados (9 worktrees + _references/* +
// _mono_repo/* + _ideia/_tasks/.agents) ao Source Control. Era a causa do
// "validando muito": um git status + watcher por repo. Só o repo raiz fica. ───
"git.autoRepositoryDetection": false,
"git.repositoryScanMaxDepth": 0,
"git.detectSubmodules": false,
"git.autofetch": false,
"git.autorefresh": true,
// ─── Performance: não seguir o symlink de node_modules criado pelas worktrees ───
// As worktrees em .worktrees/ apontam node_modules para o checkout principal;
// sem isto a busca atravessa o symlink mesmo com node_modules excluído.
"search.followSymlinks": false,
// ─── TypeScript server (monorepo grande: ~6,6k arquivos rastreados, Next.js 16) ───
"typescript.tsserver.maxTsServerMemory": 4096,
"typescript.disableAutomaticTypeAcquisition": true,
"typescript.tsc.autoDetect": "off",
"npm.autoDetect": "off",
// O tsserver tem o próprio watcher (independente de files.watcherExclude).
// excludeDirectories evita que ele vigie as árvores pesadas.
"typescript.tsserver.watchOptions": {
"excludeDirectories": [
"**/node_modules",
"**/.next",
"**/.build",
"**/dist",
"**/coverage",
"**/.worktrees"
]
},
// Para esconder os diretórios gerados da árvore do Explorer, descomente:
// "files.exclude": {
// "**/.worktrees": true,
// "**/coverage": true,
// "**/dist": true,
// "**/.build": true,
// "**/.next": true,
// "**/_references": true,
// "**/_mono_repo": true,
// "**/electron": true,
// "**/node_modules": true,
// "**/.next": true,
// "**/coverage": true,
// "**/omniroute-*.tgz": true,
// "**/_tasks": true
// "**/_tasks": true,
// "**/omniroute-*.tgz": true
// },
"files.watcherExclude": {
"**/_references/**": true,
"**/_mono_repo/**": true,
"**/electron/**": true,
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/**": true,
"**/.next/**": true,
"**/.build/**": true,
"**/dist/**": true,
"**/build/**": true,
"**/out/**": true,
"**/coverage/**": true,
"**/_tasks/**": true
"**/.coverage/**": true,
"**/.nyc_output/**": true,
"**/.cache/**": true,
"**/.turbo/**": true,
"**/.swc/**": true,
"**/.stryker-tmp/**": true,
"**/stryker-output-*/**": true,
"**/playwright-report/**": true,
"**/test-results/**": true,
"**/.worktrees/**": true,
"**/.claude/worktrees/**": true,
"**/electron/**": true,
"**/_references/**": true,
"**/_mono_repo/**": true,
"**/_tasks/**": true,
"**/logs/**": true,
"**/*.tgz": true,
"**/*.tsbuildinfo": true,
"**/OmniRoute-*/**": true,
"**/*-merge-*/**": true,
"**/*-worktree-*/**": true,
"**/*-issues-*/**": true,
"**/*-reorg*/**": true
},
"search.exclude": {
"**/_references": true,
"**/_mono_repo": true,
"**/electron": true,
"**/node_modules": true,
"**/.next": true,
"**/.build": true,
"**/dist": true,
"**/build": true,
"**/out": true,
"**/coverage": true,
"**/_tasks": true
"**/.coverage": true,
"**/.nyc_output": true,
"**/.cache": true,
"**/.turbo": true,
"**/.swc": true,
"**/.stryker-tmp": true,
"**/stryker-output-*": true,
"**/playwright-report": true,
"**/test-results": true,
"**/.worktrees": true,
"**/.claude/worktrees": true,
"**/electron": true,
"**/_references": true,
"**/_mono_repo": true,
"**/_tasks": true,
"**/logs": true,
"**/*.tgz": true,
"**/*.tsbuildinfo": true,
"**/package-lock.json": true,
"**/OmniRoute-*": true,
"**/*-merge-*": true,
"**/*-worktree-*": true,
"**/*-issues-*": true,
"**/*-reorg*": true
}
}

104
.zizmor.yml Normal file
View File

@@ -0,0 +1,104 @@
# .zizmor.yml — zizmor security audit configuration
# https://github.com/woodruffw/zizmor
#
# zizmor audits GitHub Actions workflows for security anti-patterns:
# • Unpinned third-party actions (supply-chain risk)
# • Script injection via ${{ github.* }} in run: steps
# • pull_request_target misuse (allows untrusted code to reach secrets)
# • Excessive permissions / overly broad GITHUB_TOKEN scopes
# • Cache poisoning via actions/cache with unguarded keys
# • … 20+ additional audits
#
# MOTIVATION (2026-03 incident): the trivy-action/LiteLLM supply-chain attack
# exploited exactly the kind of pull_request_target misconfiguration that zizmor
# detects statically. OmniRoute's npm/Docker/Electron publish workflows are high-
# value targets that warrant proactive audit.
#
# BASELINE STRATEGY: this file starts with an EMPTY ignores list. As zizmor is
# first run against the existing workflows, findings will be reviewed:
# • True positives → fix the workflow (preferred)
# • Accepted risk → add an entry below with a justification comment
# This enforces the same stale-enforcement discipline as other allowlists in
# this project: every ignore requires human sign-off and is subject to review
# at each release cycle.
#
# RATCHET: zizmorFindings metric in quality-baseline.json (direction: down)
# starts advisory; current baseline is frozen at first green run; new findings
# must be remediated or explicitly ignored here.
# ── Global settings ──────────────────────────────────────────────────────────
# Uncomment to pin to a specific minimum severity level.
# min-severity: low # low | medium | high | critical (default: low = all)
# ── Per-rule ignores ─────────────────────────────────────────────────────────
# zizmor ≥1.0 replaced the old top-level `ignores:` list with a `rules:` map
# keyed by audit id. Per-rule config lives under `rules.<audit-id>.ignore`,
# where each entry is `filename` or `filename:line` (line optional, column
# further-optional). See: https://docs.zizmor.sh/configuration/
#
# Format:
# rules:
# <zizmor-audit-id>: # e.g. "unpinned-uses", "template-injection"
# ignore:
# - <workflow-filename> # ignore this audit across the file
# - <workflow-filename>:<line> # or scope to a specific line
#
# Example (do not uncomment without real justification):
#
# rules:
# unpinned-uses:
# # actions/checkout@v6 is pinned at the major-version tag intentionally:
# # GitHub-managed first-party action; SHA pinning buys little against a
# # GitHub-side compromise and adds significant maintenance burden.
# ignore:
# - ci.yml
# Every entry below is an explicit, justified ignore (same stale-review
# discipline as the other allowlists in this project). New ignores require a
# per-rule entry with a justification comment.
rules:
dangerous-triggers:
# deploy-vps.yml uses `on: workflow_run` (after "Publish to Docker Hub").
# zizmor flags workflow_run as "almost always used insecurely", but this one
# is guarded and not exploitable:
# • The deploy job is gated on `github.event.workflow_run.conclusion ==
# 'success'` (deploy-vps.yml job `if:`, L15), so it only runs after a
# successful, trusted upstream run — never on a forked/PR-triggered
# failure.
# • It does NOT checkout or execute untrusted code: the deploy runs over SSH
# using repository secrets; there is no `actions/checkout` of an attacker
# ref in the privileged context.
# Accepted risk, re-review at the next release cycle (added 2026-06-15).
ignore:
- deploy-vps.yml
cache-poisoning:
# zizmor's cache-poisoning audit flags `actions/setup-node` (cache: npm) and
# `actions/cache` steps that populate a cache which a later artifact-publishing
# job could consume — the attack is: a fork PR seeds a poisoned cache that a
# trusted publish run then restores and ships. None of the four entries below
# are exploitable in that way:
#
# • electron-release.yml / npm-publish.yml — PUBLISH workflows that trigger
# ONLY on trusted events: `push: tags v*` + `workflow_dispatch` (electron)
# and `release: [released]` + `workflow_dispatch` + `workflow_call`
# (npm). They NEVER run on a `pull_request` from a fork, so an untrusted
# ref can never populate the npm/node_modules cache they restore. The
# cache key is `hashFiles('package-lock.json')` / `runner.os-node-…`, a
# first-party, lockfile-pinned key on trusted events.
#
# • opencode-plugin-ci.yml / opencode-provider-ci.yml — CI-only workflows
# (lint/test of the two opencode packages). They DO run on `pull_request`,
# but (a) they publish NO artifacts — there is no downstream publish job
# that restores their cache, and (b) GitHub Actions cache is branch-scoped:
# a fork-PR run writes to a PR-isolated cache that base-branch / release
# runs cannot read. The `cache: npm` here only speeds up `npm ci` within
# the same ephemeral CI run.
#
# Accepted risk (first-party caching on trusted/isolated events), re-review at
# the next release cycle (added 2026-06-16).
ignore:
- electron-release.yml
- npm-publish.yml
- opencode-plugin-ci.yml
- opencode-provider-ci.yml

View File

@@ -1,26 +1,67 @@
# @omniroute/opencode-plugin
First-class OpenCode plugin for the [OmniRoute AI Gateway](https://github.com/diegosouzapw/OmniRoute). Pulls a live model catalog from `/v1/models` (including `-low`/`-medium`/`-high`/`-thinking` variants as first-class IDs), aggregates combos via `/api/combos` using a least-common-denominator capability/limit join, sanitizes Gemini tool schemas in flight, and supports multiple side-by-side OmniRoute instances out of the box.
> **Recommended way to use OmniRoute with OpenCode.** Pulls a live model catalog from `/v1/models` (including `-low`/`-medium`/`-high`/`-thinking` variants as first-class IDs), aggregates combos via `/api/combos` using a least-common-denominator capability/limit join, sanitizes Gemini tool schemas in flight, and supports multiple side-by-side OmniRoute instances out of the box.
## Why this and not `@omniroute/opencode-provider`?
`@omniroute/opencode-provider` is the legacy config-generator package — it writes a frozen `provider.omniroute` block into `opencode.json` with a **hardcoded list of 8 models** ([`OMNIROUTE_DEFAULT_OPENCODE_MODELS`](https://github.com/diegosouzapw/OmniRoute/blob/main/%40omniroute/opencode-provider/src/index.ts#L48-L56)). It works on the CLI but in the **OpenCode Desktop / Web** builds (Tauri / Electron) the runtime re-runs the model picker and the static block surfaces only a few of those — and they drift behind the live OmniRoute catalog.
This plugin solves that by:
- Fetching `/v1/models` and `/api/combos` **at OpenCode startup, in Node.js** — no CORS, no WebView restrictions
- Emitting the provider block **dynamically** in the plugin's `config`/`provider` hook — so `opencode.json` only needs the plugin entry, not a static `provider.omniroute`
- Re-fetching on a configurable TTL (default 5 min), so new models / combo changes in the OmniRoute UI appear without restarting OpenCode
- Computing `limit.context` for combos as `min(member.context_length)` from the live catalog (no more `null` values that cause 4K-token truncation)
- **Auto-pickup of `interleaved` capability** for thinking models (merged via PR #3138)
**If you only have the legacy `opencode-provider` block in your `opencode.json`, replace it with a single plugin entry.** No other config changes required — the same `auth.json` API key works.
## Install
Once published to npm:
The plugin ships **pre-built inside the `omniroute` npm package** since v3.8.23.
If you have OmniRoute installed, the plugin is already on disk:
```sh
npm install @omniroute/opencode-plugin
# 1. One command — copy the plugin into OpenCode and update opencode.json
omniroute setup opencode --auth
# 2. Follow the interactive prompt to enter your OmniRoute API key
# 3. Restart OpenCode — /models lists the full live catalog
```
Until then (or for local development), reference the built artifact directly. Either extract the package into your OpenCode plugins dir and point at the extracted `dist/index.js`:
The `--auth` flag runs `opencode auth login --provider omniroute` automatically.
Use `--base-url` to point at a non-default OmniRoute address:
```sh
omniroute setup opencode --base-url https://or.example.com --auth
```
### What it does
1. Locates the bundled plugin inside the omniroute installation
2. Copies `dist/` + `package.json` to `~/.config/opencode/plugins/omniroute/`
3. Writes/updates `opencode.json` with the plugin entry (idempotent, replaces legacy entries)
4. (With `--auth`) runs `opencode auth login` so the API key is stored
Re-run any time to update the plugin or change the base URL. Older entries for
`@omniroute/opencode-provider` or the legacy `opencode-omniroute-auth` package are
automatically cleaned up.
### Manual install (without omniroute CLI)
If you cannot run `omniroute setup opencode` (local dev, CI, air-gapped), reference
the built artifact directly:
```sh
# from inside the OmniRoute repo
cd @omniroute/opencode-plugin && npm run build && npm pack
# then extract into ~/.config/opencode/plugins/omniroute-opencode-plugin/
```
And add the entry to `opencode.json` manually (see Quick Start below).
Peer dep: `@opencode-ai/plugin` (managed by your OpenCode install).
## Quick start (single instance)
## Quick start (single instance, manual)
```jsonc
// opencode.json
@@ -28,7 +69,7 @@ Peer dep: `@opencode-ai/plugin` (managed by your OpenCode install).
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@omniroute/opencode-plugin",
"./plugins/omniroute-opencode-plugin/dist/index.js",
{
"providerId": "omniroute",
"baseURL": "https://or.example.com",

View File

@@ -26,9 +26,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"cpu": [
"ppc64"
],
@@ -43,9 +43,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"cpu": [
"arm"
],
@@ -60,9 +60,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"cpu": [
"arm64"
],
@@ -77,9 +77,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"cpu": [
"x64"
],
@@ -94,9 +94,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"cpu": [
"arm64"
],
@@ -111,9 +111,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"cpu": [
"x64"
],
@@ -128,9 +128,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"cpu": [
"arm64"
],
@@ -145,9 +145,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"cpu": [
"x64"
],
@@ -162,9 +162,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"cpu": [
"arm"
],
@@ -179,9 +179,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"cpu": [
"arm64"
],
@@ -196,9 +196,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"cpu": [
"ia32"
],
@@ -213,9 +213,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"cpu": [
"loong64"
],
@@ -230,9 +230,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"cpu": [
"mips64el"
],
@@ -247,9 +247,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"cpu": [
"ppc64"
],
@@ -264,9 +264,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"cpu": [
"riscv64"
],
@@ -281,9 +281,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"cpu": [
"s390x"
],
@@ -298,9 +298,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"cpu": [
"x64"
],
@@ -315,9 +315,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"cpu": [
"arm64"
],
@@ -332,9 +332,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"cpu": [
"x64"
],
@@ -349,9 +349,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"cpu": [
"arm64"
],
@@ -366,9 +366,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"cpu": [
"x64"
],
@@ -383,9 +383,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"cpu": [
"arm64"
],
@@ -400,9 +400,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"cpu": [
"x64"
],
@@ -417,9 +417,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"cpu": [
"arm64"
],
@@ -434,9 +434,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"cpu": [
"ia32"
],
@@ -451,9 +451,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"cpu": [
"x64"
],
@@ -1165,9 +1165,9 @@
}
},
"node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -1178,32 +1178,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.7",
"@esbuild/android-arm": "0.27.7",
"@esbuild/android-arm64": "0.27.7",
"@esbuild/android-x64": "0.27.7",
"@esbuild/darwin-arm64": "0.27.7",
"@esbuild/darwin-x64": "0.27.7",
"@esbuild/freebsd-arm64": "0.27.7",
"@esbuild/freebsd-x64": "0.27.7",
"@esbuild/linux-arm": "0.27.7",
"@esbuild/linux-arm64": "0.27.7",
"@esbuild/linux-ia32": "0.27.7",
"@esbuild/linux-loong64": "0.27.7",
"@esbuild/linux-mips64el": "0.27.7",
"@esbuild/linux-ppc64": "0.27.7",
"@esbuild/linux-riscv64": "0.27.7",
"@esbuild/linux-s390x": "0.27.7",
"@esbuild/linux-x64": "0.27.7",
"@esbuild/netbsd-arm64": "0.27.7",
"@esbuild/netbsd-x64": "0.27.7",
"@esbuild/openbsd-arm64": "0.27.7",
"@esbuild/openbsd-x64": "0.27.7",
"@esbuild/openharmony-arm64": "0.27.7",
"@esbuild/sunos-x64": "0.27.7",
"@esbuild/win32-arm64": "0.27.7",
"@esbuild/win32-ia32": "0.27.7",
"@esbuild/win32-x64": "0.27.7"
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
}
},
"node_modules/fast-check": {
@@ -1843,490 +1843,6 @@
"fsevents": "~2.3.3"
}
},
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",

View File

@@ -3,19 +3,16 @@
"version": "0.1.0",
"description": "OpenCode plugin for the OmniRoute AI Gateway. Drives dynamic model discovery, /connect auth flow, and multi-instance OmniRoute providers via the official @opencode-ai/plugin contract.",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./runtime": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
@@ -26,7 +23,7 @@
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"keywords": [
@@ -69,5 +66,8 @@
"tsup": "^8.5.1",
"tsx": "^4.22.3",
"typescript": "^5.9.3"
},
"overrides": {
"esbuild": "^0.28.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,74 @@
/**
* Structured logger for the OmniRoute plugin.
*
* Levels: error < warn < info < debug
* Default: warn (matches current console.warn behavior)
* Set via features.logLevel in plugin options.
*/
export type LogLevel = "error" | "warn" | "info" | "debug";
const LEVEL_ORDER: Record<LogLevel, number> = {
error: 0,
warn: 1,
info: 2,
debug: 3,
};
const TAG = "[omniroute-plugin]";
function shouldLog(current: LogLevel, target: LogLevel): boolean {
return LEVEL_ORDER[current] >= LEVEL_ORDER[target];
}
let _level: LogLevel = "warn";
export function setLogLevel(level: LogLevel): void {
_level = level;
}
export function getLogLevel(): LogLevel {
return _level;
}
function fmt(level: LogLevel, msg: string, tag?: string): string {
const prefix = tag ? `${TAG}${tag}` : TAG;
return `${prefix} [${level.toUpperCase()}] ${msg}`;
}
export const logger = {
error(msg: string, ...args: unknown[]): void {
if (shouldLog(_level, "error")) console.error(fmt("error", msg), ...args);
},
warn(msg: string, ...args: unknown[]): void {
if (shouldLog(_level, "warn")) console.warn(fmt("warn", msg), ...args);
},
info(msg: string, ...args: unknown[]): void {
if (shouldLog(_level, "info")) console.warn(fmt("info", msg), ...args);
},
debug(msg: string, ...args: unknown[]): void {
if (shouldLog(_level, "debug")) console.warn(fmt("debug", msg), ...args);
},
/** Always emit regardless of level (for critical init breadcrumbs). */
always(msg: string, ...args: unknown[]): void {
console.warn(TAG, msg, ...args);
},
// ── Tagged child loggers ──────────────────────────────────────────────
child(tag: string) {
return {
error: (msg: string, ...args: unknown[]) =>
shouldLog(_level, "error") &&
console.error(fmt("error", msg, tag), ...args),
warn: (msg: string, ...args: unknown[]) =>
shouldLog(_level, "warn") &&
console.warn(fmt("warn", msg, tag), ...args),
info: (msg: string, ...args: unknown[]) =>
shouldLog(_level, "info") &&
console.warn(fmt("info", msg, tag), ...args),
debug: (msg: string, ...args: unknown[]) =>
shouldLog(_level, "debug") &&
console.warn(fmt("debug", msg, tag), ...args),
};
},
};

View File

@@ -0,0 +1,301 @@
/**
* Universal model naming template for the OmniRoute plugin.
*
* Naming pipeline:
* [tag] <provider-label><separator><display-name><suffix>
*
* [Free] <provider> - <name> · <budget> ← free model
* Auto: <variant> (<N>p) ← auto combo
* Combo: <name> ← DB combo
* <provider> - <name> ← regular model
*/
// ── Constants ────────────────────────────────────────────────────────────
/** Separator between provider label and model display name. */
export const PROVIDER_TAG_SEPARATOR = " - ";
/** Threshold beyond which providerDisplayName is abbreviated. */
const PROVIDER_LABEL_MAX_CHARS = 12;
/** Aliases longer than this get title-case instead of UPPER. */
const ALIAS_UPPER_MAX_CHARS = 5;
// ── Auto Combo Types ─────────────────────────────────────────────────────
export type AutoVariant =
| "coding"
| "fast"
| "cheap"
| "offline"
| "smart"
| "lkgp";
export const AUTO_VARIANTS: AutoVariant[] = [
"coding",
"fast",
"cheap",
"offline",
"smart",
"lkgp",
];
export const AUTO_VARIANT_DESCRIPTIONS: Record<
AutoVariant | "default",
string
> = {
default: "Best provider via scoring",
coding: "Quality-first for code tasks",
fast: "Latency-optimized routing",
cheap: "Cost-optimized routing",
offline: "Offline-friendly providers",
smart: "Quality-first with exploration",
lkgp: "Last-Known-Good-Provider routing",
};
// ── Free Model Types ─────────────────────────────────────────────────────
export type FreeModelFreeType =
| "recurring-daily"
| "recurring-monthly"
| "recurring-credit"
| "one-time-initial"
| "keyless"
| "discontinued";
// ── Provider Label ────────────────────────────────────────────────────────
/**
* Title-case a long, lowercase-looking alias.
* `antigravity` → `Antigravity`
*/
function titleCaseAlias(alias: string): string {
if (alias.length === 0) return alias;
return alias.charAt(0).toUpperCase() + alias.slice(1).toLowerCase();
}
/**
* Pick the short label for an upstream provider.
*
* Rules:
* 1. Trim `providerDisplayName`. If ≤12 chars → use verbatim.
* 2. Alias ≤5 chars → UPPER(alias). Alias >5 → titleCase.
* 3. Neither → undefined.
*/
export function shortProviderLabel(
enrichment:
| { providerDisplayName?: string; providerAlias?: string }
| undefined,
): string | undefined {
if (!enrichment) return undefined;
const raw =
typeof enrichment.providerDisplayName === "string"
? enrichment.providerDisplayName.trim()
: "";
if (raw.length > 0 && raw.length <= PROVIDER_LABEL_MAX_CHARS) return raw;
const alias =
typeof enrichment.providerAlias === "string"
? enrichment.providerAlias.trim()
: "";
if (alias.length > 0) {
return alias.length <= ALIAS_UPPER_MAX_CHARS
? alias.toUpperCase()
: titleCaseAlias(alias);
}
// Long displayName with no alias to fall back on: keep the long label
// rather than dropping the provider prefix entirely.
return raw.length > 0 ? raw : undefined;
}
// ── Free Label ────────────────────────────────────────────────────────────
/**
* Normalise display name so free-tier models get a consistent `[Free] ` prefix.
*
* "GPT-4.1 (Free)" → "[Free] GPT-4.1"
* "DeepSeek V4 Flash Free" → "[Free] DeepSeek V4 Flash"
* "Claude Opus 4.7" → "Claude Opus 4.7" (unchanged)
*/
export function normaliseFreeLabel(name: string): string {
// Bounded whitespace quantifiers ({0,8}/{1,8}) avoid the polynomial-ReDoS
// backtracking that unbounded \s* before an anchored \s*$ would allow on
// attacker-influenced display names. 8 covers any realistic label spacing.
const cleaned = name
.replace(/\s{0,8}\(free\)\s{0,8}$/i, "")
.replace(/[\s-]{1,8}free\s{0,8}$/i, "")
.trim();
const wasFree = cleaned.length < name.trim().length;
if (!wasFree) return name;
return `[Free] ${cleaned}`;
}
// ── Free Budget Formatting ────────────────────────────────────────────────
function fmtTokens(n: number): string {
if (n >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, "") + "B";
if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "K";
return String(n);
}
/**
* Format a free model budget into a short human-readable suffix.
*
* recurring-daily → "25M tokens/day"
* recurring-monthly → "25M tokens/month"
* recurring-credit → "10M credits"
* one-time-initial → "1M credits (one-time)"
* keyless → "(keyless)"
* discontinued → "(discontinued)"
*/
export function formatFreeBudget(params: {
freeType: FreeModelFreeType;
monthlyTokens?: number;
creditTokens?: number;
}): string {
const { freeType, monthlyTokens = 0, creditTokens = 0 } = params;
switch (freeType) {
case "recurring-daily":
return `${fmtTokens(monthlyTokens)} tokens/day`;
case "recurring-monthly":
return `${fmtTokens(monthlyTokens)} tokens/month`;
case "recurring-credit":
return `${fmtTokens(creditTokens)} credits`;
case "one-time-initial":
return `${fmtTokens(creditTokens)} credits (one-time)`;
case "keyless":
return "(keyless)";
case "discontinued":
return "(discontinued)";
default:
return "";
}
}
// ── Auto Combo Naming ─────────────────────────────────────────────────────
/**
* Format auto combo display name.
*
* "Auto: Coding (4p)"
* "Auto: Default (6p)"
* "Auto" (no candidate count when unknown)
*/
export function formatAutoComboName(
variant: AutoVariant | undefined,
candidateCount?: number,
): string {
const label = variant
? variant.charAt(0).toUpperCase() + variant.slice(1)
: "Default";
const count =
typeof candidateCount === "number" && candidateCount > 0
? ` (${candidateCount}p)`
: "";
return `Auto: ${label}${count}`;
}
/**
* Build the model ID for an auto combo entry.
* "auto/coding", "auto/fast", "auto" (default).
*/
export function autoComboModelId(variant: AutoVariant | undefined): string {
return variant ? `auto/${variant}` : "auto";
}
// ── Universal Display Name Builder ────────────────────────────────────────
export interface ModelDisplayNameParams {
/** Raw model ID (e.g. "cc/claude-sonnet-4-6"). */
rawId: string;
/** Enrichment display name (e.g. "Claude Sonnet 4.6"). */
enrichmentName?: string;
/** Provider tag enrichment. */
providerAlias?: string;
/** Human-readable upstream provider label. */
providerDisplayName?: string;
/** Whether model is free tier. */
isFree?: boolean;
/** Free model budget info. */
freeType?: FreeModelFreeType;
/** Monthly token budget (for recurring free models). */
monthlyTokens?: number;
/** Credit token budget (for credit-based free models). */
creditTokens?: number;
/** Whether this is a combo entry (skip provider tag). */
isCombo?: boolean;
/** Whether this is an auto combo entry. */
isAutoCombo?: boolean;
/** Auto combo variant. */
autoVariant?: AutoVariant;
/** Auto combo candidate count. */
autoCandidateCount?: number;
}
/**
* Build the final display name following the universal template.
*
* Priority:
* 1. Auto combo → "Auto: <variant> (<N>p)"
* 2. DB combo → "Combo: <name>"
* 3. Free + enrichment + provider tag → "[Free] <label> - <name> · <budget>"
* 4. Free + enrichment → "[Free] <name> · <budget>"
* 5. Free + raw → "[Free] <rawId> · <budget>"
* 6. Enrichment + provider tag → "<label> - <name>"
* 7. Enrichment only → "<name>"
* 8. Raw fallback → normaliseFreeLabel(rawId)
*/
export function buildModelDisplayName(params: ModelDisplayNameParams): string {
// Auto combos
if (params.isAutoCombo) {
return formatAutoComboName(params.autoVariant, params.autoCandidateCount);
}
// Determine base name — strip any existing free suffix first
const rawBase =
params.enrichmentName && params.enrichmentName.trim().length > 0
? params.enrichmentName
: params.rawId;
const cleanedBase = rawBase
.replace(/\s*\(free\)\s*$/i, "")
.replace(/[\s-]+free\s*$/i, "")
.trim();
const wasFree = cleanedBase.length < rawBase.trim().length;
const isFree = !!params.isFree || wasFree;
let baseName = cleanedBase;
// Provider tag (skip for combos)
if (!params.isCombo) {
const label = shortProviderLabel({
providerDisplayName: params.providerDisplayName,
providerAlias: params.providerAlias,
});
if (label) {
const prefix = `${label}${PROVIDER_TAG_SEPARATOR}`;
if (!baseName.startsWith(prefix)) {
baseName = `${prefix}${baseName}`;
}
}
}
// Prepend [Free] if applicable (AFTER provider tag for correct ordering)
if (isFree) {
baseName = `[Free] ${baseName}`;
}
// Free budget suffix
if (isFree && params.freeType) {
const budget = formatFreeBudget({
freeType: params.freeType,
monthlyTokens: params.monthlyTokens,
creditTokens: params.creditTokens,
});
if (budget) {
baseName = `${baseName} · ${budget}`;
}
}
return baseName;
}

View File

@@ -0,0 +1,74 @@
/**
* TDD regression — auto combos must never advertise `limit.context: 0`.
*
* opencode's overflow guard (packages/opencode/src/session/overflow.ts)
* short-circuits when `model.limit.context === 0`:
*
* if (input.model.limit.context === 0) return false // never overflow
*
* so a zero context silently DISABLES opencode's smart auto-compaction for
* auto combos. The session then grows unbounded until OmniRoute's
* server-side purifyHistory() destructively drops old messages — the
* "coding agent keeps forgetting things" bug.
*
* Fix under test: mapAutoComboToStaticEntry consumes the context_length /
* max_output_tokens now served by GET /api/combos/auto, and falls back to a
* safe positive default (128000 / 8192) for older servers that do not send
* the fields yet.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { mapAutoComboToStaticEntry } from "../src/index.ts";
import type { OmniRouteRawAutoCombo } from "../src/index.ts";
test("uses server-provided context_length and max_output_tokens", () => {
const raw = {
id: "auto/coding",
name: "Auto Coding",
variant: "coding",
candidateCount: 5,
context_length: 1048576,
max_output_tokens: 65536,
} as OmniRouteRawAutoCombo;
const entry = mapAutoComboToStaticEntry(raw);
assert.equal(entry.limit?.context, 1048576);
assert.equal(entry.limit?.output, 65536);
});
test("falls back to a safe positive default when the server omits limits (old servers)", () => {
const raw = {
id: "auto",
name: "Auto",
candidateCount: 3,
} as OmniRouteRawAutoCombo;
const entry = mapAutoComboToStaticEntry(raw);
assert.ok(
typeof entry.limit?.context === "number" && entry.limit.context > 0,
`context must be a positive number (never 0 — zero disables opencode auto-compaction), got ${entry.limit?.context}`
);
assert.ok(
typeof entry.limit?.output === "number" && entry.limit.output > 0,
`output must be a positive number, got ${entry.limit?.output}`
);
});
test("ignores non-positive server values and keeps the safe fallback", () => {
const raw = {
id: "auto/fast",
name: "Auto Fast",
variant: "fast",
candidateCount: 2,
context_length: 0,
max_output_tokens: -1,
} as OmniRouteRawAutoCombo;
const entry = mapAutoComboToStaticEntry(raw);
assert.ok(
typeof entry.limit?.context === "number" && entry.limit.context > 0,
"zero/negative server values must not propagate"
);
assert.ok(typeof entry.limit?.output === "number" && entry.limit.output > 0);
});

View File

@@ -447,13 +447,13 @@ test("models() returns combo entries merged into the map", async () => {
// 3 raw models + 1 combo = 4 entries
assert.equal(Object.keys(out).length, 4);
assert.ok(out["claude-primary"]);
assert.ok(out["claude-secondary"]);
assert.ok(out["gemini-3-flash"]);
assert.ok(out["combo/claude-tier"]);
assert.ok(out["omniroute/claude-primary"]);
assert.ok(out["omniroute/claude-secondary"]);
assert.ok(out["omniroute/gemini-3-flash"]);
assert.ok(out["omniroute/claude-tier"]);
const combo = out["combo/claude-tier"];
assert.equal(combo.name, "Combo: Claude Tier");
const combo = out["omniroute/claude-tier"];
assert.equal(combo.name, "Claude Tier");
assert.equal(combo.providerID, "omniroute");
// LCD over claude-primary (200k, reasoning) + claude-secondary (100k, no reasoning)
assert.equal(combo.limit.context, 100_000);
@@ -478,11 +478,11 @@ test("models(): combo with unknown member ids degrades to all-false LCD posture"
{ fetcher: modelsFetcher, combosFetcher }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
assert.ok(out["combo/phantom-combo"]);
assert.ok(out["omniroute/phantom-combo"]);
// With zero resolvable members, LCD = all-false (defensive posture).
assert.equal(out["combo/phantom-combo"].capabilities.toolcall, false);
assert.equal(out["combo/phantom-combo"].capabilities.reasoning, false);
assert.equal(out["combo/phantom-combo"].limit.context, 0);
assert.equal(out["omniroute/phantom-combo"].capabilities.toolcall, false);
assert.equal(out["omniroute/phantom-combo"].capabilities.reasoning, false);
assert.equal(out["omniroute/phantom-combo"].limit.context, 0);
});
test("models(): hidden combos are excluded from the map", async () => {
@@ -505,11 +505,11 @@ test("models(): hidden combos are excluded from the map", async () => {
{ fetcher: modelsFetcher, combosFetcher }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
assert.ok(out["combo/visible"]);
assert.ok(!out["combo/hidden"], "hidden combo must be omitted");
assert.ok(out["omniroute/visible"]);
assert.ok(!out["omniroute/hidden"], "hidden combo must be omitted");
});
test("models(): combo name exactly matches raw model id → raw deleted, combo lives at combo/ key, no warn", async () => {
test("models(): combo name exactly matches raw model id → raw deleted, raw deleted, no warn", async () => {
// Combo.name === raw model id triggers the dedup deletion. This mirrors
// the real OmniRoute payload where /v1/models pre-mirrors combos as
// no-slash raw entries whose ids match /api/combos friendly names.
@@ -529,10 +529,9 @@ test("models(): combo name exactly matches raw model id → raw deleted, combo l
return hook.models!({} as never, { auth: apiAuth("sk-z") as never });
});
// Raw model deleted by combo-name dedup; combo surfaces under combo/<slug>.
assert.equal(out["claude-primary"], undefined, "raw deleted by combo-name dedup");
assert.ok(out["combo/claude-primary"], "combo surfaces under combo/ namespace");
assert.equal(out["combo/claude-primary"].name, "Combo: claude-primary");
// Raw model replaced by combo of the same key; combo now lives at the bare slug.
assert.ok(out["omniroute/claude-primary"], "combo surfaces under prefixed key");
assert.equal(out["omniroute/claude-primary"].name, "claude-primary");
// No collision warning fires — dedup makes keys disjoint.
const collisionWarns = warnings.filter((w) => {
@@ -543,7 +542,7 @@ test("models(): combo name exactly matches raw model id → raw deleted, combo l
});
test("models(): two combos with same slug → second gets disambiguator suffix", async () => {
// Both combos slug to `claude` — second must get `combo/claude-<id-prefix>`.
// Both combos slug to `claude` — second must get `claude-<id-prefix>`.
const combos: OmniRouteRawCombo[] = [
{
id: "uuid-a",
@@ -566,8 +565,8 @@ test("models(): two combos with same slug → second gets disambiguator suffix",
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
// First combo gets the bare slug; second gets disambiguated.
assert.ok(out["combo/claude"], "first combo at bare slug");
assert.ok(out["combo/claude-uuid"], "second combo disambiguated by id prefix");
assert.ok(out["omniroute/claude"], "first combo at prefixed slug");
assert.ok(out["omniroute/claude-uuid"], "second combo disambiguated by id prefix");
});
test("models(): combos fetch fails → falls back to models-only, warn emitted, no throw", async () => {
@@ -584,8 +583,8 @@ test("models(): combos fetch fails → falls back to models-only, warn emitted,
// Catalog includes the models but NOT any combo entries.
assert.equal(Object.keys(out).length, 2);
assert.ok(out["claude-primary"]);
assert.ok(out["claude-secondary"]);
assert.ok(out["omniroute/claude-primary"]);
assert.ok(out["omniroute/claude-secondary"]);
// Soft-fail warning surfaced.
const softFail = warnings.find((w) => {
@@ -610,7 +609,7 @@ test("models(): combos cached + reused within TTL (one combo fetch per TTL windo
const second = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
assert.equal(combosFetcher.callCount(), 1, "combos fetched only once within TTL");
assert.equal(modelsFetcher.callCount(), 1, "models fetched only once within TTL");
assert.ok(second["combo/claude-tier"]);
assert.ok(second["omniroute/claude-tier"]);
});
test("models(): combos refetched after TTL expiry (same key as models)", async () => {
@@ -639,3 +638,74 @@ test("models(): combos fetcher receives the resolved baseURL + apiKey", async ()
await hook.models!({} as never, { auth: apiAuth("sk-spy") as never });
assert.deepEqual(combosFetcher.callsBy()[0], ["https://or.example.com/v1", "sk-spy"]);
});
test("models(): nested combo-ref context is the min of nested + raw members", async () => {
// Top-level combo MASTER-LIGHT has 1 raw model (claude-primary, 200k)
// and 2 combo-refs: OldLLM (8k member) and KIRO (32k member). The OLD
// plugin would advertise 200k (only the raw model); the fix should
// make it advertise 8k (the bottleneck across the member graph).
const modelsFetcher = stubModelsFetcher([
MODEL_PRIMARY,
{
id: "oldllm-member-1",
context_length: 8_000,
max_output_tokens: 4_000,
capabilities: {
tool_calling: false,
reasoning: false,
vision: false,
thinking: false,
temperature: true,
},
input_modalities: ["text"],
output_modalities: ["text"],
},
{
id: "kiro-member-1",
context_length: 32_000,
max_output_tokens: 8_000,
capabilities: {
tool_calling: true,
reasoning: false,
vision: false,
thinking: false,
temperature: true,
},
input_modalities: ["text"],
output_modalities: ["text"],
},
]);
const combosFetcher = stubCombosFetcher([
{
id: "oldllm",
name: "OldLLM",
models: [{ id: "s1", kind: "model", model: "oldllm-member-1", weight: 100 }],
},
{
id: "kiro",
name: "KIRO",
models: [{ id: "s1", kind: "model", model: "kiro-member-1", weight: 100 }],
},
{
id: "master-light",
name: "MASTER-LIGHT",
models: [
{ id: "r1", kind: "model", model: "claude-primary", weight: 50 },
{ id: "r2", kind: "combo-ref", comboName: "OldLLM", weight: 25 },
{ id: "r3", kind: "combo-ref", comboName: "KIRO", weight: 25 },
],
},
]);
const hook = createOmniRouteProviderHook(
{ baseURL: "https://or.example.com/v1" },
{ fetcher: modelsFetcher, combosFetcher }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
const masterLight = out["omniroute/master-light"];
assert.ok(masterLight, "MASTER-LIGHT entry must exist");
assert.equal(
masterLight.limit.context,
8_000,
`expected 8_000 (OldLLM bottleneck), got ${masterLight.limit.context}`
);
});

View File

@@ -227,7 +227,7 @@ test("config: with valid auth.json + apiKey + baseURL → mutates input.provider
// Stripped per-model shape: name + cap flags + modalities + (optional)
// cost. OC's SDK static schema accepts only `limit.{context,output}` —
// `limit.input` is NOT in the SDK shape and gets dropped silently.
const claude = entry.models["claude-sonnet-4-6"];
const claude = entry.models["omniroute/claude-sonnet-4-6"];
assert.ok(claude, "claude model surfaced");
assert.equal(claude.name, "claude-sonnet-4-6");
assert.equal(claude.attachment, true);
@@ -246,11 +246,11 @@ test("config: with valid auth.json + apiKey + baseURL → mutates input.provider
assert.deepEqual(claude.modalities?.input, ["text", "image"]);
assert.deepEqual(claude.modalities?.output, ["text"]);
// Combo surfaces under `combo/<friendly-name>` namespace + LCD'd
// Combo surfaces under bare key + LCD'd
// (gemini's reasoning=false → combo reasoning=false).
const combo = entry.models["combo/claude-tier"];
assert.ok(combo, "combo surfaced under combo/ namespace");
assert.equal(combo.name, "Combo: Claude Tier");
const combo = entry.models["omniroute/claude-tier"];
assert.ok(combo, "combo surfaced under bare key");
assert.equal(combo.name, "Claude Tier");
assert.equal(combo.reasoning, false, "LCD: any member reasoning=false → combo reasoning=false");
assert.equal(combo.tool_call, true);
assert.equal(combo.limit?.context, 200_000, "LCD: min(200_000, 1_000_000)");
@@ -409,8 +409,8 @@ test("config: combos fetcher throws → emit models-only catalog (no combos in m
.omniroute;
assert.ok(entry);
const ids = Object.keys(entry.models).sort();
assert.deepEqual(ids, ["claude-sonnet-4-6", "gemini-3-flash"]);
assert.equal(entry.models["combo-claude-tier"], undefined, "no combo entry");
assert.deepEqual(ids, ["omniroute/claude-sonnet-4-6", "omniroute/gemini-3-flash"]);
assert.equal(entry.models["omniroute/claude-tier"], undefined, "no combo entry");
assert.ok(
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
"combos-fetch breadcrumb emitted"
@@ -638,6 +638,7 @@ test("buildStaticProviderEntry: stripped per-model shape matches sibling @omniro
"cost",
"limit",
"modalities",
"providerID",
]);
for (const [id, entry] of Object.entries(block.models)) {
for (const key of Object.keys(entry)) {
@@ -653,7 +654,7 @@ test("buildStaticProviderEntry: stripped per-model shape matches sibling @omniro
}
// Sanity: claude entry has all expected stripped fields.
const claude = block.models["claude-sonnet-4-6"];
const claude = block.models["omniroute/claude-sonnet-4-6"];
assert.equal(typeof claude.name, "string");
assert.equal(typeof claude.attachment, "boolean");
assert.equal(typeof claude.reasoning, "boolean");
@@ -678,8 +679,8 @@ test("buildStaticProviderEntry: hidden combos are excluded", () => {
"https://or.example/v1",
"sk-test"
);
assert.equal(block.models["combo-claude-tier"], undefined);
assert.ok(block.models["claude-sonnet-4-6"]);
assert.equal(block.models["omniroute/claude-tier"], undefined);
assert.ok(block.models["omniroute/claude-sonnet-4-6"]);
});
// ────────────────────────────────────────────────────────────────────────────
@@ -695,7 +696,7 @@ test("buildStaticProviderEntry: emits modalities.input from raw.input_modalities
"https://or.example/v1",
"sk-test"
);
const claude = block.models["claude-sonnet-4-6"];
const claude = block.models["omniroute/claude-sonnet-4-6"];
assert.deepEqual(claude.modalities?.input, ["text", "image"]);
assert.deepEqual(claude.modalities?.output, ["text"]);
});
@@ -709,7 +710,7 @@ test("buildStaticProviderEntry: never emits limit.input (OC SDK rejects it)", ()
"https://or.example/v1",
"sk-test"
);
const claude = block.models["claude-sonnet-4-6"];
const claude = block.models["omniroute/claude-sonnet-4-6"];
assert.equal((claude.limit as Record<string, unknown>).input, undefined);
assert.equal(typeof claude.limit?.context, "number");
assert.equal(typeof claude.limit?.output, "number");
@@ -737,7 +738,7 @@ test("buildStaticProviderEntry: emits cost when enrichment carries pricing", ()
"sk-test",
enrichment
);
const claude = block.models["claude-sonnet-4-6"];
const claude = block.models["omniroute/claude-sonnet-4-6"];
assert.equal(claude.cost?.input, 3);
assert.equal(claude.cost?.output, 15);
assert.equal(claude.cost?.cache_read, 0.3);
@@ -758,8 +759,8 @@ test("buildStaticProviderEntry: emits release_date when raw carries it; omits wh
"https://or.example/v1",
"sk-test"
);
assert.equal(block.models["claude-with-date"].release_date, "2026-02-19");
assert.equal(block.models["gemini-3-flash"].release_date, undefined);
assert.equal(block.models["omniroute/claude-with-date"].release_date, "2026-02-19");
assert.equal(block.models["omniroute/gemini-3-flash"].release_date, undefined);
});
test("buildStaticProviderEntry: combo modalities = intersection of members (LCD)", () => {
@@ -788,7 +789,7 @@ test("buildStaticProviderEntry: combo modalities = intersection of members (LCD)
"https://or.example/v1",
"sk-test"
);
const combo = block.models["combo/mixed-tier"];
const combo = block.models["omniroute/mixed-tier"];
assert.ok(combo, "combo emitted under slug key");
// claude has text+image, text-only has text → intersection drops image.
assert.deepEqual(combo.modalities?.input, ["text"]);
@@ -896,10 +897,10 @@ test("config: enrichment fetched + name overlaid on raw-model entries", async ()
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry);
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
assert.equal(entry.models["gemini-3-flash"].name, "Gemini 3 Flash");
assert.equal(entry.models["omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
assert.equal(entry.models["omniroute/gemini-3-flash"].name, "Gemini 3 Flash");
// Combo names still come from /api/combos — enrichment overlay does NOT touch combos.
assert.equal(entry.models["combo/claude-tier"].name, "Combo: Claude Tier");
assert.equal(entry.models["omniroute/claude-tier"].name, "Claude Tier");
assert.equal(enrichmentFetcher.callCount(), 1);
});
@@ -927,7 +928,11 @@ test("config: features.enrichment=false skips enrichment fetch + keeps raw-id na
.omniroute;
assert.ok(entry);
assert.equal(enrichmentFetcher.callCount(), 0, "enrichment fetch suppressed by feature flag");
assert.equal(entry.models["claude-sonnet-4-6"].name, "claude-sonnet-4-6", "raw id retained");
assert.equal(
entry.models["omniroute/claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id retained"
);
});
test("config: enrichment fetcher throws → soft-fail (warn + raw-id static catalog)", async () => {
@@ -949,7 +954,11 @@ test("config: enrichment fetcher throws → soft-fail (warn + raw-id static cata
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry, "static block still published on enrichment failure");
assert.equal(entry.models["claude-sonnet-4-6"].name, "claude-sonnet-4-6", "raw id retained");
assert.equal(
entry.models["omniroute/claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id retained"
);
assert.equal(enrichmentFetcher.callCount(), 1);
assert.ok(
logger.entries.some((e) => String(e[0]).includes("/api/pricing/models fetch failed")),
@@ -1143,9 +1152,12 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry.models["claude-sonnet-4-6"], "stale snapshot hydrated into static block");
assert.ok(
entry.models["omniroute/claude-sonnet-4-6"],
"stale snapshot hydrated into static block"
);
assert.equal(
entry.models["claude-sonnet-4-6"].name,
entry.models["omniroute/claude-sonnet-4-6"].name,
"Claude Sonnet 4.6 (cached)",
"stale enrichment also reused"
);
@@ -1192,7 +1204,7 @@ test("config: cached rawEnrichment from earlier provider hook is reused (no refe
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
assert.equal(entry.models["omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
});
// ─────────────────────────────────────────────────────────────────────
@@ -1241,10 +1253,10 @@ test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry);
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
assert.equal(entry.models["gemini-3-flash"].name, "Gemini-cli - Gemini 3 Flash");
assert.equal(entry.models["omniroute/claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
assert.equal(entry.models["omniroute/gemini-3-flash"].name, "Gemini-cli - Gemini 3 Flash");
// Combos stay untouched — `Combo: ` prefix already conveys multi-upstream.
assert.equal(entry.models["combo/claude-tier"].name, "Combo: Claude Tier");
assert.equal(entry.models["omniroute/claude-tier"].name, "Claude Tier");
});
test("config: providerTag=false suppresses the suffix", async () => {
@@ -1270,7 +1282,7 @@ test("config: providerTag=false suppresses the suffix", async () => {
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(
entry.models["claude-sonnet-4-6"].name,
entry.models["omniroute/claude-sonnet-4-6"].name,
"Claude Sonnet 4.6",
"enriched name kept, provider tag suppressed"
);
@@ -1301,7 +1313,7 @@ test("config: providerTag falls back to UPPER(alias) when providerDisplayName mi
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.models["claude-sonnet-4-6"].name, "CC - Claude Sonnet 4.6");
assert.equal(entry.models["omniroute/claude-sonnet-4-6"].name, "CC - Claude Sonnet 4.6");
});
test("config: providerTag skipped entirely when neither providerDisplayName nor providerAlias set", async () => {
@@ -1327,7 +1339,7 @@ test("config: providerTag skipped entirely when neither providerDisplayName nor
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
assert.equal(entry.models["omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
});
test("config: providerTag is idempotent — second hook call doesn't double-suffix", async () => {
@@ -1353,12 +1365,66 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
await hook(inputA);
const entryA = (inputA as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entryA.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
assert.equal(entryA.models["omniroute/claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
// Second invocation (cache hit) — name must still be single-suffixed.
const inputB = makeInput();
await hook(inputB);
const entryB = (inputB as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entryB.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
assert.equal(entryB.models["omniroute/claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
});
// ────────────────────────────────────────────────────────────────────────────
// T-NN: nested combo-ref resolution in the static catalog
// (mirrors the dynamic-catalog fix in combos.test.ts)
// ────────────────────────────────────────────────────────────────────────────
test("buildStaticProviderEntry: nested combo-ref context is the bottleneck across the graph", () => {
const resolved = resolveOmniRoutePluginOptions({ providerId: "omniroute" });
const rawModels: OmniRouteRawModelEntry[] = [
{
id: "raw-big",
context_length: 200_000,
max_output_tokens: 64_000,
capabilities: { tool_calling: true, reasoning: true, vision: false, temperature: true },
input_modalities: ["text"],
output_modalities: ["text"],
},
{
id: "raw-tiny",
context_length: 8_000,
max_output_tokens: 4_000,
capabilities: { tool_calling: false, reasoning: false, vision: false, temperature: true },
input_modalities: ["text"],
output_modalities: ["text"],
},
];
const rawCombos: OmniRouteRawCombo[] = [
{
id: "tiny-combo",
name: "TinyCombo",
models: [{ id: "m1", kind: "model", model: "raw-tiny", weight: 100 }],
},
{
id: "parent",
name: "Parent",
models: [
{ id: "p1", kind: "model", model: "raw-big", weight: 50 },
{ id: "p2", kind: "combo-ref", comboName: "TinyCombo", weight: 50 },
],
},
];
const block = buildStaticProviderEntry(
rawModels,
rawCombos,
resolved,
"https://or.example/v1",
"sk-test"
);
// Pre-fix: Parent would advertise 200_000 (only raw-big counted).
// Post-fix: Parent should advertise 8_000 (TinyCombo bottleneck).
const parent = block.models["omniroute/parent"];
assert.ok(parent, "Parent combo must be in the static catalog");
assert.equal(parent.limit?.context, 8_000);
});

View File

@@ -376,7 +376,7 @@ test("provider hook: enrichment fetcher called when features.enrichment !== fals
);
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
assert.equal(called, 1, "enrichment fetcher called once");
const m = out["claude-sonnet-4-6"];
const m = out["omniroute/claude-sonnet-4-6"];
assert.equal(m.name, "Claude Sonnet 4.6", "enrichment name overlay applied");
assert.equal(m.cost.input, 3, "enrichment pricing applied");
assert.equal(m.cost.output, 15);
@@ -401,7 +401,7 @@ test("provider hook: enrichment fetcher NOT called when features.enrichment:fals
);
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
assert.equal(called, 0, "enrichment fetcher NOT called when gated off");
assert.equal(out["claude-sonnet-4-6"].name, "claude-sonnet-4-6", "raw id preserved");
assert.equal(out["omniroute/claude-sonnet-4-6"].name, "claude-sonnet-4-6", "raw id preserved");
});
test("provider hook: compression metadata fetcher NOT called by default (opt-in)", async () => {
@@ -459,7 +459,7 @@ test("provider hook: compression metadata fetcher called when opted in", async (
);
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
assert.equal(called, 1, "compression metadata fetcher called");
const combo = out["combo/claude-primary"];
const combo = out["omniroute/claude-primary"];
assert.ok(combo, "combo entry present");
assert.match(
combo.name,

View File

@@ -0,0 +1,291 @@
/**
* Tests for the 3 mrmm-fork features backported to @omniroute/opencode-plugin:
*
* 1. `normaliseFreeLabel` — free-tier model display names get a consistent
* `[Free] ` prefix instead of trailing "(Free)" or ad-hoc "free" words.
*
* 2. `resolveApiBlock` — per-provider-prefix API format routing. Anthropic
* prefixes (`cc/`, `claude/`, `anthropic/`, `kiro/`, `kr/`) get the
* Anthropic SDK block; everything else gets OpenAI-compat.
*
* 3. `debugLog` — JSONL request/response capture, gated by
* `features.debugLog` and togglable at runtime via
* `debugLogEnabled/SetEnabled`.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import {
normaliseFreeLabel,
resolveApiBlock,
DEFAULT_ANTHROPIC_PREFIXES,
ensureV1Suffix,
debugLogEnabled,
debugLogSetEnabled,
debugLogClear,
debugLogRead,
debugLogAppend,
createDebugLoggingFetch,
DebugLogEntry,
} from "../src/index.js";
// ── 1. normaliseFreeLabel ────────────────────────────────────────────────────
test("normaliseFreeLabel: '(Free)' suffix becomes [Free] prefix", () => {
assert.equal(normaliseFreeLabel("GPT-4.1 (Free)"), "[Free] GPT-4.1");
});
test("normaliseFreeLabel: trailing ' Free' word becomes [Free] prefix", () => {
assert.equal(
normaliseFreeLabel("DeepSeek V4 Flash Free"),
"[Free] DeepSeek V4 Flash"
);
});
test("normaliseFreeLabel: trailing '-free' (hyphen) becomes [Free] prefix", () => {
assert.equal(normaliseFreeLabel("Llama 4 Scout-free"), "[Free] Llama 4 Scout");
});
test("normaliseFreeLabel: case-insensitive (FREE, Free, free all match)", () => {
assert.equal(normaliseFreeLabel("Model A FREE"), "[Free] Model A");
assert.equal(normaliseFreeLabel("Model A free"), "[Free] Model A");
assert.equal(normaliseFreeLabel("Model A Free"), "[Free] Model A");
});
test("normaliseFreeLabel: names without 'free' pass through unchanged", () => {
assert.equal(normaliseFreeLabel("Claude 4.7 Opus"), "Claude 4.7 Opus");
assert.equal(normaliseFreeLabel("GPT-5"), "GPT-5");
});
test("normaliseFreeLabel: 'free' in the middle of a name is NOT rewritten", () => {
// Only trailing/standalone "free" markers count; embedded "freedom" stays
assert.equal(
normaliseFreeLabel("Freedom Model"),
"Freedom Model"
);
});
test("normaliseFreeLabel: empty / whitespace-only inputs are handled", () => {
// Empty input returns empty; pure whitespace input passes through (no Free marker)
assert.equal(normaliseFreeLabel(""), "");
assert.equal(normaliseFreeLabel(" "), " ");
});
// ── 2. resolveApiBlock ───────────────────────────────────────────────────────
test("resolveApiBlock: cc/* models get the Anthropic SDK block (no /v1)", () => {
const block = resolveApiBlock("cc/claude-opus-4-7", "https://api.example.com");
assert.equal(block.id, "anthropic");
assert.equal(block.npm, "@ai-sdk/anthropic");
assert.equal(block.url, "https://api.example.com"); // NO /v1 suffix
});
test("resolveApiBlock: claude/*, anthropic/*, kiro/*, kr/* all route to Anthropic", () => {
for (const id of [
"claude/claude-opus-4-7",
"anthropic/claude-sonnet-4",
"kiro/claude-sonnet-4-5",
"kr/claude-opus-4-6",
]) {
const block = resolveApiBlock(id, "https://api.example.com");
assert.equal(block.id, "anthropic", `${id} should route to Anthropic`);
assert.equal(block.npm, "@ai-sdk/anthropic");
}
});
test("resolveApiBlock: non-Anthropic models get OpenAI-compat with /v1", () => {
const block = resolveApiBlock("gpt-4o", "https://api.example.com");
assert.equal(block.id, "openai-compatible");
assert.equal(block.npm, "@ai-sdk/openai-compatible");
assert.equal(block.url, "https://api.example.com/v1");
});
test("resolveApiBlock: user can override anthropicPrefixes to add custom prefixes", () => {
const block = resolveApiBlock("myproxy/claude-opus", "https://api.example.com", {
anthropicPrefixes: ["myproxy"],
});
assert.equal(block.id, "anthropic");
assert.equal(block.npm, "@ai-sdk/anthropic");
});
test("resolveApiBlock: empty anthropicPrefixes forces OpenAI-compat for everything", () => {
const block = resolveApiBlock("cc/claude-opus", "https://api.example.com", {
anthropicPrefixes: [],
});
assert.equal(block.id, "openai-compatible");
});
test("resolveApiBlock: baseURL that already ends in /v1 is not double-suffixed (OpenAI path)", () => {
const block = resolveApiBlock("gpt-4o", "https://api.example.com/v1");
assert.equal(block.url, "https://api.example.com/v1"); // idempotent
});
test("resolveApiBlock: model id without '/' uses the id as prefix", () => {
const block = resolveApiBlock("claude-opus-4-7", "https://api.example.com");
// The whole id is the prefix, which doesn't match "cc"/"claude" etc.
// So it falls through to OpenAI-compat.
assert.equal(block.id, "openai-compatible");
});
test("DEFAULT_ANTHROPIC_PREFIXES: contains the canonical Anthropic aliases", () => {
assert.deepEqual(DEFAULT_ANTHROPIC_PREFIXES, [
"cc",
"claude",
"anthropic",
"kiro",
"kr",
]);
});
test("ensureV1Suffix: idempotent for URLs that already end in /v1", () => {
assert.equal(ensureV1Suffix("https://api.example.com/v1"), "https://api.example.com/v1");
assert.equal(
ensureV1Suffix("https://api.example.com/v1/"),
"https://api.example.com/v1" // trailing slash is stripped
);
});
test("ensureV1Suffix: appends /v1 when missing", () => {
assert.equal(ensureV1Suffix("https://api.example.com"), "https://api.example.com/v1");
assert.equal(ensureV1Suffix("https://api.example.com/"), "https://api.example.com/v1");
});
// ── 3. debugLog ──────────────────────────────────────────────────────────────
test("debugLog: default state is disabled", () => {
debugLogClear("test-provider-disabled-default");
assert.equal(debugLogEnabled("test-provider-disabled-default"), false);
});
test("debugLogSetEnabled + debugLogEnabled: roundtrip", () => {
debugLogSetEnabled("test-provider-toggle", true);
assert.equal(debugLogEnabled("test-provider-toggle"), true);
debugLogSetEnabled("test-provider-toggle", false);
assert.equal(debugLogEnabled("test-provider-toggle"), false);
});
test("debugLogAppend + debugLogRead: roundtrip preserves entry shape", () => {
const providerId = "test-provider-readroundtrip";
debugLogClear(providerId);
const entry: DebugLogEntry = {
reqId: "req-1",
providerId,
ts: 1700000000000,
url: "https://api.example.com/v1/chat",
method: "POST",
reqHeaders: { "content-type": "application/json" },
reqBody: { model: "gpt-4o", messages: [] },
resStatus: 200,
resHeaders: { "content-type": "application/json" },
resBody: { choices: [] },
durationMs: 42,
};
debugLogAppend(entry);
const read = debugLogRead(providerId, 10);
assert.equal(read.length, 1);
assert.deepEqual(read[0], entry);
});
test("createDebugLoggingFetch: passes through when disabled", async () => {
const providerId = "test-provider-passthrough";
debugLogClear(providerId);
debugLogSetEnabled(providerId, false);
const calls: unknown[] = [];
const inner: typeof fetch = async (input) => {
calls.push(input);
return new Response("ok", { status: 200 });
};
const wrapped = createDebugLoggingFetch(inner, providerId, false);
const res = await wrapped("https://api.example.com/v1/chat");
assert.equal(res.status, 200);
assert.equal(calls.length, 1);
// No log entry should be written when disabled
assert.equal(debugLogRead(providerId).length, 0);
});
test("createDebugLoggingFetch: captures request/response when enabled", async () => {
const providerId = "test-provider-captures";
debugLogClear(providerId);
const inner: typeof fetch = async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "content-type": "application/json" },
});
const wrapped = createDebugLoggingFetch(inner, providerId, true);
const res = await wrapped("https://api.example.com/v1/chat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ model: "gpt-4o" }),
});
assert.equal(res.status, 200);
const entries = debugLogRead(providerId);
assert.equal(entries.length, 1);
assert.equal(entries[0].method, "POST");
assert.equal(entries[0].resStatus, 200);
assert.equal(entries[0].url, "https://api.example.com/v1/chat");
assert.deepEqual(entries[0].reqBody, { model: "gpt-4o" });
});
test("createDebugLoggingFetch: records error without crashing the wrapped fetch", async () => {
const providerId = "test-provider-error";
debugLogClear(providerId);
const inner: typeof fetch = async () => {
throw new Error("network down");
};
const wrapped = createDebugLoggingFetch(inner, providerId, true);
await assert.rejects(wrapped("https://api.example.com/v1/chat"), /network down/);
const entries = debugLogRead(providerId);
assert.equal(entries.length, 1);
assert.equal(entries[0].resStatus, null);
assert.equal(entries[0].error, "network down");
});
// ── Regression tests for the 3 HIGH-priority bot review fixes ───────────────
test("createDebugLoggingFetch: URL instance input is captured (not 'undefined')", async () => {
const providerId = "test-provider-url-input";
debugLogClear(providerId);
const inner: typeof fetch = async () =>
new Response("ok", { status: 200 });
const wrapped = createDebugLoggingFetch(inner, providerId, true);
await wrapped(new URL("https://api.example.com/v1/chat"));
const entries = debugLogRead(providerId);
assert.equal(entries.length, 1);
assert.equal(entries[0].url, "https://api.example.com/v1/chat");
assert.notEqual(entries[0].url, undefined);
});
test("createDebugLoggingFetch: Request object input captures URL and headers", async () => {
const providerId = "test-provider-request-input";
debugLogClear(providerId);
const inner: typeof fetch = async () =>
new Response("ok", { status: 200 });
const wrapped = createDebugLoggingFetch(inner, providerId, true);
const req = new Request("https://api.example.com/v1/chat", {
method: "POST",
headers: { "x-test": "yes" },
});
await wrapped(req);
const entries = debugLogRead(providerId);
assert.equal(entries.length, 1);
assert.equal(entries[0].url, "https://api.example.com/v1/chat");
assert.equal(entries[0].reqHeaders["x-test"], "yes");
});
test("createDebugLoggingFetch: SSE response is NOT buffered (resBody is the stream marker)", async () => {
const providerId = "test-provider-sse";
debugLogClear(providerId);
const inner: typeof fetch = async () =>
new Response("data: hello\n\n", {
status: 200,
headers: { "content-type": "text/event-stream" },
});
const wrapped = createDebugLoggingFetch(inner, providerId, true);
const res = await wrapped("https://api.example.com/v1/stream");
// The response body must remain readable downstream
const txt = await res.text();
assert.equal(txt, "data: hello\n\n");
const entries = debugLogRead(providerId);
assert.equal(entries.length, 1);
assert.equal(entries[0].resBody, "[stream]", "SSE responses must not be buffered");
});

View File

@@ -101,7 +101,7 @@ test("models: extracts apiKey from ctx.auth (type=api) and calls fetcher with it
assert.equal(fetcher.callCount(), 1);
assert.deepEqual(fetcher.callsBy()[0], ["https://or.example.com/v1", "sk-abc"]);
assert.equal(Object.keys(out).length, 3);
assert.ok(out["claude-primary"]);
assert.ok(out["omniroute/claude-primary"]);
});
test("models: returns {} when ctx.auth is null/undefined/wrong-type/empty-key", async () => {
@@ -152,9 +152,11 @@ test("models: maps a sample /v1/models entry to ModelV2 (sanity)", async () => {
{ fetcher, combosFetcher: async () => [] }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-abc") as never });
const claude = out["claude-primary"];
const claude = out["omniroute/claude-primary"];
assert.ok(claude, "claude-primary present");
assert.equal(claude.id, "claude-primary");
// `mapRawModelToModelV2` stamps the provider prefix on the id so OC's
// static-catalog reader resolves `(providerID, modelID)` from the key.
assert.equal(claude.id, "omniroute/claude-primary");
assert.equal(claude.name, "claude-primary");
assert.equal(claude.providerID, "omniroute");
assert.equal(claude.api.id, "openai-compatible");

View File

@@ -1,6 +1,5 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import {
OmniRoutePlugin,
OMNIROUTE_PROVIDER_KEY,
@@ -63,11 +62,12 @@ test("OmniRoutePlugin: returns an empty hooks object (scaffold)", async () => {
assert.notEqual(hooks, null);
});
test("scaffold: CJS default export resolves via require() with v1 shape", () => {
const require_ = createRequire(import.meta.url);
const cjs = require_("../dist/index.cjs");
// after cjsInterop:true, default export is on cjs.default
assert.strictEqual(typeof cjs.default, "object");
assert.strictEqual(cjs.default.id, "@omniroute/opencode-plugin");
assert.strictEqual(typeof cjs.default.server, "function");
test("scaffold: built ESM default export resolves with the v1 plugin shape", async () => {
// The plugin is ESM-only now — the CJS bundle was dropped to fix the OpenCode
// loader (#3883), so there is no more ../dist/index.cjs. Validate that the built
// distributable's default export still carries the OpenCode v1 { id, server } shape.
const mod = await import("../dist/index.js");
assert.strictEqual(typeof mod.default, "object");
assert.strictEqual(mod.default.id, "@omniroute/opencode-plugin");
assert.strictEqual(typeof mod.default.server, "function");
});

View File

@@ -2,7 +2,7 @@ import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
format: ["esm"],
dts: true,
clean: true,
sourcemap: false,
@@ -11,7 +11,7 @@ export default defineConfig({
target: "node22",
outDir: "dist",
minify: false,
cjsInterop: true,
cjsInterop: false,
// Bundle runtime deps so the .tgz / npm install is self-contained.
// `zod` is required at runtime by the options schema and would otherwise
// need a peer install when the plugin is loaded directly from a file path

View File

@@ -1,5 +1,23 @@
# @omniroute/opencode-provider
> ## ⚠️ Deprecated — use [`@omniroute/opencode-plugin`](https://www.npmjs.com/package/@omniroute/opencode-plugin) instead
>
> This package writes a **static** `provider.omniroute` block to `opencode.json` from a hardcoded default model list, so it **drifts behind your live OmniRoute catalog** — adding a model in OmniRoute won't show up in OpenCode until you re-run the generator, and OpenCode Desktop/Web only surfaces a subset of the static models.
>
> **`@omniroute/opencode-plugin`** solves this by fetching `GET /v1/models` from your OmniRoute instance at OpenCode startup, so the model list is always live (see [#3419](https://github.com/diegosouzapw/OmniRoute/issues/3419)). It is now the recommended path.
>
> **One-line migration** — replace the static `provider.omniroute` block in `opencode.json` with a single plugin entry:
>
> ```jsonc
> // opencode.json
> {
> "$schema": "https://opencode.ai/config.json",
> "plugin": ["@omniroute/opencode-plugin"]
> }
> ```
>
> This package is **not removed** and still works for static/offline config generation, but it is no longer actively recommended and won't track new models automatically.
Helper for connecting [OpenCode](https://opencode.ai) to a running [OmniRoute](https://github.com/diegosouzapw/OmniRoute) AI gateway.
The package emits a **schema-valid entry** for `opencode.json` (`https://opencode.ai/config.json`) that delegates the actual runtime to [`@ai-sdk/openai-compatible`](https://www.npmjs.com/package/@ai-sdk/openai-compatible). It does not ship any new HTTP client — OmniRoute already exposes an OpenAI-compatible surface, and OpenCode already speaks it through the AI SDK.

View File

@@ -19,9 +19,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"cpu": [
"ppc64"
],
@@ -36,9 +36,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"cpu": [
"arm"
],
@@ -53,9 +53,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"cpu": [
"arm64"
],
@@ -70,9 +70,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"cpu": [
"x64"
],
@@ -87,9 +87,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"cpu": [
"arm64"
],
@@ -104,9 +104,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"cpu": [
"x64"
],
@@ -121,9 +121,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"cpu": [
"arm64"
],
@@ -138,9 +138,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"cpu": [
"x64"
],
@@ -155,9 +155,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"cpu": [
"arm"
],
@@ -172,9 +172,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"cpu": [
"arm64"
],
@@ -189,9 +189,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"cpu": [
"ia32"
],
@@ -206,9 +206,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"cpu": [
"loong64"
],
@@ -223,9 +223,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"cpu": [
"mips64el"
],
@@ -240,9 +240,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"cpu": [
"ppc64"
],
@@ -257,9 +257,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"cpu": [
"riscv64"
],
@@ -274,9 +274,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"cpu": [
"s390x"
],
@@ -291,9 +291,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"cpu": [
"x64"
],
@@ -308,9 +308,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"cpu": [
"arm64"
],
@@ -325,9 +325,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"cpu": [
"x64"
],
@@ -342,9 +342,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"cpu": [
"arm64"
],
@@ -359,9 +359,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"cpu": [
"x64"
],
@@ -376,9 +376,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"cpu": [
"arm64"
],
@@ -393,9 +393,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"cpu": [
"x64"
],
@@ -410,9 +410,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"cpu": [
"arm64"
],
@@ -427,9 +427,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"cpu": [
"ia32"
],
@@ -444,9 +444,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"cpu": [
"x64"
],
@@ -974,9 +974,9 @@
}
},
"node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -987,32 +987,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.7",
"@esbuild/android-arm": "0.27.7",
"@esbuild/android-arm64": "0.27.7",
"@esbuild/android-x64": "0.27.7",
"@esbuild/darwin-arm64": "0.27.7",
"@esbuild/darwin-x64": "0.27.7",
"@esbuild/freebsd-arm64": "0.27.7",
"@esbuild/freebsd-x64": "0.27.7",
"@esbuild/linux-arm": "0.27.7",
"@esbuild/linux-arm64": "0.27.7",
"@esbuild/linux-ia32": "0.27.7",
"@esbuild/linux-loong64": "0.27.7",
"@esbuild/linux-mips64el": "0.27.7",
"@esbuild/linux-ppc64": "0.27.7",
"@esbuild/linux-riscv64": "0.27.7",
"@esbuild/linux-s390x": "0.27.7",
"@esbuild/linux-x64": "0.27.7",
"@esbuild/netbsd-arm64": "0.27.7",
"@esbuild/netbsd-x64": "0.27.7",
"@esbuild/openbsd-arm64": "0.27.7",
"@esbuild/openbsd-x64": "0.27.7",
"@esbuild/openharmony-arm64": "0.27.7",
"@esbuild/sunos-x64": "0.27.7",
"@esbuild/win32-arm64": "0.27.7",
"@esbuild/win32-ia32": "0.27.7",
"@esbuild/win32-x64": "0.27.7"
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
}
},
"node_modules/fdir": {
@@ -1482,490 +1482,6 @@
"fsevents": "~2.3.3"
}
},
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "@omniroute/opencode-provider",
"version": "0.1.0",
"description": "OpenCode provider helper for the OmniRoute AI Gateway. Generates a schema-valid provider entry for opencode.json that delegates the runtime to @ai-sdk/openai-compatible.",
"description": "DEPRECATED — use @omniroute/opencode-plugin instead (it fetches the live OmniRoute /v1/models catalog at startup, so models never drift). This static-config generator still works but is no longer the recommended path. OpenCode provider helper for the OmniRoute AI Gateway.",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
@@ -56,5 +56,8 @@
"tsup": "^8.5.1",
"tsx": "^4.22.3",
"typescript": "^5.9.3"
},
"overrides": {
"esbuild": "^0.28.1"
}
}

View File

@@ -129,8 +129,8 @@ export interface OmniRouteProviderOptions {
apiKey: string;
/** Override the display name shown in OpenCode. Default: `"OmniRoute"`. */
displayName?: string;
/** Override the model catalog. Defaults to `OMNIROUTE_DEFAULT_OPENCODE_MODELS`. */
models?: readonly string[];
/** Override the model catalog. Accepts model ids (strings) or live model entries from `fetchLiveModels`. When entries carry a `contextLength`, it is used directly — no hardcoded map needed. */
models?: readonly (string | { id: string; contextLength?: number })[];
/** Optional human-readable labels keyed by model id. Overridden by `modelCapabilities[id].label`. */
modelLabels?: Record<string, string>;
/**
@@ -139,6 +139,12 @@ export interface OmniRouteProviderOptions {
* for custom ids the override is used verbatim.
*/
modelCapabilities?: Record<string, ModelCapabilities>;
/**
* Optional per-model context-length overrides (tokens). Takes precedence
* over the static `OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS` map but is
* superseded by `contextLength` on live model entries passed via `models`.
*/
modelContextLengths?: Record<string, string | number>;
/**
* Primary model for OpenCode (top-level `model` key).
* Emitted as `"omniroute/<id>"`. When omitted the key is not written.
@@ -248,7 +254,12 @@ export function createOmniRouteProvider(options: OmniRouteProviderOptions): Open
const models: Record<string, OpenCodeModelEntry> = {};
const seen = new Set<string>();
for (const raw of modelList) {
const id = typeof raw === "string" ? raw.trim() : "";
const id =
typeof raw === "object" && raw !== null && "id" in raw && typeof (raw as any).id === "string"
? (raw as { id: string }).id.trim()
: typeof raw === "string"
? raw.trim()
: "";
if (!id || seen.has(id)) continue;
seen.add(id);
const defaults = OMNIROUTE_DEFAULT_MODEL_CAPABILITIES[id] ?? {};
@@ -266,10 +277,18 @@ export function createOmniRouteProvider(options: OmniRouteProviderOptions): Open
if (typeof merged.temperature === "boolean") entry.temperature = merged.temperature;
if (typeof merged.tool_call === "boolean") entry.tool_call = merged.tool_call;
// Include context window limit when known — OpenCode reads this to
// determine usable context length for compaction & overflow detection.
const contextLength = OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id];
if (typeof contextLength === "number" && contextLength > 0) {
// Context window: live model entry (from API catalog) > modelContextLengths > static defaults
const liveContext =
typeof raw === "object" && raw !== null
? (raw as { contextLength?: number }).contextLength
: undefined;
const rawContextLength =
liveContext ??
options.modelContextLengths?.[id] ??
OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id];
const contextLength =
typeof rawContextLength === "string" ? parseInt(rawContextLength, 10) : rawContextLength;
if (typeof contextLength === "number" && !isNaN(contextLength) && contextLength > 0) {
entry.limit = { context: contextLength };
}
@@ -508,7 +527,7 @@ export interface OmniRouteLiveModel {
* const config = buildOmniRouteOpenCodeConfig({
* baseURL: "http://localhost:20128",
* apiKey: "sk_omniroute",
* models: models.map((m) => m.id),
* models, // OmniRouteLiveModel[] — contextLength auto-extracted
* modelLabels: Object.fromEntries(models.map((m) => [m.id, m.name])),
* });
* ```

View File

@@ -2,6 +2,9 @@ import test from "node:test";
import assert from "node:assert/strict";
import { createServer } from "node:http";
import type { Server } from "node:http";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import {
buildOmniRouteOpenCodeConfig,
@@ -430,6 +433,34 @@ test("createOmniRouteProvider omits limit.context for unknown model ids", () =>
assert.equal(entry.limit, undefined);
});
test("createOmniRouteProvider reads contextLength from a live model entry for ids absent from the static map", () => {
// #3298 regression guard: the static OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS
// map only covers the legacy 8 Claude/Gemini ids. Before this change, any
// other model got `undefined` context (see the test above, string form) and
// OpenCode silently fell back to its 128K internal default. A live model
// entry carrying `contextLength` must now surface as `limit.context`.
const provider = createOmniRouteProvider({
baseURL: "http://localhost:20128",
apiKey: "sk_omniroute",
models: [{ id: "completely-unknown-model", contextLength: 262_144 }],
});
const entry = provider.models["completely-unknown-model"];
assert.ok(entry.limit, "a live contextLength should produce a limit field even for ids absent from the static map");
assert.equal(entry.limit!.context, 262_144);
});
test("createOmniRouteProvider: a live model contextLength wins over the static default map", () => {
// `cc/claude-opus-4-8` has a static default (1_000_000). A live entry carrying
// a different contextLength must take precedence (live > modelContextLengths >
// static defaults).
const provider = createOmniRouteProvider({
baseURL: "http://localhost:20128",
apiKey: "sk_omniroute",
models: [{ id: "cc/claude-opus-4-8", contextLength: 524_288 }],
});
assert.equal(provider.models["cc/claude-opus-4-8"].limit!.context, 524_288);
});
test("createOmniRouteProvider serialises limit.context to JSON", () => {
const provider = createOmniRouteProvider({
baseURL: "http://localhost:20128",
@@ -639,3 +670,17 @@ test("createOmniRouteModesBlock honours numeric overrides limited to OC schema",
assert.equal(block.build.temperature, 0.7);
assert.equal(block.build.top_p, 0.9);
});
// #3419 — soft-deprecation in favour of @omniroute/opencode-plugin. Guard the
// deprecation notice so it can't be silently dropped while the package is kept
// publishing (it still works; it is just no longer the recommended path).
test("package is marked deprecated in favour of @omniroute/opencode-plugin (#3419)", () => {
const here = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
assert.match(pkg.description, /DEPRECATED/);
assert.match(pkg.description, /@omniroute\/opencode-plugin/);
const readme = readFileSync(join(here, "..", "README.md"), "utf8");
assert.match(readme, /Deprecated/i);
assert.match(readme, /@omniroute\/opencode-plugin/);
});

193
AGENTS.md
View File

@@ -3,19 +3,53 @@
## Project
Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support
with **212 providers** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
with **231 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
Cohere, NVIDIA, Cerebras, Pollinations, Puter, Cloudflare AI, HuggingFace, DeepInfra,
SambaNova, Meta Llama API, Moonshot AI, AI21 Labs, Databricks, Snowflake, and many more)
with **MCP Server** (37 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
with **MCP Server** (87 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
> **Live counts (v3.8.31)**: providers 231 · MCP tools 87 · MCP scopes 30 · A2A skills 6 ·
> open-sse services 115 · routing strategies 15 · auto-combo scoring factors 9 ·
> DB modules 83 · DB migrations 97 · base tables 17 · search providers 11 ·
> i18n locales 42. **Refresh with `npm run check:docs-all`.**
## Doc Accuracy Discipline (read before writing any doc)
> **If `grep -rn "name" src/ open-sse/ bin/` returns nothing, the name does not exist. Do not document it.**
The recurring failure mode in AI-generated docs is _plausible-but-unverified specifics_.
Every claim in a `.md` file under `docs/` should be verifiable against the source.
**Rules (enforced by `npm run check:fabricated-docs`):**
1. **Never state an API name, endpoint, path, CLI command, or env var without grepping for it first.**
```bash
grep -rn "theName" src/ open-sse/ bin/
# 0 hits → do not document
```
2. **Never write a line count, file size, migration count, provider count, or strategy count from memory.**
```bash
wc -l <file> # exact line count
ls <dir>/*.ts | wc -l # file count
```
3. **Every code example should be copy-pasted from real usage or actually run** — not synthesized.
Link to a real call site (`path:line`) instead of inventing a signature.
4. **Prefer citing real source (`file.ts:line`) over paraphrasing behavior** — verifiable and self-correcting.
5. **A shorter doc that is 100% accurate beats a comprehensive one with fabrications.**
Wrong docs cost more than missing docs, because people trust and act on them.
The script `scripts/check/check-fabricated-docs.mjs` extracts every route path, env var, hook
name, function name, and file reference from `docs/**/*.md` and verifies each one against the
codebase. Run it locally before pushing docs; it runs in CI via `npm run check:docs-all`.
## Stack
- **Runtime**: Next.js 16 (App Router), Node.js `>=20.20.2 <21`, `>=22.22.2 <23`, or `>=24.0.0 <25`, ES Modules (`"type": "module"`)
- **Language**: TypeScript 5.9 (`src/`) + JavaScript (`open-sse/`, `electron/`)
- **Runtime**: Next.js 16 (App Router), Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Language**: TypeScript 6.0 (`src/`) + JavaScript (`open-sse/`, `electron/`)
- **Database**: better-sqlite3 (SQLite) — `DATA_DIR` configurable, default `~/.omniroute/`
- **Streaming**: SSE via `open-sse` internal workspace package
- **Styling**: Tailwind CSS v4
- **i18n**: next-intl with 40+ languages
- **i18n**: next-intl with 42 locales (`src/i18n/messages/`) — refresh with `ls src/i18n/messages/*.json | wc -l`
- **Desktop**: Electron (cross-platform: Windows, macOS, Linux)
- **Schemas**: Zod v4 for all API / MCP input validation
@@ -23,19 +57,32 @@ with **MCP Server** (37 tools), **A2A v0.3 Protocol**, and **Electron desktop ap
## Build, Lint, and Test Commands
| Command | Description |
| ----------------------------------- | --------------------------------- |
| `npm run dev` | Start Next.js dev server |
| `npm run build` | Production build (isolated) |
| `npm run start` | Run production build |
| `npm run build:cli` | Build CLI package |
| `npm run lint` | ESLint on all source files |
| `npm run typecheck:core` | TypeScript core type checking |
| `npm run typecheck:noimplicit:core` | Strict checking (no implicit any) |
| `npm run check` | Run lint + test |
| `npm run check:cycles` | Check for circular dependencies |
| `npm run electron:dev` | Run Electron app in dev mode |
| `npm run electron:build` | Build Electron app for current OS |
| Command | Description |
| ----------------------------------- | ------------------------------------------------------------------ |
| `npm run dev` | Start Next.js dev server |
| `npm run build` | Production build: `next build` → `.build/next/` + assemble `dist/` |
| `npm run build:release` | Clean rebuild + HEAD sentinel (`dist/BUILD_SHA`) — use for deploy |
| `npm run start` | Run production build |
| `npm run build:cli` | Build CLI package |
| `npm run lint` | ESLint on all source files |
| `npm run typecheck:core` | TypeScript core type checking |
| `npm run typecheck:noimplicit:core` | Strict checking (no implicit any) |
| `npm run check` | Run lint + test |
| `npm run check:cycles` | Check for circular dependencies |
| `npm run electron:dev` | Run Electron app in dev mode |
| `npm run electron:build` | Build Electron app for current OS |
**Build output layout:**
| Directory | Purpose | Gitignored |
| --------- | -------------------------------------------------- | ---------- |
| `src/` | Application source (TypeScript / TSX) | No |
| `.build/` | Build intermediates (`distDir = .build/next`) | Yes |
| `dist/` | Shippable bundle assembled by `assembleStandalone` | Yes |
The pipeline is a single `next build` pass — intermediates land in `.build/next/`, the
assembled bundle in `dist/`. VPS deploys rsync `dist/` into the remote
`/usr/lib/node_modules/omniroute/app/` directory (VPS image path is unchanged).
### Running Tests
@@ -131,7 +178,7 @@ Always run `prettier --write` on changed files.
### Data Layer (`src/lib/db/`)
All persistence uses SQLite through **45+ domain-specific modules** in `src/lib/db/`. Top modules:
All persistence uses SQLite through **83 domain-specific modules** in `src/lib/db/`. Top modules:
- Core: `core.ts`, `migrationRunner.ts`, `encryption.ts`, `stateReset.ts`
- Providers / catalog: `providers.ts`, `models.ts`, `providerLimits.ts`, `compressionAnalytics.ts`
@@ -141,17 +188,17 @@ All persistence uses SQLite through **45+ domain-specific modules** in `src/lib/
- Storage: `backup.ts`, `cleanup.ts`, `jsonMigration.ts`, `healthCheck.ts`, `databaseSettings.ts`
- Extension modules: `evals.ts`, `webhooks.ts`, `reasoningCache.ts`, `readCache.ts`, `tierConfig.ts`, `compressionCombos.ts`, `compressionScheduler.ts`, `batches.ts`, `files.ts`, `syncTokens.ts`, `proxies.ts`, `oneproxy.ts`, `upstreamProxy.ts`, `versionManager.ts`, `cliToolState.ts`, `prompts.ts`, `detailedLogs.ts`, `contextHandoffs.ts`, `compression.ts`, `stats.ts`
Live count: `ls src/lib/db/*.ts | wc -l` (currently 45).
Schema migrations live in `db/migrations/` (55 files) and run via `migrationRunner.ts`.
Live count: `ls src/lib/db/*.ts | wc -l` (currently 83). Drift detection: `npm run check:docs-counts`.
Schema migrations live in `db/migrations/` (**97 files** as of v3.8.24) and run via `migrationRunner.ts`.
`src/lib/localDb.ts` is a **re-export layer only** — never add logic there.
#### DB Internals
- **`core.ts`**: `getDbInstance()` returns a singleton `better-sqlite3` instance with WAL
journaling. `SCHEMA_SQL` defines 15 base tables. Helpers: `rowToCamel`, `encryptConnectionFields`.
journaling. `SCHEMA_SQL` defines **17 base tables** (verify with `grep -c "CREATE TABLE" src/lib/db/core.ts` minus 1 for the bookkeeping `_omniroute_migrations` table). Helpers: `rowToCamel`, `encryptConnectionFields`.
- **`migrationRunner.ts`**: Applies versioned SQL files from `db/migrations/` inside transactions.
Tracks applied migrations in `_omniroute_migrations` table.
- **Migrations**: 55 files (`001_initial_schema.sql``055_command_code_auth_sessions.sql`).
- **Migrations**: 97 files (`001_initial_schema.sql` → `099_*.sql`).
Each migration is idempotent and runs in a transaction. Live count: `ls src/lib/db/migrations/*.sql | wc -l`.
- **Domain modules** import `getDbInstance()` from `core.ts` for all CRUD operations.
Each module owns a specific table/set of tables (e.g., `providers.ts` → `provider_connections`,
@@ -167,19 +214,19 @@ Route → CORS preflight → Body validation (Zod) → Optional auth (extractApi
→ API key policy enforcement (enforceApiKeyPolicy) → Handler delegation (open-sse)
```
| Route | Handler | Notes |
| ------------------------------- | ------------------------- | ----------------------------------------- |
| `chat/completions/route.ts` | `handleChat()` | + prompt injection guard (clones request) |
| `responses/route.ts` | `handleChat()` (unified) | Responses API format |
| `embeddings/route.ts` | `handleEmbedding()` | Model listing + creation |
| `images/generations/route.ts` | `handleImageGeneration()` | Model listing + creation |
| `audio/transcriptions/route.ts` | audio handler | Multipart form data |
| `audio/speech/route.ts` | TTS handler | Binary audio response |
| `videos/generations/route.ts` | video handler | ComfyUI/SD WebUI |
| `music/generations/route.ts` | music handler | ComfyUI workflows |
| `moderations/route.ts` | moderation handler | Content safety |
| `rerank/route.ts` | rerank handler | Document relevance |
| `search/route.ts` | search handler | Web search (5 providers) |
| Route | Handler | Notes |
| ------------------------------- | ------------------------- | ------------------------------------------------------------- |
| `chat/completions/route.ts` | `handleChat()` | + prompt injection guard (clones request) |
| `responses/route.ts` | `handleChat()` (unified) | Responses API format |
| `embeddings/route.ts` | `handleEmbedding()` | Model listing + creation |
| `images/generations/route.ts` | `handleImageGeneration()` | Model listing + creation |
| `audio/transcriptions/route.ts` | audio handler | Multipart form data |
| `audio/speech/route.ts` | TTS handler | Binary audio response |
| `videos/generations/route.ts` | video handler | ComfyUI/SD WebUI |
| `music/generations/route.ts` | music handler | ComfyUI workflows |
| `moderations/route.ts` | moderation handler | Content safety |
| `rerank/route.ts` | rerank handler | Document relevance |
| `search/route.ts` | search handler | Web search (12 providers per `open-sse/handlers/search.ts:6`) |
**No global Next.js middleware file** — interception is route-specific. Auth is optional
(controlled by `REQUIRE_API_KEY` env). Prompt injection guard is unique to chat completions.
@@ -289,7 +336,8 @@ Includes request/response translators with helpers for image handling.
### Services (`open-sse/services/`)
36+ service modules including: `combo.ts` (routing engine), `usage.ts`, `tokenRefresh.ts`,
115 service modules in `open-sse/services/` (top-level only; 184 including sub-dirs like `autoCombo/` and `compression/`). Refresh: `ls open-sse/services/*.ts | wc -l`. Key modules:
`combo.ts` (routing engine), `usage.ts`, `tokenRefresh.ts`,
`rateLimitManager.ts`, `accountFallback.ts`, `sessionManager.ts`, `wildcardRouter.ts`,
`autoCombo/`, `intentClassifier.ts`, `taskAwareRouter.ts`, `thinkingBudget.ts`,
`contextManager.ts`, `modelDeprecation.ts`, `modelFamilyFallback.ts`,
@@ -330,8 +378,8 @@ Modular prompt compression that runs proactively before the existing reactive co
and iterates through targets in order until one succeeds or all fail.
- **`resolveComboTargets()`**: Expands a combo configuration into an ordered array of
`ResolvedComboTarget[]`, each specifying provider + model + account + credentials.
- **Strategies** (14): priority, weighted, fill-first, round-robin, P2C, random, least-used, reset-aware (v3.8),
cost-optimized, strict-random, auto, lkgp, context-optimized, context-relay.
- **Strategies** (15): priority, weighted, fill-first, round-robin, P2C, random, least-used, reset-aware (v3.8),
reset-window, cost-optimized, strict-random, auto, lkgp, context-optimized, context-relay. Source: `ROUTING_STRATEGY_VALUES` in `src/shared/constants/routingStrategies.ts`.
- Each target calls **`handleSingleModel()`** which wraps `handleChatCore()` with
per-target error handling and circuit breaker checks.
@@ -343,7 +391,7 @@ Policy engine modules: `policyEngine.ts`, `comboResolver.ts`, `costRules.ts`,
### MCP Server (`open-sse/mcp-server/`)
37 tools (30 base + 3 memory + 4 skills), 3 transports (stdio / SSE / Streamable HTTP). Scoped auth (~13 scopes), Zod schemas. See [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md).
**87 tools** total (`TOTAL_MCP_TOOL_COUNT`, `open-sse/mcp-server/server.ts`): a 33-entry base registry (`MCP_TOOLS` in `schemas/tools.ts`, bundling the core / cache / compression / 1proxy / advanced tools) **plus** standalone module sets — memory (3), skill (4), agentSkill (3), gamification (8), plugin (8), notion (6), obsidian (22). 3 transports (stdio / SSE / Streamable HTTP). Scoped auth (30 scopes — see `OMNIROUTE_MCP_SCOPES`), Zod schemas. See [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md).
**Core tools** (20): get_health, list_combos, get_combo_metrics, switch_combo, check_quota,
route_request, cost_report, list_models_catalog, web_search, simulate_route, set_budget_guard,
@@ -361,6 +409,14 @@ list_compression_combos, compression_combo_stats.
**Skill tools** (4): skills_list, skills_enable, skills_execute, skills_executions.
**Agent-skill tools** (3): A2A skill discovery / invocation bridges.
**Gamification tools** (8): levels, badges, leaderboard, and community-federation queries.
**Plugin tools** (8): plugin marketplace listing, install/enable/disable, and runtime inspection.
**Notion tools** (6) + **Obsidian tools** (22): knowledge-base read/write integrations (the largest tool family — vault search, note CRUD, WebDAV-backed file ops).
#### MCP Internals
- **Tool registration**: Each tool is an object with `{ name, description, inputSchema: ZodSchema,
@@ -369,7 +425,7 @@ handler: async (args) => {...} }`. Zod validates inputs before the handler fires
`createMcpServer()` wires all tool sets; `startMcpStdio()` launches the stdio transport.
- **Transports**: stdio (CLI `omniroute --mcp`), SSE (`/api/mcp/sse`), Streamable HTTP
(`/api/mcp/stream`). All share the same tool/scope engine.
- **Scopes** (10): Control which tool categories an API key can access. Enforcement happens
- **Scopes** (30): Control which tool categories an API key can access. Enforcement happens
before handler dispatch.
- **Audit**: Every tool invocation is logged to SQLite (`mcp_audit` table) with tool name,
args, success/failure, API key attribution, and timestamp.
@@ -378,7 +434,7 @@ handler: async (args) => {...} }`. Zod validates inputs before the handler fires
JSON-RPC 2.0, SSE streaming, Task Manager with TTL cleanup.
Agent Card at `/.well-known/agent.json`.
Skills (5): `smartRouting.ts`, `quotaManagement.ts`, `providerDiscovery.ts`, `costAnalysis.ts`, `healthReport.ts`.
Skills (6): `smartRouting.ts`, `quotaManagement.ts`, `providerDiscovery.ts`, `costAnalysis.ts`, `healthReport.ts`, `listCapabilities.ts`.
#### A2A Internals
@@ -471,7 +527,6 @@ Cloudflare Quick/Named, ngrok, Tailscale Funnel. See [`docs/ops/TUNNELS_GUIDE.md
## Subdirectory AGENTS.md Files
- **[`open-sse/AGENTS.md`](open-sse/AGENTS.md)** — Streaming engine, request pipeline, handlers, and executors
- **[`src/lib/db/AGENTS.md`](src/lib/db/AGENTS.md)** — SQLite persistence, domain modules, migrations
- **[`open-sse/services/AGENTS.md`](open-sse/services/AGENTS.md)** — Routing engine, combo resolution, strategy selection
@@ -479,31 +534,33 @@ Cloudflare Quick/Named, ngrok, Tailscale Funnel. See [`docs/ops/TUNNELS_GUIDE.md
For any non-trivial change, read the matching deep-dive first:
| Area | Doc |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| Repo navigation | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md) |
| Architecture | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
| Engineering reference | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
| Auto-Combo (9-factor, 14 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
| Resilience (3 layers) | [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) |
| Skills | [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
| Memory | [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) |
| Cloud agents | [`docs/frameworks/CLOUD_AGENT.md`](docs/frameworks/CLOUD_AGENT.md) |
| Guardrails | [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md) |
| Evals | [`docs/frameworks/EVALS.md`](docs/frameworks/EVALS.md) |
| Compliance | [`docs/security/COMPLIANCE.md`](docs/security/COMPLIANCE.md) |
| Webhooks | [`docs/frameworks/WEBHOOKS.md`](docs/frameworks/WEBHOOKS.md) |
| Authz | [`docs/architecture/AUTHZ_GUIDE.md`](docs/architecture/AUTHZ_GUIDE.md) |
| Stealth | [`docs/security/STEALTH_GUIDE.md`](docs/security/STEALTH_GUIDE.md) |
| Reasoning replay | [`docs/routing/REASONING_REPLAY.md`](docs/routing/REASONING_REPLAY.md) |
| Agent protocols (A2A / ACP / Cloud) | [`docs/frameworks/AGENT_PROTOCOLS_GUIDE.md`](docs/frameworks/AGENT_PROTOCOLS_GUIDE.md) |
| MCP server | [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md) |
| A2A server | [`docs/frameworks/A2A-SERVER.md`](docs/frameworks/A2A-SERVER.md) |
| API reference | [`docs/reference/API_REFERENCE.md`](docs/reference/API_REFERENCE.md) + [`docs/reference/openapi.yaml`](docs/reference/openapi.yaml) |
| Provider catalog (auto-generated) | [`docs/reference/PROVIDER_REFERENCE.md`](docs/reference/PROVIDER_REFERENCE.md) |
| Tunnels | [`docs/ops/TUNNELS_GUIDE.md`](docs/ops/TUNNELS_GUIDE.md) |
| Electron desktop | [`docs/guides/ELECTRON_GUIDE.md`](docs/guides/ELECTRON_GUIDE.md) |
| Release flow | [`docs/ops/RELEASE_CHECKLIST.md`](docs/ops/RELEASE_CHECKLIST.md) |
| Area | Doc |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| Repo navigation | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md) |
| Architecture | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
| Engineering reference | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
| Auto-Combo (12-factor, 15 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
| Resilience (3 layers) | [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) |
| Skills | [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
| Memory | [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) |
| Cloud agents | [`docs/frameworks/CLOUD_AGENT.md`](docs/frameworks/CLOUD_AGENT.md) |
| Guardrails | [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md) |
| Evals | [`docs/frameworks/EVALS.md`](docs/frameworks/EVALS.md) |
| Compliance | [`docs/security/COMPLIANCE.md`](docs/security/COMPLIANCE.md) |
| Webhooks | [`docs/frameworks/WEBHOOKS.md`](docs/frameworks/WEBHOOKS.md) |
| Authz | [`docs/architecture/AUTHZ_GUIDE.md`](docs/architecture/AUTHZ_GUIDE.md) |
| Stealth | [`docs/security/STEALTH_GUIDE.md`](docs/security/STEALTH_GUIDE.md) |
| Reasoning replay | [`docs/routing/REASONING_REPLAY.md`](docs/routing/REASONING_REPLAY.md) |
| Agent protocols (A2A / ACP / Cloud) | [`docs/frameworks/AGENT_PROTOCOLS_GUIDE.md`](docs/frameworks/AGENT_PROTOCOLS_GUIDE.md) |
| MCP server | [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md) |
| A2A server | [`docs/frameworks/A2A-SERVER.md`](docs/frameworks/A2A-SERVER.md) |
| API reference | [`docs/reference/API_REFERENCE.md`](docs/reference/API_REFERENCE.md) + [`docs/reference/openapi.yaml`](docs/reference/openapi.yaml) |
| Provider catalog (auto-generated) | [`docs/reference/PROVIDER_REFERENCE.md`](docs/reference/PROVIDER_REFERENCE.md) |
| Tunnels | [`docs/ops/TUNNELS_GUIDE.md`](docs/ops/TUNNELS_GUIDE.md) |
| Electron desktop | [`docs/guides/ELECTRON_GUIDE.md`](docs/guides/ELECTRON_GUIDE.md) |
| Release flow | [`docs/ops/RELEASE_CHECKLIST.md`](docs/ops/RELEASE_CHECKLIST.md) |
| Quality gates (35 gates, allowlist policy) | [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md) |
| Cluster opt-in profiles (memory, bifrost) | [`docs/architecture/cluster-decisions.md`](docs/architecture/cluster-decisions.md) |
---

File diff suppressed because it is too large Load Diff

180
CLAUDE.md
View File

@@ -11,7 +11,7 @@ npm run build # Production build (Next.js 16 standalone)
npm run lint # ESLint (0 errors expected; warnings are pre-existing)
npm run typecheck:core # TypeScript check (should be clean)
npm run typecheck:noimplicit:core # Strict check (no implicit any)
npm run test:coverage # Unit tests + coverage gate (40/40/40/40 — statements/lines/functions/branches)
npm run test:coverage # Unit tests + coverage gate (60/60/60/60 — statements/lines/functions/branches)
npm run check # lint + test combined
npm run check:cycles # Detect circular dependencies
```
@@ -35,22 +35,22 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit
## Project at a Glance
**OmniRoute** — unified AI proxy/router. One endpoint, 160+ LLM providers, auto-fallback.
**OmniRoute** — unified AI proxy/router. One endpoint, 231 LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------ |
| API Routes | `src/app/api/v1/` | Next.js App Router — entry points |
| Handlers | `open-sse/handlers/` | Request processing (chat, embeddings, etc) |
| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
| Database | `src/lib/db/` | SQLite domain modules (45+ files, 55 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 43 tools (30 base + 3 memory + 4 skills + 6 notion), 3 transports, ~13 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
| Skills | `src/lib/skills/` | Extensible skill framework |
| Memory | `src/lib/memory/` | Persistent conversational memory |
| Layer | Location | Purpose |
| ------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| API Routes | `src/app/api/v1/` | Next.js App Router — entry points |
| Handlers | `open-sse/handlers/` | Request processing (chat, embeddings, etc) |
| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
| Database | `src/lib/db/` | SQLite domain modules (83 files, 97 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 87 tools (33 base + memory/skill/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 30 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
| Skills | `src/lib/skills/` | Extensible skill framework |
| Memory | `src/lib/memory/` | Persistent conversational memory |
Monorepo: `src/` (Next.js 16 app), `open-sse/` (streaming engine workspace), `electron/` (desktop app), `tests/`, `bin/` (CLI entry point).
@@ -72,7 +72,7 @@ Client → /v1/chat/completions (Next.js route)
API routes follow a consistent pattern: `Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse)`. No global Next.js middleware — interception is route-specific.
**Combo routing** (`open-sse/services/combo.ts`): 14 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, strict-random, auto, lkgp, context-optimized, context-relay). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. See `docs/routing/AUTO-COMBO.md` for the 9-factor Auto-Combo scoring and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
**Combo routing** (`open-sse/services/combo.ts`): 15 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, reset-window, strict-random, auto, lkgp, context-optimized, context-relay). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. See `docs/routing/AUTO-COMBO.md` for the 9-factor Auto-Combo scoring and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
---
@@ -327,32 +327,33 @@ connection continue serving other models.
For any non-trivial change, read the matching deep-dive first:
| Area | Doc |
| -------------------------------------------- | ----------------------------------------------------------------- |
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
| Architecture | `docs/architecture/ARCHITECTURE.md` |
| Engineering reference | `docs/architecture/CODEBASE_DOCUMENTATION.md` |
| Auto-Combo (9-factor scoring, 14 strategies) | `docs/routing/AUTO-COMBO.md` |
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
| Skills framework | `docs/frameworks/SKILLS.md` |
| Memory system (FTS5 + Qdrant) | `docs/frameworks/MEMORY.md` |
| Cloud agents | `docs/frameworks/CLOUD_AGENT.md` |
| Guardrails (PII / injection / vision) | `docs/security/GUARDRAILS.md` |
| Public upstream credentials (Gemini/etc.) | `docs/security/PUBLIC_CREDS.md` |
| Error message sanitization | `docs/security/ERROR_SANITIZATION.md` |
| Evals | `docs/frameworks/EVALS.md` |
| Compliance / audit | `docs/security/COMPLIANCE.md` |
| Webhooks | `docs/frameworks/WEBHOOKS.md` |
| Authorization pipeline | `docs/architecture/AUTHZ_GUIDE.md` |
| Stealth (TLS / fingerprint) | `docs/security/STEALTH_GUIDE.md` |
| Agent protocols (A2A / ACP / Cloud) | `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` |
| MCP server | `docs/frameworks/MCP-SERVER.md` |
| A2A server | `docs/frameworks/A2A-SERVER.md` |
| API reference + OpenAPI | `docs/reference/API_REFERENCE.md` + `docs/reference/openapi.yaml` |
| Provider catalog (auto-generated) | `docs/reference/PROVIDER_REFERENCE.md` |
| Release flow | `docs/ops/RELEASE_CHECKLIST.md` |
| Embedded services | `docs/frameworks/EMBEDDED-SERVICES.md` |
| Area | Doc |
| --------------------------------------------- | ----------------------------------------------------------------- |
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
| Architecture | `docs/architecture/ARCHITECTURE.md` |
| Engineering reference | `docs/architecture/CODEBASE_DOCUMENTATION.md` |
| Auto-Combo (9-factor scoring, 15 strategies) | `docs/routing/AUTO-COMBO.md` |
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
| Skills framework | `docs/frameworks/SKILLS.md` |
| Memory system (FTS5 + Qdrant) | `docs/frameworks/MEMORY.md` |
| Cloud agents | `docs/frameworks/CLOUD_AGENT.md` |
| Guardrails (PII / injection / vision) | `docs/security/GUARDRAILS.md` |
| Public upstream credentials (Gemini/etc.) | `docs/security/PUBLIC_CREDS.md` |
| Error message sanitization | `docs/security/ERROR_SANITIZATION.md` |
| Evals | `docs/frameworks/EVALS.md` |
| Compliance / audit | `docs/security/COMPLIANCE.md` |
| Webhooks | `docs/frameworks/WEBHOOKS.md` |
| Authorization pipeline | `docs/architecture/AUTHZ_GUIDE.md` |
| Stealth (TLS / fingerprint) | `docs/security/STEALTH_GUIDE.md` |
| Agent protocols (A2A / ACP / Cloud) | `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` |
| MCP server | `docs/frameworks/MCP-SERVER.md` |
| A2A server | `docs/frameworks/A2A-SERVER.md` |
| API reference + OpenAPI | `docs/reference/API_REFERENCE.md` + `docs/reference/openapi.yaml` |
| Provider catalog (auto-generated) | `docs/reference/PROVIDER_REFERENCE.md` |
| Release flow | `docs/ops/RELEASE_CHECKLIST.md` |
| Embedded services | `docs/frameworks/EMBEDDED-SERVICES.md` |
| Quality gates (~48 scripts, allowlist policy) | `docs/architecture/QUALITY_GATES.md` |
---
@@ -366,14 +367,24 @@ For any non-trivial change, read the matching deep-dive first:
| E2E (Playwright) | `npm run test:e2e` |
| Protocol E2E (MCP+A2A) | `npm run test:protocols:e2e` |
| Ecosystem | `npm run test:ecosystem` |
| Coverage gate | `npm run test:coverage` (40/40/40/40 — statements/lines/functions/branches) |
| Coverage gate | `npm run test:coverage` (60/60/60/60 — statements/lines/functions/branches) |
| Coverage report | `npm run coverage:report` |
**PR rule**: If you change production code in `src/`, `open-sse/`, `electron/`, or `bin/`, you must include or update tests in the same PR.
**Test layer preference**: unit first → integration (multi-module or DB state) → e2e (UI/workflow only). Encode bug reproductions as automated tests before or alongside the fix.
**Copilot coverage policy**: When a PR changes production code and coverage is below 40% (statements/lines/functions/branches), do not just report — add or update tests, rerun the coverage gate, then ask for confirmation. Include commands run, changed test files, and final coverage result in the PR report.
**Both test runners must pass**: `npm run test:unit` (Node native — most tests) AND `npm run test:vitest` (MCP server, autoCombo, cache) cover **non-overlapping files**. Both are wired in CI (jobs `test-unit` and `test-vitest`) and must be green before merging. A PR where only one suite passes may silently ship broken MCP tools or routing regressions.
**Bug fix / issue triage protocol (Hard Rule #18)**: Every fix for a reported issue must be validated by one of the following — no exceptions:
1. **TDD (preferred)** — write a failing test reproducing the bug → fix it → confirm the test passes. The test becomes the permanent regression guard. Touch only the files the test proves need changing; nothing more.
2. **Real-environment test (when TDD is not possible)** — deploy to the production VPS (`root@192.168.0.15`) and run a documented live test. Record the exact command + result in the PR description. Applies to: OAuth upstream flows, Cloudflare/WS upstream behavior, UI-only regressions, hardware-dependent behavior.
3. "It worked locally without a test" does not count. A fix without a test or a VPS validation record is not a fix — it is a guess.
Why this matters: fixing bug A while opening bug B is worse than not fixing at all. The TDD/VPS gate enforces surgical scope — you touch only what the failing test proves is broken. Examples where this paid off: #3090 (claude-web 403), #3113 (WS HTTP fallback), #3052 (heap-guard auto-calibration).
**Copilot coverage policy**: When a PR changes production code and coverage is below 60% (statements/lines/functions/branches), do not just report — add or update tests, rerun the coverage gate, then ask for confirmation. Include commands run, changed test files, and final coverage result in the PR report.
---
@@ -393,14 +404,51 @@ git push -u origin feat/your-feature
**Husky hooks**:
- **pre-commit**: lint-staged + `check-docs-sync` + `check:any-budget:t11`
- **pre-push**: `npm run test:unit`
- **pre-push**: fast deterministic gates (`check:any-budget:t11` + `check:tracked-artifacts`); intentionally excludes `test:unit` (slow — covered by the CI `test-unit` job). Activated 2026-06-13 (Quality Gates Fase 6A.12).
### Worktree isolation (MANDATORY for every development task)
Multiple sessions/agents work this repo in parallel. The main checkout is **shared**, so a
`git checkout`/branch switch in it silently discards another session's uncommitted work and
yanks the branch out from under whatever else is running (incidents: 2026-06-05, 2026-06-13).
**Rule: never develop on the shared main checkout. Every task gets its own git worktree on its
own dedicated branch, and you MUST confirm the base branch with the operator before creating it.**
1. **Ask first — which base branch?** Before creating anything, ask the operator (via
`AskUserQuestion`, unless they already told you) from which branch the new worktree/branch
should be cut. Do NOT assume `main` or "whatever I'm on" — the answer is usually the active
`release/vX.Y.Z`, but it can be another feature/release branch. Get the base explicitly.
2. **Create an isolated worktree + branch off that base** (never reuse the main checkout):
```bash
BASE_BRANCH="release/vX.Y.Z" # ← the branch the operator confirmed in step 1
TASK="feat/your-feature" # feat/ fix/ refactor/ docs/ test/ chore/
git fetch origin "$BASE_BRANCH"
git worktree add ".worktrees/${TASK##*/}" -b "$TASK" "origin/$BASE_BRANCH"
cd ".worktrees/${TASK##*/}"
# symlink node_modules from the main checkout to skip a per-worktree npm install:
ln -s "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
```
In Claude Code prefer the native `EnterWorktree` tool (create the worktree with the command
above, then call `EnterWorktree` with its `path`).
3. **Work, commit, push, open the PR — all from inside the worktree.** Never `git checkout` a
different branch inside a worktree another session might share.
4. **Tear down only your own** worktree + branch when done, from the main checkout:
`git worktree remove .worktrees/<dir>` then `git branch -D <task>`. Never blanket-delete
`fix/*`/`feat/*` — other sessions keep their own; delete only the branches you created, by name.
5. **Never touch another session's worktree, branch, or uncommitted changes.** If `git worktree
list` shows worktrees you didn't create, leave them alone. End every session with the main
checkout back on the branch it started on (the active `release/vX.Y.Z`, never `main`).
---
## Environment
- **Runtime**: Node.js ≥20.20.2 <21 || ≥22.22.2 <23 || ≥24 <25, ES Modules
- **TypeScript**: 5.9+, target ES2022, module esnext, resolution bundler
- **Runtime**: Node.js ≥22.0.0 <23 || ≥24.0.0 <27, ES Modules
- **TypeScript**: 6.0+, target ES2022, module esnext, resolution bundler
- **Path aliases**: `@/*` → `src/`, `@omniroute/open-sse` → `open-sse/`, `@omniroute/open-sse/*` → `open-sse/*`
- **Default port**: 20128 (API + dashboard on same port)
- **Data directory**: `DATA_DIR` env var, defaults to `~/.omniroute/`
@@ -409,6 +457,33 @@ git push -u origin feat/your-feature
---
## Quality Gates & Ratchets
OmniRoute has **~48 quality-gate scripts** (`scripts/check/` + `scripts/quality/`) wired
across **9 gate-running jobs** in `.github/workflows/ci.yml` (`lint`, `quality-gate`,
`quality-extended`, `docs-sync-strict`, `i18n-ui-coverage`, `i18n`, `pr-test-policy`,
`test-vitest`, `sonarqube`), plus the `quality.yml` fast-gates job (PR→`release/**`) and
3 nightly workflows (`nightly-property`, `nightly-resilience`, `nightly-llm-security`;
`nightly-mutation` once merged). Full inventory, per-job breakdown, and operational
procedures are in [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md).
**Quick reference:**
- Gates in jobs `lint` + `docs-sync-strict`: pass/fail policy gates —
fix the violation or add an allowlist entry with a justification comment + tracking issue.
- Gates in job `quality-gate`: ratchet — metrics (ESLint warnings, code coverage, duplication,
complexity) must not regress vs `quality-baseline.json`. Update via
`npm run quality:ratchet -- --update` when a metric genuinely improves.
- Job `test-vitest` runs `npm run test:vitest` (MCP tools, autoCombo, cache) — blocking.
`test:vitest:ui` is advisory until UI component tests are triaged.
**Allowlist policy (short form):** Fix the cause; use the allowlist only for pre-existing
violations you cannot fix in the same PR. Add a comment with justification + issue number.
Stale allowlist entries (suppressing a violation that no longer exists) will be caught by
the stale-enforcement added in Fase 6A.3.
---
## Hard Rules
1. Never commit secrets or credentials
@@ -419,25 +494,30 @@ git push -u origin feat/your-feature
6. Never silently swallow errors in SSE streams
7. Always validate inputs with Zod schemas
8. Always include tests when changing production code
9. Coverage must stay ≥40% (statements, lines, functions, branches).
9. Coverage must not regress below the baseline frozen in `quality-baseline.json` (ratchet); absolute floor is 60% (statements/lines/functions/branches). Update the baseline via `npm run quality:ratchet -- --update` only when coverage genuinely improves. See `docs/architecture/QUALITY_GATES.md`.
10. Never bypass Husky hooks (`--no-verify`, `--no-gpg-sign`) without explicit operator approval.
11. Never embed public upstream OAuth client_id/secret or Firebase Web keys as string literals — always go through `resolvePublicCred()` (`open-sse/utils/publicCreds.ts`). See `docs/security/PUBLIC_CREDS.md`.
12. Never return raw `err.stack` / `err.message` in HTTP / SSE / executor responses — always route through `buildErrorBody()` or `sanitizeErrorMessage()` (`open-sse/utils/error.ts`). See `docs/security/ERROR_SANITIZATION.md`.
13. Never string-interpolate external paths or runtime values into shell scripts passed to `exec()`/`spawn()` — pass via the `env` option instead. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
14. Never dismiss a CodeQL / Secret-Scanning alert without (a) first checking the pattern docs above to see if the helper applies, and (b) recording the technical justification in the dismissal comment. Precedent: `js/stack-trace-exposure` raised on callsites that already route through `sanitizeErrorMessage()` is a known CodeQL limitation (custom sanitizers not recognized) — dismiss as `false positive` referencing `docs/security/ERROR_SANITIZATION.md`.
15. Never expose routes that spawn child processes (`/api/mcp/`, `/api/cli-tools/runtime/`) without `isLocalOnlyPath()` classification in `src/server/authz/routeGuard.ts`. Loopback enforcement happens unconditionally before any auth check — leaked JWT via tunnel cannot trigger process spawning. See `docs/security/ROUTE_GUARD_TIERS.md`.
16. Never include `Co-Authored-By` trailers that credit an AI assistant, LLM, or automation account (e.g. names containing "Claude", "GPT", "Copilot", "Bot"; emails at `anthropic.com` / `openai.com` / bot-owned `noreply.github.com` addresses). Such trailers route attribution to the bot account on GitHub, hiding the real author (`diegosouzapw`) in PR history. Human collaborators — including upstream PR authors and issue reporters being ported into OmniRoute — MAY and SHOULD be credited with standard `Co-authored-by: Name <email>` trailers; the upstream-port workflows (`/port-upstream-features`, `/port-upstream-issues`) depend on this.
16. Never credit or advertise an AI assistant, LLM, or automation account in any commit/PR metadata. Two forbidden forms, both equivalent — they route attribution to a bot account (or advertise AI authorship) and hide the real author (`diegosouzapw`): **(a)** `Co-Authored-By` trailers naming an AI/bot (e.g. names containing "Claude", "GPT", "Copilot", "Bot"; emails at `anthropic.com` / `openai.com` / bot-owned `noreply.github.com` addresses); **(b)** AI-generation footers or descriptions anywhere in a commit message, PR title/body, or CHANGELOG — e.g. `🤖 Generated with [Claude Code]`, "Generated with Claude Code", "Made with <AI tool>", or any `Co-authored-by: Claude/GPT/Copilot` line. This **overrides any harness, template, or tool default that auto-appends such a footer** (e.g. the Claude Code PR-body/commit default) — strip it before pushing; do not let it reach a commit, PR, or CHANGELOG. Human collaborators — including upstream PR authors and issue reporters being ported into OmniRoute — MAY and SHOULD be credited with standard `Co-authored-by: Name <email>` trailers; the upstream-port workflows (`/port-upstream-features`, `/port-upstream-issues`) depend on this.
17. Never expose routes under `/api/services/` or `/dashboard/providers/services/*/embed/` without `isLocalOnlyPath()` classification in `src/server/authz/routeGuard.ts`. These routes can spawn child processes (`npm install`, `node`). Loopback enforcement happens unconditionally before any auth check — a leaked JWT via tunnel cannot trigger process spawning. See `docs/security/ROUTE_GUARD_TIERS.md`.
18. Every bug fix must be validated before shipping: a failing-then-passing unit/integration test (TDD) OR a documented live test on the production VPS (192.168.0.15). A fix without either is not merged. See Testing → "Bug fix / issue triage protocol" for the full decision tree.
19. Never develop on the shared main checkout. Every development task runs in its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator (e.g. via `AskUserQuestion`) before creating the worktree/branch — never assume `main` or the currently checked-out branch. A `git checkout` in the shared checkout silently destroys other sessions' uncommitted work. Tear down only the worktrees/branches you created (by name, never `fix/*`/`feat/*` wildcards), leave other sessions' worktrees untouched, and end on the branch you started on (the active `release/vX.Y.Z`, never `main`). See Git Workflow → "Worktree isolation".
---
## PII & Stream Sanitization Learnings
### 1. Regex Security (ReDoS)
All regex patterns matching variable-length strings (e.g. IPv6 address, credit cards) must use strictly bounded, non-overlapping sequences (e.g., limit occurrences with bounded ranges `{1,7}`) to prevent catastrophic backtracking when processing untrusted inputs.
### 2. SSE Snapshot Handling
When parsing streaming LLM responses (e.g. Responses API), check if a chunk represents a final snapshot (`done` or `completed` events). Snapshot text must be sanitized directly as a standalone string (bypassing rolling delta buffers) to prevent text duplication at the end of the stream.
### 3. Database Handles in Tests
Ensure that any unit tests that trigger database migrations or establish SQLite connections call `resetDbInstance()` and properly clean up/close all DB handles in a `test.after(...)` hook. Failure to release database connection handles will cause Node's native test runner to hang indefinitely.

View File

@@ -59,13 +59,40 @@ These settings are stored in the database and persist across restarts, overridin
npm run dev
# Production build
npm run build
npm run build # next build → .build/next/ then assembleStandalone → dist/
npm run start
# Release build (clean rebuild + HEAD sentinel — required for deploy)
npm run build:release # rm -rf .build dist && build + writes dist/BUILD_SHA
# Common port configuration
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
```
### Build Output Layout
| Directory | Contents | Tracked |
| --------- | ------------------------------------------------------------------------- | ------- |
| `src/` | Application source (TypeScript / TSX) | Yes |
| `.build/` | Intermediates — `next build` output (gitignored, `distDir = .build/next`) | No |
| `dist/` | Shippable bundle — assembled by `assembleStandalone` (gitignored) | No |
The build pipeline is a single pass:
```
npm run build
└─ next build → .build/next/standalone (Next.js output)
└─ assembleStandalone() (copies standalone + static + public + native assets)
└─ output: dist/ (server.js, .next/static/, public/, node_modules/)
```
`npm run build:release` additionally cleans both directories first and writes
`dist/BUILD_SHA` (= `git rev-parse --short HEAD`) as a deploy integrity sentinel.
> **VPS deploy note:** the remote image directory `/usr/lib/node_modules/omniroute/app/`
> is unchanged. The deploy skills rsync the contents of `dist/` into it.
> Only the in-repo build output path moved (`app/` → `dist/`).
Default URLs:
- **Dashboard**: `http://localhost:20128/dashboard`
@@ -225,7 +252,7 @@ open-sse/ # @omniroute/open-sse workspace
electron/ # Electron desktop app (cross-platform)
tests/
├── unit/ # Node.js test runner (122 test files)
├── unit/ # Node.js test runner (1,574 test files)
├── integration/ # Integration tests
├── e2e/ # Playwright tests
├── security/ # Security tests
@@ -314,6 +341,10 @@ Write unit tests in `tests/unit/` covering at minimum:
Releases are managed via the `/generate-release` workflow. When a new GitHub Release is created, the package is **automatically published to npm** via GitHub Actions.
For VPS deploys, use `npm run build:release` (not `npm run build`) — it performs a clean
rebuild, assembles the bundle into `dist/`, and writes the `dist/BUILD_SHA` sentinel.
Then use the `/deploy-vps-*-cc` skills which rsync `dist/` to the remote `app/` directory.
---
## Getting Help

254
DESING.md Normal file
View File

@@ -0,0 +1,254 @@
# OmniRoute — Design System & Visual Identity
> **Status:** analysis + standardization plan (no code applied yet — this doc is the spec to approve before implementation).
> **Date:** 2026-06-16 · **Scope:** unify the OmniRoute dashboard (`src/`) with the marketing site (`_mono_repo/omnirouteSite/`) into **one visual identity** — same graph-paper grid background, same color tokens, standardized components.
---
## 1. Purpose
The marketing site (`viral.omniroute.online`, `why.omniroute.online`, `omniroute.online`) and the product dashboard should look like **one product**. The site already borrowed its palette from the dashboard — its `css/tokens.css` even says _"Palette mirrors the OmniRoute dashboard (src/app/globals.css)"_. So the two are already ~80% aligned at the color level. What's missing on the dashboard:
1. The **graph-paper grid wallpaper** the site uses on every page.
2. A handful of **shared design tokens** the site has but the dashboard lacks (radius scale, brand gradient, `surface-2`, mono font).
3. **Component-level consistency** — a number of dashboard components bypass the theme tokens with hardcoded hex/rgba.
This document is the analysis and the plan. **Nothing is changed until approved.**
---
## 2. Principles
- **Single source of truth = `src/app/globals.css`.** The site mirrors the dashboard, never the other way around. New tokens land in `globals.css` first.
- **Tokens, never literals.** Components consume semantic tokens (`bg-surface`, `text-primary`, `border-border`), never raw `#hex`.
- **Subtle, not loud.** The grid is a faint wallpaper that sits behind content — it must never reduce text contrast or fight the UI.
- **Theme-aware.** Everything works in both `.dark` (default-ish, the product's signature look) and light.
- **Surgical rollout.** Ship the grid + tokens first (low risk, high visibility), then component cleanups in waves.
---
## 3. Current state — what's already aligned vs. what's not
### 3.1 Colors — already unified ✅
Every brand color and surface already matches the site **by value** (only the names differ — dashboard prefixes with `--color-`). Verified in `src/app/globals.css:30-128`:
| Concept | Site token (`tokens.css`) | Dashboard token (`globals.css`) | Match |
| -------------------------- | ------------------------------------------- | ------------------------------- | ------------ |
| primary | `--primary #e54d5e` | `--color-primary #e54d5e` | ✅ |
| primary-hover | `--primary-hover #c93d4e` | `--color-primary-hover #c93d4e` | ✅ |
| accent | `--accent #6366f1` | `--color-accent #6366f1` | ✅ |
| accent-2 | `--accent-2 #8b5cf6` | `--color-accent-hover #8b5cf6` | ✅ (renamed) |
| accent-3 | `--accent-3 #a855f7` | `--color-accent-light #a855f7` | ✅ (renamed) |
| success / warning / error | `#22c55e / #f59e0b / #ef4444` | identical | ✅ |
| traffic lights | `#ff5f56 / #ffbd2e / #27c93f` | identical | ✅ |
| dark bg / surface / border | `#0b0e14 / #161b22 / rgba(255,255,255,.08)` | identical | ✅ |
| light bg / surface / text | `#f9f9fb / #fff / #1a1a2e` | identical | ✅ |
**Conclusion:** there is no color migration to do. The identity is already shared; we are _finishing_ it, not rebuilding it.
### 3.2 Gaps — what the dashboard is missing
| Gap | Site has | Dashboard | Action |
| ----------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------- | ---------------------- |
| **Grid wallpaper** | `body::before` graph-paper, `--grid-line`, `--grid-size 46px`, `--section-alt` | none (flat `--color-bg`) | **Part A** |
| **Radius scale** | `--radius 14px`, `--radius-sm 9px` | none — primitives use ad-hoc `rounded-md/lg/xl` (6/8/12px) | **Part B** |
| **Brand gradient** | `--grad-brand 135deg primary→accent-3` | none — only a one-off `.bg-hero-gradient` | **Part B** |
| **Nested surface** | `--surface-2 #1c2230` | none | **Part B** |
| **Mono font** | `--font-mono` (ui-monospace stack) | none (code/terminal areas have no token) | **Part B** |
| **`text-muted` (dark)** | `#8b8b9e` | `#a1a1aa` (zinc-400) | reconcile — **Part B** |
### 3.3 Theming mechanics (so we don't break anything)
- **Tailwind v4, CSS-first** (no `tailwind.config.*`). Tokens are defined in `:root`/`.dark` and exposed to utilities via `@theme inline` (`globals.css:130-179`).
- **Dark via `.dark` class** on `<html>` (`@custom-variant dark` at `globals.css:22`), toggled by a custom Zustand store (`src/store/themeStore.ts`), default theme = `system` (`src/shared/constants/appConfig.ts:11`). The site uses `html[data-theme="light"]` instead — **the mechanisms differ but never meet** (separate origins), so no conflict. We keep the dashboard's `.dark` mechanism.
- **Runtime primary override** exists (`themeStore.ts:85-97`, presets in `COLOR_THEMES`) — users can swap `--color-primary`. Any new token (gradient, etc.) that references `--color-primary` will inherit those overrides for free. ✅
---
## 4. Part A — The graph-paper grid background (headline ask)
### 4.1 What it is
The exact recipe from the site (`_mono_repo/omnirouteSite/css/base.css`): a **fixed, full-viewport pseudo-element** painting two 1px line gradients, sitting at `z-index:-1` behind all content.
```css
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background-image:
linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
background-size: var(--grid-size) var(--grid-size);
}
```
**Why this works even though `body` has an opaque `background-color`:** a `::before` with `z-index:-1` paints _above_ the element's own background but _below_ its in-flow content. So `--color-bg` is the base fill, the grid is layered on top of it, and the app renders above the grid.
### 4.2 Precedent already in the codebase
`src/app/landing/page.tsx:16-26` **already implements this same grid per-page** — but with **red** lines (`#E54D5E`, opacity `0.06`) at **50px**, plus animated orbs. So the pattern is proven in the product; we are promoting it to a **global, theme-aware** wallpaper and (optionally) retiring the duplicate.
### 4.3 Tokens to add (in `globals.css`)
```css
:root {
/* light */
--grid-line: rgba(0, 0, 0, 0.045);
--grid-size: 46px;
--section-alt: rgba(0, 0, 0, 0.022);
}
.dark {
/* dark */
--grid-line: rgba(255, 255, 255, 0.035);
--section-alt: rgba(255, 255, 255, 0.018);
}
```
### 4.4 The single blocker
The grid is global by construction (it covers the panel, `auth`/`login`, error pages — every route — at once). Exactly **one** element hides it inside the panel:
- `src/shared/components/layouts/DashboardLayout.tsx:62` — the outer wrapper paints an opaque `bg-bg`:
```jsx
<div className="flex h-dvh min-h-0 w-full overflow-hidden bg-bg">
```
Everything below it is already transparent — `<main>` (`:93`), the scroll container (`:102`), the `max-w-7xl` inner (`:103`). So **removing `bg-bg` from this one line** lets the body grid show through the entire content area (the body's `--color-bg` remains the base fill underneath the grid).
```diff
- <div className="flex h-dvh min-h-0 w-full overflow-hidden bg-bg">
+ <div className="flex h-dvh min-h-0 w-full overflow-hidden">
```
### 4.5 Chrome interaction (sidebar / header)
- `Header` (`src/shared/components/Header.tsx:207`, `bg-bg`) and `Sidebar` (`src/shared/components/Sidebar.tsx:430`, `bg-sidebar`) stay **opaque** → the grid shows in the **content area only**, with solid chrome framing it. This is the recommended, calm default and matches how the site separates chrome from canvas.
- _Optional vibrancy variant:_ make the header translucent (`bg-bg/80 backdrop-blur`) so the grid runs behind it. A `.bg-vibrancy` helper already exists (`globals.css:370`). **Decision D3 below.**
### 4.6 Login / auth / error pages
These render directly under `<body>` (no panel chrome) and their page wrappers are mostly transparent — the global grid appears behind them automatically. One exception: `src/app/login/page.tsx:124,139` uses opaque `bg-bg` wrappers; soften the same way if we want the grid there too (minor, **D4**).
### 4.7 Landing page
`landing/page.tsx` keeps its richer animated background (orbs + vignette). Options: (a) leave it as-is (its own splash identity), or (b) align its grid to the global tokens (46px, neutral lines) for consistency. **Recommend (a)** — it's a marketing splash, not a panel screen. **Decision D5.**
---
## 5. Part B — Token unification
Add to `globals.css` (`:root` + `@theme inline`) so the dashboard gains the site's missing tokens. None of these change existing colors; they add the _missing_ primitives.
```css
:root {
--surface-2: #f5f5fa; /* light: nested panels */
--radius: 14px;
--radius-sm: 9px;
--grad-brand: linear-gradient(135deg, var(--color-primary), var(--color-accent-light));
--font-mono: ui-monospace, "JetBrains Mono", "Fira Code", "SF Mono", monospace;
}
.dark {
--surface-2: #1c2230;
}
@theme inline {
--color-surface-2: var(--surface-2); /* enables bg-surface-2 */
--radius-lg: var(--radius); /* enables rounded-lg = 14px */
--radius-md: var(--radius-sm); /* enables rounded-md = 9px */
--font-mono: var(--font-mono); /* enables font-mono */
}
```
| Token | Why | Consumers |
| -------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------- |
| `--radius` / `--radius-sm` | One radius scale (14/9) instead of 6/8/12 ad-hoc | Button, Card, Modal, Input, Select |
| `--grad-brand` | Brand gradient for primary CTAs (red→violet), matching the site | Button `primary`, hero/CTA surfaces |
| `--surface-2` | Nested panels / table headers / inset rows | Card.Section, DataTable header, inputs |
| `--font-mono` | Code blocks, terminal, IDs, endpoints | ConsoleLogViewer, code snippets, `localhost:20128/v1` chips |
| `--text-muted` reconcile | Pick one value site↔panel | global |
**Decision D2 (text-muted):** site `#8b8b9e` vs dashboard `#a1a1aa`. Recommend keeping the **dashboard's `#a1a1aa`** (it's the live product, slightly higher contrast) and updating the _site_ to match. Low priority, cosmetic.
---
## 6. Part C — Component standardization
The component layer is **custom** (no shadcn/Radix), Tailwind v4, semantic tokens **mostly** adopted (`bg-surface`, `border-white/10`, `ring-primary`) — good adoption (195 files import the shared barrel). The work is removing the **bypasses**. Home: `src/shared/components/`.
Ranked by impact × reach:
| # | Item | File(s) | Problem → Target |
| --- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| C1 | **Radius alignment** | `Button.tsx:14-18`, `Card.tsx:39`, `Modal.tsx`, `Input.tsx`, `Select.tsx` | mixed 6/8/12px → repoint to `--radius`/`--radius-sm` (14/9) |
| C2 | **Button gradient + `accent` variant** | `Button.tsx:5-12` | primary is flat red→red (`from-primary to-primary-hover`); align to `--grad-brand` (red→violet) and add the missing `accent` variant (indigo `#6366f1` is unused by buttons) — **highest visibility, ~195 importers**. **Decision D1.** |
| C3 | **Tables** | `DataTable.tsx:122-176`, `logTableStyles.ts`, `globals.css:405-414` (Ant remnants) | `DataTable` is 100% inline hardcoded rgba + references non-existent vars (`--text-secondary`, `--bg-table-header`); migrate to tokens, retire the 2 divergent table styles. Tables are everywhere (providers/connections/logs) — worst offender. |
| C4 | **Centralize status colors** | `flow/edgeStyles.ts:7-12`, `TokenHealthBadge.tsx:14-19`, `DegradationBadge.tsx`, `ProviderCascadeNode.tsx`, `Badge.tsx`, +5 `statusColor` helpers | 6+ copies of the same `#22c55e/#f59e0b/#ef4444` hex; create one `statusColors` module driven off `--color-success/warning/error`. Critical for circuit-breaker / cooldown / lockout badges to read consistently. |
| C5 | **Card border** | `Card.tsx:39` | uses `border-white/5`; brand border is `/8` → align |
| C6 | **Focus ring reconcile** | `globals.css:183` vs component `ring-primary/30` | global `:focus-visible` is indigo (`--color-accent`), components are red (`ring-primary`) — pick one (recommend **accent/indigo** globally, it reads as the "interactive" color) |
| C7 | **Add `Checkbox` + `Textarea` primitives** | currently raw `<input>`/`<textarea>` with inline `accentColor:#6366f1` (e.g. `ColumnToggle.tsx:91`) | create token-driven primitives |
| C8 | **Hardcoded-hex sweep** | `ConsoleLogViewer.tsx:240` (`#161b22`/`#30363d`), `ComboLiveStudio.tsx:306` (`#6366f120`), Modal traffic dots `Modal.tsx:149-159`, ~14 chart/component files with literal `#6366f1`/`#a855f7` | replace literals with `bg-surface`/`border-border`/`text-accent` etc. |
| C9 | **`cn()` → clsx + tailwind-merge** | `src/shared/utils/cn.ts` | current `cn` just joins; conflicting classes stack (a `className="rounded-2xl"` override won't replace a primitive's `rounded-lg`). Needed for C1 overrides to behave. |
**Already on-brand (token-driven, only need radius):** `Badge`, `Toggle`, `SegmentedControl`, `Input`, `Select`.
---
## 7. Rollout plan (phased, each phase shippable + testable)
- **Phase 1 — Grid + tokens (low risk, high visibility).**
1. Add grid + identity tokens to `globals.css` (Part A §4.3, Part B §5).
2. Add `body::before` grid.
3. Remove `bg-bg` from `DashboardLayout.tsx:62`.
4. Verify across themes + key screens (dashboard, providers, logs, login, an error page). Confirm contrast unchanged.
→ _Delivers the headline ask. Reversible in one commit._
- **Phase 2 — Primitives radius + Button (C1, C2, C5, C9).** The visible "feel" pass. `cn()` upgrade first so overrides behave.
- **Phase 3 — Tables + status colors (C3, C4).** The largest consistency win; touch the data-heavy screens.
- **Phase 4 — Cleanup (C6, C7, C8).** Focus ring, new primitives, hardcoded-hex sweep.
Each phase: `npm run lint` + `npm run typecheck:core` + a visual pass. Per repo rule, production-code changes ship with tests where applicable (token/CSS changes are visual — validated by screenshots; component API changes get unit coverage).
---
## 8. Open decisions (need your call before/while implementing)
- **D1 — Button primary look.** Keep the current **red→red** gradient, or switch the product's primary buttons to the **red→violet `--grad-brand`** (matches the site CTAs)? _(Affects every primary button.)_ Recommend: **red→violet**, with `--grad-brand`.
- **D2 — Grid line color.** **Neutral** lines (site style: faint white/black, `rgba(255,255,255,0.035)`) — calm, content-first — **or** the landing's **brand-red** lines? Recommend: **neutral** (matches the site's interior pages; red is louder and can tint readability). Size **46px** (site) to retire the landing's 50px drift.
- **D3 — Chrome vibrancy.** Sidebar/header stay **solid** (grid in content area only), or go **translucent** so the grid runs behind them? Recommend: **solid** (calmer; less risk).
- **D4 — Auth/login grid.** Soften `login/page.tsx` wrappers so the grid shows there too? Recommend: **yes** (cheap, more cohesive).
- **D5 — Landing page.** Leave its animated splash bg as-is, or align it to the global grid? Recommend: **leave as-is**.
- **D6 — Radius value.** Adopt **14/9** everywhere (bigger, softer, site-matching) — confirm you want this product-wide shift. Recommend: **yes**, it's the single biggest "one identity" signal.
- **D7 — Scope of first PR.** Ship **Phase 1 only** first (grid + tokens), then iterate? Recommend: **yes** — validate the wallpaper live before the component waves.
---
## 9. Out of scope / risks
- **No palette change** — colors already match; we only add missing tokens. Zero risk of recoloring the product.
- **No theme-engine change** — keep `.dark` + Zustand store; don't migrate to `next-themes` or to the site's `data-theme`.
- **Radius shift is broad** (D6) — it touches every card/button/input; that's the point, but it's the one change worth eyeballing on busy screens (tables, modals) before merge.
- **Tables (C3)** carry the most hardcoded styling and the highest regression surface — isolate in its own PR with before/after screenshots.
- **Worktree isolation (repo hard-rule #19):** implementation runs in a dedicated worktree on a branch cut from the confirmed base (likely `release/v3.8.28`), never on the shared checkout. This doc is the only artifact written to the working tree so far.
---
## 10. Reference index
| Area | Path |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Dashboard tokens | `src/app/globals.css:30-179` (`:root`, `.dark`, `@theme inline`), `body` `:206` |
| Theme store | `src/store/themeStore.ts`, `src/shared/components/ThemeProvider.tsx`, `src/shared/constants/appConfig.ts:9-11` |
| Panel shell (grid blocker) | `src/shared/components/layouts/DashboardLayout.tsx:62` |
| Chrome | `src/shared/components/Header.tsx:207`, `src/shared/components/Sidebar.tsx:430` |
| Grid precedent | `src/app/landing/page.tsx:16-26` |
| Primitives | `src/shared/components/{Button,Card,Input,Select,Badge,Modal,Toggle,SegmentedControl,Loading,Tooltip,DataTable}.tsx`, barrel `index.tsx` |
| Status-color sources | `flow/edgeStyles.ts`, `TokenHealthBadge.tsx`, `DegradationBadge.tsx`, `logTableStyles.ts` |
| `cn` util | `src/shared/utils/cn.ts` |
| Site reference | `_mono_repo/omnirouteSite/css/tokens.css`, `css/base.css` (grid `body::before`) |

View File

@@ -38,11 +38,27 @@ RUN --mount=type=cache,target=/root/.npm \
&& npm rebuild better-sqlite3 \
&& node -e "require('better-sqlite3')(':memory:').close()"
# Use Turbopack for significant build speedup
ENV OMNIROUTE_USE_TURBOPACK=1
# Build with webpack (stable). Turbopack hit a non-recoverable internal panic on this
# Next.js version during the v3.8.27 release build — TurbopackInternalError "entered
# unreachable code: there must be a path to a root" in ImportTracer::get_traces, on both
# linux/amd64 and linux/arm64. Webpack is the proven engine (build:release / VPS / CI Build
# all green). Re-enable Turbopack (=1) once the upstream tracer bug is fixed.
# See docs/ops/QUALITY_GATE_PLAYBOOK.md Parte 6.
ENV OMNIROUTE_USE_TURBOPACK=0
# Raise the V8 heap ceiling for the build. The webpack production optimization
# pass (forced above since Turbopack panics) needs more than V8's default ceiling
# (~2 GB) for a codebase this size; a memory-constrained Docker build otherwise
# dies with "FATAL ERROR: ... JavaScript heap out of memory" at `[builder] npm run
# build` (#4076). NODE_OPTIONS propagates to the spawned `next build` child
# (build-next-isolated.mjs → resolveNextBuildEnv spreads process.env). Build-only;
# the runtime heap is set separately on the runner stage (OMNIROUTE_MEMORY_MB).
# Override for hosts with more/less RAM: `--build-arg OMNIROUTE_BUILD_MEMORY_MB=6144`.
ARG OMNIROUTE_BUILD_MEMORY_MB=4096
ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_BUILD_MEMORY_MB}"
COPY . ./
RUN --mount=type=cache,target=/app/.next/cache \
RUN --mount=type=cache,target=/app/.build/next/cache \
mkdir -p /app/data && npm run build
# ── Runner base ────────────────────────────────────────────────────────────
@@ -64,25 +80,27 @@ ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_MEMORY_MB}"
ENV DATA_DIR=/app/data
RUN mkdir -p /app/data
# The standalone build + syncStandaloneExtraModules bundles all runtime files
# (.next, node_modules, migrations, scripts, docs, etc.) into .next/standalone/.
# Explicit overrides below cover modules that NFT tracing may miss.
COPY --from=builder /app/.next/standalone ./
# Explicitly copy @swc/helpers — not always traced by standalone output but needed at runtime
COPY --from=builder /app/node_modules/@swc/helpers ./node_modules/@swc/helpers
# Explicitly copy better-sqlite3 — native bindings are not reliably traced by
# Next.js standalone output, but bootstrap-env requires SQLite before startup.
# `npm run build` (build-next-isolated → assembleStandalone) bundles ALL runtime
# files into .build/next/standalone/ — .next, node_modules, migrations, scripts,
# docs, and the previously hand-COPY'd modules below (@swc/helpers, pino-*, split2,
# migrations). assembleStandalone copies them straight from the builder's
# node_modules, so they are present regardless of NFT/Turbopack trace behaviour.
# The old per-module overrides were therefore pure duplication and were removed
# (build-output-isolation cleanup). See scripts/build/assembleStandalone.mjs
# (EXTRA_MODULE_ENTRIES) for the single source of truth.
COPY --from=builder /app/.build/next/standalone ./
# better-sqlite3 is the one exception still copied explicitly: assembleStandalone
# only syncs its native build/ dir; the JS wrapper (lib/, package.json) is left to
# Next.js tracing. bootstrap-env requires SQLite BEFORE the standalone server
# starts, so guarantee the complete package independent of trace behaviour.
COPY --from=builder /app/node_modules/better-sqlite3 ./node_modules/better-sqlite3
# Explicitly copy pino transport dependencies — pino spawns a worker that requires
# pino-abstract-transport at runtime; Next.js standalone trace does not capture it (#449)
COPY --from=builder /app/node_modules/pino-abstract-transport ./node_modules/pino-abstract-transport
COPY --from=builder /app/node_modules/pino-pretty ./node_modules/pino-pretty
COPY --from=builder /app/node_modules/split2 ./node_modules/split2
# Migration SQL files are read via fs.readFileSync at runtime and are NOT
# traced by Next.js standalone output — copy them explicitly.
COPY --from=builder /app/src/lib/db/migrations ./migrations
# migrations land at <standalone>/migrations via assembleStandalone; point the runtime at them.
ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations
# Docker healthcheck script — not traced by Next.js standalone output, so copy
# it explicitly. The HEALTHCHECK CMD references it as `node healthcheck.mjs`.
COPY --from=builder /app/scripts/dev/healthcheck.mjs ./healthcheck.mjs
# Hand /app over to the baked-in `node` non-root user (UID/GID 1000) so the
# runtime process never holds root privileges. The chown happens after all
# COPYs so it covers files originally owned by root in the builder stage.
@@ -122,6 +140,14 @@ FROM runner-base AS runner-web
USER root
# Copy playwright and playwright-core from the builder stage.
# The slim runtime image does not have playwright in node_modules, so npx falls
# back to a registry download — unreliable on CI runners (exits 127 on failure).
# Copying from the builder avoids any network access at image-build time and also
# ensures the same playwright version is available at runtime for web-session providers.
COPY --from=builder /app/node_modules/playwright-core ./node_modules/playwright-core
COPY --from=builder /app/node_modules/playwright ./node_modules/playwright
# Install Playwright browser binaries + OS dependencies under root, then hand
# ownership of the browsers cache to the node user.
# PLAYWRIGHT_BROWSERS_PATH overrides the default ~/.cache/ms-playwright so the
@@ -131,7 +157,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
apt-get update \
&& npx playwright install chromium --with-deps \
&& node node_modules/playwright/cli.js install chromium --with-deps \
&& chown -R node:node /home/node/.cache \
&& rm -rf /var/lib/apt/lists/*

Some files were not shown because too many files have changed in this diff Show More