Compare commits

..

28 Commits

Author SHA1 Message Date
diegosouzapw
f3d3590482 chore(release): v3.8.10 — 2026-06-04 (finalize CHANGELOG) 2026-06-04 19:38:40 -03:00
Diego Rodrigues de Sa e Souza
ee791000a2 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.
2026-06-04 19:05:00 -03:00
Diego Rodrigues de Sa e Souza
3ac40829c0 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.
2026-06-04 18:33:34 -03:00
Diego Rodrigues de Sa e Souza
42df1703c6 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.
2026-06-04 18:33:31 -03:00
Diego Rodrigues de Sa e Souza
1be9606cc2 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.
2026-06-04 18:33:28 -03:00
PizzaV
76838f6594 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.
2026-06-04 18:21:52 -03:00
PizzaV
9d3c0d10af 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.
2026-06-04 18:18:24 -03:00
Nicolas Lorin
989d4811f2 fix(providerRegistry): update kilocode format and executor (#3166)
kilocode: openai format + default executor (matches kilo-gateway) + registry test. Integrated into release/v3.8.10.
2026-06-04 18:15:49 -03:00
dependabot[bot]
05aafa1374 deps: bump hono from 4.12.18 to 4.12.23 (#3179)
Bump hono to 4.12.23. Integrated into release/v3.8.10.
2026-06-04 18:10:35 -03:00
PizzaV
17ea178dc8 fix(kiro): minor OAuth social exchange tweaks (#3176)
Kiro social OAuth: optional targetProvider passthrough. Integrated into release/v3.8.10.
2026-06-04 18:10:31 -03:00
PizzaV
0ff8949dc5 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.
2026-06-04 18:10:27 -03:00
Wilson
afe8f11bf8 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.
2026-06-04 18:10:23 -03:00
Diego Rodrigues de Sa e Souza
8fb51f44d0 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>
2026-06-04 17:06:27 -03:00
Diego Rodrigues de Sa e Souza
0909b31f50 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>
2026-06-04 16:57:55 -03:00
Diego Rodrigues de Sa e Souza
cb94bf695d 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>
2026-06-04 16:57:52 -03:00
Diego Rodrigues de Sa e Souza
f8fb3d524d 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>
2026-06-04 16:57:48 -03:00
Diego Rodrigues de Sa e Souza
a2fd56c313 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>
2026-06-04 16:57:45 -03:00
Diego Rodrigues de Sa e Souza
58a9573904 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>
2026-06-04 16:57:41 -03:00
Markus Hartung
f0e3a9bec9 Feature/batch allow big (#3128)
Podman deployment options + larger upload body-size limits (+CONTAINER_HOST docs). Integrated into release/v3.8.10.
2026-06-04 14:49:18 -03:00
Hernan Javier Ardila Sanchez
67addc63ad 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.
2026-06-04 14:40:24 -03:00
Hernan Javier Ardila Sanchez
28b44b01d4 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.
2026-06-04 14:25:29 -03:00
Hernan Javier Ardila Sanchez
d3c753ecbe 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.
2026-06-04 14:25:25 -03:00
M.M
44b9d0e5f7 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.
2026-06-04 14:24:51 -03:00
Jan Leon
273268c18d 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.
2026-06-04 14:10:45 -03:00
Xiangzhe
3fd8f2ce11 fix(api-manager): preserve API key expiration local time (#3146)
Preserve API key expiration local time + clear button. Integrated into release/v3.8.10.
2026-06-04 14:10:41 -03:00
Wilson
32bb4b9652 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.
2026-06-04 14:10:36 -03:00
diegosouzapw
2eb0f0500c 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.
2026-06-04 13:42:25 -03:00
diegosouzapw
406a150f15 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.
2026-06-04 06:36:10 -03:00
6029 changed files with 136543 additions and 735472 deletions

View File

@@ -0,0 +1,52 @@
---
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

@@ -0,0 +1,52 @@
---
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

@@ -0,0 +1,52 @@
---
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

@@ -0,0 +1,106 @@
---
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 a clean `build:release` + `rsync` + PM2 restart.
**Akamai VPS:** `69.164.221.35`
**Process manager:** PM2 (`omniroute`)
**Port:** `20128`
**Remote install dir:** `/usr/lib/node_modules/omniroute/app/` (VPS image dir — unchanged)
> [!IMPORTANT]
> **Build must run where `node_modules` is REAL.** A git worktree with symlinked
> `node_modules` breaks the Next.js standalone assembly. Always build from the main
> checkout (or an `npm ci`'d worktree), never from `OmniRoute-*` worktrees unless
> you first ran `npm ci` inside them.
## Steps
### 1. Clean build + sentinel
Run from a checkout with a **real** `node_modules`:
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute && npm run build:release
```
`build:release` does:
1. Deletes `.build/` and `dist/` (clean rebuild — no stale cache)
2. Runs `next build``.build/next/` (intermediates)
3. Assembles the shippable bundle into `dist/` via `assembleStandalone`
4. Writes `dist/BUILD_SHA` = `git rev-parse --short HEAD` (deploy sentinel)
Verify the sentinel before shipping:
```bash
cat /home/diegosouzapw/dev/proxys/OmniRoute/dist/BUILD_SHA && git -C /home/diegosouzapw/dev/proxys/OmniRoute rev-parse --short HEAD
```
Both lines must match. If they differ, the build is stale — re-run `build:release`.
### 2. Back up the running bundle on the VPS
```bash
OMNIROUTE_VERSION=$(node -p "require('/home/diegosouzapw/dev/proxys/OmniRoute/package.json').version")
ssh root@69.164.221.35 "cp -a /usr/lib/node_modules/omniroute/app /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION}"
```
### 3. Stop the process, then rsync `dist/` → remote `app/`
The VPS image directory is still named `app/`; we ship the **contents** of our local `dist/`. **Stop the running process FIRST** so `rsync --delete` never removes chunk files out from under the live server — that race produces the transient `[Shutdown] Cannot find module ./chunks/NNNNN.js` seen mid-deploy:
// turbo-all
```bash
ssh root@69.164.221.35 "pm2 stop omniroute"
rsync -az --delete /home/diegosouzapw/dev/proxys/OmniRoute/dist/ root@69.164.221.35:/usr/lib/node_modules/omniroute/app/
```
### 4. Start + health check
`pm2 restart` on a stopped app starts it again with a clean module graph:
```bash
ssh root@69.164.221.35 "pm2 restart omniroute --update-env && pm2 save"
```
```bash
sleep 5 && curl -sf http://69.164.221.35:20128/api/monitoring/health | jq '{status:.status, version:.version}'
```
Expected: HTTP 200, `"status": "ok"` (or equivalent health payload).
### 5. Verify the deployment
```bash
curl -s -o /dev/null -w 'AKAMAI HTTP %{http_code}\n' http://69.164.221.35:20128/
```
## Rollback
If the health check fails, restore the backup:
```bash
OMNIROUTE_VERSION=$(node -p "require('/home/diegosouzapw/dev/proxys/OmniRoute/package.json').version")
ssh root@69.164.221.35 "rm -rf /usr/lib/node_modules/omniroute/app && cp -a /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION} /usr/lib/node_modules/omniroute/app && pm2 restart omniroute --update-env"
```
## Gotchas
- **Never build in a worktree with symlinked `node_modules`** — the standalone copy step
follows symlinks and produces a broken bundle. Use the main checkout or run `npm ci`
inside the worktree first.
- **`pm2 restart --update-env` does NOT re-read `ecosystem.config.cjs`** — if you changed
env vars in the ecosystem file, use `pm2 reload` or `pm2 delete + pm2 start` instead.
- **Parallel dev + release builds**: to avoid clobbering the dev server's `.build/next`,
pass `NEXT_DIST_DIR=.build/next-release` to `build:release` in CI or concurrent deploys:
`NEXT_DIST_DIR=.build/next-release npm run build:release`
then adjust the rsync source to `dist-release/` (or whatever `build:release` assembles into).
- **`dist/BUILD_SHA` must match the deployed HEAD** — always confirm before rsyncing.
A mismatch means the build ran from a dirty or incorrect commit.

View File

@@ -0,0 +1,122 @@
---
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 both production VPSs using a clean `build:release` + `rsync` + PM2 restart.
**Akamai VPS:** `69.164.221.35`
**Local VPS:** `192.168.0.15`
**Process manager:** PM2 (`omniroute`)
**Port:** `20128`
**Remote install dir:** `/usr/lib/node_modules/omniroute/app/` (VPS image dir — unchanged)
> [!IMPORTANT]
> **Build must run where `node_modules` is REAL.** A git worktree with symlinked
> `node_modules` breaks the Next.js standalone assembly. Always build from the main
> checkout (or an `npm ci`'d worktree), never from `OmniRoute-*` worktrees unless
> you first ran `npm ci` inside them.
## Steps
### 1. Clean build + sentinel
Build **once** locally — the same `dist/` is shipped to both VPSs:
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute && npm run build:release
```
`build:release` does:
1. Deletes `.build/` and `dist/` (clean rebuild — no stale cache)
2. Runs `next build``.build/next/` (intermediates)
3. Assembles the shippable bundle into `dist/` via `assembleStandalone`
4. Writes `dist/BUILD_SHA` = `git rev-parse --short HEAD` (deploy sentinel)
Verify the sentinel before shipping:
```bash
cat /home/diegosouzapw/dev/proxys/OmniRoute/dist/BUILD_SHA && git -C /home/diegosouzapw/dev/proxys/OmniRoute rev-parse --short HEAD
```
Both lines must match. If they differ, the build is stale — re-run `build:release`.
### 2. Back up the running bundle on both VPSs
```bash
OMNIROUTE_VERSION=$(node -p "require('/home/diegosouzapw/dev/proxys/OmniRoute/package.json').version")
ssh root@69.164.221.35 "cp -a /usr/lib/node_modules/omniroute/app /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION}" &
ssh root@192.168.0.15 "cp -a /usr/lib/node_modules/omniroute/app /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION}" &
wait
```
### 3. Stop the processes, then rsync `dist/` → remote `app/` on both VPSs
The VPS image directory is still named `app/`; we ship the **contents** of our local `dist/`. **Stop both running processes FIRST** so `rsync --delete` never removes chunk files out from under a live server — that race produces the transient `[Shutdown] Cannot find module ./chunks/NNNNN.js` seen mid-deploy:
// turbo-all
```bash
ssh root@69.164.221.35 "pm2 stop omniroute" & ssh root@192.168.0.15 "pm2 stop omniroute" & wait
rsync -az --delete /home/diegosouzapw/dev/proxys/OmniRoute/dist/ root@69.164.221.35:/usr/lib/node_modules/omniroute/app/ && rsync -az --delete /home/diegosouzapw/dev/proxys/OmniRoute/dist/ root@192.168.0.15:/usr/lib/node_modules/omniroute/app/
```
### 4. Start + health check on both VPSs
`pm2 restart` on a stopped app starts it again with a clean module graph:
```bash
ssh root@69.164.221.35 "pm2 restart omniroute --update-env && pm2 save"
```
```bash
ssh root@192.168.0.15 "pm2 restart omniroute --update-env && pm2 save"
```
```bash
sleep 5
curl -sf http://69.164.221.35:20128/api/monitoring/health | jq '{status:.status, version:.version}'
curl -sf http://192.168.0.15:20128/api/monitoring/health | jq '{status:.status, version:.version}'
```
Expected: HTTP 200, `"status": "ok"` on both.
### 5. 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/
```
## Rollback
If either VPS fails its health check, restore from backup (adjust host as needed):
```bash
OMNIROUTE_VERSION=$(node -p "require('/home/diegosouzapw/dev/proxys/OmniRoute/package.json').version")
# Akamai rollback:
ssh root@69.164.221.35 "rm -rf /usr/lib/node_modules/omniroute/app && cp -a /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION} /usr/lib/node_modules/omniroute/app && pm2 restart omniroute --update-env"
# Local rollback:
ssh root@192.168.0.15 "rm -rf /usr/lib/node_modules/omniroute/app && cp -a /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION} /usr/lib/node_modules/omniroute/app && pm2 restart omniroute --update-env"
```
## Gotchas
- **Never build in a worktree with symlinked `node_modules`** — the standalone copy step
follows symlinks and produces a broken bundle. Use the main checkout or run `npm ci`
inside the worktree first.
- **`pm2 restart --update-env` does NOT re-read `ecosystem.config.cjs`** — if you changed
env vars in the ecosystem file, use `pm2 reload` or `pm2 delete + pm2 start` instead.
- **Parallel dev + release builds**: to avoid clobbering the dev server's `.build/next`,
pass `NEXT_DIST_DIR=.build/next-release` to `build:release` in CI or concurrent deploys:
`NEXT_DIST_DIR=.build/next-release npm run build:release`
then adjust the rsync source to `dist-release/`.
- **`dist/BUILD_SHA` must match the deployed HEAD** — always confirm before rsyncing.
A mismatch means the build ran from a dirty or incorrect commit.
- **Build once, deploy to both** — never run two separate builds for the two VPSs.
The same `dist/` artifact (same `BUILD_SHA`) must land on both hosts to keep
versions in sync.

View File

@@ -0,0 +1,40 @@
---
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

@@ -0,0 +1,106 @@
---
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 a clean `build:release` + `rsync` + PM2 restart.
**Local VPS:** `192.168.0.15`
**Process manager:** PM2 (`omniroute`)
**Port:** `20128`
**Remote install dir:** `/usr/lib/node_modules/omniroute/app/` (VPS image dir — unchanged)
> [!IMPORTANT]
> **Build must run where `node_modules` is REAL.** A git worktree with symlinked
> `node_modules` breaks the Next.js standalone assembly. Always build from the main
> checkout (or an `npm ci`'d worktree), never from `OmniRoute-*` worktrees unless
> you first ran `npm ci` inside them.
## Steps
### 1. Clean build + sentinel
Run from a checkout with a **real** `node_modules`:
// turbo
```bash
cd /home/diegosouzapw/dev/proxys/OmniRoute && npm run build:release
```
`build:release` does:
1. Deletes `.build/` and `dist/` (clean rebuild — no stale cache)
2. Runs `next build``.build/next/` (intermediates)
3. Assembles the shippable bundle into `dist/` via `assembleStandalone`
4. Writes `dist/BUILD_SHA` = `git rev-parse --short HEAD` (deploy sentinel)
Verify the sentinel before shipping:
```bash
cat /home/diegosouzapw/dev/proxys/OmniRoute/dist/BUILD_SHA && git -C /home/diegosouzapw/dev/proxys/OmniRoute rev-parse --short HEAD
```
Both lines must match. If they differ, the build is stale — re-run `build:release`.
### 2. Back up the running bundle on the VPS
```bash
OMNIROUTE_VERSION=$(node -p "require('/home/diegosouzapw/dev/proxys/OmniRoute/package.json').version")
ssh root@192.168.0.15 "cp -a /usr/lib/node_modules/omniroute/app /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION}"
```
### 3. Stop the process, then rsync `dist/` → remote `app/`
The VPS image directory is still named `app/`; we ship the **contents** of our local `dist/`. **Stop the running process FIRST** so `rsync --delete` never removes chunk files out from under the live server — that race produces the transient `[Shutdown] Cannot find module ./chunks/NNNNN.js` seen mid-deploy:
// turbo-all
```bash
ssh root@192.168.0.15 "pm2 stop omniroute"
rsync -az --delete /home/diegosouzapw/dev/proxys/OmniRoute/dist/ root@192.168.0.15:/usr/lib/node_modules/omniroute/app/
```
### 4. Start + health check
`pm2 restart` on a stopped app starts it again with a clean module graph:
```bash
ssh root@192.168.0.15 "pm2 restart omniroute --update-env && pm2 save"
```
```bash
sleep 5 && curl -sf http://192.168.0.15:20128/api/monitoring/health | jq '{status:.status, version:.version}'
```
Expected: HTTP 200, `"status": "ok"` (or equivalent health payload).
### 5. Verify the deployment
```bash
curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/
```
## Rollback
If the health check fails, restore the backup:
```bash
OMNIROUTE_VERSION=$(node -p "require('/home/diegosouzapw/dev/proxys/OmniRoute/package.json').version")
ssh root@192.168.0.15 "rm -rf /usr/lib/node_modules/omniroute/app && cp -a /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION} /usr/lib/node_modules/omniroute/app && pm2 restart omniroute --update-env"
```
## Gotchas
- **Never build in a worktree with symlinked `node_modules`** — the standalone copy step
follows symlinks and produces a broken bundle. Use the main checkout or run `npm ci`
inside the worktree first.
- **`pm2 restart --update-env` does NOT re-read `ecosystem.config.cjs`** — if you changed
env vars in the ecosystem file, use `pm2 reload` or `pm2 delete + pm2 start` instead.
- **Parallel dev + release builds**: to avoid clobbering the dev server's `.build/next`,
pass `NEXT_DIST_DIR=.build/next-release` to `build:release` in CI or concurrent deploys:
`NEXT_DIST_DIR=.build/next-release npm run build:release`
then adjust the rsync source to `dist-release/` (or whatever `build:release` assembles into).
- **`dist/BUILD_SHA` must match the deployed HEAD** — always confirm before rsyncing.
A mismatch means the build ran from a dirty or incorrect commit.

View File

@@ -0,0 +1,45 @@
---
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

@@ -0,0 +1,427 @@
---
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

@@ -0,0 +1,513 @@
---
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

@@ -0,0 +1,515 @@
---
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

@@ -0,0 +1,891 @@
---
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

@@ -0,0 +1,903 @@
---
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

@@ -0,0 +1,899 @@
---
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

@@ -0,0 +1,51 @@
---
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

@@ -0,0 +1,51 @@
---
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

@@ -0,0 +1,51 @@
---
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

@@ -0,0 +1,545 @@
---
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

@@ -0,0 +1,396 @@
---
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

@@ -0,0 +1,521 @@
---
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

@@ -0,0 +1,366 @@
---
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

@@ -0,0 +1,262 @@
---
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

@@ -0,0 +1,263 @@
---
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

@@ -0,0 +1,269 @@
---
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

@@ -0,0 +1,271 @@
---
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

@@ -0,0 +1,272 @@
---
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

@@ -0,0 +1,278 @@
---
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

@@ -0,0 +1,257 @@
---
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

@@ -0,0 +1,257 @@
---
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

@@ -0,0 +1,268 @@
---
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

@@ -0,0 +1,342 @@
---
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

@@ -0,0 +1,342 @@
---
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

@@ -0,0 +1,347 @@
---
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

@@ -39,15 +39,10 @@ playwright-report
blob-report
# Documentation
# 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.
# 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).
docs/i18n/**
docs/diagrams/**/*.png
docs/diagrams/**/*.jpg
@@ -77,8 +72,6 @@ bun.lock
# Agent config
.agents
.gemini
.claude
.source
# Misc
llm.txt
@@ -125,4 +118,3 @@ app.__qa_backup/
.worktrees
.next-playwright/
cloud/
electron/dist-electron

View File

@@ -1,12 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.sh]
indent_size = 4

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +0,0 @@
# Homologação E2E real — copie para .env.homolog (NUNCA commitar o real)
HOMOLOG_BASE_URL=http://192.168.0.15:20128
# Senha de management do dashboard da VPS (a mesma do /login)
HOMOLOG_ADMIN_PASSWORD=
# Deixe vazio: a suíte cria uma API key efêmera via admin e revoga no fim.
# Só preencha para depurar uma camada isolada com uma key fixa.
HOMOLOG_API_KEY=
# Tier crítico (chat real, max_tokens=5). Demais providers: só validação de catálogo.
HOMOLOG_CRITICAL_PROVIDERS=openai,anthropic,gemini,codex,grok,glm,deepseek,openrouter

5
.github/FUNDING.yml vendored
View File

@@ -1,5 +0,0 @@
# 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"]

View File

@@ -1,29 +0,0 @@
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,29 +24,6 @@ updates:
update-types: ["version-update:semver-major"]
- dependency-name: "eslint-config-next"
update-types: ["version-update:semver-major"]
# typescript majors are peer-blocked by typescript-eslint, which pins a hard
# upper bound (8.64.0 → peerDependencies.typescript ">=4.8.4 <6.1.0"). A TS 7
# bump therefore violates the peer and takes down the whole toolchain at once —
# #7068 grouped it with 6 harmless bumps and turned Build + Lint + Quality Ratchet
# + Unit (6/8, 8/8) + Integration (1/2, 2/2) + dast-smoke red in one shot, blocking
# the innocuous updates riding along with it. Un-ignore once typescript-eslint
# widens the peer, and migrate TS majors intentionally (own PR, own CI run).
- dependency-name: "typescript"
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,10 +7,9 @@ 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
@@ -20,14 +19,9 @@ 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@v7
with:
persist-credentials: false
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,6 @@ 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: |
@@ -30,9 +26,8 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 1
- name: Run Claude Code

View File

@@ -1,31 +0,0 @@
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@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
languages: javascript-typescript
queries: security-extended
- uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
category: "/language:javascript-typescript"

View File

@@ -1,65 +0,0 @@
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
# Build CLI bundle alone varies 6-11min on GitHub-hosted runners (3 consecutive
# timeouts observed on 2026-07-14 with the old 12min cap killing schemathesis
# mid-run) — 25min leaves real headroom for the actual DAST steps.
timeout-minutes: 25
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/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,82 +17,27 @@ 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: 60s
command_timeout: 15m
timeout: 30s
command_timeout: 5m
script: |
set -euo pipefail
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"
npm install -g omniroute@latest 2>&1
INSTALLED_VERSION=$(omniroute --version 2>/dev/null || echo "unknown")
echo "Installed 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
echo "=== Restarting PM2 ==="
pm2 restart omniroute || pm2 start omniroute --name omniroute -- --port 20128
pm2 save
# 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 "=== Health Check ==="
sleep 3
curl -sf http://localhost:20128/api/settings > /dev/null && echo "✅ OmniRoute is healthy" || echo "❌ Health check failed"
echo "=== Deploy complete ==="

View File

@@ -25,10 +25,9 @@ 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:
@@ -42,9 +41,8 @@ jobs:
IMAGE_NAME: diegosouzapw/omniroute
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
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
@@ -98,13 +96,11 @@ jobs:
PROMOTE="${PROMOTE_INPUT:-false}"
else
git fetch --tags --quiet || true
# Decide via the extracted helper, which folds VERSION into the
# candidate set so the result is independent of git-tag sync timing
# on `release` events (#5301). Without that, the freshly-created tag
# is often not yet visible here and :latest stays a release behind.
PROMOTE=$(git tag -l 'v[0-9]*' | bash scripts/ci/should-promote-latest.sh "$VERSION")
if [ "$PROMOTE" != "true" ]; then
echo "Version $VERSION is not the highest stable semver. Not promoting :latest."
HIGHEST=$(git tag -l 'v[0-9]*' | sed 's/^v//' | grep -vE -- '-(rc|alpha|beta|pre|next)' | sort -V | tail -1 || echo "")
if [ -n "$HIGHEST" ] && [ "$VERSION" = "$HIGHEST" ]; then
PROMOTE="true"
else
echo "Version $VERSION is not the highest semver tag (highest=${HIGHEST:-<none>}). Not promoting :latest."
fi
fi
echo "promote_latest=$PROMOTE" >> "$GITHUB_OUTPUT"
@@ -127,9 +123,6 @@ jobs:
needs: prepare
if: needs.prepare.outputs.skip != 'true'
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
@@ -145,9 +138,8 @@ jobs:
GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
fetch-depth: 0
@@ -184,46 +176,20 @@ jobs:
env:
DOCKER_BUILDKIT_INLINE_CACHE: 1
- 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
- name: Export digest
env:
DOCKER_BUILDKIT_INLINE_CACHE: 1
- name: Export digests
env:
DIGEST_BASE: ${{ steps.build.outputs.digest }}
DIGEST_WEB: ${{ steps.build-web.outputs.digest }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
mkdir -p /tmp/digests/base /tmp/digests/web
touch "/tmp/digests/base/${DIGEST_BASE#sha256:}"
touch "/tmp/digests/web/${DIGEST_WEB#sha256:}"
mkdir -p /tmp/digests
digest="${DIGEST#sha256:}"
touch "/tmp/digests/${digest}"
- name: Upload base digests
- name: Upload digest
uses: actions/upload-artifact@v7
with:
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/*
name: digests-${{ matrix.arch }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
@@ -234,10 +200,6 @@ 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
@@ -245,9 +207,8 @@ jobs:
PROMOTE_LATEST: ${{ needs.prepare.outputs.promote_latest }}
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
fetch-depth: 0
@@ -267,134 +228,60 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download base digests
- name: Download digests
uses: actions/download-artifact@v8
with:
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
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Create Docker Hub manifest
run: |
set -euo pipefail
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[@]}"
}
tags=(-t "${IMAGE_NAME}:${VERSION}")
if [ "$PROMOTE_LATEST" = "true" ]; then
tags+=(-t "${IMAGE_NAME}:latest")
fi
create_manifest "${IMAGE_NAME}" "" /tmp/digests/base
create_manifest "${IMAGE_NAME}" "-web" /tmp/digests/web
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[@]}"
- name: Create GHCR manifest
run: |
set -euo pipefail
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[@]}"
}
tags=(-t "${GHCR_IMAGE_NAME}:${VERSION}")
if [ "$PROMOTE_LATEST" = "true" ]; then
tags+=(-t "${GHCR_IMAGE_NAME}:latest")
fi
create_manifest "${GHCR_IMAGE_NAME}" "" /tmp/digests/base
create_manifest "${GHCR_IMAGE_NAME}" "-web" /tmp/digests/web
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[@]}"
- 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.
#
# ignore-unfixed mirrors the blocking gate: the Security tab must surface only
# ACTIONABLE vulnerabilities — ones with a published fix we can pull by rebuilding
# on a patched base or bumping the dep. Without it the advisory upload floods the
# tab with unfixable base-image OS CVEs (Debian trixie packages with no upstream
# patch yet, overwhelmingly local-only and not reachable from the proxy request
# surface), which is noise an operator cannot act on. trivyignores points at the
# repo-root .trivyignore so accepted-risk fixable CVEs have one auditable home.
# See docs/security/SUPPLY_CHAIN.md.
- 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
ignore-unfixed: true
trivyignores: .trivyignore
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,56 +11,43 @@ 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: read
contents: write
id-token: write
packages: write
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@v7
uses: actions/checkout@v6
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 [[ "$EVENT_NAME" == "push" ]]; then
if [[ "${{ github.event_name }}" == "push" ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
else
VERSION="$INPUT_VERSION"
VERSION="${{ inputs.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:
@@ -84,9 +71,7 @@ jobs:
deb_ext: .deb
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
@@ -94,7 +79,7 @@ jobs:
cache: npm
- name: Cache node_modules
uses: actions/cache@v6.1.0
uses: actions/cache@v5
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
@@ -114,7 +99,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:
@@ -124,13 +109,8 @@ 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');
@@ -201,12 +181,6 @@ jobs:
[ -f "$file" ] && cp "$file" "../../release-assets/OmniRoute.exe" && break
done
fi
# electron-updater manifests (latest.yml / latest-mac.yml / latest-linux.yml)
# must be published alongside the installers, or autoUpdater fails with
# "Cannot find latest.yml in the latest release artifacts" (#6766).
for file in latest*.yml; do
[ -f "$file" ] && cp "$file" ../../release-assets/
done
- name: Upload artifacts
uses: actions/upload-artifact@v7
@@ -218,13 +192,10 @@ 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@v7
uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- name: Download all artifacts
@@ -234,20 +205,14 @@ 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-${VERSION}.source.tar.gz"
export ZIPBALL="OmniRoute-${VERSION}.source.zip"
export TARBALL="OmniRoute-${{ needs.validate.outputs.version }}.source.tar.gz"
export ZIPBALL="OmniRoute-${{ needs.validate.outputs.version }}.source.zip"
# Use git archive for clean source export
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"
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"
echo "✓ Created source archives:"
ls -lh "release-assets/$TARBALL" "release-assets/$ZIPBALL"
@@ -269,7 +234,6 @@ jobs:
release-assets/*.AppImage
release-assets/*.deb
release-assets/*.blockmap
release-assets/*.yml
release-assets/*.source.tar.gz
release-assets/*.source.zip
env:
@@ -278,14 +242,6 @@ 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

@@ -1,63 +0,0 @@
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

@@ -1,126 +0,0 @@
name: Nightly Node Compat
# Plano mestre testes+CI (Eixo D2, aprovado 2026-07-04): as matrizes de compatibilidade
# Node 24/26 custavam ~28% de CADA run do CI pesado (2 execuções completas da suíte por
# sync da release-PR) para pegar uma classe de quebra que raramente nasce num PR típico.
# Elas rodam aqui 1×/dia contra o tip da release ativa (mesmo alvo do nightly-release-green)
# e continuam obrigatórias no gate de release via workflow_dispatch do ci.yml se preciso.
# fail-fast desligado: numa quebra queremos saber TODAS as versões afetadas de uma vez.
on:
schedule:
- cron: "47 6 * * *" # 06:47 UTC diário — slot distinto dos demais nightlies
workflow_dispatch:
inputs:
branch:
description: "Branch to validate (default: highest release/vX.Y.Z)"
required: false
type: string
permissions:
contents: read
issues: write
concurrency:
group: nightly-compat
cancel-in-progress: true
jobs:
resolve-branch:
name: Resolve active release branch
runs-on: ubuntu-latest
outputs:
target: ${{ steps.branch.outputs.target }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- name: Resolve active release branch
id: branch
env:
INPUT_BRANCH: ${{ github.event.inputs.branch }}
run: |
set -euo pipefail
if [ -n "${INPUT_BRANCH:-}" ]; then
TARGET="$INPUT_BRANCH"
else
TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \
| sed 's#origin/##' \
| sort -t/ -k2 -V \
| tail -1)
fi
case "$TARGET" in
release/v[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "Refusing non-canonical branch name: $TARGET"; exit 1 ;;
esac
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
compat-build-26:
name: Node 26 Compatibility Build
runs-on: ubuntu-latest
timeout-minutes: 25
needs: resolve-branch
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-branch.outputs.target }}
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "26"
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run build
compat-tests:
name: Node ${{ matrix.node }} Compat Tests (${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 25
needs: resolve-branch
strategy:
fail-fast: false
matrix:
node: [24, 26]
shard: [1, 2, 3, 4]
env:
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-nightly-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
TEST_SHARD: ${{ matrix.shard }}/4
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-branch.outputs.target }}
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run test:unit:ci:shard
report:
name: Open / update tracking issue on failure
runs-on: ubuntu-latest
if: ${{ !cancelled() && (needs.compat-tests.result == 'failure' || needs.compat-build-26.result == 'failure') }}
needs: [resolve-branch, compat-build-26, compat-tests]
permissions:
issues: write
steps:
- name: Open or update issue
env:
GH_TOKEN: ${{ github.token }}
TARGET: ${{ needs.resolve-branch.outputs.target }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
TITLE="🌙 nightly-compat: Node 24/26 failures on $TARGET"
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "$TITLE in:title" --json number --jq '.[0].number')
BODY="Nightly Node-compat run failed on \`$TARGET\`: $RUN_URL — triage which Node version/shard broke (fail-fast off, all versions reported)."
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body "$BODY"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body "$BODY"
fi

View File

@@ -1,102 +0,0 @@
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

View File

@@ -1,163 +0,0 @@
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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
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

View File

@@ -1,35 +0,0 @@
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"],
});

View File

@@ -1,303 +0,0 @@
name: Release-Green (continuous)
# Solution D — continuous, NON-BLOCKING drift signal for the active release branch.
#
# WHY: the full gate (ci.yml) only runs on the release PR (PR → main), so reds
# accrue silently on release/** and explode — in layers — at release time. This
# workflow reproduces the release-equivalent validation on the release branch and,
# when there are HARD failures, opens/updates a single tracking issue.
#
# WS5.1 (v3.8.49 quality plan) — two modes:
# push to release/v* (code paths) → --quick (fast HARD gates, ~5-8min). Catches the
# captain's direct pushes (sync-back — the one ungated write path) AND the merged
# COMBINATION right after every PR merge, attributing the offending push range in
# the issue. Base-red MTTD drops from ≤24h to ≤~15min after the offending push.
# schedule (3×/day) → full --with-build --full-ci (the deep sweep incl. build+suites).
#
# It is NOT a required status check and never touches a contributor PR — it only
# reports. Ratchet drift (eslint warnings / cognitive-complexity / file-size) is
# expected mid-cycle and is reported but never raises the alarm on its own; only
# real defects (typecheck / lint errors / unit / vitest / db-rules / public-creds /
# package-artifact) flip the issue open.
on:
push:
branches: ["release/v*", "main"]
paths:
- "src/**"
- "open-sse/**"
- "bin/**"
- "electron/**"
- "scripts/**"
- "tests/**"
- "config/**"
- "package.json"
- "package-lock.json"
- "tsconfig*.json"
schedule:
- cron: "23 5 * * *" # full sweep — off-peak, distinct from other nightlies
- cron: "23 12 * * *" # full sweep — midday (WS5.1: 3×/day instead of 1×)
- cron: "23 18 * * *" # full sweep — evening
workflow_dispatch:
inputs:
branch:
description: "Release branch to validate (default: highest release/vX.Y.Z)"
required: false
type: string
permissions:
contents: read
issues: write
concurrency:
# push storms during merge campaigns collapse to the newest commit per branch;
# scheduled full sweeps keep their own single lane.
group: release-green-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
env:
OMNIROUTE_SKIP_SYSTEM_TRUST: "1"
jobs:
release-green:
name: Validate active release branch
# On a push, only run for release/* pushes — a push to main is handled by the
# main-green job below. Schedule/dispatch always run (they validate the highest release).
if: ${{ github.event_name != 'push' || startsWith(github.ref_name, 'release/') }}
# Dynamic runner: with USE_VPS_RUNNER=true (release window / on-demand pre-flight)
# this runs on the dedicated VPS runner — clean env (no operator OMNIROUTE_API_KEY,
# no local noauth CLIs => zero machine-specific false positives) and no contention.
# Nightly cron normally finds the var false (VM off) and falls back to hosted.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
env:
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-nightly-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- name: Resolve active release branch
id: branch
env:
INPUT_BRANCH: ${{ github.event.inputs.branch }}
EVENT_NAME: ${{ github.event_name }}
PUSHED_REF: ${{ github.ref_name }}
run: |
set -euo pipefail
if [ -n "${INPUT_BRANCH:-}" ]; then
TARGET="$INPUT_BRANCH"
elif [ "$EVENT_NAME" = "push" ]; then
# validate exactly what was pushed, not the highest branch
TARGET="$PUSHED_REF"
else
# highest release/vX.Y.Z by semver among remote branches
TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \
| sed 's#origin/##' \
| sort -t/ -k2 -V \
| tail -1)
fi
if [ -z "$TARGET" ]; then echo "No release/v* branch found"; exit 1; fi
# Strict format guard — reject anything that isn't release/vX.Y.Z (blocks
# ref/command injection via the workflow_dispatch input).
if ! printf '%s' "$TARGET" | grep -qE '^release/v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Refusing non-canonical branch name: $TARGET"; exit 1
fi
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
echo "Active release branch: $TARGET"
- name: Checkout the release branch
env:
TARGET: ${{ steps.branch.outputs.target }}
run: |
set -euo pipefail
git checkout "$TARGET"
git log -1 --oneline
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Release-green validation (full)
id: validate
env:
EVENT_NAME: ${{ github.event_name }}
run: |
set +e
# --hermetic: scrub live-test trigger vars (self-hosted runner may carry
# operator env; hosted ignores the unknown flag before #6300 lands).
# push → --quick: fast HARD gates only (~5-8min), per-merge signal.
# schedule/dispatch → --with-build --full-ci: ALSO run every static gate from
# ci.yml's gate jobs (lint, quality-gate, quality-extended, docs-sync-strict,
# pr-test-policy) + build + full suites. PRs into release/** only get the
# fast-gates, so these accrue silently and explode in layers on the release PR
# (v3.8.46: 11 static base-reds leaked).
if [ "$EVENT_NAME" = "push" ]; then
MODE="--quick"
else
MODE="--with-build --full-ci"
fi
echo "[release-green] mode: $MODE (event: $EVENT_NAME)"
# shellcheck disable=SC2086 — MODE is an intentional flag list
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
1> release-green.json 2> release-green.log
echo "exit=$?" >> "$GITHUB_OUTPUT"
echo "------- report -------"
cat release-green.log
- name: Open / update tracking issue on HARD failure
if: steps.validate.outputs.exit != '0'
env:
GH_TOKEN: ${{ github.token }}
TARGET: ${{ steps.branch.outputs.target }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
AFTER_SHA: ${{ github.event.after }}
run: |
set -euo pipefail
TITLE="🔴 Release branch not green: ${TARGET}"
{
echo "The **release-green** validation found HARD failures on \`${TARGET}\`."
echo "These are real defects that would block the release PR — fix them in the"
echo "originating PR branch (via co-authorship), not by demanding it from contributors."
echo ""
echo "**Run:** ${RUN_URL} (mode: ${EVENT_NAME})"
# WS5.1 attribution: on push events the offending change IS this push's range
# (one merge per push in the normal queue), so name it — no bisect needed.
if [ "$EVENT_NAME" = "push" ] && [ -n "${BEFORE_SHA:-}" ] && \
git cat-file -e "$BEFORE_SHA" 2>/dev/null; then
echo ""
echo "**Offending push range** (\`${BEFORE_SHA:0:9}..${AFTER_SHA:0:9}\`):"
echo '```'
git log --no-decorate --oneline "${BEFORE_SHA}..${AFTER_SHA}" | head -20
echo '```'
fi
echo ""
echo '```'
sed -n '/──────── verdict ────────/,$p' release-green.log || tail -40 release-green.log
echo '```'
echo ""
echo "_Ratchet drift (eslint warnings / cognitive-complexity / file-size) listed above is expected mid-cycle and is rebaselined at release — it is NOT a contributor concern and did not, on its own, open this issue._"
} > issue-body.md
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file issue-body.md
echo "Updated existing issue #$EXISTING"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body-file issue-body.md
fi
- name: Upload report artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: release-green-report
path: |
release-green.json
release-green.log
if-no-files-found: ignore
# Companion arm for `main`. Under the parallel-cycle model, main only receives merged
# work at the release squash — so a gate/infra fix that lands only on release leaves
# main red the whole cycle, and repo-wide gates (CodeQL alert count, ratchet baselines)
# turn EVERY PR into main red on a check unrelated to its diff. This detects that and
# opens a "🔴 main not green" tracking issue. The PREVENTION is the companion-PR reflex
# (Hard Rule #21 area / _shared/merge-gates.md §8); this is the automated backstop.
main-green:
name: Validate main branch
# On a push, only run for a push to main — a push to release/* is handled by
# release-green above. Schedule/dispatch always run (they also sweep main).
if: ${{ github.event_name != 'push' || github.ref_name == 'main' }}
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
env:
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-nightly-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
ref: main # literal — no injection surface; scheduled runs default to the repo default branch (a release/v*), so pin main explicitly
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Main-green validation
id: validate
env:
EVENT_NAME: ${{ github.event_name }}
run: |
set +e
# push (a merge into main) → --quick fast HARD gates; schedule/dispatch → full sweep.
if [ "$EVENT_NAME" = "push" ]; then
MODE="--quick"
else
MODE="--with-build --full-ci"
fi
echo "[main-green] mode: $MODE (event: $EVENT_NAME)"
# shellcheck disable=SC2086 — MODE is an intentional flag list
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
1> main-green.json 2> main-green.log
echo "exit=$?" >> "$GITHUB_OUTPUT"
echo "------- report -------"
cat main-green.log
- name: Open / update tracking issue on HARD failure
if: steps.validate.outputs.exit != '0'
env:
GH_TOKEN: ${{ github.token }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
EVENT_NAME: ${{ github.event_name }}
run: |
set -euo pipefail
TITLE="🔴 main branch not green"
{
echo "The **main-green** validation found HARD failures on \`main\`."
echo ""
echo "Because \`main\` only receives merged work at the release squash, a gate/infra"
echo "fix that landed only on the release branch leaves \`main\` broken for the whole"
echo "cycle — and repo-wide gates (CodeQL alert count, ratchet baselines) then turn"
echo "**every open PR into main** red on a check unrelated to its diff. The fix is a"
echo "companion PR \`--base main\` carrying the release-side fix (see"
echo "\`_shared/merge-gates.md\` §8), NOT chasing each contributor PR."
echo ""
echo "**Run:** ${RUN_URL} (mode: ${EVENT_NAME})"
echo ""
echo '```'
sed -n '/──────── verdict ────────/,$p' main-green.log || tail -40 main-green.log
echo '```'
echo ""
echo "_Ratchet drift (eslint warnings / cognitive-complexity / file-size) is expected mid-cycle and did NOT, on its own, open this issue._"
} > issue-body.md
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file issue-body.md
echo "Updated existing issue #$EXISTING"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body-file issue-body.md
fi
- name: Upload report artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: main-green-report
path: |
main-green.json
main-green.log
if-no-files-found: ignore

View File

@@ -1,110 +0,0 @@
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@v6.1.0
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

@@ -1,72 +0,0 @@
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/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

@@ -22,14 +22,6 @@ on:
- latest
- next
- historic
publish_mode:
description: "staged = npm stage publish (owner approves with 2FA after the staged boot-verify); direct = legacy immediate publish (emergency fallback only)"
required: false
default: "staged"
type: choice
options:
- staged
- direct
workflow_call:
inputs:
version:
@@ -45,11 +37,10 @@ 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"
@@ -57,15 +48,10 @@ 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@v7
uses: actions/checkout@v6
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
@@ -155,53 +141,8 @@ jobs:
if: steps.resolve.outputs.skip != 'true'
run: npm run check:pack-artifact
- name: Generate CycloneDX SBOM (npm)
- name: Publish to 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
# WS1.2/WS1.3 (#7065 class): the artifact that is about to be published must
# BOOT. build:cli already assembled dist/ above; this packs+installs+boots the
# real tarball and fails the publish before anything reaches the registry.
- name: Boot-smoke the tarball before ANY publish
if: steps.resolve.outputs.skip != 'true'
run: npm run check:pack-boot
# WS1.3 (D2, v3.8.49 plan): STAGED publishing by default — `npm stage publish`
# parks the exact bytes on the registry WITHOUT making them installable; the
# owner then verifies and approves with 2FA (`npm stage approve`), moving the
# human gate to AFTER the proof instead of before it. Requires npm >= 11.15
# (staged publishing GA 2026-05-22). publish_mode=direct is the emergency
# fallback (legacy immediate publish) via workflow_dispatch.
- name: Ensure npm supports staged publishing
if: steps.resolve.outputs.skip != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct')
run: |
set -euo pipefail
CUR=$(npm --version)
if ! node -e "const [a,b]='$(npm --version)'.split('.').map(Number); process.exit(a>11||(a===11&&b>=15)?0:1)"; then
# Pinned exact version (supply-chain: never float @latest in the publish
# job); bump deliberately when a newer npm is required.
echo "npm $CUR < 11.15 — installing pinned npm 11.15.0 for staged publishing"
npm install -g --ignore-scripts npm@11.15.0
fi
npm --version
- name: Publish to npm (staged — owner approves with 2FA)
if: steps.resolve.outputs.skip != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct')
env:
VERSION: ${{ steps.resolve.outputs.version }}
TAG: ${{ steps.resolve.outputs.tag }}
@@ -209,32 +150,10 @@ jobs:
run: |
set -euo pipefail
# Always pass --tag explicitly. Defense in depth: even if VERSION is
# accidentally an older release, the historic tag will NOT claim `@latest`.
npm stage publish --provenance --access public --tag "$TAG"
{
echo "## 📦 omniroute@$VERSION STAGED (not yet installable)"
echo ""
echo "The exact bytes are parked on the registry. To release them:"
echo '```'
echo "npm stage list omniroute # find the stage id"
echo "npm stage approve <id> # owner 2FA — THE publish"
echo '```'
echo "To verify the staged bytes first: npm stage download <id> → run"
echo "scripts/check/check-pack-boot.mjs against them (see RELEASE_CHECKLIST)."
echo "To discard: npm stage reject <id>."
} >> "$GITHUB_STEP_SUMMARY"
echo "✅ Staged omniroute@$VERSION (dist-tag=$TAG) — awaiting owner 'npm stage approve'"
- name: Publish to npm (DIRECT — emergency fallback)
if: steps.resolve.outputs.skip != 'true' && github.event_name == 'workflow_dispatch' && inputs.publish_mode == 'direct'
env:
VERSION: ${{ steps.resolve.outputs.version }}
TAG: ${{ steps.resolve.outputs.tag }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
npm publish --provenance --access public --tag "$TAG"
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG) [DIRECT mode]"
# accidentally an older release, `npm publish --tag historic` will
# NOT promote it to `@latest`.
npm publish --access public --tag "$TAG"
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG)"
- name: Publish to GitHub Packages
if: steps.resolve.outputs.skip != 'true'
@@ -253,16 +172,9 @@ jobs:
publish-opencode-plugin:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # npm provenance
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
# Full history needed for auto-bump: git diff against previous release tag
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -270,47 +182,6 @@ jobs:
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
registry-url: https://registry.npmjs.org
- name: Auto-bump plugin version if plugin changed since last release
id: bump
working-directory: "@omniroute/opencode-plugin"
env:
CURRENT_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
PKG_VERSION=$(node -p "require('./package.json').version")
PKG_NAME=$(node -p "require('./package.json').name")
# 1) Skip if current version is not yet published (no bump needed)
PUBLISHED="$(npm view "${PKG_NAME}@${PKG_VERSION}" version 2>/dev/null || true)"
if [ "$PUBLISHED" != "$PKG_VERSION" ]; then
echo "✅ ${PKG_NAME}@${PKG_VERSION} is new — no bump needed."
echo "bumped=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# 2) Find the previous release tag (exclude the current one)
PREV_TAG=$(git tag -l 'v*' --sort=-version:refname \
| grep -v "^${CURRENT_TAG}$" | head -1 || echo "")
if [ -z "$PREV_TAG" ]; then
echo "No previous tag to compare — skipping bump."
echo "bumped=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# 3) Check if plugin dir actually changed since that tag
if git diff --quiet "$PREV_TAG" -- "@omniroute/opencode-plugin/"; then
echo "⏭️ No plugin changes since $PREV_TAG — nothing to publish."
echo "bumped=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# 4) Auto-bump patch version
npm version patch --no-git-tag-version --allow-same-version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "bumped=true" >> "$GITHUB_OUTPUT"
echo "📦 Auto-bumped ${PKG_NAME} from ${PKG_VERSION} to ${NEW_VERSION}"
- name: Install plugin dependencies
working-directory: "@omniroute/opencode-plugin"
run: npm install --no-audit --no-fund
@@ -337,5 +208,5 @@ jobs:
echo "⚠️ ${PKG_NAME}@${PKG_VERSION} is already published on npm — skipping."
exit 0
fi
npm publish --provenance --access public --ignore-scripts
npm publish --access public --ignore-scripts
echo "✅ Published ${PKG_NAME}@${PKG_VERSION}"

View File

@@ -32,9 +32,7 @@ jobs:
matrix:
node: ["22", "24"]
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
@@ -49,9 +47,7 @@ jobs:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: "22"

View File

@@ -32,9 +32,7 @@ jobs:
matrix:
node: ["20", "22", "24"]
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
@@ -48,9 +46,7 @@ jobs:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: "20"

View File

@@ -1,354 +0,0 @@
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 must never mutate the runner's OS trust store (2026-07-05: a cert-flow
# test installed a fake PEM on a persistent self-hosted runner and broke all
# system TLS). Belt-and-suspenders with tests/_setup/isolateDataDir.ts.
OMNIROUTE_SKIP_SYSTEM_TRUST: "1"
CI_NODE_VERSION: "24"
jobs:
# Same classifier as ci.yml (scripts/quality/classify-pr-changes.mjs) so PR→release
# path filters share existence reasons: code / docs / i18n / workflow.
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@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
- 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
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
node scripts/quality/classify-pr-changes.mjs changed-files.txt >> "$GITHUB_OUTPUT"
# Docs/OpenAPI contract gates only — existence reason is doc accuracy + route refs.
# Split out of fast-gates so pure-docs PRs skip typecheck/unit while still validating docs.
docs-gates:
name: Docs Gates (fast-path)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
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
- run: npm ci
# One walk of src/app/api for openapi-routes + docs-symbols (both still fail independently).
- run: npm run check:api-docs-refs
- name: Docs accuracy (fabricated-docs + i18n mirrors, strict)
run: npm run check:docs-all
fast-gates:
name: Fast Quality Gates
needs: changes
# Code surface only — pure docs/i18n PRs skip this bag (docs-gates covers docs).
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner (same rule as ci.yml): use the self-hosted VPS pool only when the
# release captain has USE_VPS_RUNNER=true AND this is not a fork PR (own-origin
# branches only — a fork PR must never execute on the LAN runner). Var unset/false
# or a fork PR falls back to ubuntu-latest, so this is inert until the flag flips.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || '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
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
- run: npm run check:provider-consistency
- run: npm run check:fetch-targets
# docs-all / openapi-routes / docs-symbols live in docs-gates (path-filtered).
- 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:test-runner-api
# Guards tap.testFiles drift: a covering unit test absent from stryker.conf.json
# tap.testFiles makes its module's mutants survive on a cold nightly-mutation run,
# false-failing the blocking mutationScore ratchet. See check-mutation-test-coverage.mjs.
- run: npm run check:mutation-test-coverage
- run: npm run check:any-budget:t11
# Build-scope guard: fails if worktrees/cruft leak into the tsconfig include
# scope (would OOM `next build`). Instant. See incident 2026-06-25 / #5031.
- run: npm run check:build-scope
# Pack-policy (unexpected-files allowlist) WITHOUT a build — catches a stray file
# leaking into the npm tarball (v3.8.36: 6 ops bin/*.sh) per-PR instead of only on
# the release PR's heavy Package Artifact job.
- run: npm run check:pack-policy
# Complexity + cognitive-complexity: ONE ESLint walk (both baselines still
# enforced separately by ruleId). Avoids two cold tree walks on fast-path.
- run: npm run check:complexity-ratchets
- name: Typecheck (core)
run: npm run typecheck:core
# #7033: dashboard-scoped typecheck gate — src/app/(dashboard) TSX is not
# covered by typecheck:core's curated allowlist. See check-dashboard-typecheck.mjs.
- name: Typecheck (dashboard)
run: npm run check:dashboard-typecheck
# WS4.2 (v3.8.49 plan): TypeScript 7 native-compiler SHADOW — advisory only.
# TS7 went GA 2026-07-08 with 8-12x type-check speedups; its Compiler API only
# arrives in 7.1, so typescript-eslint / type-coverage / Stryker stay on 6.x
# (the hybrid is the officially documented pattern). Isolated npx on purpose:
# installing an alias package could collide node_modules/.bin/tsc with 6.x.
# Promote to the blocking gate after ~1 week of parity with the step above.
- name: Typecheck (core) — TS7 native shadow (advisory)
continue-on-error: true
run: |
RC=0
START=$(date +%s)
npx -y -p typescript@7 tsc --pretty false -p tsconfig.typecheck-core.json || RC=$?
echo "[ts7-shadow] exit=$RC elapsed=$(( $(date +%s) - START ))s — the 6.x step above stays authoritative"
exit $RC
# TIA: build the impact map at runtime (gitignored, ~21MB) and run only the
# unit tests impacted by this PR's changed files. On hub/unmapped changes the
# selector returns __RUN_ALL__ — full-suite authority is the parallel
# `fast-unit` 4-shard job (test:unit:ci:shard; was 2-shard, #6781), NOT an
# unsharded re-run here. Stacking unsharded test:unit:ci on top of fast-unit
# doubled wall time (~16 min extra on ubuntu-latest) without extra coverage.
#
# BLOCKING for the *impacted subset* (flipped 2026-06-17). Fail-safe full
# coverage remains required via `Unit Tests fast-path` (fast-unit).
- name: Impacted unit tests (TIA subset; 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: __RUN_ALL__ — deferring FULL unit suite to fast-unit (4-shard)."
echo "Not re-running unsharded test:unit:ci here (duplicate of fast-unit coverage)."
exit 0
fi
echo "Running impacted tests:"; echo "$SEL"
mapfile -t FILES <<< "$SEL"
# Loader parity with test:unit:ci:shard (#6787): tests/unit/dashboard/** runs
# under `--import tsx` (CJS transform — required for ESM-only deep imports like
# @lobehub/icons/es/* reached via lobeProviderIcons.ts); everything else under
# `--import tsx/esm`. A single tsx/esm invocation false-reds every dashboard
# module-shape test the impact map selects ("Unexpected token 'export'").
DASH=(); REST=()
for f in "${FILES[@]}"; do
case "$f" in
tests/unit/dashboard/*) DASH+=("$f") ;;
*) REST+=("$f") ;;
esac
done
RC=0
if [ ${#REST[@]} -gt 0 ]; then
node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${REST[@]}" || RC=$?
fi
if [ ${#DASH[@]} -gt 0 ]; then
node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${DASH[@]}" || RC=$?
fi
exit $RC
fast-vitest:
name: Vitest (fast-path)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
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:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# WS5.2/5.3: JUnit feeds Trunk Flaky Tests — the fast-path runs on EVERY PR,
# which is where flaky-detection volume actually comes from (ci.yml's heavy
# jobs only run on the release PR). Advisory upload, own-origin only.
- run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-fastpath.xml
- name: Upload test results to Trunk (advisory)
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
continue-on-error: true
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
with:
junit-paths: trunk-junit/**/*.xml
org-slug: omniroute
token: ${{ secrets.TRUNK_TOKEN }}
fast-unit:
name: Unit Tests fast-path (${{ matrix.shard }}/4)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
# This is the heaviest fast-path job; 4-way sharding (was 2, #6781) halves the
# critical path again (~8.5min → ~4.5min on ubuntu-latest; ~2min on the 8-slot
# runner box). Node's native --test-shard=N/total takes any denominator — only
# this matrix and the TEST_SHARD env below encode the shard count.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
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:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# QW-d: fonte única — o mesmo npm script do CI pesado/local. Fecha dois drifts do
# comando inline antigo: os dirs `memory` e `usage` estavam FORA do glob (testes
# silenciosamente não rodavam no fast path) e o setupPolyfill não era importado.
- run: npm run test:unit:ci:shard
env:
TEST_SHARD: ${{ matrix.shard }}/4
# ── Pacote 4 (plano mestre testes+CI, aprovado 2026-07-04) ─────────────────────────
# No-new-warnings por PR via ESLint bulk suppressions nativo (>=9.24). O baseline
# config/quality/eslint-suppressions.json congela as violações EXISTENTES por
# arquivo+regra; qualquer warning NOVO aparece e o --max-warnings 0 falha o job — o
# drift de +41/+88 warnings por ciclo passa a morrer no PR que o introduz, em vez de
# ser rebaselinado às cegas na release. Aperto do baseline (na reconciliação da
# release): npx eslint . --prune-suppressions --suppressions-location config/quality/eslint-suppressions.json
#
# Princípio Zero: bloqueante SÓ para branches internas (as campanhas/sessões são a
# origem do drift). PR de FORK roda em modo report (continue-on-error → o job fica
# verde com anotação; a campanha /green-prs aplica o fix via co-autoria — o
# contribuidor NUNCA é bloqueado nem cobrado).
lint-guard:
name: No new ESLint warnings
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
steps:
- 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
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
- name: ESLint (baseline congelado — warning novo = vermelho)
# lint:json writes the report; --max-warnings 0 keeps no-new-warnings policy.
run: npm run lint:json -- --max-warnings 0
# Merge-integrity: pega no PR os dois vazamentos crônicos de merge que hoje só
# explodem na release-PR. (1) CHANGELOG-eat — o auto-resolve do merge come
# bullets vizinhos/seções inteiras (incidente #6193, 2026-07-05: 212 linhas /
# 130 bullets); o checkout de PR é refs/pull/N/merge, então comparar contra a
# base detecta o eat ANTES do merge. (2) SKILL.md gerado stale vs o catálogo de
# agent-skills (#6186 mergeou um id de catálogo sem rodar o gerador → 8 reds de
# integration invisíveis até a release).
#
# Princípio Zero: bloqueante SÓ para branches internas; PR de FORK roda em modo
# report (continue-on-error) — a campanha corrige via co-autoria, o contribuidor
# nunca é bloqueado.
merge-integrity:
name: Merge integrity (changelog + generated skills)
# Always on non-draft PRs — CHANGELOG/skills can break on docs-only merges too.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) }}
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: CHANGELOG integrity (nenhum bullet da base pode sumir no merge-result)
run: npm run check:changelog-integrity
- name: Agent-skills generator sync (SKILL.md gerado ≡ catálogo)
run: npm run check:agent-skills-sync

View File

@@ -1,40 +0,0 @@
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

View File

@@ -1,29 +0,0 @@
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

View File

@@ -1,69 +0,0 @@
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 first.
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

57
.gitignore vendored
View File

@@ -17,17 +17,11 @@ _tasks/
.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)
memory-bank/
@@ -49,10 +43,6 @@ docs/new-features/
# dependencies
node_modules/
# 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
@@ -72,10 +62,6 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
!.env.homolog.example
# Provider API keys (never commit)
*.api-key
.nvidia-api-key
# vercel
.vercel
@@ -162,11 +148,6 @@ typescript
# Superpowers plans/specs (internal tooling, not project code)
docs/superpowers/
# Superpowers visual-companion brainstorm mockups (ephemeral)
.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
@@ -211,41 +192,3 @@ 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
# mise configuration
mise.toml
_artifacts/
.claude-flow/
# ESLint file cache (npm run lint --cache / complexity ratchets)
.eslintcache
.eslintcache-complexity
# CI/local quality artifacts (eslint-results.json, etc.)
.artifacts/
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
.env.homolog
tests/homolog/.auth/
tests/homolog/ui/.auth/
homolog-report/

View File

@@ -1,89 +0,0 @@
# .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/''',
# ]
#
[[rules]]
# Falsos-positivos comprovados do generic-api-key — zerados em 2026-07-13 (WS6/D3,
# plano v3.8.49). Revisar em v3.9.0. Nenhum é credencial: dois são NOMES DE CAMPO
# de métricas de latência; o terceiro é o valor PÚBLICO de um beta header da API
# da Anthropic (documentado publicamente, não é segredo).
id = "generic-api-key"
[rules.allowlist]
description = "Field names + public Anthropic beta-header value (não são segredos)"
regexes = [
'''latencyP\d{2}Ms''',
'''interleaved-thinking-2025-05-14''',
]

View File

@@ -1,13 +1,31 @@
#!/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
# 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
# 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

View File

@@ -1,15 +1,8 @@
#!/usr/bin/env sh
# .husky/pre-push — intentionally light.
# any-budget + tracked-artifacts already run on pre-commit; re-running them on
# every push only doubles local wall time for the same existence reason (CI still
# enforces both). Keep this hook as a PATH/npm sanity check + reminder.
# Intentionally excludes test:unit / typecheck (slow; covered by CI).
#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
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
# No-op success: real local gates live in pre-commit; CI owns the rest.
exit 0
#npm run test:unit

View File

@@ -1,14 +0,0 @@
{
"_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
}

View File

@@ -1,55 +0,0 @@
# Mergify merge queue — WS3.4/D5 of the v3.8.49 quality/velocity master plan.
#
# WHY: ~85-100 active PR authors/month and 300+ PRs/week peaks, all merged by ONE
# identity. The manual merge-train validated batches by hand; this queue automates
# it with batching + automatic batch bisection (a red batch of N costs ~log2(N)
# revalidations instead of N). Mergify Open Source plan: free, unlimited, public repo.
#
# GOVERNANCE (non-negotiable, mirrors CLAUDE.md Hard Rules #21/#22 + the owner's
# pre-merge ⭐ gate):
# • A PR enters the queue ONLY via the `queue` label — applied by the owner (or a
# session acting for the owner) AFTER the pre-merge ⭐ report/decision. The label
# IS the merge approval; Mergify only executes it.
# • During a release-freeze (open issue labeled `release-freeze`), do NOT label PRs
# targeting the frozen branch — the freeze is a human-honored coordination signal
# the queue cannot see. Retarget to the active release/vX+1 first (Hard Rule #21).
# • Never label a PR another session is actively working (Hard Rule #22b).
# • Fallback path if Mergify misbehaves or the OSS plan changes: the manual
# merge-train runbook (docs/ops/MERGE_TRAIN.md) — remove labels, proceed by hand.
queue_rules:
- name: release
# Any current or future release branch — the reason GitHub's native queue was
# rejected (no wildcard support on personal-account repos).
queue_conditions:
- base~=^release/v\d+\.\d+\.\d+$
- label=queue
- -draft
- -conflict
# "Everything that ran is green, nothing still running, AND the always-on
# anchor check succeeded" — robust to the path-filtered fast-gates (docs-only
# PRs skip code jobs; matrix shard names vary) while never fail-open: a PR with
# zero checks cannot vacuously merge, because `Merge integrity` runs on EVERY
# non-draft PR (quality.yml) and must be an affirmative success. Review approval
# is intentionally NOT a condition here: the owner-applied `queue` label IS the
# approval in this repo's single-maintainer model (see governance header).
merge_conditions:
- "#check-failure=0"
- "#check-pending=0"
- "#check-success>=1"
- check-success=Merge integrity (changelog + generated skills)
# Batching: validate up to 10 queued PRs together (the manual train's sweet spot);
# don't hold a lone PR hostage waiting for siblings.
batch_size: 10
batch_max_wait_time: 5 min
# Squash keeps the one-commit-per-PR history the CHANGELOG reconciliation expects.
merge_method: squash
pull_request_rules:
- name: clean up the queue label after merge
conditions:
- merged
actions:
label:
remove:
- queue

View File

@@ -9,16 +9,6 @@ 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/
@@ -28,17 +18,6 @@ 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

10
.npmrc
View File

@@ -2,13 +2,3 @@
# Keeping peer auto-install disabled prevents npm from pulling @lobehub/ui/mermaid
# back into the tree and reopening npm audit findings for unused packages.
legacy-peer-deps=true
# Network resilience: enlarge npm's fetch retry budget so a transient registry
# socket reset (ECONNRESET) mid-download retries instead of failing the job.
# npm defaults to only 2 retries with short timeouts; `npm ci` in
# electron-release.yml hit ECONNRESET during v3.8.41 publish. Applies to every
# CI workflow (electron / docker / unit) and local installs.
fetch-retries=5
fetch-retry-factor=4
fetch-retry-mintimeout=20000
fetch-retry-maxtimeout=120000

View File

@@ -1,5 +0,0 @@
# Long reference tables are manually aligned; formatting the whole file causes noisy diffs.
docs/reference/ENVIRONMENT.md
# Dense auto-generated free-tier budget rows (one object per line) — prettier multi-line expand blows past file-size cap 800.
open-sse/config/freeModelCatalog.data.ts

View File

@@ -1,22 +0,0 @@
[
{
"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"
}
]

View File

@@ -1,22 +0,0 @@
# .trivyignore — accepted-risk suppressions for the container image scan
#
# Policy (see docs/security/SUPPLY_CHAIN.md):
# - The Trivy steps in .github/workflows/docker-publish.yml run with
# `ignore-unfixed: true`, so vulnerabilities WITHOUT a published fix are
# already excluded from both the blocking CRITICAL gate and the advisory
# Security-tab upload. You do NOT need an entry here for an unfixable
# base-image OS CVE — it will not be reported.
# - This file is the single auditable home for the rare case where a *fixable*
# CVE must be temporarily accepted (e.g. the upstream fix is not yet in the
# pinned base tag, or the affected package/binary is provably unreachable
# from the proxy request surface and rebuilding now is not justified).
#
# Format — one CVE id per line, each with a justification comment and, where
# possible, an expiry, e.g.:
# # CVE-XXXX-YYYY — <why accepted>; revisit on next base-image bump (YYYY-MM-DD)
# CVE-XXXX-YYYY
#
# Keep this list SHORT and reviewed every release. Prefer fixing (rebuild on a
# patched base / bump the dep) over suppressing. Stale entries are debt.
#
# (No accepted-risk suppressions at present — ignore-unfixed covers the noise.)

View File

@@ -1,15 +0,0 @@
# 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

@@ -1,50 +0,0 @@
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

107
.vscode/settings.json vendored
View File

@@ -19,87 +19,30 @@
}
},
"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",
"**/.claude/worktrees",
"**/electron",
"**/_references",
"**/_mono_repo",
"**/_tasks"
]
},
// Para esconder os diretórios gerados da árvore do Explorer, descomente:
// (MANTIDO comentado — o dono precisa ver _references/_mono_repo/_tasks na árvore.
// A performance é resolvida por watcherExclude + search.exclude + tsserver, sem
// precisar escondê-los do Explorer.)
// "files.exclude": {
// "**/.worktrees": true,
// "**/coverage": true,
// "**/dist": true,
// "**/.build": true,
// "**/.next": true,
// "**/_references": true,
// "**/_mono_repo": true,
// "**/_tasks": true,
// "**/omniroute-*.tgz": true
// "**/electron": true,
// "**/node_modules": true,
// "**/.next": true,
// "**/coverage": true,
// "**/omniroute-*.tgz": true,
// "**/_tasks": true
// },
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/_references/**": true,
"**/_mono_repo/**": true,
"**/electron/**": true,
"**/node_modules/**": true,
"**/.next/**": true,
"**/.build/**": true,
"**/coverage/**": true,
"**/_tasks/**": true,
"**/.git/objects/**": true,
"**/dist/**": true,
"**/build/**": true,
"**/out/**": true,
"**/coverage/**": 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,
@@ -107,32 +50,16 @@
"**/*-reorg*/**": true
},
"search.exclude": {
"**/_references": true,
"**/_mono_repo": true,
"**/electron": true,
"**/node_modules": true,
"**/.next": true,
"**/.build": true,
"**/coverage": true,
"**/_tasks": true,
"**/dist": true,
"**/build": true,
"**/out": true,
"**/coverage": 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,

View File

@@ -1,104 +0,0 @@
# .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,67 +1,26 @@
# @omniroute/opencode-plugin
> **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.
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.
## Install
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:
Once published to npm:
```sh
# 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
npm install @omniroute/opencode-plugin
```
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:
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`:
```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, manual)
## Quick start (single instance)
```jsonc
// opencode.json
@@ -69,7 +28,7 @@ Peer dep: `@opencode-ai/plugin` (managed by your OpenCode install).
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"./plugins/omniroute-opencode-plugin/dist/index.js",
"@omniroute/opencode-plugin",
{
"providerId": "omniroute",
"baseURL": "https://or.example.com",
@@ -196,7 +155,7 @@ Every field is optional. Defaults mirror v0.1.0 behaviour so existing `opencode.
| `combos` | `boolean` | `true` | Discover `/api/combos` and surface them as pseudo-models with LCD capabilities. Combos are keyed under the `combo/<slug>` namespace and labelled `Combo: <name>` in the model picker so they're distinguishable from raw provider/model pairs. |
| `enrichment` | `boolean` | `true` | Pull display names from `/api/pricing/models` AND per-million-token pricing (`input`, `output`, `cached``cacheRead`, `cache_creation``cacheWrite`) from `/api/pricing`, then overlay both onto the live catalog (so the UI shows `Claude 4.7 Opus` with `cost.input: 5`, `cost.output: 25` instead of raw IDs and zeroed cost). |
| `compressionMetadata` | `boolean` | `false` | Pull `/api/context/combos` so combo names get tagged with their compression pipeline, e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`. Intensity tokens render as traffic-light emoji (🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra) so the picker advertises "how compressed" each combo is at a glance. |
| `providerTag` | `boolean` | `true` | Prepend a short upstream-provider label to the enriched display name with `" - "` separator, so `cc/claude-opus-4-7 → Claude - Claude Opus 4.7` differs visibly from `kr/claude-opus-4-7 → Kiro - Claude Opus 4.7` in the OC TUI model picker. Label resolution: use `/api/pricing/models[<alias>].name` verbatim when ≤8 chars (e.g. `Claude`, `Kiro`, `Codex`, `Qwen`), otherwise fall back to `UPPER(alias)` (e.g. `GitHub Models``GHM`, `Gemini``GEMINI`). Idempotent. Combos intentionally skipped (the `Combo: ` prefix already conveys multi-upstream). |
| `providerTag` | `boolean` | `true` | Prepend a short upstream-provider label to the enriched display name with `" - "` separator, so `cc/claude-opus-4-7 → Claude - Claude Opus 4.7` differs visibly from `kr/claude-opus-4-7 → Kiro - Claude Opus 4.7` in the OC TUI model picker. Label resolution: use `/api/pricing/models[<alias>].name` verbatim when ≤8 chars (e.g. `Claude`, `Kiro`, `Codex`, `Qwen`), otherwise fall back to `UPPER(alias)` (e.g. `GitHub Models``GHM`, `Gemini-cli``GEMINI-CLI`). Idempotent. Combos intentionally skipped (the `Combo: ` prefix already conveys multi-upstream). |
| `usableOnly` | `boolean` | `false` | Read `/api/providers` and filter the catalog to providers that have at least one connection with `isActive: true` AND `testStatus: 'active'`. Subtract-filter semantics: providers unknown to BOTH the pricing-models catalog AND the connection table pass through (so synthetic prefixes like `agentrouter/*` survive). On fetch failure the filter is disabled for the refresh — never hides the whole catalog. |
| `diskCache` | `boolean` | `true` | Persist the last successful `/v1/models` + `/api/combos` + enrichment + connections + compression snapshot to `${OPENCODE_DATA_DIR ?? ~/.local/share/opencode}/plugins/omniroute-<providerId>.json`. On a subsequent cold start where `/v1/models` throws (network down / IP whitelist drop / 5xx) the static block hydrates from the snapshot so OC's model picker survives offline. Soft-fail on read/write — never blocks publishing. |
| `geminiSanitization` | `boolean` | `true` | Strip `$schema`/`$ref`/`additionalProperties` from tool params when the model id matches `gemini` |

View File

@@ -1,12 +1,12 @@
{
"name": "@omniroute/opencode-plugin",
"version": "0.2.0",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@omniroute/opencode-plugin",
"version": "0.2.0",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"zod": "^4.4.3"
@@ -26,9 +26,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"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==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"cpu": [
"ppc64"
],
@@ -43,9 +43,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"cpu": [
"arm"
],
@@ -60,9 +60,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"cpu": [
"arm64"
],
@@ -77,9 +77,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"cpu": [
"x64"
],
@@ -94,9 +94,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"cpu": [
"arm64"
],
@@ -111,9 +111,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"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==",
"cpu": [
"x64"
],
@@ -128,9 +128,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"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==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"cpu": [
"arm64"
],
@@ -145,9 +145,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -162,9 +162,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"cpu": [
"arm"
],
@@ -179,9 +179,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"cpu": [
"arm64"
],
@@ -196,9 +196,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"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==",
"cpu": [
"ia32"
],
@@ -213,9 +213,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"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==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"cpu": [
"loong64"
],
@@ -230,9 +230,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"cpu": [
"mips64el"
],
@@ -247,9 +247,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"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==",
"cpu": [
"ppc64"
],
@@ -264,9 +264,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"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==",
"cpu": [
"riscv64"
],
@@ -281,9 +281,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"cpu": [
"s390x"
],
@@ -298,9 +298,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"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==",
"cpu": [
"x64"
],
@@ -315,9 +315,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"cpu": [
"arm64"
],
@@ -332,9 +332,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"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==",
"cpu": [
"x64"
],
@@ -349,9 +349,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"cpu": [
"arm64"
],
@@ -366,9 +366,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"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==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"cpu": [
"x64"
],
@@ -383,9 +383,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"cpu": [
"arm64"
],
@@ -400,9 +400,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"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==",
"cpu": [
"x64"
],
@@ -417,9 +417,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"cpu": [
"arm64"
],
@@ -434,9 +434,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"cpu": [
"ia32"
],
@@ -451,9 +451,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"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==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"cpu": [
"x64"
],
@@ -1165,9 +1165,9 @@
}
},
"node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -1178,32 +1178,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@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"
"@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"
}
},
"node_modules/fast-check": {
@@ -1843,6 +1843,490 @@
"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,18 +1,21 @@
{
"name": "@omniroute/opencode-plugin",
"version": "0.2.0",
"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.js",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./runtime": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": [
@@ -23,7 +26,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 tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.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",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"keywords": [
@@ -66,8 +69,5 @@
"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

@@ -1,74 +0,0 @@
/**
* 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

@@ -1,301 +0,0 @@
/**
* 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

@@ -13,12 +13,12 @@ import { createOmniRouteAuthHook } from "../src/index.js";
test("createOmniRouteAuthHook: default providerId is 'omniroute'", () => {
const hook = createOmniRouteAuthHook();
assert.equal(hook.provider, "opencode-omniroute");
assert.equal(hook.provider, "omniroute");
});
test("createOmniRouteAuthHook: custom providerId binds to hook.provider (multi-instance)", () => {
const hook = createOmniRouteAuthHook({ providerId: "omniroute-preprod" });
assert.equal(hook.provider, "opencode-omniroute-preprod");
assert.equal(hook.provider, "omniroute-preprod");
});
test("createOmniRouteAuthHook: methods[0] is type 'api' with label including displayName", () => {
@@ -30,7 +30,7 @@ test("createOmniRouteAuthHook: methods[0] is type 'api' with label including dis
assert.equal(m.label, "OmniRoute API Key");
const custom = createOmniRouteAuthHook({ providerId: "omniroute-preprod" });
assert.equal(custom.methods[0].label, "OmniRoute (opencode-omniroute-preprod) API Key");
assert.equal(custom.methods[0].label, "OmniRoute (omniroute-preprod) API Key");
});
test("createOmniRouteAuthHook: prompts[0] uses key='apiKey' per @opencode-ai/plugin contract", () => {

View File

@@ -1,74 +0,0 @@
/**
* 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["omniroute/claude-primary"]);
assert.ok(out["omniroute/claude-secondary"]);
assert.ok(out["omniroute/gemini-3-flash"]);
assert.ok(out["omniroute/claude-tier"]);
assert.ok(out["claude-primary"]);
assert.ok(out["claude-secondary"]);
assert.ok(out["gemini-3-flash"]);
assert.ok(out["combo/claude-tier"]);
const combo = out["omniroute/claude-tier"];
assert.equal(combo.name, "Claude Tier");
const combo = out["combo/claude-tier"];
assert.equal(combo.name, "Combo: 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["omniroute/phantom-combo"]);
assert.ok(out["combo/phantom-combo"]);
// With zero resolvable members, LCD = all-false (defensive posture).
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);
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);
});
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["omniroute/visible"]);
assert.ok(!out["omniroute/hidden"], "hidden combo must be omitted");
assert.ok(out["combo/visible"]);
assert.ok(!out["combo/hidden"], "hidden combo must be omitted");
});
test("models(): combo name exactly matches raw model id → raw deleted, raw deleted, no warn", async () => {
test("models(): combo name exactly matches raw model id → raw deleted, combo lives at combo/ key, 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,9 +529,10 @@ test("models(): combo name exactly matches raw model id → raw deleted, raw del
return hook.models!({} as never, { auth: apiAuth("sk-z") as never });
});
// 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");
// 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");
// No collision warning fires — dedup makes keys disjoint.
const collisionWarns = warnings.filter((w) => {
@@ -542,7 +543,7 @@ test("models(): combo name exactly matches raw model id → raw deleted, raw del
});
test("models(): two combos with same slug → second gets disambiguator suffix", async () => {
// Both combos slug to `claude` — second must get `claude-<id-prefix>`.
// Both combos slug to `claude` — second must get `combo/claude-<id-prefix>`.
const combos: OmniRouteRawCombo[] = [
{
id: "uuid-a",
@@ -565,8 +566,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["omniroute/claude"], "first combo at prefixed slug");
assert.ok(out["omniroute/claude-uuid"], "second combo disambiguated by id prefix");
assert.ok(out["combo/claude"], "first combo at bare slug");
assert.ok(out["combo/claude-uuid"], "second combo disambiguated by id prefix");
});
test("models(): combos fetch fails → falls back to models-only, warn emitted, no throw", async () => {
@@ -583,8 +584,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["omniroute/claude-primary"]);
assert.ok(out["omniroute/claude-secondary"]);
assert.ok(out["claude-primary"]);
assert.ok(out["claude-secondary"]);
// Soft-fail warning surfaced.
const softFail = warnings.find((w) => {
@@ -609,7 +610,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["omniroute/claude-tier"]);
assert.ok(second["combo/claude-tier"]);
});
test("models(): combos refetched after TTL expiry (same key as models)", async () => {
@@ -638,74 +639,3 @@ 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

@@ -203,7 +203,7 @@ function makeInput(initialProvider: Record<string, unknown> = {}): Config {
test("config: with valid auth.json + apiKey + baseURL → mutates input.provider[id] with stripped models block", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test-1", baseURL: "https://or.example.com/v1" },
omniroute: { type: "api", key: "sk-test-1", baseURL: "https://or.example.com/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE, MODEL_GEMINI]);
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
@@ -217,8 +217,8 @@ test("config: with valid auth.json + apiKey + baseURL → mutates input.provider
await hook(input);
const provider = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider;
const entry = provider["opencode-omniroute"];
assert.ok(entry, "input.provider['opencode-omniroute'] set");
const entry = provider.omniroute;
assert.ok(entry, "input.provider.omniroute set");
assert.equal(entry.npm, "@ai-sdk/openai-compatible");
assert.equal(entry.name, "OmniRoute");
assert.equal(entry.options.baseURL, "https://or.example.com/v1");
@@ -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["opencode-omniroute/claude-sonnet-4-6"];
const claude = entry.models["claude-sonnet-4-6"];
assert.ok(claude, "claude model surfaced");
assert.equal(claude.name, "claude-sonnet-4-6");
assert.equal(claude.attachment, true);
@@ -246,75 +246,16 @@ 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 bare key + LCD'd
// Combo surfaces under `combo/<friendly-name>` namespace + LCD'd
// (gemini's reasoning=false → combo reasoning=false).
const combo = entry.models["opencode-omniroute/claude-tier"];
assert.ok(combo, "combo surfaced under bare key");
assert.equal(combo.name, "Claude Tier");
const combo = entry.models["combo/claude-tier"];
assert.ok(combo, "combo surfaced under combo/ namespace");
assert.equal(combo.name, "Combo: 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)");
});
// ────────────────────────────────────────────────────────────────────────────
// 1b. Dual-key fallback (#5027) — auth.json stored under the BARE providerId
// (pre-auto-prefix login) must still resolve when the active providerId is
// prefixed (`opencode-omniroute`). Without the fallback the lookup misses
// the stored key and the user is forced to re-auth.
// ────────────────────────────────────────────────────────────────────────────
test("config: auth.json under bare key (pre-prefix login) resolves via dual-key fallback", async () => {
// Stored under bare `omniroute` (the key OC wrote before the auto-prefix fix),
// but the resolved providerId is now `opencode-omniroute`.
const readAuthJson = stubReadAuthJson({
omniroute: { type: "api", key: "sk-bare-1", baseURL: "https://or.example.com/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
const logger = captureWarn();
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute" }, // resolves to opencode-omniroute internally
{ readAuthJson, fetcher, combosFetcher, logger }
);
const input = makeInput();
await hook(input);
const provider = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider;
const entry = provider["opencode-omniroute"];
assert.ok(entry, "provider entry published from bare-key apiKey");
assert.equal(entry.options.apiKey, "sk-bare-1", "apiKey resolved from the bare auth.json key");
assert.equal(entry.options.baseURL, "https://or.example.com/v1");
});
test("config: prefixed key wins over bare key when both present (dual-key precedence)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-prefixed", baseURL: "https://pref.example/v1" },
omniroute: { type: "api", key: "sk-bare", baseURL: "https://bare.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
const logger = captureWarn();
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute" },
{ readAuthJson, fetcher, combosFetcher, logger }
);
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(entry);
assert.equal(
entry.options.apiKey,
"sk-prefixed",
"prefixed key takes precedence (looked up first)"
);
assert.equal(entry.options.baseURL, "https://pref.example/v1");
});
// ────────────────────────────────────────────────────────────────────────────
// 2. Missing auth.json → no-op, no throw, no mutation
// ────────────────────────────────────────────────────────────────────────────
@@ -382,7 +323,7 @@ test("config: existing input.provider[id] → no overwrite (respect manual overr
models: { "manual-model": { name: "manual-model" } },
};
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -392,11 +333,11 @@ test("config: existing input.provider[id] → no overwrite (respect manual overr
{ providerId: "omniroute" },
{ readAuthJson, fetcher, combosFetcher, logger }
);
const input = makeInput({ "opencode-omniroute": manual });
const input = makeInput({ omniroute: manual });
await hook(input);
const provider = (input as { provider: Record<string, unknown> }).provider;
assert.equal(provider["opencode-omniroute"], manual, "manual override preserved by reference");
assert.equal(provider.omniroute, manual, "manual override preserved by reference");
assert.equal(fetcher.callCount(), 0, "no fetch — short-circuited before I/O");
assert.equal(readAuthJson.callCount(), 0, "no auth.json read either");
assert.ok(
@@ -411,7 +352,7 @@ test("config: existing input.provider[id] → no overwrite (respect manual overr
test("config: fetchers throw → warn + emit stub entry with models: {}", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = throwingModelsFetcher();
const combosFetcher = throwingCombosFetcher();
@@ -427,9 +368,8 @@ test("config: fetchers throw → warn + emit stub entry with models: {}", async
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry, "stub provider entry published even when fetchers fail");
assert.equal(entry.npm, "@ai-sdk/openai-compatible");
assert.deepEqual(entry.models, {}, "models stub is empty object");
@@ -452,7 +392,7 @@ test("config: fetchers throw → warn + emit stub entry with models: {}", async
test("config: combos fetcher throws → emit models-only catalog (no combos in models block)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE, MODEL_GEMINI]);
const combosFetcher = throwingCombosFetcher();
@@ -465,16 +405,12 @@ test("config: combos fetcher throws → emit models-only catalog (no combos in m
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry);
const ids = Object.keys(entry.models).sort();
assert.deepEqual(ids, [
"opencode-omniroute/claude-sonnet-4-6",
"opencode-omniroute/gemini-3-flash",
]);
assert.equal(entry.models["opencode-omniroute/claude-tier"], undefined, "no combo entry");
assert.deepEqual(ids, ["claude-sonnet-4-6", "gemini-3-flash"]);
assert.equal(entry.models["combo-claude-tier"], undefined, "no combo entry");
assert.ok(
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
"combos-fetch breadcrumb emitted"
@@ -487,7 +423,7 @@ test("config: combos fetcher throws → emit models-only catalog (no combos in m
test("config: baseURL from auth.json takes precedence when opts.baseURL absent", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://creds.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://creds.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -501,15 +437,14 @@ test("config: baseURL from auth.json takes precedence when opts.baseURL absent",
await hook(input);
assert.equal(fetcher.callsBy()[0][0], "https://creds.example/v1");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.options.baseURL, "https://creds.example/v1");
});
test("config: opts.baseURL wins over auth.json's stored baseURL", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://creds.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://creds.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -523,15 +458,14 @@ test("config: opts.baseURL wins over auth.json's stored baseURL", async () => {
await hook(input);
assert.equal(fetcher.callsBy()[0][0], "https://opts.example/v1");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.options.baseURL, "https://opts.example/v1");
});
test("config: no baseURL resolvable (no opts, no auth.json baseURL) → no-op", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test" }, // NO baseURL on the credential
omniroute: { type: "api", key: "sk-test" }, // NO baseURL on the credential
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -559,12 +493,12 @@ test("config: no baseURL resolvable (no opts, no auth.json baseURL) → no-op",
test("config: multi-instance — two plugins with different providerIds publish to their own keys without collision", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute-prod": {
"omniroute-prod": {
type: "api",
key: "sk-prod",
baseURL: "https://prod.example/v1",
},
"opencode-omniroute-preprod": {
"omniroute-preprod": {
type: "api",
key: "sk-preprod",
baseURL: "https://preprod.example/v1",
@@ -588,18 +522,15 @@ test("config: multi-instance — two plugins with different providerIds publish
await hookB(input);
const provider = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider;
assert.ok(provider["opencode-omniroute-prod"], "prod block present");
assert.ok(provider["opencode-omniroute-preprod"], "preprod block present");
assert.equal(provider["opencode-omniroute-prod"].options.apiKey, "sk-prod");
assert.equal(provider["opencode-omniroute-preprod"].options.apiKey, "sk-preprod");
assert.equal(provider["opencode-omniroute-prod"].options.baseURL, "https://prod.example/v1");
assert.equal(
provider["opencode-omniroute-preprod"].options.baseURL,
"https://preprod.example/v1"
);
assert.ok(provider["omniroute-prod"], "prod block present");
assert.ok(provider["omniroute-preprod"], "preprod block present");
assert.equal(provider["omniroute-prod"].options.apiKey, "sk-prod");
assert.equal(provider["omniroute-preprod"].options.apiKey, "sk-preprod");
assert.equal(provider["omniroute-prod"].options.baseURL, "https://prod.example/v1");
assert.equal(provider["omniroute-preprod"].options.baseURL, "https://preprod.example/v1");
assert.notEqual(
provider["opencode-omniroute-prod"],
provider["opencode-omniroute-preprod"],
provider["omniroute-prod"],
provider["omniroute-preprod"],
"blocks are distinct references"
);
});
@@ -611,7 +542,7 @@ test("config: multi-instance — two plugins with different providerIds publish
test("config + provider share cache: second call uses cached fetch result (single fetch per TTL)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-shared", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-shared", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
@@ -643,7 +574,7 @@ test("config + provider share cache: second call uses cached fetch result (singl
test("provider → config order also dedupes (cache populated by provider, consumed by config)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-reverse", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-reverse", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -707,7 +638,6 @@ 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)) {
@@ -723,7 +653,7 @@ test("buildStaticProviderEntry: stripped per-model shape matches sibling @omniro
}
// Sanity: claude entry has all expected stripped fields.
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = block.models["claude-sonnet-4-6"];
assert.equal(typeof claude.name, "string");
assert.equal(typeof claude.attachment, "boolean");
assert.equal(typeof claude.reasoning, "boolean");
@@ -748,8 +678,8 @@ test("buildStaticProviderEntry: hidden combos are excluded", () => {
"https://or.example/v1",
"sk-test"
);
assert.equal(block.models["opencode-omniroute/claude-tier"], undefined);
assert.ok(block.models["opencode-omniroute/claude-sonnet-4-6"]);
assert.equal(block.models["combo-claude-tier"], undefined);
assert.ok(block.models["claude-sonnet-4-6"]);
});
// ────────────────────────────────────────────────────────────────────────────
@@ -765,7 +695,7 @@ test("buildStaticProviderEntry: emits modalities.input from raw.input_modalities
"https://or.example/v1",
"sk-test"
);
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = block.models["claude-sonnet-4-6"];
assert.deepEqual(claude.modalities?.input, ["text", "image"]);
assert.deepEqual(claude.modalities?.output, ["text"]);
});
@@ -779,7 +709,7 @@ test("buildStaticProviderEntry: never emits limit.input (OC SDK rejects it)", ()
"https://or.example/v1",
"sk-test"
);
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = block.models["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");
@@ -807,7 +737,7 @@ test("buildStaticProviderEntry: emits cost when enrichment carries pricing", ()
"sk-test",
enrichment
);
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = block.models["claude-sonnet-4-6"];
assert.equal(claude.cost?.input, 3);
assert.equal(claude.cost?.output, 15);
assert.equal(claude.cost?.cache_read, 0.3);
@@ -828,8 +758,8 @@ test("buildStaticProviderEntry: emits release_date when raw carries it; omits wh
"https://or.example/v1",
"sk-test"
);
assert.equal(block.models["opencode-omniroute/claude-with-date"].release_date, "2026-02-19");
assert.equal(block.models["opencode-omniroute/gemini-3-flash"].release_date, undefined);
assert.equal(block.models["claude-with-date"].release_date, "2026-02-19");
assert.equal(block.models["gemini-3-flash"].release_date, undefined);
});
test("buildStaticProviderEntry: combo modalities = intersection of members (LCD)", () => {
@@ -858,7 +788,7 @@ test("buildStaticProviderEntry: combo modalities = intersection of members (LCD)
"https://or.example/v1",
"sk-test"
);
const combo = block.models["opencode-omniroute/mixed-tier"];
const combo = block.models["combo/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"]);
@@ -882,7 +812,7 @@ test("OmniRoutePlugin factory exposes config hook alongside auth + provider", as
test("config: auth.json entry of wrong type (oauth) → no-op", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "oauth", refresh: "r", access: "a", expires: 0 },
omniroute: { type: "oauth", refresh: "r", access: "a", expires: 0 },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -919,7 +849,7 @@ test("config: readAuthJson throws → treat as missing file (silent fallback)",
test("config: initialises input.provider when undefined", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -934,7 +864,7 @@ test("config: initialises input.provider when undefined", async () => {
await hook(input);
const provider = (input as { provider?: Record<string, unknown> }).provider;
assert.ok(provider, "provider bag initialised");
assert.ok(provider!["opencode-omniroute"]);
assert.ok(provider!.omniroute);
});
// ────────────────────────────────────────────────────────────────────────────
@@ -944,7 +874,7 @@ test("config: initialises input.provider when undefined", async () => {
test("config: enrichment fetched + name overlaid on raw-model entries", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE, MODEL_GEMINI]);
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
@@ -963,20 +893,19 @@ test("config: enrichment fetched + name overlaid on raw-model entries", async ()
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry);
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
assert.equal(entry.models["opencode-omniroute/gemini-3-flash"].name, "Gemini 3 Flash");
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
assert.equal(entry.models["gemini-3-flash"].name, "Gemini 3 Flash");
// Combo names still come from /api/combos — enrichment overlay does NOT touch combos.
assert.equal(entry.models["opencode-omniroute/claude-tier"].name, "Claude Tier");
assert.equal(entry.models["combo/claude-tier"].name, "Combo: Claude Tier");
assert.equal(enrichmentFetcher.callCount(), 1);
});
test("config: features.enrichment=false skips enrichment fetch + keeps raw-id names", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -994,21 +923,16 @@ test("config: features.enrichment=false skips enrichment fetch + keeps raw-id na
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry);
assert.equal(enrichmentFetcher.callCount(), 0, "enrichment fetch suppressed by feature flag");
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id retained"
);
assert.equal(entry.models["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 () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -1022,15 +946,10 @@ test("config: enrichment fetcher throws → soft-fail (warn + raw-id static cata
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry, "static block still published on enrichment failure");
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id retained"
);
assert.equal(entry.models["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")),
@@ -1071,7 +990,7 @@ const MODEL_NV_LLAMA: OmniRouteRawModelEntry = {
test("config: usableOnly=false → no filter (existing behavior)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CC_OPUS, MODEL_NV_LLAMA]);
const combosFetcher = stubCombosFetcher([]);
@@ -1093,9 +1012,8 @@ test("config: usableOnly=false → no filter (existing behavior)", async () => {
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry.models["cc/claude-opus-4-7"], "claude kept");
assert.ok(entry.models["nvidia/llama-3-70b"], "nvidia kept (filter off)");
assert.equal(providersFetcher.callCount(), 0, "providers fetch not called when feature off");
@@ -1103,7 +1021,7 @@ test("config: usableOnly=false → no filter (existing behavior)", async () => {
test("config: usableOnly=true → drops models for non-usable providers, keeps usable + unknown", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([
MODEL_CC_OPUS,
@@ -1143,9 +1061,8 @@ test("config: usableOnly=true → drops models for non-usable providers, keeps u
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry.models["cc/claude-opus-4-7"], "claude kept (active)");
assert.equal(entry.models["nvidia/llama-3-70b"], undefined, "nvidia dropped (error status)");
assert.ok(entry.models["agentrouter/synthetic-1"], "unknown prefix kept (subtract-filter)");
@@ -1154,7 +1071,7 @@ test("config: usableOnly=true → drops models for non-usable providers, keeps u
test("config: usableOnly=true + providers fetch fails → soft-fail keeps everything", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CC_OPUS, MODEL_NV_LLAMA]);
const combosFetcher = stubCombosFetcher([]);
@@ -1175,9 +1092,8 @@ test("config: usableOnly=true + providers fetch fails → soft-fail keeps everyt
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry.models["cc/claude-opus-4-7"]);
assert.ok(entry.models["nvidia/llama-3-70b"], "soft-fail keeps both");
assert.ok(
@@ -1188,7 +1104,7 @@ test("config: usableOnly=true + providers fetch fails → soft-fail keeps everyt
test("config: diskCache hydrates stale snapshot when /v1/models throws", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = throwingModelsFetcher();
const combosFetcher = stubCombosFetcher([]);
@@ -1225,15 +1141,11 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(
entry.models["opencode-omniroute/claude-sonnet-4-6"],
"stale snapshot hydrated into static block"
);
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.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
entry.models["claude-sonnet-4-6"].name,
"Claude Sonnet 4.6 (cached)",
"stale enrichment also reused"
);
@@ -1246,7 +1158,7 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
test("config: cached rawEnrichment from earlier provider hook is reused (no refetch)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-shared", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-shared", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -1278,10 +1190,9 @@ test("config: cached rawEnrichment from earlier provider hook is reused (no refe
await configHook(input);
assert.equal(enrichmentFetcher.callCount(), 1, "config reused cached enrichment");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
});
// ─────────────────────────────────────────────────────────────────────
@@ -1292,7 +1203,7 @@ test("config: cached rawEnrichment from earlier provider hook is reused (no refe
test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-model names", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE, MODEL_GEMINI]);
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
@@ -1311,9 +1222,9 @@ test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-
"gemini-3-flash",
{
name: "Gemini 3 Flash",
providerAlias: "gemini",
providerCanonical: "gemini",
providerDisplayName: "Gemini",
providerAlias: "gemini-cli",
providerCanonical: "gemini-cli",
providerDisplayName: "Gemini-cli",
},
],
])
@@ -1327,22 +1238,18 @@ test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry);
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
assert.equal(entry.models["opencode-omniroute/gemini-3-flash"].name, "Gemini - Gemini 3 Flash");
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");
// Combos stay untouched — `Combo: ` prefix already conveys multi-upstream.
assert.equal(entry.models["opencode-omniroute/claude-tier"].name, "Claude Tier");
assert.equal(entry.models["combo/claude-tier"].name, "Combo: Claude Tier");
});
test("config: providerTag=false suppresses the suffix", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -1360,11 +1267,10 @@ test("config: providerTag=false suppresses the suffix", async () => {
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
entry.models["claude-sonnet-4-6"].name,
"Claude Sonnet 4.6",
"enriched name kept, provider tag suppressed"
);
@@ -1372,7 +1278,7 @@ test("config: providerTag=false suppresses the suffix", async () => {
test("config: providerTag falls back to UPPER(alias) when providerDisplayName missing", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -1393,15 +1299,14 @@ test("config: providerTag falls back to UPPER(alias) when providerDisplayName mi
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "CC - Claude Sonnet 4.6");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.models["claude-sonnet-4-6"].name, "CC - Claude Sonnet 4.6");
});
test("config: providerTag skipped entirely when neither providerDisplayName nor providerAlias set", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -1420,15 +1325,14 @@ test("config: providerTag skipped entirely when neither providerDisplayName nor
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
});
test("config: providerTag is idempotent — second hook call doesn't double-suffix", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -1447,76 +1351,14 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
const inputA = makeInput();
await hook(inputA);
const entryA = (inputA as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(
entryA.models["opencode-omniroute/claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
const entryA = (inputA as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entryA.models["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[
"opencode-omniroute"
];
assert.equal(
entryB.models["opencode-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["opencode-omniroute/parent"];
assert.ok(parent, "Parent combo must be in the static catalog");
assert.equal(parent.limit?.context, 8_000);
const entryB = (inputB as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entryB.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
});

View File

@@ -376,8 +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");
// #6859: dynamic-hook catalog keys use the unprefixed omnirouteProviderId.
const m = out["omniroute/claude-sonnet-4-6"];
const m = out["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);
@@ -402,11 +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["omniroute/claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id preserved"
);
assert.equal(out["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 () => {
@@ -464,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["omniroute/claude-primary"];
const combo = out["combo/claude-primary"];
assert.ok(combo, "combo entry present");
assert.match(
combo.name,
@@ -478,7 +473,7 @@ test("provider hook: compression metadata fetcher called when opted in", async (
// ─────────────────────────────────────────────────────────────────────────
const stubAuthJson = (apiKey: string) => async () => ({
"opencode-omniroute": { type: "api" as const, key: apiKey },
omniroute: { type: "api" as const, key: apiKey },
});
test("config hook: MCP auto-emit OFF by default (no mcp entry)", async () => {
@@ -493,7 +488,7 @@ test("config hook: MCP auto-emit OFF by default (no mcp entry)", async () => {
);
const input: { provider?: Record<string, unknown>; mcp?: Record<string, unknown> } = {};
await hook(input as never);
assert.ok(input.provider?.["opencode-omniroute"], "provider block written");
assert.ok(input.provider?.omniroute, "provider block written");
assert.equal(input.mcp, undefined, "no mcp block written");
});
@@ -513,7 +508,7 @@ test("config hook: features.mcpAutoEmit:true writes mcp entry with provider apiK
);
const input: { provider?: Record<string, unknown>; mcp?: Record<string, unknown> } = {};
await hook(input as never);
const entry = input.mcp?.["opencode-omniroute"] as
const entry = input.mcp?.omniroute as
| { type: string; url: string; enabled: boolean; headers: Record<string, string> }
| undefined;
assert.ok(entry, "mcp entry written");
@@ -543,7 +538,7 @@ test("config hook: features.mcpToken overrides provider apiKey in mcp Bearer", a
);
const input: { provider?: Record<string, unknown>; mcp?: Record<string, unknown> } = {};
await hook(input as never);
const entry = input.mcp?.["opencode-omniroute"] as { headers: Record<string, string> };
const entry = input.mcp?.omniroute as { headers: Record<string, string> };
assert.equal(
entry.headers.Authorization,
"Bearer sk-mcp-narrower",
@@ -566,11 +561,11 @@ test("config hook: existing operator mcp.<providerId> wins (no overwrite)", asyn
}
);
const input: { provider?: Record<string, unknown>; mcp?: Record<string, unknown> } = {
mcp: { "opencode-omniroute": { type: "custom-user-entry", url: "https://manual.example/mcp" } },
mcp: { omniroute: { type: "custom-user-entry", url: "https://manual.example/mcp" } },
};
await hook(input as never);
assert.deepEqual(
input.mcp?.["opencode-omniroute"],
input.mcp?.omniroute,
{ type: "custom-user-entry", url: "https://manual.example/mcp" },
"operator override preserved"
);
@@ -585,7 +580,7 @@ test("config hook: features.mcpAutoEmit:true with /v1 in baseURL → strips corr
},
{
readAuthJson: async () => ({
"opencode-omniroute-preprod": { type: "api" as const, key: "sk-preprod" },
"omniroute-preprod": { type: "api" as const, key: "sk-preprod" },
}),
fetcher: async () => SAMPLE_RAW,
combosFetcher: async () => [],
@@ -594,7 +589,7 @@ test("config hook: features.mcpAutoEmit:true with /v1 in baseURL → strips corr
);
const input: { provider?: Record<string, unknown>; mcp?: Record<string, unknown> } = {};
await hook(input as never);
const entry = input.mcp?.["opencode-omniroute-preprod"] as { url: string };
const entry = input.mcp?.["omniroute-preprod"] as { url: string };
assert.equal(
entry.url,
"https://or-preprod.example.com/api/mcp/stream",

View File

@@ -1,291 +0,0 @@
/**
* 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

@@ -212,8 +212,8 @@ test("shouldSanitizeForGemini: google-vertex/gemini-1.5-flash → true", () => {
assert.equal(shouldSanitizeForGemini({ model: "google-vertex/gemini-1.5-flash" }), true);
});
test("shouldSanitizeForGemini: gemini/gemini-2.5-pro → true", () => {
assert.equal(shouldSanitizeForGemini({ model: "gemini/gemini-2.5-pro" }), true);
test("shouldSanitizeForGemini: gemini-cli/gemini-2.5-pro → true (real OmniRoute alias)", () => {
assert.equal(shouldSanitizeForGemini({ model: "gemini-cli/gemini-2.5-pro" }), true);
});
test("shouldSanitizeForGemini: claude-sonnet-4 → false", () => {

View File

@@ -38,8 +38,8 @@ test("multi-instance: two plugin invocations bind to their own providerId", asyn
baseURL: "https://b.example/v1",
});
assert.equal(a.auth?.provider, "opencode-omniroute-prod");
assert.equal(b.auth?.provider, "opencode-omniroute-preprod");
assert.equal(a.auth?.provider, "omniroute-prod");
assert.equal(b.auth?.provider, "omniroute-preprod");
});
test("multi-instance: hook objects + nested arrays are independent references", async () => {
@@ -70,8 +70,8 @@ test("multi-instance: identical opts twice still yield independent objects", asy
assert.notEqual(first.auth, second.auth);
assert.notEqual(first.auth?.methods, second.auth?.methods);
// Same provider id is fine — what matters is no shared mutable state.
assert.equal(first.auth?.provider, "opencode-twin");
assert.equal(second.auth?.provider, "opencode-twin");
assert.equal(first.auth?.provider, "twin");
assert.equal(second.auth?.provider, "twin");
});
test("multi-instance: mutating instance A's auth.methods does not affect instance B", async () => {
@@ -132,5 +132,5 @@ test("multi-instance: invalid opts on one instance does not poison the other", a
providerId: "recovered",
baseURL: "https://ok.example/v1",
});
assert.equal(ok.auth?.provider, "opencode-recovered");
assert.equal(ok.auth?.provider, "recovered");
});

View File

@@ -1,99 +0,0 @@
/**
* Regression test for #6859.
*
* `resolveOmniRoutePluginOptions()` auto-prefixes `providerId` with
* `"opencode-"` (commit 75b52e286) so OpenCode 1.17.8+'s native-adapter gate
* accepts it as an OC-registered provider id. That prefixed value must stay
* OC-internal (AuthHook.provider / provider registration keys) — it must
* NEVER leak into the identifiers OmniRoute's own server parses to resolve
* credentials (`mapRawModelToModelV2`'s `id`/`providerID`,
* `mapComboToModelV2`'s `providerID`, and the dynamic-hook catalog keys).
*
* OmniRoute's server-side `parseModel()` (open-sse/services/model.ts) splits
* a dispatched model string on `/` to recover the provider name and look up
* credentials. If the plugin embeds the OC-gate-prefixed id in that string,
* the server looks up credentials for a provider named "opencode-omniroute"
* (which never exists in `src/shared/constants/providers.ts`) instead of
* "omniroute" — producing the exact "No credentials for opencode-omniroute" /
* "No active credentials for provider: opencode-omniroute" errors reported
* in #6859.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
createOmniRouteProviderHook,
mapRawModelToModelV2,
resolveOmniRoutePluginOptions,
} from "../src/index.js";
/**
* Minimal stand-in for OmniRoute's own `parseModel()` (open-sse/services/
* model.ts), which splits a dispatched `<providerID>/<modelID>` string on the
* FIRST "/" to recover the provider name used for credential lookup. Kept
* local (rather than cross-importing the real module) so this package's
* self-contained test suite (`cd @omniroute/opencode-plugin && npm test`)
* doesn't depend on the root repo's `@/*` path-alias resolution.
*/
function splitProviderFromDispatchedModel(modelStr: string): string {
const idx = modelStr.indexOf("/");
return idx === -1 ? modelStr : modelStr.slice(0, idx);
}
const apiAuth = (key: string) => ({ type: "api" as const, key });
test("#6859: server-facing model id/providerID must resolve to the unprefixed provider name", () => {
const resolved = resolveOmniRoutePluginOptions();
// The OC-gate-compatible id stays prefixed — it is legitimate for
// AuthHook.provider / provider registration.
assert.equal(resolved.providerId, "opencode-omniroute");
// A second, unprefixed id must be exposed for anything that reaches
// OmniRoute's own server (model id prefix, ModelV2.providerID, combo keys).
assert.equal(
resolved.omnirouteProviderId,
"omniroute",
"resolveOmniRoutePluginOptions() must expose an unprefixed omnirouteProviderId"
);
// A bare raw /v1/models entry (no existing "/" in its id — the common
// case for OmniRoute's catalog) mapped with the server-facing id.
const model = mapRawModelToModelV2(
{ id: "claude-opus-4-7" },
{ providerId: resolved.omnirouteProviderId, baseURL: "http://localhost:20128" }
);
assert.equal(model.providerID, "omniroute");
assert.equal(model.id, "omniroute/claude-opus-4-7");
// OpenCode dispatches back to OmniRoute using `providerID/modelKey`
// (matches the issue's own repro: `-m opencode-omniroute/oc/big-pickle`).
const dispatchedModelString = `${model.providerID}/claude-opus-4-7`;
const parsedProvider = splitProviderFromDispatchedModel(dispatchedModelString);
assert.equal(
parsedProvider,
"omniroute",
`server-side provider split resolved '${parsedProvider}', expected 'omniroute' — ` +
`credentials lookup would fail for an OC-gate-prefixed provider id`
);
});
test("#6859: createOmniRouteProviderHook end-to-end — catalog keys/providerID never carry the OC-gate prefix", async () => {
const hook = createOmniRouteProviderHook(
{ baseURL: "https://or.example.com/v1" },
{
fetcher: async () => [{ id: "claude-opus-4-7" }],
combosFetcher: async () => [],
}
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-test") as never });
const model = out["omniroute/claude-opus-4-7"];
assert.ok(model, "catalog keyed under the unprefixed provider name");
assert.equal(model.providerID, "omniroute");
assert.ok(
!model.providerID.startsWith("opencode-"),
"the OC-gate prefix must never leak into ModelV2.providerID"
);
});

View File

@@ -75,7 +75,7 @@ const apiAuth = (key: string, baseURL?: string): unknown =>
test("createOmniRouteProviderHook: default providerId is 'omniroute'", () => {
const hook = createOmniRouteProviderHook(undefined, { combosFetcher: async () => [] });
assert.equal(hook.id, "opencode-omniroute");
assert.equal(hook.id, "omniroute");
});
test("createOmniRouteProviderHook: custom providerId binds to hook.id (multi-instance)", () => {
@@ -87,8 +87,8 @@ test("createOmniRouteProviderHook: custom providerId binds to hook.id (multi-ins
{ providerId: "omniroute-local" },
{ combosFetcher: async () => [] }
);
assert.equal(a.id, "opencode-omniroute-preprod");
assert.equal(b.id, "opencode-omniroute-local");
assert.equal(a.id, "omniroute-preprod");
assert.equal(b.id, "omniroute-local");
});
test("models: extracts apiKey from ctx.auth (type=api) and calls fetcher with it", async () => {
@@ -101,10 +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);
// #6859: dynamic-hook catalog keys use the unprefixed omnirouteProviderId
// ("omniroute"), not the OC-gate-prefixed hook.id ("opencode-omniroute") —
// that prefix must never leak into anything OmniRoute's server parses.
assert.ok(out["omniroute/claude-primary"]);
assert.ok(out["claude-primary"]);
});
test("models: returns {} when ctx.auth is null/undefined/wrong-type/empty-key", async () => {
@@ -155,15 +152,9 @@ 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 });
// #6859: dynamic-hook catalog keys/ids/providerID use the unprefixed
// omnirouteProviderId ("omniroute") — the OC-gate prefix ("opencode-")
// must stay OC-internal (hook.id / AuthHook.provider) and never leak into
// anything OmniRoute's own server parses for credential lookup.
const claude = out["omniroute/claude-primary"];
const claude = out["claude-primary"];
assert.ok(claude, "claude-primary present");
// `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.id, "claude-primary");
assert.equal(claude.name, "claude-primary");
assert.equal(claude.providerID, "omniroute");
assert.equal(claude.api.id, "openai-compatible");

