Files
OmniRoute/AGENTS.md
Diego Rodrigues de Sa e Souza c40d4b17ee fix(ci): clear the NEW base-reds from the 08-06 merge batch (migration collision #2 + broken import) (#9688)
* test(base): realign six suites with contracts that #9100/#8990/#9009 deliberately changed

Continuing the base-red drain — every one of these reproduces on the pure tip.

- tests/snapshots/provider/translate-path.json: regenerated via UPDATE_GOLDEN=1.
  The diff is ADDITION-ONLY — the unorouter block from #9009; no existing
  provider entry changed. 3/3.
- tests/unit/provider-models-route.test.ts: ff012ff420 added onboardUser as a
  bootstrap fallback next to loadCodeAssist; the mock now excludes it from the
  discovery-URL ledger like it already excluded loadCodeAssist, otherwise it
  consumed the injected 503 and the retry assertion misfired. 59/59.
- tests/unit/responses-commentary-passthrough-6199.test.ts: #8990 (c996dc93c2)
  deliberately preserves `tools` on the TERMINAL response.completed snapshot
  (Codex CLI rebuilds its tool list from it); the assertion now pins the echoed
  tools instead of their absence. Still stripped on created/in_progress. 7/7.
- tests/unit/vision-compression-authoritative-capability-7237.test.ts:
  68cb678780 added the 'gpt-5' fragment, so the heuristic-vs-spec DRIFT this
  suite documented no longer exists; the cases now guard the agreement, keep a
  conservative-for-unknown-ids probe, and reproduce the strip-bug shape with an
  explicit false instead of deriving it. 4/4.
- tests/unit/provider-limits-proxy-fail-closed.test.ts +
  tests/unit/image-generation-route.test.ts: #9100 made the proxy reachability
  probe NON-BLOCKING (optimistic dispatch; the probe aborts only in-flight
  requests — its own t14 sibling was updated to this exact pattern). Instant
  mocks therefore won the race and the PROXY_UNREACHABLE 503 became unobservable
  (a success or a generic 502). The mocks now stay in flight (never-resolving,
  so the aborted continuation cannot reach the restored real fetch), and the
  fail-closed proof is the settled rejection itself plus zero egress AFTER the
  fast-fail. Production fail-closed semantics are unchanged — the proxy dispatch
  path still throws; only the mock timing was stale. 3/3 and 20/20.

Refs #9298

* fix(guardrails): forward the router deps seam through callVisionModel

tests/unit/guardrails/vision-bridge-sse-and-reasoning.test.ts was 7/7 red on any
clean box (CI shard 3/4): callVisionModel() called getBestVisionModel()/
getFallbackModels() WITHOUT the routers' existing VisionBridgeRouterDeps seam,
so the credential check always hit the live connections DB — no vision-capable
connection meant 'No vision-capable provider connected' before the mocked fetch
was ever reached, and on a dev box auto-selection could swap the fixed model
under the assertions.

The routers already accepted deps; only the forwarding was missing. Added the
optional 5th param (backward compatible — the sole production caller,
visionBridge.ts, injects its own callVisionModel and is unaffected) and the
suite now pins selection with hasUsableCredentials: async () => null
(indeterminate → the fixed model is honored, DB untouched). 7/7.

Sibling suites re-run green: vision-bridge-callmodel 2/2, visionBridge 25/25,
visionBridgeHelpers.callVisionModel 8/8, visionBridgeRouter 10/10,
vision-bridge-cc-no-reroute 8/8.

Refs #9298

* fix(db,combo): clear the NEW base-reds the 08-06 merge batch introduced

The tip moved while the first sweep PR (#9600) was in review, and three fresh
base-reds landed with it — same classes as before, all reproduced on the pure
tip 9995bc4893:

1. ANOTHER migration collision: #9061 shipped 134_ccr_blocks.sql onto the slot
   134_proxy_logs_egress_ip.sql (#9291) has held since 08-04. getMigrationFiles()
   throws on collision, so every DB-touching test died at bootstrap again.
   Renumbered to 139 (next free slot). No retroactive guard needed this time:
   both statements are IF NOT EXISTS, and no DB can have applied it as 134 —
   the runner refused to run at all while the collision existed.
2. BROKEN IMPORT killing the combo module graph: #8894 imported
   preferAntigravityConnectionsWithStoredProject from
   ../antigravityProjectPersistence.ts — a module that exists NOWHERE in the
   repo (it came from an unmerged sibling branch). Anything importing
   quotaStrategies.ts died with ERR_MODULE_NOT_FOUND. Implemented the helper in
   the real persistence module (antigravityProjectPersist.ts, #8491) with the
   semantics the call site needs — prefer connections that already carry a
   stored projectId, never emptying the pool — and pointed the import there.
   New regression suite tests/unit/antigravity-prefer-stored-project.test.ts
   (5/5), including an import-graph probe that reproduces the break shape.
3. Sibling-test drift from #9106 (gemini-3.1-pro-high now user-callable): its
   own suites were updated but provider-models-route.test.ts was not. Expected
   discovery list realigned; testFrozen 1784->1787 justified in the baseline
   (irreducible +2 after comment compression; gate counts split-newlines).

Also regenerated tests/snapshots/provider/translate-path.json — addition-only:
devin-cli-agentic, raycast, regolo (today's provider merges), zero removals.

image-generation-route 20/20 (was import-dead), provider-models-route 59/59,
antigravity-prefer-stored-project 5/5, provider-translate-path-golden 3/3.

Refs #9298

* fix(changelog): convert the #9415 fragment to the required bullet shape

Another base-red from the 08-06 batch: bd4407cb64 landed
changelog.d/features/9415-newapi-sub2api-aggregator-balance.md as YAML
frontmatter + a prose paragraph. Every other fragment in changelog.d/ is a
single markdown bullet, and both consumers enforce that —
scripts/check/check-changelog-integrity.mjs:97 and the release aggregator
(scripts/release/aggregate-changelog.mjs:57) reject anything that does not
start with '- ', so 'Merge integrity (changelog + generated skills)' was red
for every PR targeting the release branch.

Rewritten as a bullet with the standard issue link, preserving the feature
description (aggregator gateway toggle, /api/user/self balance read, dashboard
badge, quota-preflight skip, NEWAPI_AGGREGATOR_BALANCE flag default off,
quotaPerUnit override). Swept the rest of changelog.d/ — this was the only
malformed fragment.

check:changelog-integrity OK.

Refs #9298

* fix(types,docs): clear the 5 typecheck errors and the fabricated env vars on the base

Third pass over the base-reds, from the 2026-08-06T22:51Z verdict on #9298 —
it reported "Typecheck (core)" with only the FIRST error; there are five, all on
the pure tip 9995bc4893. Two are real production defects.

**Real bugs**

- open-sse/services/compression/engines/ccr/index.ts:295 called
  enforceGlobalBudget(entry.bytes) against an (owner, bytes) signature. The
  `bytes` argument arrived undefined, so `ccrTotalBytes + undefined` is NaN,
  `NaN > MAX` is false (the eviction loop exits immediately) and `NaN <= MAX` is
  false (the re-admit is refused). The #9061 durable tier therefore NEVER
  repopulated its in-memory map: every retrieve after a restart or an eviction
  re-read from SQLite forever, and evictions could not prefer the owning
  principal. Fixed and pinned by a new case in
  tests/unit/ccr-durable-store-9061.test.ts (11/11) — verified failing against
  the buggy call and passing against the fix.
- open-sse/services/combo/fusionPanel.ts:54 read `step.model` after #8894
  widened ComboStep with ComboProviderWildcardStep (which carries modelPattern,
  not model), so a wildcard step in a fusion panel pushed `undefined` onto the
  panel. Now resolved through getComboModelString(), which already handles every
  step shape and returns null for the ones without a concrete model id.

**Type-only**

- accountSemaphore.ts:203 — isBypassed() returns a plain boolean and cannot
  narrow `number | null` (an `x is null | undefined` predicate would be unsound:
  0 bypasses too). Added resolveActiveCap(), the narrowing companion isBypassed
  is now defined in terms of; the acquire path uses the narrowed value.
- comboStructure.ts:140 — same #8894 widening: `prompt` only exists on a model
  step, so it is now read under a kind check.
- firecrawlQuotaFetcher.ts:136 — the function returns full FirecrawlQuota
  objects but was annotated Promise<QuotaInfo | null>, which made the
  custom-base literal an excess-property error. Widened to the accurate type
  (FirecrawlQuota extends QuotaInfo, so callers are unaffected).

**Fabricated docs (the "Docs sync + fabricated-docs (strict)" HARD failure)**

docs/ops/VM_DEPLOYMENT_GUIDE.md recommended OMNIROUTE_MAX_POOL_SIZE and
OMNIROUTE_DB_POOL_SIZE (#9471). Neither is read anywhere in the codebase.
Replaced with the two knobs that do exist and are already documented in
ENVIRONMENT.md: OMNIROUTE_MEMORY_MB and OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT.

typecheck:core 5 errors -> 0. check:fabricated-docs + check:env-doc-sync OK.
accountSemaphore 6/6, ccr-durable-store 11/11, ccr-protocol 9/9,
combo-fusion-strategy 10/10, combo-fusion-comboref 5/5, combo-fusion-warn 4/4,
firecrawl-executor 7/7, executor-firecrawl-fetch 4/4.

Refs #9298

* fix(tests): type the #3440 vertex helpers instead of `any` (the 3 base ESLint errors)

The "ESLint errors: 3 error(s)" HARD failure in the #9298 verdict is
tests/unit/vertex-functioncall-id-3440.test.ts lines 32/41/50: the three
find*(result: any) walkers. `@typescript-eslint/no-explicit-any` is an ERROR in
tests/ (and open-sse/) since #6218, and this file landed on 2026-08-04 without a
suppressions entry, so every run of `lint:json --max-warnings 0` failed. That
step prints nothing on failure, which is why the gate looked like a silent
crash across the open PRs.

Replaced with a GeminiRequestLike interface describing exactly what the three
walkers traverse (contents[].parts[]), so the assertions keep their meaning and
nothing is cast away.

eslint on the file: clean. Suite: 6/6.

Refs #9298

* docs(proxy): use an RFC 5737 documentation IP in the proxy examples

The #9298 verdict headlines its docs failure with
`L810 [stale-version] 1.2.3: const removed = await failOneproxyProxy("1.2.3.4", 8080)`.
That is a false positive: check-deprecated-versions.mjs matches
`/\bv?[12]\.\d+\.\d+\b/`, and the example IP literal 1.2.3.4 contains "1.2.3".

Swapped both occurrences in PROXY_GUIDE.md (and its pl mirror) for 203.0.113.7,
from the RFC 5737 documentation range that exists precisely for examples — it
cannot collide with a version pattern and is the correct thing to print in docs
regardless. Drift count 64 -> 62; no gate threshold was touched.

The gate that actually FAILED under "Docs sync + fabricated-docs (strict)" was
check:fabricated-docs (the invented pool env vars), fixed in the previous
commit; this one removes the misleading line the verdict quotes.

* test(base): allowlist probeUtils and realign the #7849 suite to the replacement bound

Two more base-reds, both visible only after the migration collision stopped
killing the shards.

**check-db-rules — src/lib/db/probeUtils.ts not classified**

#9541 added probeUtils.ts (transient-error retry for the SQLite corruption
probe). It is imported ONLY by src/lib/db/core.ts, exactly like its siblings
schemaColumns / optimizationSettings / providerNodeSelect, so re-exporting it
through localDb.ts would push callers toward the barrel-import anti-pattern the
gate exists to prevent. Added to INTENTIONALLY_INTERNAL with that rationale.
check-db-rules 22/22, check:db-rules exit 0.

**session-dedup-memory-7849 — pinned a mechanism that was replaced**

7f36b192f0 (#7855 follow-up) swapped the shared "suffix work budget" for the
MAX_SUFFIX_STARTS / MAX_TOTAL_BLOCK_BYTES guards and deleted both the budget and
its SUFFIX_WORK_BUDGET_WARNING string. It updated session-dedup.test.ts but not
this sibling, so 3 of its 4 cases asserted a warning that can no longer be
emitted.

Realigned to the contract that actually survives — which is the invariant #7849
was opened for, not the mechanism:
- the pathological pair must stay BOUNDED (completes in <4s, body intact) —
  measured at ~280ms on the current guards;
- it must FAIL OPEN — original body returned by identity, compressed false,
  stats null (the explanatory zero-savings stats belonged to the removed
  budget path, which skipped before producing any);
- the 512 MiB child fixture must still exit 0 with the full engine chain
  (session-dedup, lite, rtk, headroom, caveman) — that IS the OOM guard — and
  session-dedup must still report its skip, now pinned by prefix since the
  reason string moved with the mechanism.

No threshold was loosened and no case was deleted: 4/4 here, 8/8 on the sibling
session-dedup.test.ts.

Refs #9298

* docs(mcp): bump the tool count to 105 and realign two vitest count pins

Three more base-reds from the same 08-06 batch, all count/contract drift that
the merged PRs left in sibling files.

**Docs Gates (fast-path) — 3 STRICT drifts**

check:docs-counts measures the MCP tool set from live code: it is 105 now
(#8925 added omniroute_create_combo), while README.md, AGENTS.md and
docs/frameworks/MCP-SERVER.md still claimed 104. Updated all five occurrences
(two of them inside SVG alt text). check:docs-all exits 0.

**Vitest (fast-path) — 2 failures**

- open-sse/mcp-server/__tests__/essentialTools.test.ts pinned 11 phase-1 tools;
  #8925 shipped omniroute_create_combo as phase 1, making it 12. Verified by
  enumerating MCP_ESSENTIAL_TOOLS directly.
- tests/unit/autoCombo/provider-family-combos.test.ts pinned the auto/glm
  provider set to [auggie, glm, zai]. #8914 (Devin ACP bridge) added
  devin-cli-agentic, whose catalog (registry/devin/catalog.ts:90-93) advertises
  the glm-5-2* line — so it belongs in the family pool for exactly the reason
  the test's own comment gives for auggie: a no-auth backend that genuinely
  serves a family model is a legitimate member. Expected set updated, invariant
  unchanged.

npm run test:vitest 36/36 files, 340/340 tests.

Refs #9298

* fix(combo,usage,oauth): drain the base-reds the shard fix exposed

With the migration collision and the broken import out of the way the four unit
shards actually run, and a further layer of base-reds became visible on the pure
tip 9995bc4893. Three are production defects.

**Production defects**

- open-sse/services/combo/runtimeUnitCapacity.ts:58 called resolveComboTargets()
  WITHOUT the hidden-model snapshot, so it fell back to the default
  getHiddenModelsByProvider() — a fresh full key_value read PER nested combo-ref
  unit, on every request. #8878 threaded the snapshot through the other call
  sites and missed this one. Threaded it from executeRuntimeUnitCombo (and from
  the dispatchPrelude call site), restoring the one-snapshot-per-request
  invariant combo-hidden-leaf-routing.test.ts pins. 9/9.
- open-sse/services/usage/firecrawl.ts silently ignored its own `apiKey`
  parameter: 91bb6aa619 moved the fetch to
  fetchFirecrawlQuota(connectionId, connection), which reads the key off the
  connection record, so any caller passing the key directly got "Firecrawl API
  key not available". The explicit key is now merged into the connection passed
  down. firecrawl-usage 8/8.
- src/lib/oauth/constants/oauth.ts was missing a RAYCAST entry in PROVIDERS
  while src/lib/oauth/providers/index.ts registers `raycast` (#8895), so every
  consumer reading PROVIDERS did not know Raycast Pro exists. Also added its
  OAUTH_TEST_CONFIG entry (checkExpiry only — it is an `import_token` provider
  with refreshToken always null), which #8408's guard explicitly requires rather
  than grandfathering. oauth-providers-config 25/25, oauth-test-config-8408 2/2.

**Count / contract drift from the same batch**

- feature flags 45 -> 46, APIKEY_PROVIDERS 197 -> 198 (Raycast Pro #8895),
  unique MCP tools 107 -> 108. Each re-derived from the source of truth.
- vi + pt-BR locales: translated the 8 keys #9415 added
  (providers.newApiAggregator* and providers.modelTestQuotaTooltip) instead of
  relaxing the parity guard. i18n-vi 5/5, i18n-pt-br 3/3.
- login-bootstrap-route: #9491 added `authenticated` to the require-login
  payload so /login can redirect an active session; the three deepEqual bodies
  now carry it. 10/10.

**Flaky-by-construction, made deterministic**

tests/unit/chat-combo-live-test.test.ts asserted the early-keepalive frame with
a 100ms mocked upstream while resolveKeepaliveThreshold() is 2000ms for
openai/*. It only ever passed while unrelated handler latency happened to push
the total past the threshold — incidental, not deterministic, and it stopped
holding once the handler got faster. The mock now sleeps 2400ms so the slow path
is guaranteed and the assertion means what it says. 5/5.

typecheck:core exit 0. check:file-size (base-relative) OK.

Refs #9298

* test(base): run the orphaned #8890 suite and realign three mechanism pins

**check:test-discovery — a suite that had NEVER executed**

#8890 landed open-sse/services/__tests__/fail-fast-concurrency-gate.test.ts into
a directory no runner collects (only one explicit file from that folder is in
vitest.mcp.config.ts), so it ran zero times since it merged. Wired it into the
runner AND into check-test-discovery.mjs's mirrored collector list, which the
gate keeps in sync deliberately. It passes 4/4 now that it actually runs —
test:vitest goes 36 -> 37 files, 340 -> 344 tests.

**check-db-rules-classification** — 37 -> 38 audited modules, adding probeUtils
alongside the INTENTIONALLY_INTERNAL entry from the previous commit.

**ratelimit-reservoir-refresh** — #9604 (rolling RPM leases) DELETED Bottleneck's
fixed-window reservoir, so currentReservoir() is null and the poll for
`reservoir === 2` could never settle. It updated several sibling suites but not
this one. The pin on the removed mechanism is gone; what remains is the
invariant the original Bottleneck heartbeat bug actually broke and that #9529
opened this test for — after a header-learned updateSettings() the limiter must
keep admitting work, proven by racing a post-exhaustion request against a 5s
timer. 1/1.

**translator-openai-to-gemini** — #9568 (c9a3361e5a) made
buildChangedToolNameMap emit IDENTITY entries too, because Gemini lowercases
tool names in functionCall responses and the response translator needs a key to
map them back. Any request carrying tools therefore carries `_toolNameMap` in
the Antigravity envelope now. Expected key list updated and the map's contents
asserted explicitly rather than left implicit. 45/45.

Refs #9298

* fix(db): restore node-backed synced catalogs and realign the #8944 context hints

**Production regression from #9294 (d69f521491)**

lookupModelMeta moved from getSyncedAvailableModels(providerId) to
getActiveSyncedCatalog(providerId). The new reader unions models only from rows
in `provider_connections` with isActive = 1 — but a provider NODE lives in
`provider_nodes` and NEVER has a connections row, so filtering by active
connection ids silently dropped every node's synced catalog.

The consequence was not just a missing list: lookupModelMeta reads that catalog
for RUNTIME METADATA, so for openai-compatible nodes it took out
- `supportedThinkingEfforts`, which is what splitSyncedEffortSuffix needs — so
  `<prefix>/<model>-high` stopped resolving to the base id and the effort was
  never derived (#7694), and
- `contextWindow` / `maxInputTokens`, used by the combo context-window filter.

getActiveSyncedCatalog now falls back to the provider-wide key_value set — the
exact pre-#9294 source — when no active connection carries a catalog, and marks
that fallback explicitly NON-authoritative. #9294's live-catalog gating is about
what an active connection actually serves, so a node-backed catalog informs
metadata while never being able to reject a model as unavailable. `available`
therefore stays fail-open for nodes, as it was before.

sync-reasoning-supported-efforts-7694 23/23 (was 21/2).
live-model-catalog-reconciliation-8926 11/11 and combo-provider-wildcard 23/23
confirm #9294's own coverage is untouched.

**#8944 sibling-test drift**

714a315a1a ("Treat context metadata as a routing hint") deliberately turned the
context-window check from a HARD filter into an ordering hint: a catalog-too-small
target is demoted, not removed, because a stale catalog entry must never delete
the only target that could accept the request at runtime. The PR updated one case
in this suite and left three asserting the old drop behaviour. Realigned to the
new contract — the too-small target must lose the ordering to the fitting one
while remaining present — and renamed them from "still rejects"/"still dropped"
to "is demoted"/"ordered last" so the names stop describing the removed
behaviour. 14/14.

**file-size**

tests/unit/translator-openai-to-gemini.test.ts testFrozen 1616 -> 1619: the
frozen value sat exactly at the base size, so the 3 lines the previous commit's
_toolNameMap alignment needs could not fit. Justified in the baseline.

typecheck:core exit 0.

Refs #9298

* chore(stryker): register the two covering suites missing from tap.testFiles

check:mutation-test-coverage flags any unit test that covers a mutated module but
is absent from stryker.conf.json tap.testFiles — without the entry its mutant
kills do not count toward the module's score.

- tests/unit/antigravity-prefer-stored-project.test.ts covers
  open-sse/services/combo/quotaStrategies.ts (added earlier in this PR).
- tests/unit/executor-devin-cli-agentic-acp.test.ts covers
  src/sse/services/auth.ts — pre-existing drift, same gate, same fix.

Inserted in alphabetical position only; the rest of the file is byte-identical
(it is not prettier-formatted upstream and reformatting it is out of scope here).

Refs #9298

* fix(db): drop the never-wired getSessionModelUsageCounts (knip regression)

The dead-code ratchet only ran once the earlier Fast Quality Gates steps stopped
failing, and it lands at 228 vs baseline 227.

The extra symbol is src/lib/db/contextHandoffs.ts::getSessionModelUsageCounts,
added by #8894 "for least-used strategy" and never wired: the least-used branch
in applyStrategyOrdering.ts uses the pre-existing sortTargetsByUsage(), and the
helper has no caller in src/, open-sse/ or tests/. It is the same incomplete-PR
shape as that PR's import of a module which does not exist in the repo (fixed
earlier in this branch).

Removed rather than baselined — bumping the ratchet would loosen the gate, and
removal is exactly the remedy the gate prescribes. Same treatment the Dario
installer's never-wired uninstall() got in #9600. The implementation is
recoverable from a598fbb090 whenever someone actually wires a session-aware
least-used strategy.

check:dead-code 228 -> 227 (baseline untouched). check:db-rules exit 0.
context-handoff 13/13, db-context-handoffs 7/7, service-context-handoff 11/11.

Refs #9298

* fix(security): embed the Raycast signature secret via resolvePublicCred (HR#11)

The secret-scan ratchet only ran once the earlier Fast Quality Gates steps
stopped failing, and it lands at 1 finding vs baseline 0.

The finding is open-sse/services/raycast.ts:19 —
RAYCAST_DEFAULT_SIG_SECRET, a 64-hex request-signature secret that #8895
committed as a bare string literal. It is genuinely public (community-extracted
from the Raycast macOS client; the SAME value ships to every install, it is not
a per-user credential), which is exactly the category Hard Rule #11 governs:
public upstream credentials MUST go through resolvePublicCred()
(open-sse/utils/publicCreds.ts), never a literal — see
docs/security/PUBLIC_CREDS.md.

So the fix is the mandated pattern, not a .gitleaks.toml allowlist entry: added
`raycast_sig_secret` to EMBEDDED_DEFAULTS as the XOR-masked byte sequence and
resolved it with the existing RAYCAST_SIG_SECRET env override. The
providerSpecificData.sigSecret override is untouched. Verified the decoded value
is byte-identical to the literal it replaces.

check:secrets secretFindings 1 -> 0. check:public-creds exit 0.
publicCreds 12/12, raycast-auth 6/6, raycast-local-extract 1/1,
trae-publiccred 3/3. typecheck:core exit 0.

Refs #9298

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 11:13:20 -03:00

54 KiB
Raw Blame History

OmniRoute agent guide

Single source of truth. This file holds ALL project rules, conventions, architecture notes and Hard Rules for every AI assistant working this repository (Claude Code, Gemini, Codex, Copilot, and any other agent). CLAUDE.md and GEMINI.md only add assistant-specific deltas and point back here. When a rule needs to change, change it HERE — never re-fork it into an assistant-specific file.

Quick Start

npm install                    # Install deps (auto-generates .env from .env.example)
npm run dev                    # Dev server at http://localhost:20128
npm run build                  # Production build (Next.js 16 standalone)
npm run build:release          # Release build
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 (60/60/60/60 — statements/lines/functions/branches)
npm run check                  # lint + test combined
npm run check:cycles           # Detect circular dependencies
npm run check:docs-all         # Run after changing documentation (includes fabricated-docs validation)

Running Tests

Run the most focused test for changed code first:

# Single test file (Node.js native test runner — most tests)
node --import tsx/esm --test tests/unit/your-file.test.ts

# Vitest (MCP server, autoCombo, cache)
npm run test:vitest

# All suites
npm run test:all

Other suites: npm run test:e2e, npm run test:protocols:e2e, npm run test:ecosystem.

For full test matrix, see CONTRIBUTING.md → "Running Tests". For deep architecture, see the Repository map and Reference Documentation sections below.


Project at a Glance

OmniRoute — unified AI proxy/router. One endpoint, 291 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 (130 migrations)
Domain/Policy src/domain/ Policy engine, cost rules, fallback logic
MCP Server open-sse/mcp-server/ 105 tools (42 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 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).


Request Pipeline

Client → /v1/chat/completions (Next.js route)
  → CORS → Zod validation → auth? → policy check → prompt injection guard
  → handleChatCore() [open-sse/handlers/chatCore.ts]
    → cache check → rate limit → combo routing?
      → resolveComboTargets() → handleSingleModel() per target
    → translateRequest() → getExecutor() → executor.execute()
      → fetch() upstream → retry w/ backoff
    → response translation → SSE stream or JSON
    → If Responses API: responsesTransformer.ts TransformStream

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): 19 public strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline). Each target calls handleSingleModel() which wraps handleChatCore() with per-target error handling and circuit breaker checks. The fusion strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (open-sse/services/fusion.ts). See docs/routing/AUTO-COMBO.md for the 13-factor Auto-Combo scoring + the full strategy table and docs/architecture/RESILIENCE_GUIDE.md for the 3 resilience layers.


Resilience Runtime State

OmniRoute has three related but distinct temporary-failure mechanisms. Keep their scope separate when debugging routing behavior. See the 3-layer resilience diagram (source: docs/diagrams/resilience-3layers.mmd) for an at-a-glance map.

Provider Circuit Breaker

Scope: whole provider, e.g. glm, openai, anthropic.

Purpose: stop sending traffic to a provider that is repeatedly failing at the upstream/service level, so one unhealthy provider does not slow down every request.

Implementation:

  • Core class: src/shared/utils/circuitBreaker.ts
  • Chat gate/execution wiring: src/sse/handlers/chatHelpers.ts, src/sse/handlers/chat.ts
  • Runtime status API: src/app/api/monitoring/health/route.ts
  • Shared wrappers: open-sse/services/accountFallback.ts
  • Persisted state table: domain_circuit_breakers

States:

  • CLOSED: normal traffic is allowed.
  • OPEN: provider is temporarily blocked; callers get a provider-circuit-open response or combo routing skips to another target.
  • HALF_OPEN: reset timeout has elapsed; allow a probe request. Success closes the breaker, failure opens it again.

Defaults (open-sse/config/constants.ts):

  • OAuth providers: threshold 3, reset timeout 60s.
  • API-key providers: threshold 5, reset timeout 30s.
  • Local providers: threshold 2, reset timeout 15s.

Only provider-level failure statuses should trip the provider breaker:

(408, 500, 502, 503, 504);

Do not trip the whole-provider breaker for normal account/key/model errors like most 401, 403, or 429 cases. Those usually belong to connection cooldown or model lockout. A generic API-key provider 403 should be recoverable unless it is classified as a terminal provider/account error.

The breaker uses lazy recovery, not a background timer. When OPEN expires, reads such as getStatus(), canExecute(), and getRetryAfterMs() refresh the state to HALF_OPEN, so dashboards and combo candidate builders do not keep excluding an expired provider forever.

Connection Cooldown

Scope: one provider connection/account/key.

Purpose: temporarily skip one bad key/account while allowing other connections for the same provider to continue serving requests.

Implementation:

  • Write/update path: src/sse/services/auth.ts::markAccountUnavailable()
  • Account selection/filtering: src/sse/services/auth.ts::getProviderCredentials...
  • Cooldown calculation: open-sse/services/accountFallback.ts::checkFallbackError()
  • Settings: src/lib/resilience/settings.ts

Important fields on provider connections:

rateLimitedUntil;
testStatus: "unavailable";
lastError;
lastErrorType;
errorCode;
backoffLevel;

During account selection, a connection is skipped while:

new Date(rateLimitedUntil).getTime() > Date.now();

Cooldowns are also lazy: when rateLimitedUntil is in the past, the connection becomes eligible again. On successful use, clearAccountError() clears testStatus, rateLimitedUntil, error fields, and backoffLevel.

Default connection cooldown behavior:

  • OAuth base cooldown: 5s.
  • API-key base cooldown: 3s.
  • API-key 429 should prefer upstream retry hints (Retry-After, reset headers, or parseable reset text) when available.
  • Repeated recoverable failures use exponential backoff:
baseCooldownMs * 2 ** failureIndex;

The anti-thundering-herd guard prevents concurrent failures on the same connection from repeatedly extending the cooldown or double-incrementing backoffLevel.

Terminal states are not cooldowns. banned, expired, and credits_exhausted are intended to stay unavailable until credentials/settings change or an operator resets them. Do not overwrite terminal states with transient cooldown state.

Model Lockout

Scope: provider + connection + model.

Purpose: avoid disabling a whole connection when only one model is unavailable or quota-limited for that connection.

Examples:

  • Per-model quota providers returning 429.
  • Local providers returning 404 for one missing model.
  • Provider-specific mode/model permission failures such as selected Grok modes.

Model lockout lives in open-sse/services/accountFallback.ts and lets the same connection continue serving other models.

Debugging Guidance

  • If all keys for a provider are skipped, inspect both provider breaker state and each connection's rateLimitedUntil/testStatus.
  • If a provider appears permanently excluded after the reset window, check whether code is reading raw state instead of using getStatus()/canExecute().
  • If one provider key fails but others should work, prefer connection cooldown over provider breaker.
  • If only one model fails, prefer model lockout over connection cooldown.
  • If a state should self-recover, it should have a future timestamp/reset timeout and a read path that refreshes expired state. Permanent statuses require manual credential or config changes.

Repository map

Read the nearest AGENTS.md and the linked deep-dive before making a non-trivial change.

Area Location Start here
API routes src/app/api/v1/ docs/architecture/ARCHITECTURE.md
Streaming request handling open-sse/handlers/ docs/architecture/ARCHITECTURE.md
Provider execution and translation open-sse/executors/, open-sse/translator/ docs/architecture/CODEBASE_DOCUMENTATION.md
Routing and resilience open-sse/services/ open-sse/services/AGENTS.md, docs/routing/AUTO-COMBO.md
Database and migrations src/lib/db/, db/migrations/ src/lib/db/AGENTS.md
Domain policy src/domain/ docs/architecture/ARCHITECTURE.md
MCP and A2A open-sse/mcp-server/, src/lib/a2a/ docs/frameworks/MCP-SERVER.md, docs/frameworks/A2A-SERVER.md
Agent features src/lib/{acp,memory,skills,cloudAgent}/ docs/frameworks/AGENT_PROTOCOLS_GUIDE.md, docs/frameworks/SKILLS.md
Safety and governance src/lib/{guardrails,compliance}/, src/server/authz/ docs/security/GUARDRAILS.md, docs/architecture/AUTHZ_GUIDE.md
Operations src/mitm/, tunnel modules, electron/ docs/ops/TUNNELS_GUIDE.md, docs/guides/ELECTRON_GUIDE.md

File placement & repo-root hygiene

  • Test files: ALL unit tests, integration tests, ecosystem tests, or Vitest files MUST strictly be placed within the tests/ directory (e.g., tests/unit/, tests/integration/). NEVER create test files in the project root (/).
  • Scripts and utilities: ALL maintenance, debugging, generation, or experimental scripts (.cjs, .mjs, .js, .ts) MUST be placed strictly inside one of the scripts/ subfolders (build/, dev/, check/, docs/, i18n/, ad-hoc/). One-shot or experimental code goes under scripts/ad-hoc/. NEVER dump loose scripts in the project root (/) or the top-level scripts/ folder.

The project root MUST ONLY contain:

  • Configuration files (vitest.config.ts, next.config.mjs, eslint.config.mjs, tsconfig*.json, playwright.config.ts, prettier.config.mjs, postcss.config.mjs, sonar-project.properties, fly.toml, docker-compose*.yml, Dockerfile)
  • Dependency files (package.json, package-lock.json)
  • Documentation files (README.md, CHANGELOG.md, LICENSE, AGENTS.md, CLAUDE.md, GEMINI.md, CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md, llm.txt, Tuto_Qdrant.md)
  • CI/CD files and ignore definitions (.gitignore, .dockerignore, .npmignore, .npmrc, .node-version, .nvmrc, .env.example)

When creating any validation tests or one-off logic scripts, default to scripts/ad-hoc/ or tests/unit/ according to your goals. Do not pollute the / root context.


Key Conventions

Code Style

  • 2 spaces, semicolons, double quotes, 100 char width, es5 trailing commas (enforced by lint-staged via Prettier) — run Prettier on changed files
  • Imports: external → internal (@/, @omniroute/open-sse) → relative
  • Naming: files=camelCase/kebab, components=PascalCase, constants=UPPER_SNAKE
  • ESLint: no-eval, no-implied-eval, no-new-func = error everywhere; no-explicit-any = error in open-sse/ and tests/ (since #6218 — pre-existing violations are frozen in config/quality/eslint-suppressions.json, new ones must be fixed; npm run lint applies the suppressions and is what CI runs)
  • TypeScript: strict: false, target ES2022, module esnext, resolution bundler. Prefer explicit types.

Database

  • Always go through src/lib/db/ domain modules — never write raw SQL in routes or handlers
  • Never add logic to src/lib/localDb.ts (re-export layer only)
  • Never barrel-import from localDb.ts — import specific db/ modules instead
  • DB singleton: getDbInstance() from src/lib/db/core.ts (WAL journaling)
  • Migrations: src/lib/db/migrations/ — versioned SQL files, idempotent, run in transactions

Error Handling

  • try/catch with specific error types, log with pino context
  • Never swallow errors in SSE streams — use abort signals for cleanup
  • Return proper HTTP status codes (4xx/5xx)

Security

  • Never use eval(), new Function(), or implied eval
  • Validate all inputs with Zod schemas
  • Encrypt credentials at rest (AES-256-GCM); never log SQLite encryption keys
  • Sanitize user HTML with DOMPurify
  • Upstream header denylist: src/shared/constants/upstreamHeaders.ts — keep sanitize, Zod schemas, and unit tests aligned when editing
  • Public upstream credentials (Gemini/Antigravity/Windsurf-style OAuth client_id/secret + Firebase Web keys extracted from public CLIs): MUST be embedded via resolvePublicCred() from open-sse/utils/publicCreds.tsnever as string literals. See docs/security/PUBLIC_CREDS.md for the mandatory pattern.
  • Error responses (HTTP / SSE / executor / MCP handler): MUST route through buildErrorBody() or sanitizeErrorMessage() from open-sse/utils/error.tsnever put raw err.stack or err.message in a response body. See docs/security/ERROR_SANITIZATION.md.
  • Shell commands built from variables: when calling exec()/spawn() with a script that needs runtime values, pass them via the env option (shell-escaped automatically) — never string-interpolate untrusted/external paths into the script body. Reference: src/mitm/cert/install.ts::updateNssDatabases.
  • Secure-by-default libraries (tldrsec/awesome-secure-defaults): prefer Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink over custom implementations whenever adding new security-sensitive surfaces.

Documentation accuracy

Documentation must describe verified behavior, not plausible behavior.

  1. Before documenting an API name, endpoint, path, CLI command, or environment variable, search for it: rg -n "name" src/ open-sse/ bin/. If it has no source match, do not document it.
  2. Measure mutable counts instead of writing them from memory: use wc -l <file> or a directory-specific count command.
  3. Copy code examples from working usage or run them. Prefer a source link such as path/to/file.ts:line to an invented signature.
  4. Run npm run check:docs-all for edits under docs/; it includes the fabricated-docs validation.

Common Modification Scenarios

Adding a New Provider

  1. Register in src/shared/constants/providers.ts (Zod-validated at load)
  2. Add executor in open-sse/executors/ if custom logic needed (extend BaseExecutor)
  3. Add translator in open-sse/translator/ if non-OpenAI format
  4. Add OAuth config in src/lib/oauth/constants/oauth.ts if OAuth-based — if the upstream CLI ships a public client_id/secret, embed via resolvePublicCred() (see docs/security/PUBLIC_CREDS.md), never as a literal
  5. Register models in open-sse/config/providerRegistry.ts
  6. Write tests in tests/unit/ (include the publicCreds shape assertion if you added a new embedded default)

Adding a New API Route

  1. Create directory under src/app/api/v1/your-route/
  2. Create route.ts with GET/POST handlers
  3. Follow pattern: CORS → Zod body validation → optional auth → handler delegation
  4. Handler goes in open-sse/handlers/ (import from there, not inline)
  5. Error responses use buildErrorBody() / errorResponse() from open-sse/utils/error.ts (auto-sanitized — never put err.stack or err.message raw in the body). See docs/security/ERROR_SANITIZATION.md.
  6. Add tests — including at least one assertion that error responses do not leak stack traces (!body.error.message.includes("at /"))

Adding a New DB Module

  1. Create src/lib/db/yourModule.ts — import getDbInstance from ./core.ts
  2. Export CRUD functions for your domain table(s)
  3. Add migration in src/lib/db/migrations/ if new tables needed
  4. Re-export from src/lib/localDb.ts (add to the re-export list only)
  5. Write tests

Adding a New MCP Tool

  1. Add tool definition in open-sse/mcp-server/tools/ with Zod input schema + async handler
  2. Register in tool set (wired by createMcpServer())
  3. Assign to appropriate scope(s)
  4. Write tests (tool invocation logged to mcp_audit table)

Adding a New A2A Skill

  1. Create skill in src/lib/a2a/skills/ (5 already exist: smart-routing, quota-management, provider-discovery, cost-analysis, health-report)
  2. Skill receives task context (messages, metadata) → returns structured result
  3. Register in A2A_SKILL_HANDLERS in src/lib/a2a/taskExecution.ts
  4. Expose in src/app/.well-known/agent.json/route.ts (Agent Card)
  5. Write tests in tests/unit/
  6. Document in docs/frameworks/A2A-SERVER.md skill table

Adding a New Cloud Agent

  1. Create agent class in src/lib/cloudAgent/agents/ extending CloudAgentBase (3 already exist: codex-cloud, devin, jules)
  2. Implement createTask, getStatus, approvePlan, sendMessage, listSources
  3. Register in src/lib/cloudAgent/registry.ts
  4. Add OAuth/credentials handling if needed (src/lib/oauth/providers/)
  5. Tests + document in docs/frameworks/CLOUD_AGENT.md

Adding a New Embedded Service

  1. Create installer in src/lib/services/installers/{name}.ts modeled on ninerouter.ts (use runNpm from installers/utils.ts — no shell interpolation, hard rule #13).
  2. Register the service in src/lib/services/bootstrap.ts (add to SERVICES[] array and extend buildSpawnArgsFactory()).
  3. Add a DB seed row for the new service in src/lib/db/migrations/ (version_manager table, status='not_installed', auto_start=0).
  4. Create 7 API endpoints under src/app/api/services/{name}/ (_lib.ts, install, start, stop, restart, update, status, auto-start). All delegate errors through createErrorResponse(). The shared logs endpoint is already wired via [name]/logs/route.ts.
  5. Verify /api/services/ is in LOCAL_ONLY_API_PREFIXES in src/server/authz/routeGuard.ts; add a test asserting isLocalOnlyPath() returns true for the new prefix if you add one (hard rule #17).
  6. Add a UI tab in src/app/(dashboard)/dashboard/providers/services/tabs/ reusing ServiceStatusCard, ServiceLifecycleButtons, ServiceLogsPanel.
  7. Document in docs/frameworks/EMBEDDED-SERVICES.md (update §1 service table + §4 API reference) and docs/openapi.yaml.
  8. Write tests: unit (tests/unit/services/), integration (tests/integration/services/, gated by RUN_SERVICES_INT=1), and update docs/ops/RELEASE_CHECKLIST.md smoke section.

Adding a New Guardrail / Eval / Skill / Webhook event

  • Guardrail: src/lib/guardrails/ → docs: docs/security/GUARDRAILS.md
  • Eval suite: src/lib/evals/ → docs: docs/frameworks/EVALS.md
  • Skill (sandbox): src/lib/skills/ → docs: docs/frameworks/SKILLS.md
  • Webhook event: src/lib/webhookDispatcher.ts → docs: docs/frameworks/WEBHOOKS.md

Reference Documentation

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 (13-factor scoring, 19 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
Radar (free-model catalog overlay) docs/frameworks/RADAR.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/openapi.yaml
Provider catalog (auto-generated) docs/reference/PROVIDER_REFERENCE.md
Tunnels docs/ops/TUNNELS_GUIDE.md
Electron desktop app docs/guides/ELECTRON_GUIDE.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

Testing

What Command
Unit tests npm run test:unit
Single file node --import tsx/esm --test tests/unit/your-file.test.ts
Vitest (MCP, autoCombo) npm run test:vitest
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 (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.

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.


Review focus

  • Keep database operations in src/lib/db/; do not issue raw SQL from routes.
  • Send provider requests through open-sse/handlers/.
  • Keep MCP and A2A pages as tabs inside /dashboard/endpoint.
  • Preserve SSE cleanup, rate-limit header parsing, Zod validation, and provider-schema validation.
  • Treat Memory and Skills as cross-cutting changes that can affect MCP tools, the request pipeline, and A2A skills.
  • Do not close a contributor pull request after using its code; merge it through GitHub so the contributor receives credit.

Planning & Research Artifacts

_tasks/ is a separate, isolated git repository that is gitignored by the main repo (.gitignore_tasks/). It is the canonical home for working artifacts — plans, specs/designs, research, hand-offs — so they stay versioned in their own repo instead of polluting the main OmniRoute tree.

Hard rule — never write planning / research output under docs/ or the repo root. Whenever any plan/spec/research generator runs in this project (superpowers or otherwise), save to _tasks/ using the filename convention:

Artifact Save here
Plans _tasks/superpowers/plans/YYYY-MM-DD-<feature>.md
Specs / design _tasks/superpowers/specs/YYYY-MM-DD-<topic>-design.md
Research _tasks/research/…
Hand-offs _tasks/hands-off/<YYYY-MM-DD>_<branch>_v<versão>_sess-<id>/

Commit those artifacts inside the _tasks/ repo (git -C _tasks …), never in the main repo.


Git Workflow

# Never commit directly to main
git checkout -b feat/your-feature
git commit -m "feat: describe your change"
git push -u origin feat/your-feature

Branch prefixes: feat/, fix/, refactor/, docs/, test/, chore/

Commit format (Conventional Commits): feat(db): add circuit breaker — scopes: db, sse, oauth, dashboard, api, cli, docker, ci, mcp, a2a, memory, skills

Husky hooks:

  • pre-commit: lint-staged + check-docs-sync + check:any-budget:t11 + check:tracked-artifacts
  • pre-push: intentionally light (PATH/npm sanity only). any-budget + tracked-artifacts already run on pre-commit; re-running them on every push was pure double-pay. CI still enforces both. (Was Fase 6A.12 full pre-push gate; folded into pre-commit in #6716.)

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 (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). 🔴 MANDATORY PATH: every worktree lives under .claude/worktrees/ — and nowhere else. This is the single canonical location. It is gitignored AND in the tsconfig.json / .dockerignore excludes, so worktrees never leak into the build scope. Never use .worktrees/, repo-root, or any other path — a worktree outside .claude/worktrees/ (a) escapes the build-scope excludes and poisons next build (the tsconfig include: **/* globs ~70× the codebase → OOM; incident 2026-06-25) and (b) scatters worktrees across two dirs.

    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 ".claude/worktrees/${TASK##*/}" -b "$TASK" "origin/$BASE_BRANCH"
    cd ".claude/worktrees/${TASK##*/}"
    # Reuse the main checkout's node_modules to skip a per-worktree npm install.
    # HARD LINKS (`cp -al`), never a symlink: ~5s for the whole tree and near-zero extra
    # disk (the inodes are shared), and unlike a symlink it does not break the dev server.
    cp -al "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
    

    Never ln -s node_modules. Turbopack rejects a symlink that resolves outside the project root, so npm run dev dies with a FATAL panic (Symlink [project]/node_modules is invalid, it points out of the filesystem root) while typecheck, lint and the test runners all keep passing — the error names "filesystem root", not the worktree, so it reads like a Next/build bug and costs real time to trace (incident 2026-07-31, #9043).

  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 .claude/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).

Base-green check (PRs must not be born red)

Before cutting a branch, merging the base into a PR branch, mass-retargeting PRs, or opening a PR: check whether the base tip is green. The Release-Green (continuous) workflow (.github/workflows/nightly-release-green.yml) publishes the verdict in a single deduplicated issue titled 🔴 Release branch not green: <branch> (label base-red). One call replaces any local suite run for this purpose:

gh issue list --repo diegosouzapw/OmniRoute --state open \
  --search "Release branch not green: <base> in:title"

If the base is red: never treat the inherited failures as your branch's defect; never "fix" them inside your feature branch (a base-red fix is its own freeze-gated fix/release-vX.Y.Z-basereds PR); and if you must open a PR anyway, add ⚠️ base-red inherited: #<issue> to the PR body so reviewers and CI babysitters do not chase ghosts.


Upstream contributions

This checkout is a fork of diegosouzapw/OmniRoute. Keep fork-only deployment and personal automation changes out of upstream PRs.

Start upstream work from the active upstream default branch, not main:

git fetch upstream
git switch -c <branch-name> upstream/<default-branch>

Target that same release branch in the pull request. Stage only the intended files, run the focused checks, and use a Conventional Commit message (for example, docs: slim AGENTS.md).


Environment

  • Runtime: Node.js ≥22.0.0 <23 || ≥24.0.0 <27, ES Modules. This is the only supported runtime for the published omniroute CLI, the server, and the test suites (node:test + vitest) — engines.node is authoritative and end users never need Bun. A best-effort bun:sqlite compatibility path exists so a global Bun install (bun install -g omniroute) can start without better-sqlite3 (driver adapter + Bun-aware process spawning); it is not a supported runtime — no support guarantees — and every Bun-specific runtime change MUST preserve the Node driver/fallback chain and ship a Bun test (test:bun:db) or an explicit reason why the path is Node-only.
  • Bun (build/dev script runner + compatibility smoke only): Bun 1.3.14 is pinned as an exact devDependency (provisioned through the existing npm ci via the lockfile's @oven/bun-* platform binaries — no setup-bun/ad-hoc install). It is used only to execute a small, allow-listed set of TypeScript gate/generator scripts (replacing node --import tsx for startup speed): the CI checks check:provider-consistency, check:compression-budget, check:known-symbols, and the non-CI gen:provider-reference, bench:compression — plus the focused test:bun:db compatibility smoke suite for the best-effort bun:sqlite path. Do NOT widen Bun to npm install, the build (build:cli*), check:pack-artifact, the supported published runtime, or the main test runners — those stay on Node. Any new Bun-invoking gate/generator script must be validated byte-identical against its node --import tsx output first. After pulling the lockfile change, run npm install so bun resolves locally (a stale node_modules will fail those scripts with bun: not found).
  • TypeScript: 6.0+, target ES2022, module esnext, resolution bundler
  • Path aliases: @/*src/, @omniroute/open-sseopen-sse/, @omniroute/open-sse/*open-sse/*
  • Default port: 20128 (API + dashboard on same port)
  • Data directory: DATA_DIR env var, defaults to ~/.omniroute/
  • Key env vars: PORT, JWT_SECRET, API_KEY_SECRET, INITIAL_PASSWORD, REQUIRE_API_KEY, APP_LOG_LEVEL
  • Setup: cp .env.example .env then generate JWT_SECRET (openssl rand -base64 48) and API_KEY_SECRET (openssl rand -hex 32)

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.

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
  2. Never add logic to localDb.ts
  3. Never use eval() / new Function() / implied eval
  4. Never commit directly to main
  5. Never write raw SQL in routes — use src/lib/db/ modules
  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 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 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 ", or any Co-authored-by: Claude/GPT/Copilot line. This overrides any harness, template, or tool default that auto-appends such a footer — 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 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".
  20. PII redaction/sanitization is opt-in — never on by default. OmniRoute proxies for self-hosted/local LLMs where the operator owns the data, so mutating request/response payloads by default would silently corrupt legitimate traffic. The two data-mutating PII feature flags MUST keep defaultValue: "false" in src/shared/constants/featureFlagDefinitions.ts: PII_REDACTION_ENABLED (request-side) and PII_RESPONSE_SANITIZATION (response + streaming). All three application points — src/lib/guardrails/piiMasker.ts (request guardrail), src/lib/piiSanitizer.ts (response), src/lib/streamingPiiTransform.ts (SSE) — are gated on these flags; with both off the pii-masker guardrail still runs but never mutates payloads (data passes through untouched). Flipping either default to "true" requires explicit operator approval. The regression guard is tests/unit/pii-opt-in-default.test.ts (asserts both definition defaults + behavioral pass-through). Opt-in is per-operator via env or the settings/DB override (src/lib/db/featureFlags.ts), never a silent default. See docs/security/GUARDRAILS.md.
  21. Release-freeze — the FROZEN release branch belongs to the release captain; development does NOT stop (parallel-cycle model, 2026-07-04). /generate-release opens a marker issue labeled release-freeze at the start of reconciliation (Phase 0a), immediately cuts the next cycle's branch release/vX+1 from the frozen tip (Phase 0a.0b — bump + living release PR + re-home of open PRs), and closes the freeze once the release PR squash-merges to main. Before merging any PR, every campaign workflow (/review-prs, /review-group-prs, /merge-prs, /triage-fix-bugs, /implement-fix-bugs, /triage-features, /implement-features, /green-prs, /port-upstream-*) MUST check gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open — if a freeze is active: NEVER merge into the frozen release/vX.Y.Z named in the freeze title; instead resolve the ACTIVE development branch (the highest release/v* by semver — normally release/vX+1, announced in a freeze-issue comment) and retarget the PR there (gh pr edit <N> --base release/vX+1, then VERIFY with gh pr view <N> --json baseRefName — the edit fails silently) and merge normally. HOLD only when the highest release/v* branch IS the frozen one (the short window before 0a.0b completes, or a pre-parallel-cycle release) — in that case leave the PR ready and open, tell the operator, and resume when the next branch appears or the freeze lifts. The just-shipped fixes reach release/vX+1 via the Phase 5 sync-back (scripts/release/sync-next-cycle.mjs); do not try to sync mid-release. This is a coordination signal, not a permission lock: the release captain and the campaign sessions share the diegosouzapw identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced release/vX.Y.Z by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they are the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on main first via fix/release-vX.Y.Z-*. ONLY /generate-release may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to main. No campaign, session, or agent may open a release-freeze marker at any other time — a freeze is never a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the /generate-release flow, it MUST first ask the operator (diegosouzapw) in chat, explicitly alert "estou criando um freeze" and get an explicit yes — never open, extend, or re-open a release-freeze autonomously. Conversely, do not close/lift an active /generate-release freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open release-freeze whose title/body references an OPEN release PR (gh pr view <N> --json state) is the authorized captain freeze — hold, don't touch. (Cycle-model proposal: _tasks/finished/release-flow/2026-07-04_proposta-ciclo-paralelo-v2.md.)
  22. Cross-session safety — this repo is worked by MANY parallel sessions/agents at once; never step on another's in-flight work. Two absolute bans, both recurring incidents (this rule exists because they keep happening):
    • (a) Never git stash / git stash pop — ANYWHERE in this repo, including inside an isolated worktree, and including inside any subagent you dispatch. git stash operates on the shared repository object store, not the per-worktree working tree — so a stash pushed or popped in one session can silently clobber or resurrect another parallel session's uncommitted changes. This is not hypothetical: 2026-07-02 a #5923 quotaCache change leaked into the unrelated #2296 worktree via a global stash pop, and the same class reincided through a subagent. To compare working changes against a base ref without stashing, use git show <ref>:<path> or git diff <ref> -- <path>; to confirm a typecheck/lint error is pre-existing on the base, inspect the base ref directly (git show origin/release/vX.Y.Z:<path>) — never stash your tree away to "get it clean". Put this ban verbatim in the prompt of every subagent that touches git (agents don't inherit this file's context — the recurrence was a subagent).
    • (b) Never merge, push, rebase, or force-push a PR / branch / worktree that another session is actively working. An open PR whose head is a live fix worktree in .claude/worktrees/ you did not create (e.g. fix-5852/fix-5923 carrying fresh commits, even when they share your diegosouzapw identity), or any branch another session owns, is off-limits — HOLD, and let the owning session merge it. Before merging or pushing to any PR you did not create this session, run git worktree list to check for a matching in-flight worktree and re-check gh pr view <N> --json state,headRefOid. Only the owning session merges its own in-flight PR; mid-flight merges race the owner and re-trigger the exact commit/CHANGELOG races Rule #19 and Rule #21 guard against. (Reinforces Rule #19.)

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.


Local development access

The dashboard is reachable at the operator's chosen URL/port (default http://localhost:20128). Credentials are operator-specific:

  • Initial admin password is read from the INITIAL_PASSWORD env var on first install (defaults to CHANGEME in .env.example; rotate immediately after first login).
  • Local VPS / shared dev environments: ask the operator for the URL and current credentials — they live in their personal vault, NOT in this repo.

Any credential observed in a previous version of this file was a non-production demo value; treat it as compromised and do not reuse it.