Compare commits

..
Author SHA1 Message Date
dev-driver 270a28cb40 docs: the portability report fails only with --strict
developing.md said findings never fail the portability report. Since the
report gained --strict, that's true only by default: --strict exits 1 when
it finds any. The workflow still runs it without --strict.
2026-11-14 00:44:20 +00:00
Dery Ferdika 45d390d157 feat(portability): an opt-in --strict exit status when the report finds matches (#3202)
`scripts/portability-report.mjs` gains an opt-in `--strict` flag. The report is produced exactly as before, and the run then exits 1 when it found matches. Without the flag nothing changes: it exits 0 with or without findings, and 2 on an operational error such as a bad argument or a git failure.

Exit 1 means the report found matches, not that a change is unsafe or must be rejected. The checks are heuristic: a missed match is possible, and an empty report is not a safety claim. OpenRig's own portability workflow doesn't pass `--strict` and stays advisory.
2026-11-13 16:42:05 -08:00
4 changed files with 45 additions and 32 deletions

No files matched your search

+1 -1
View File
@@ -33,7 +33,7 @@ suite. Both sets are readable in the root `package.json` scripts.
| Lane | Command | Meaning |
|---|---|---|
| Portability report | `node scripts/portability-report.mjs` | lists added lines with machine-specific values; findings never fail it (`.github/workflows/portability-report.yml`) |
| Portability report | `node scripts/portability-report.mjs` | lists added lines with machine-specific values; findings don't fail it by default, and `--strict` exits 1 when it finds any. The workflow (`.github/workflows/portability-report.yml`) runs it without `--strict`. |
## The norm (web-UI freeze at 0.5.0)
@@ -70,24 +70,11 @@ codex mcp add openrig --env OPENRIG_SESSION_NAME=<address> -- rig mcp serve --fr
- **VS Code:** verified on 1.145. Older versions, such as 1.110, don't load Agent Plugins.
- **Cursor:** verified on 3.27, as a local plugin.
- **Copilot CLI:** verified on 1.0.107. Install from this repository's marketplace, and trust the folder when Copilot
CLI asks:
- **Copilot CLI:** not yet verified; the steps follow its documentation.
```bash
copilot plugin marketplace add mvschwarz/openrig
copilot plugin install openrig@openrig-app
```
Unlike the other apps, this installs the plugin from this repository's main branch, not from your OpenRig package, so
its skill can be newer than the OpenRig you run.
Copilot CLI also loads the skills in `~/.agents/skills`. Where OpenRig's own skills are installed there, it can choose
OpenRig's `openrig-skills` skill before this plugin's `working-with-an-openrig-team`. If it does, it may ask for a
skill pack that isn't there and get `pack_not_found`: ask it to use `working-with-an-openrig-team`.
In VS Code and Cursor, install the plugin from this folder with the app's own install-from-folder step. In VS Code
that's **Chat: Install Plugin from Source**; in Cursor, copy the folder to `~/.cursor/plugins/local/openrig` and reload.
The app starts one `openrig` server, `rig mcp serve --front-door`, from the root `mcp.json`, and should load the skill.
Install the plugin from this folder with the app's own install-from-folder step. In VS Code that's **Chat: Install Plugin
from Source**; in Cursor, copy the folder to `~/.cursor/plugins/local/openrig` and reload. The app starts one `openrig`
server, `rig mcp serve --front-door`, from the root `mcp.json`, and should load the skill.
- **No seat:** these apps can't set a seat for a plugin's server, so it starts with none. An app started from a shell
where `OPENRIG_SESSION_NAME` (or the older `RIGGED_SESSION_NAME`) is set passes it on, but only for that launch: on
@@ -103,20 +90,14 @@ The app starts one `openrig` server, `rig mcp serve --front-door`, from the root
- Environment export needs an HTTP work grant, which a plugin's local server never has.
- **To give the app a seat,** add your own MCP server that runs `rig mcp serve --front-door` with
`OPENRIG_SESSION_NAME=<address>` in its environment, and turn the plugin's `openrig` server off, so the app runs one
OpenRig server. The plugin's skill stays installed. Verified on VS Code 1.145, Cursor 3.27 and Copilot CLI 1.0.107,
where the team's questions then reach you and your answers are recorded, across a reopen:
OpenRig server. The plugin's skill stays installed. Verified on VS Code 1.145 and Cursor 3.27, where the team's
questions then reach you and your answers are recorded, across a reopen:
- **VS Code:** add it to your user `mcp.json` under a name other than `openrig`, such as `openrig-seat`, and disable
the plugin's `openrig` in **MCP: List Servers**. Two servers named `openrig` are hard to tell apart, and starting
one can stop the other.
- **Cursor:** add it to `.cursor/mcp.json` (the project's or your user one), turn it on, and disable
**openrig · Plugin** in **Customize → MCPs**.
- **Copilot CLI:** add it, then turn the plugin's server off. Copilot CLI saves both, so the seat holds in new
sessions:
```bash
copilot mcp add openrig-seat --env OPENRIG_SESSION_NAME=<address> -- rig mcp serve --front-door
copilot mcp disable openrig
```
- **Copilot CLI:** add it the CLI's own way and turn the plugin's server off; not yet verified.
## What it doesn't do
+8 -5
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env node
// Lists lines added by a change that contain values tied to one machine, network or account, so
// the author can replace them with portable ones or keep them on purpose. Findings never fail it: a finding is a question, not a verdict. It exits 0
// with or without findings and 2 on an operational error (bad argument, git failure).
// the author can replace them with portable ones or keep them on purpose. A finding is a question, not a verdict: the default
// exits 0 with or without findings, --strict exits 1 when findings exist, and 2 is an operational error (bad argument, git failure).
//
// What it looks for:
// 1. Credentials: keys, tokens, passwords, key blocks.
@@ -17,6 +17,7 @@
// node scripts/portability-report.mjs --staged # what is staged for the next commit
// add --out report.md to also write the report to a file
// add --locations-only to list file and line without quoting the matched text (for public CI output)
// add --strict to exit 1 when findings exist (the default stays advisory: exit 0 either way)
import { execFileSync } from "node:child_process";
import { writeFileSync, realpathSync } from "node:fs";
@@ -133,11 +134,12 @@ export function renderReport(findings, label, { locationsOnly = false } = {}) {
}
function parseArguments(argv) {
const options = { staged: false, locationsOnly: false };
const options = { staged: false, locationsOnly: false, strict: false };
for (let index = 0; index < argv.length; index += 1) {
const key = argv[index];
if (key === "--staged") options.staged = true;
else if (key === "--locations-only") options.locationsOnly = true;
else if (key === "--strict") options.strict = true;
else if (["--from", "--to", "--out", "--repo"].includes(key)) options[key.slice(2)] = argv[++index];
else throw new Error(`Unknown argument: ${key}`);
}
@@ -163,12 +165,13 @@ export function main(argv = process.argv.slice(2)) {
label = `${from.slice(0, 8)}..${to.length > 12 ? to.slice(0, 8) : to}`;
}
const diff = git(repo, ["-c", "core.quotePath=false", "diff", "-U0", "--no-color", "--src-prefix=a/", "--dst-prefix=b/", "--diff-filter=ACMR", ...range]);
const report = renderReport(findPortabilityIssues(addedLines(diff)), label, {
const findings = findPortabilityIssues(addedLines(diff));
const report = renderReport(findings, label, {
locationsOnly: options.locationsOnly,
});
process.stdout.write(report);
if (options.out) writeFileSync(options.out, report);
return 0;
return options.strict && findings.length > 0 ? 1 : 0;
}
function invokedDirectly() {
+29
View File
@@ -173,3 +173,32 @@ test("strips Git's separator tab and leaves C-quoted filenames quoted", () => {
}
assert.deepEqual(addedLines("+++ b/ordinary name.md\t\n@@ -0,0 +1 @@\n+see /Users/alice/project\n"), [{ file: "ordinary name.md", line: 1, text: "see /Users/alice/project" }]);
});
test("--strict exits 1 on findings, keeps the advisory default at 0, and exits 0 on a clean stage", async () => {
const { mkdtempSync, writeFileSync, rmSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const { fileURLToPath } = await import("node:url");
const { execFileSync, spawnSync } = await import("node:child_process");
const repo = mkdtempSync(join(tmpdir(), "openrig-portability-strict-"));
const script = fileURLToPath(new URL("./portability-report.mjs", import.meta.url));
try {
execFileSync("git", ["init", "--quiet", repo]);
execFileSync("git", ["-C", repo, "config", "user.email", "[email protected]"]);
execFileSync("git", ["-C", repo, "config", "user.name", "t"]);
writeFileSync(join(repo, "a.md"), "see /Users/alice/work\n");
execFileSync("git", ["-C", repo, "add", "--", "a.md"]);
const strictWithFindings = spawnSync(process.execPath, [script, "--repo", repo, "--staged", "--strict"], { encoding: "utf8" });
assert.equal(strictWithFindings.status, 1, strictWithFindings.stdout + strictWithFindings.stderr);
const advisoryWithFindings = spawnSync(process.execPath, [script, "--repo", repo, "--staged"], { encoding: "utf8" });
assert.equal(advisoryWithFindings.status, 0, advisoryWithFindings.stdout + advisoryWithFindings.stderr);
execFileSync("git", ["-C", repo, "commit", "--quiet", "-m", "seed"]);
writeFileSync(join(repo, "b.md"), "Nothing machine-specific here.\n");
execFileSync("git", ["-C", repo, "add", "--", "b.md"]);
const strictClean = spawnSync(process.execPath, [script, "--repo", repo, "--staged", "--strict"], { encoding: "utf8" });
assert.equal(strictClean.status, 0, strictClean.stdout + strictClean.stderr);
} finally { rmSync(repo, { recursive: true, force: true }); }
});