View File

@@ -1,5 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import {
OmniRoutePlugin,
OMNIROUTE_PROVIDER_KEY,
@@ -26,7 +27,7 @@ test("scaffold: default export is v1 plugin shape { id, server: OmniRoutePlugin
test("resolveOmniRoutePluginOptions: defaults", () => {
const r = resolveOmniRoutePluginOptions();
assert.equal(r.providerId, "opencode-omniroute");
assert.equal(r.providerId, "omniroute");
assert.equal(r.displayName, "OmniRoute");
assert.equal(r.modelCacheTtl, 300_000);
assert.equal(r.baseURL, undefined);
@@ -34,8 +35,8 @@ test("resolveOmniRoutePluginOptions: defaults", () => {
test("resolveOmniRoutePluginOptions: custom providerId derives displayName", () => {
const r = resolveOmniRoutePluginOptions({ providerId: "omniroute-preprod" });
assert.equal(r.providerId, "opencode-omniroute-preprod");
assert.equal(r.displayName, "OmniRoute (opencode-omniroute-preprod)");
assert.equal(r.providerId, "omniroute-preprod");
assert.equal(r.displayName, "OmniRoute (omniroute-preprod)");
});
test("resolveOmniRoutePluginOptions: explicit displayName wins", () => {
@@ -62,12 +63,11 @@ test("OmniRoutePlugin: returns an empty hooks object (scaffold)", async () => {
assert.notEqual(hooks, null);
});
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");
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");
});

View File

@@ -2,7 +2,7 @@ import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
format: ["esm", "cjs"],
dts: true,
clean: true,
sourcemap: false,
@@ -11,7 +11,7 @@ export default defineConfig({
target: "node22",
outDir: "dist",
minify: false,
cjsInterop: false,
cjsInterop: true,
// 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,23 +1,5 @@
# @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.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"cpu": [
"ppc64"
],
@@ -36,9 +36,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"cpu": [
"arm"
],
@@ -53,9 +53,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"cpu": [
"arm64"
],
@@ -70,9 +70,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"cpu": [
"x64"
],
@@ -87,9 +87,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"cpu": [
"arm64"
],
@@ -104,9 +104,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"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==",
"cpu": [
"x64"
],
@@ -121,9 +121,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"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==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"cpu": [
"arm64"
],
@@ -138,9 +138,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -155,9 +155,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"cpu": [
"arm"
],
@@ -172,9 +172,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"cpu": [
"arm64"
],
@@ -189,9 +189,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"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==",
"cpu": [
"ia32"
],
@@ -206,9 +206,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"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==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"cpu": [
"loong64"
],
@@ -223,9 +223,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"cpu": [
"mips64el"
],
@@ -240,9 +240,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"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==",
"cpu": [
"ppc64"
],
@@ -257,9 +257,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"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==",
"cpu": [
"riscv64"
],
@@ -274,9 +274,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"cpu": [
"s390x"
],
@@ -291,9 +291,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"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==",
"cpu": [
"x64"
],
@@ -308,9 +308,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"cpu": [
"arm64"
],
@@ -325,9 +325,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"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==",
"cpu": [
"x64"
],
@@ -342,9 +342,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"cpu": [
"arm64"
],
@@ -359,9 +359,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"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==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"cpu": [
"x64"
],
@@ -376,9 +376,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"cpu": [
"arm64"
],
@@ -393,9 +393,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"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==",
"cpu": [
"x64"
],
@@ -410,9 +410,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"cpu": [
"arm64"
],
@@ -427,9 +427,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"cpu": [
"ia32"
],
@@ -444,9 +444,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"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==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"cpu": [
"x64"
],
@@ -974,9 +974,9 @@
}
},
"node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -987,32 +987,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@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"
"@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"
}
},
"node_modules/fdir": {
@@ -1482,6 +1482,490 @@
"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": "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.",
"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.",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
@@ -56,8 +56,5 @@
"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. 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 })[];
/** Override the model catalog. Defaults to `OMNIROUTE_DEFAULT_OPENCODE_MODELS`. */
models?: readonly string[];
/** Optional human-readable labels keyed by model id. Overridden by `modelCapabilities[id].label`. */
modelLabels?: Record<string, string>;
/**
@@ -139,12 +139,6 @@ 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.
@@ -254,12 +248,7 @@ 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 === "object" && raw !== null && "id" in raw && typeof (raw as any).id === "string"
? (raw as { id: string }).id.trim()
: typeof raw === "string"
? raw.trim()
: "";
const id = typeof raw === "string" ? raw.trim() : "";
if (!id || seen.has(id)) continue;
seen.add(id);
const defaults = OMNIROUTE_DEFAULT_MODEL_CAPABILITIES[id] ?? {};
@@ -277,18 +266,10 @@ 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;
// 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) {
// 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) {
entry.limit = { context: contextLength };
}
@@ -527,7 +508,7 @@ export interface OmniRouteLiveModel {
* const config = buildOmniRouteOpenCodeConfig({
* baseURL: "http://localhost:20128",
* apiKey: "sk_omniroute",
* models, // OmniRouteLiveModel[] — contextLength auto-extracted
* models: models.map((m) => m.id),
* modelLabels: Object.fromEntries(models.map((m) => [m.id, m.name])),
* });
* ```

Some files were not shown because too many files have changed in this diff Show More