Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,37 @@ jobs:
- run: npm ci
- name: ESLint (baseline congelado — warning novo = vermelho)
run: npx eslint . --suppressions-location config/quality/eslint-suppressions.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)
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@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: 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,4 @@ omniroute.md

# mise configuration
mise.toml
_artifacts/
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@
"check:tracked-artifacts": "node scripts/check/check-tracked-artifacts.mjs",
"check:test-masking": "node scripts/check/check-test-masking.mjs",
"check:test-runner-api": "node scripts/check/check-test-runner-api.mjs",
"check:changelog-integrity": "node scripts/check/check-changelog-integrity.mjs",
"check:agent-skills-sync": "node --import tsx/esm scripts/skills/generate-agent-skills.mjs",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

De acordo com o Guia de Estilo do Repositório (Seção 1: File Placement & Organization, Linha 8), todos os scripts de manutenção, depuração, geração ou experimentais devem ser colocados estritamente dentro de uma das subpastas permitidas de scripts/ (build/, dev/, check/, docs/, i18n/, ad-hoc/).

A pasta scripts/skills/ utilizada no script "check:agent-skills-sync" não faz parte das subpastas permitidas pelo guia de estilo. Para manter a conformidade com as regras do repositório, o script generate-agent-skills.mjs deve ser movido para uma pasta adequada (como scripts/check/ ou scripts/build/), ou a regra do guia de estilo deve ser atualizada para incluir skills/ oficialmente.

Suggested change
"check:agent-skills-sync": "node --import tsx/esm scripts/skills/generate-agent-skills.mjs",
"check:agent-skills-sync": "node --import tsx/esm scripts/check/generate-agent-skills.mjs",
References
  1. ALL maintenance, debugging, generation, or experimental scripts MUST be placed strictly inside one of the scripts/ subfolders (build/, dev/, check/, docs/, i18n/, ad-hoc/). (link)

"check:build-scope": "node scripts/check/check-build-scope.mjs",
"check:error-helper": "node scripts/check/check-error-helper.mjs",
"check:migration-numbering": "node scripts/check/check-migration-numbering.mjs",
Expand Down
121 changes: 121 additions & 0 deletions scripts/check/check-changelog-integrity.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env node
// scripts/check/check-changelog-integrity.mjs
//
// Anti "CHANGELOG-eat" gate: no bullet line that exists in the BASE branch's
// CHANGELOG.md may disappear in the merge result. The chronic failure mode is
// git's merge auto-resolve silently dropping sibling bullets (or whole version
// sections) when two branches touch adjacent CHANGELOG lines — incident
// 2026-07-05: PR #6193's merge ate 212 lines (the entire [3.8.45] + [3.8.44]
// sections, 130 bullets), only recovered by hand from the pre-merge ref.
//
// On pull_request CI the checkout is refs/pull/N/merge — the auto-resolved
// merge result — so comparing it against origin/<base> catches the eat BEFORE
// the merge lands, in the PR that would cause it.
//
// Policy (Princípio Zero): this only ever ADDS work for the maintainer side —
// quality.yml runs it blocking for own-origin PRs and report-only for forks.
// The release captain's reconciliation rewrites the CHANGELOG legitimately,
// but that happens on the release PR (PR → main, ci.yml), which does not run
// this gate. Escape hatch for intentional removals (e.g. reverting a reverted
// feature's bullet): ALLOW_CHANGELOG_REMOVALS=1 turns failures into a report.
//
// Usage:
// node scripts/check/check-changelog-integrity.mjs
// env GITHUB_BASE_REF PR base branch (CI); local fallback: current release/*
// env CHANGELOG_BASE_REF explicit ref override (e.g. origin/release/v3.8.45)
// env ALLOW_CHANGELOG_REMOVALS=1 report-only (never fails)

import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
const CHANGELOG = "CHANGELOG.md";

/** Extract the set of bullet lines (trimmed) from a CHANGELOG text. */
export function extractBullets(text) {
const bullets = new Set();
for (const raw of String(text || "").split("\n")) {
const line = raw.trim();
if (line.startsWith("- ") && line.length > 4) bullets.add(line);
}
return bullets;
}

/**
* Bullet lines present in the base CHANGELOG but absent from the head
* CHANGELOG — the "eaten" set. Pure so it has a unit test.
*/
export function findLostBullets(baseText, headText) {
const headBullets = extractBullets(headText);
const lost = [];
for (const b of extractBullets(baseText)) {
if (!headBullets.has(b)) lost.push(b);
}
return lost;
}

