mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14a2dd81ff | ||
|
|
b2732ddd02 | ||
|
|
dba86d34c9 |
@@ -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.**
|
||||
|
||||
+2
-2
@@ -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
@@ -20,6 +20,6 @@
|
||||
},
|
||||
"overrides": {
|
||||
"@babel/core": "^7.29.6",
|
||||
"postcss": "^8.5.10"
|
||||
"postcss": "^8.5.23"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user