Compare commits

...
Author SHA1 Message Date
Garry Tan 14a2dd81ff fix(deps): bump four override pins past newly-published CVEs
osv-scan passed on the last two release PRs and fails on this one, so these are
newly published, not tolerated debt: 2 High + 4 Medium across fast-uri (3.1.4 →
3.1.5), hono (4.12.25 → 4.12.34), ip-address (10.1.1 → 10.3.1), and admin's
postcss (8.5.10 → 8.5.23). All four are transitive and pinned through the
overrides block, so the fix is the pin, not a dependency addition.

Resolved: fast-uri@3.1.5, hono@4.13.0, ip-address@10.4.0, postcss@8.5.25.
2026-08-04 20:21:55 +07:00
Garry Tan b2732ddd02 fix(ci): a permission failure must never red-X a contributor's PR (live incident)
The gate's first live runs put a red X on an outside contributor's PR five
times over, with no comment explaining why. This repo's GITHUB_TOKEN is
read-only, so every comment and label call returned 403; the throw reached the
top-level handler as exit 2.

The gate is advisory. A repository permission problem is an operator condition,
never a statement about the PR under review. 401/403/404 from GitHub now emit a
loud operator-facing warning naming both blockers (workflow permissions and the
missing ANTHROPIC_API_KEY secret) and exit 0. A genuine outage or a bug in here
still fails visibly at exit 2. Pinned by four tests including the entry-handler
routing; mutation-tested.
2026-08-04 20:21:55 +07:00
Garry Tan dba86d34c9 v0.42.73.0 feat(ci): strict PR usefulness gate + five contributed correctness fixes
Release-only commit: VERSION, package.json, CHANGELOG. All code already on
master. Covers the 8 gate commits (#3794, closes #3698), plus #3764 import
stdout, #3759 dry-run chmod, #3726 cycle model telemetry, #3751 integrity
counter, #3739 doc comment, and #2655 slug_filter.
2026-08-04 10:12:15 +07:00
9 changed files with 115 additions and 15 deletions
+36
View File
@@ -2,6 +2,42 @@
All notable changes to GBrain will be documented in this file.
## [0.42.73.0] - 2026-08-04
**Every incoming pull request now gets a verdict before anyone reads it — and five contributed fixes for silent wrong answers.**
**The PR gate.** Open a pull request against gbrain and an automated check now posts a single verdict comment within a minute: **merge-lane**, **close-lane**, or **needs-maintainer**, with its reasons and a checklist of what a human reviewer should verify for that specific diff. It also checks mechanically that the description carries the human-written intent paragraph and the screenshot of gbrain in use that `CONTRIBUTING.md` requires, and that the title leads with its version.
It is deliberately **advisory** — a triage signal and a reviewer checklist, not an authorization boundary. A green verdict is not permission to merge; a maintainer still decides. Pull-request code is never checked out or executed: the verdict comes from the description and the diff read through the API. Maintainer, bot, and draft pull requests are exempt from the intent-and-screenshot floor only (release automation cannot screenshot itself); they still receive the full verdict. Where the rubric can be argued with, the decision is taken away from it: a merge-lane recommendation is downgraded automatically when a diff adds a dependency, a new provider recipe, or new config keys, edits workflows, deletes a test, exceeds 40 files or 400 net source lines, or changes `src/` without touching a single test.
**Your import output parses again.** `gbrain import <dir> --json` printed five informational lines to stdout ahead of the JSON payload, so anything parsing that output read zero imports while its own bookkeeping recorded the files as ingested — and the next run skipped them permanently. Those lines now go to stderr under `--json`; human output is byte-for-byte unchanged.
**`sources harden --dry-run` no longer changes anything.** It reset the helper's executable bit before reaching the dry-run check, so a documented preview quietly mutated permissions.
**Telemetry records the model that actually ran.** Two nightly-cycle phases wrote a hardcoded or unrelated model name into their verdict cache, evidence signature, and spend metering while the gateway ran whatever chat model you configured. On any brain with a non-default model, the recorded history was fiction.
**`gbrain integrity` stops contradicting itself.** Dead-link findings were counted in the "Review queue" total but written to a different file, so `integrity review` disagreed with `integrity auto`'s own summary. They now get their own line.
**Retype rules can address API-ingested pages.** Mapping rules could only filter on a file path, which is empty for every page written through `put_page` — so no rule could target that whole class. A new `slug_filter` filters on the slug instead, and combines with the path filter when both are given.
Also: the `integrity` source comment no longer documents a `--dry-run` subcommand form that exits with an error.
### To take advantage of v0.42.73.0
```bash
gbrain upgrade
gbrain import <dir> --json | jq . # now parses
gbrain integrity auto # dead links reported separately
```
Nothing to configure for the gate — it runs on pull requests to this repository. If you maintain a fork and want it, the workflow needs an `ANTHROPIC_API_KEY` secret; without one it skips loudly rather than blocking anyone.
### For contributors
The gate went through six rounds against two independent blind reviewers, each judging cold. The findings that changed the design most were not exploits but false positives: a code fence that swallowed the rest of a description, an explanation written as bullet points scoring zero words, a word floor stricter than the published policy, and a comment telling contributors to reopen a pull request that was never closed. Those four descriptions are now permanent regression fixtures — a gate that insults a first-time contributor is worse than no gate. Two properties are deliberate and documented rather than fixed: the mechanical floor is a floor (a determined author clears it in seconds), and a bare URL in a cited reason still autolinks.
Contributed by @YiconZiwei (#2655), @time-attack (#3764, #3759, #3726, #3751, #3739, and the gate groundwork in #3573/#3698).
## [0.42.72.1] - 2026-08-02
**Every issue and pull request now needs a human-written paragraph and a screenshot of gbrain actually being used.**
+1 -1
View File
@@ -1 +1 @@
0.42.72.1
0.42.73.0
+2 -2
View File
@@ -19,7 +19,7 @@
},
"overrides": {
"@babel/core": "^7.29.6",
"postcss": "^8.5.10",
"postcss": "^8.5.23",
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
@@ -232,7 +232,7 @@
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="],
"postcss": ["postcss@8.5.25", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw=="],
"react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
+1 -1
View File
@@ -20,6 +20,6 @@
},
"overrides": {
"@babel/core": "^7.29.6",
"postcss": "^8.5.10"
"postcss": "^8.5.23"
}
}
+6 -6
View File
@@ -53,12 +53,12 @@
"overrides": {
"@hono/node-server": "^2.0.5",
"body-parser": "^2.3.0",
"fast-uri": "^3.1.4",
"fast-uri": "^3.1.5",
"fast-xml-builder": "^1.1.7",
"fast-xml-parser": "^5.7.0",
"form-data": "^4.0.6",
"hono": "^4.12.25",
"ip-address": "^10.1.1",
"hono": "^4.12.34",
"ip-address": "^10.3.1",
"js-yaml": "^3.15.0",
"qs": "^6.15.2",
},
@@ -401,7 +401,7 @@
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-uri": ["fast-uri@3.1.4", "", {}, "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw=="],
"fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="],
"fast-xml-builder": ["fast-xml-builder@1.3.0", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ=="],
@@ -437,7 +437,7 @@
"heic-decode": ["heic-decode@2.1.0", "", { "dependencies": { "libheif-js": "^1.19.8" } }, "sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A=="],
"hono": ["hono@4.12.30", "", {}, "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog=="],
"hono": ["hono@4.13.0", "", {}, "sha512-jhunvfHWxd7J5EFfSgH4xsYJzSe/lfqbUCxiyyeaQasUsXeEHXtzVid+7EOGByc5JnFa23SSFL3Y2RV/z1T+eQ=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
@@ -447,7 +447,7 @@
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
"ip-address": ["ip-address@10.4.0", "", {}, "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+4 -4
View File
@@ -148,16 +148,16 @@
"bun": ">=1.3.10"
},
"license": "MIT",
"version": "0.42.72.1",
"version": "0.42.73.0",
"overrides": {
"@hono/node-server": "^2.0.5",
"fast-uri": "^3.1.4",
"fast-uri": "^3.1.5",
"body-parser": "^2.3.0",
"fast-xml-builder": "^1.1.7",
"fast-xml-parser": "^5.7.0",
"form-data": "^4.0.6",
"hono": "^4.12.25",
"ip-address": "^10.1.1",
"hono": "^4.12.34",
"ip-address": "^10.3.1",
"qs": "^6.15.2",
"js-yaml": "^3.15.0"
}
+3
View File
@@ -92,3 +92,6 @@ export declare function runGate(
env?: Record<string, string | undefined>,
fetchImpl?: typeof fetch,
): Promise<number>;
export declare function isPermissionFailure(err: unknown): boolean;
export declare const PERMISSION_HELP: (msg: string) => string;
+27 -1
View File
@@ -1171,6 +1171,28 @@ export async function runGate(dir, env = process.env, fetchImpl = fetch) {
return lane === 'close-lane' ? 1 : 0;
}
/**
* A missing WRITE permission (or a token that cannot see the resource) is an
* operator condition, never a statement about the PR under review.
*
* Observed on this gate's first live run: the repository's GITHUB_TOKEN was
* read-only, so every comment and label call returned 403, the throw reached
* the top-level handler as exit 2, and the gate put a red X on every open PR —
* including an outside contributor's — with no comment saying why. The gate is
* advisory. It must never fail a contributor's check because it could not talk
* to the API. 401/403/404 from the GitHub side warn loudly and exit 0; a real
* outage or a bug in here still fails visibly.
*/
export function isPermissionFailure(err) {
return /\b(401|403|404)\b/.test(String(err?.message ?? err));
}
export const PERMISSION_HELP = (msg) =>
`PR gate could not post its verdict: ${msg}. This is a repository permission ` +
'problem, not a finding about this PR. Operator: Settings → Actions → General → ' +
'Workflow permissions must allow read and write, and ANTHROPIC_API_KEY must be ' +
'set for the usefulness verdict to run.';
// Import side-effect guard: only run when executed directly (node/bun),
// never when the exports are imported by tests.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -1182,7 +1204,11 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
runGate(dir).then(
(code) => process.exit(code),
(err) => {
// Infrastructure failure (GitHub API down, bad inputs): fail visibly.
if (isPermissionFailure(err)) {
console.log(`::warning::${PERMISSION_HELP(String(err?.message ?? err))}`);
process.exit(0);
}
// Anything else (GitHub down, malformed inputs, a bug here): fail visibly.
console.error(`::error::PR gate crashed: ${err?.stack ?? err}`);
process.exit(2);
},
+35
View File
@@ -39,6 +39,8 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
checkTitle,
isPermissionFailure,
PERMISSION_HELP,
detectRedFlags,
detectPolicyMisses,
hasScreenshot,
@@ -324,6 +326,39 @@ describe('pr-gate script rubric pins', () => {
});
});
describe('permission failures never red-X a PR (live incident, 2026-08-04)', () => {
// The gate's first live run: the repo's GITHUB_TOKEN was read-only, every
// comment/label call 403'd, the throw became exit 2, and an outside
// contributor's PR got a red X with no comment explaining it. The gate is
// advisory — it must degrade, not accuse.
test('GitHub permission/visibility failures are not the PR\'s fault', () => {
expect(isPermissionFailure(new Error('comment upsert failed: 403'))).toBe(true);
expect(isPermissionFailure(new Error('label add failed: 403'))).toBe(true);
expect(isPermissionFailure(new Error('label remove failed: 401'))).toBe(true);
expect(isPermissionFailure(new Error('pr fetch failed: 404'))).toBe(true);
});
test('real outages and bugs still fail visibly', () => {
expect(isPermissionFailure(new Error('comment upsert failed: 500'))).toBe(false);
expect(isPermissionFailure(new Error('comment upsert failed: 502'))).toBe(false);
expect(isPermissionFailure(new TypeError('x is not a function'))).toBe(false);
expect(isPermissionFailure(undefined)).toBe(false);
});
test('the operator, not the contributor, is told what to fix', () => {
const help = PERMISSION_HELP('comment upsert failed: 403');
expect(help).toContain('not a finding about this PR');
expect(help).toContain('Workflow permissions');
expect(help).toContain('ANTHROPIC_API_KEY');
});
test('the entry handler routes permission failures to exit 0', () => {
const handler = SCRIPT.slice(SCRIPT.indexOf('runGate(dir).then'));
expect(handler).toMatch(/isPermissionFailure\(err\)[\s\S]*process\.exit\(0\)/);
expect(handler).toMatch(/process\.exit\(2\)/);
});
});
describe('checkTitle (version-first rule)', () => {
test('accepts version-first titles', () => {
expect(