function git(args) {
return execFileSync("git", args, { cwd: ROOT, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
}

function resolveBaseRef() {
if (process.env.CHANGELOG_BASE_REF) return process.env.CHANGELOG_BASE_REF;
if (process.env.GITHUB_BASE_REF) return `origin/${process.env.GITHUB_BASE_REF}`;
// Local fallback: the highest release/v* on origin (the active development base).
try {
const branches = git(["branch", "-r", "--list", "origin/release/v*", "--format=%(refname:short)"])
.split("\n")
.map((s) => s.trim())
.filter(Boolean)
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
return branches[branches.length - 1] || null;
} catch {
return null;
}
}

function main() {
const baseRef = resolveBaseRef();
if (!baseRef) {
console.log("[changelog-integrity] SKIP — could not resolve a base ref (offline/fresh clone).");
return 0;
}
Comment on lines +80 to +84

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

No ambiente de CI (GitHub Actions), o checkout padrão do repositório é raso (depth: 1) e contém apenas a referência do commit do PR. Como resultado, referências remotas como origin/<base_ref> não estarão disponíveis localmente.

Isso fará com que o comando git show falhe silenciosamente no bloco try/catch subsequente, exibindo a mensagem [changelog-integrity] SKIP — CHANGELOG.md not readable at origin/... e retornando 0. Consequentemente, o gate de integridade do CHANGELOG será ignorado silenciosamente no CI, perdendo sua eficácia de bloqueio.

Para garantir que a referência base esteja disponível, podemos tentar fazer um git fetch rápido da branch base antes de tentar ler o CHANGELOG.

Suggested change
const baseRef = resolveBaseRef();
if (!baseRef) {
console.log("[changelog-integrity] SKIP — could not resolve a base ref (offline/fresh clone).");
return 0;
}
const baseRef = resolveBaseRef();
if (!baseRef) {
console.log("[changelog-integrity] SKIP — could not resolve a base ref (offline/fresh clone).");
return 0;
}
if (process.env.CI && baseRef.startsWith("origin/")) {
try {
const branch = baseRef.replace("origin/", "");
git(["fetch", "--no-tags", "origin", branch, "--depth=50"]);
} catch {
// Best-effort fetch
}
}


let baseText;
try {
baseText = git(["show", `${baseRef}:${CHANGELOG}`]);
} catch {
console.log(`[changelog-integrity] SKIP — ${CHANGELOG} not readable at ${baseRef}.`);
return 0;
}
const headText = readFileSync(join(ROOT, CHANGELOG), "utf8");

const lost = findLostBullets(baseText, headText);
if (lost.length === 0) {
console.log(`[changelog-integrity] OK — no base bullets lost vs ${baseRef}.`);
return 0;
}

console.error(
`[changelog-integrity] ${lost.length} bullet(s) present in ${baseRef} are MISSING from this tree's ${CHANGELOG}:`
);
for (const b of lost.slice(0, 15)) console.error(` ✗ ${b.slice(0, 160)}`);
if (lost.length > 15) console.error(` … and ${lost.length - 15} more`);
console.error(
"\nThis is the CHANGELOG-eat pattern (merge auto-resolve dropping sibling bullets)." +
"\nFix: restore the base CHANGELOG (`git checkout <base> -- CHANGELOG.md`), re-insert ONLY" +
"\nyour own bullet, and prove the net diff is additive. Intentional removals (rare):" +
"\nre-run with ALLOW_CHANGELOG_REMOVALS=1 and justify in the PR body."
);
if (process.env.ALLOW_CHANGELOG_REMOVALS === "1") {
console.error("[changelog-integrity] ALLOW_CHANGELOG_REMOVALS=1 — reporting only, not failing.");
return 0;
}
return 1;
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
process.exit(main());
}
42 changes: 39 additions & 3 deletions scripts/quality/validate-release-green.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,40 @@
// orchestration lives in the /green-prs + review-prs flows that call it.
//
// Usage:
// node scripts/quality/validate-release-green.mjs [--json] [--with-build] [--quick]
// node scripts/quality/validate-release-green.mjs [--json] [--with-build] [--quick] [--hermetic]
// --json emit machine-readable JSON to stdout (report goes to stderr)
// --with-build also run check:pack-artifact (needs a dist/ build — slow)
// --quick skip the slow unit + vitest + integration suites (drift + fast
// gates only)
// --hermetic scrub OMNIROUTE_API_KEY/OMNIROUTE_URL from gate env so live
// tests self-skip exactly like CI (dev machines otherwise run
// them against localhost and produce false-positive reds)
//
// Per-gate output is saved to _artifacts/release-green/<gate>.log (gitignored) —
// diagnose a red from the file instead of re-running the gate.

import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..", "..");
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";

// Per-gate captured output. execFileSync buffers everything and the report only
// shows a one-line summary, so without these files every red requires RE-RUNNING
// the gate just to see the detail (the dominant cost of the 2026-07-05 pre-flight).
const LOG_DIR = join(ROOT, "_artifacts", "release-green");
function saveGateLog(id, out) {
try {
mkdirSync(LOG_DIR, { recursive: true });
writeFileSync(join(LOG_DIR, `${id}.log`), String(out ?? ""));
} catch {
/* log persistence is best-effort — never fails a gate */
}
}

// ─── Pure helpers (exported for tests) ──────────────────────────────────────

/** Read the committed ratchet baseline value for a metric (null if unknown). */
Expand Down Expand Up @@ -141,14 +160,25 @@ export function classifyRunError(err, timeoutMs) {
};
}

// --hermetic: scrub the live-test trigger vars so the pre-flight behaves like CI
// (a dev machine with OMNIROUTE_API_KEY set runs 17+ live tests that CI skips —
// every one a false-positive red against the release branch).
const HERMETIC_SCRUB = ["OMNIROUTE_API_KEY", "OMNIROUTE_URL"];
let hermetic = false;
function buildGateEnv(extra) {
const env = { ...process.env, FORCE_COLOR: "0", ...(extra || {}) };
if (hermetic) for (const k of HERMETIC_SCRUB) delete env[k];
return env;
}

function run(cmd, cmdArgs, opts = {}) {
try {
const out = execFileSync(cmd, cmdArgs, {
cwd: ROOT,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 256 * 1024 * 1024,
env: { ...process.env, FORCE_COLOR: "0", ...(opts.env || {}) },
env: buildGateEnv(opts.env),
// A hard ceiling for the long, silent test suites (execFileSync buffers all output until
// exit, so they show no progress while running). undefined = no timeout for fast gates.
...(opts.timeout ? { timeout: opts.timeout } : {}),
Expand All @@ -164,6 +194,7 @@ function main() {
const JSON_OUT = args.has("--json");
const WITH_BUILD = args.has("--with-build");
const QUICK = args.has("--quick");
hermetic = args.has("--hermetic");

const results = [];
const record = (r) => {
Expand All @@ -180,6 +211,7 @@ function main() {
const hardCmd = (id, label, cmd, cmdArgs, opts) => {
announce(label);
const { code, out } = run(cmd, cmdArgs, opts);
saveGateLog(id, out);
record({
id,
label,
Expand All @@ -197,6 +229,7 @@ function main() {
const driftCmd = (id, label, cmd, cmdArgs, okDetail = "within baseline", opts) => {
announce(label);
const { code, out } = run(cmd, cmdArgs, opts);
saveGateLog(id, out);
record({
id,
label,
Expand Down Expand Up @@ -229,6 +262,7 @@ function main() {
],
{ timeout: 30 * 60 * 1000 }
);
saveGateLog("lint", out);
const parsed = parseEslintJson(out);
if (!parsed) {
record({
Expand Down Expand Up @@ -269,6 +303,7 @@ function main() {
{
announce("Cognitive complexity (ratchet)");
const { out } = run(npmCmd, ["run", "check:cognitive-complexity"]);
saveGateLog("cognitive", out);
const current = parseCognitiveCount(out);
const base = baselineValue("cognitiveComplexity");
const over = isDrift(current, base);
Expand Down Expand Up @@ -308,6 +343,7 @@ function main() {
const { code, out } = run(npmCmd, ["run", "check:test-masking"], {
env: { GITHUB_BASE_REF: "main" },
});
saveGateLog("test-masking", out);
record({
id: "test-masking",
label: "Test-masking (weakened-assert guard)",
Expand Down
9 changes: 8 additions & 1 deletion skills/cli-contexts/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ Add a new context

- `--api-key <k>`
- `--api-key-stdin`
- `--access-token <t>`
- `--access-token-stdin`
- `--scope <s>`
- `--description <d>`

**Example:**
Expand All @@ -247,7 +250,11 @@ omniroute contexts use <name>

### `contexts current`

Show current active context name
Show the active context (server, auth, scope)

**Flags:**

- `--name-only`

**Example:**

Expand Down
3 changes: 2 additions & 1 deletion skills/cli-serve/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
name: cli-serve
description: Start, stop, and restart the OmniRoute server from the CLI. Manage daemon mode, port configuration, auto-recovery, system tray integration, and the dashboard open shortcut.
---

<!-- generated by src/lib/agentSkills/generator.ts; manual edits will be overwritten -->

## Overview
Expand Down Expand Up @@ -56,6 +55,8 @@ omniroute restart
- `--max-restarts <n>`
- `--tray`
- `--no-tray`
- `--tls-cert <path>`
- `--tls-key <path>`

**Example:**

Expand Down
8 changes: 8 additions & 0 deletions skills/cli-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ omniroute autostart enable
omniroute autostart disable
```

### `autostart toggle`

**Example:**

```bash
omniroute autostart toggle
```

### `autostart status`

**Example:**
Expand Down
Loading
Loading