v1.94.8.0 fix(approvals): no identifying approval text in public-repo PR bodies (GST-1160) (#3918)

* fix(approvals): render no identifying approval text on public repositories (GST-1160)

gstack-approvals render asks gh for the repo's visibility. A public repo, or one gh cannot read, gets only 'Approved: yes|no (record r-<id>)'; a PR description keeps every edit, so the owner's words, run ids and plan hashes stay in the private record. --full prints the rows only when the repo is not reported public.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (check the origin repository's visibility too, GST-1160)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: say render prints one line on public repositories (GST-1160)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: leave docs/captain.md for a follow-up; /ship reads it as agent prose (GST-1160)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-03 06:49:00 +00:00
1 parent 22b6de6b76
commit c197a139b7
13 files changed
+138 -19

No files matched your search

+9
View File
@@ -1,5 +1,14 @@
# Changelog
## [1.94.8.0] - 2026-11-03
**`gstack-approvals render` no longer puts the owner's exact words, autoplan run ids or plan hashes into PR bodies on public repositories (GST-1160).** On a public repo, /ship pasted the full `## Approvals` table into the PR description. That table carries the owner's verbatim quote, the run id and the plan sha256, and GitHub keeps every earlier version of a description in its edit history, which no API can delete.
### Fixed
- **`bin/gstack-approvals render`** asks gh for the visibility of the repository gh picks by default (`GH_REPO`, its default repo, an `upstream` remote) and of the `origin` repository the PR is opened against. When every one of them is private (or internal), it prints the full table as before. When any is public, or gh can't tell, it prints only `Approved: yes|no (record r-<12 hex>)`. The record id is a hash of the row ids, so the Captain can match it to the private record without anything identifying being published. A stderr line, `APPROVALS_RENDER: redacted (repository visibility …)`, says which mode was used.
- **`--full`** prints the full table when gh can't read the visibility and you know the repository is private. It never prints the full table for a repository GitHub reports as public. `gstack-approvals list` still shows the full record.
- **/ship Step 19** (`ship/sections/capy-pr.md`) says a public repository gets only the one line, and never to pass `--full` for a PR body.
## [1.94.7.0] - 2026-11-03
### Fixed
+1 -1
View File
@@ -1 +1 @@
1.94.7.0
1.94.8.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.94.7.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.94.8.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+12 -5
View File
@@ -6,7 +6,7 @@
* [--pending <text>] [--outcome approved|rejected|pending|standing]
* [--date YYYY-MM-DD] [--source <ref>] [--branch <name>] [--json]
* gstack-approvals import --autoplan <run dir> [--by <whose words>] [--branch <name> | --stack <b1,b2,...>] [--json]
* gstack-approvals render [--no-heading] [--branch <name>]
* gstack-approvals render [--no-heading] [--full] [--branch <name>]
* gstack-approvals list [--json] [--branch <name>]
* gstack-approvals sync [--branch <name>]
* gstack-approvals path [--branch <name>]
@@ -20,7 +20,7 @@
*/
import * as path from 'node:path';
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
import { appendRows, branchExists, buildRow, importAutoplan, locate, readRecord, renderRecord, syncRecord, type RecordError, type StoreLocation } from '../lib/approval-record';
import { appendRows, branchExists, buildRow, importAutoplan, locate, readRecord, renderMode, renderRecord, repoVisibility, syncRecord, type RecordError, type StoreLocation } from '../lib/approval-record';
import { resultLine } from '../lib/result-codes';
const HELP = `gstack-approvals — the per-thread approval record (capy/skill/gstack/captain.md)
@@ -29,7 +29,9 @@ const HELP = `gstack-approvals — the per-thread approval record (capy/skill/gs
[--pending <text>] [--outcome approved|rejected|pending|standing] [--date YYYY-MM-DD] [--source <ref>]
import --autoplan <run dir> [--by <whose words, e.g. Garry>] [--stack <b1,b2,...>]
--stack writes the same rows to every branch of a stack; each branch is checked first, and one unknown branch writes none
render [--no-heading] the ## Approvals section for a PR body or report
render [--no-heading] [--full] the ## Approvals section for a PR body or report; a public repository, or one whose
visibility gh cannot read, gets only "Approved: yes|no (record <id>)", because a PR description keeps
its edit history; --full prints the rows when gh cannot read the visibility and you know the repo is private
list [--json]
sync re-mirror the record (GSTACK_APPROVALS_DIR, the Captain's drive approvals/, or the project-user Drive)
path print the record and mirror paths, and which rule chose the mirror
@@ -52,7 +54,7 @@ function parse(args: string[]): { flags: Record<string, string>; bools: Set<stri
const v = args[i + 1];
if (v === undefined) return { flags, bools, error: `${a} needs a value` };
flags[a] = v; i++;
} else if (a === '--json' || a === '--no-heading') bools.add(a);
} else if (a === '--json' || a === '--no-heading' || a === '--full') bools.add(a);
else return { flags, bools, error: `unknown argument ${a}` };
}
return { flags, bools };
@@ -134,7 +136,12 @@ function main(argv: string[]): number {
if (read.restored) process.stderr.write(`gstack-approvals: read the Drive mirror ${loc.mirror} (no local copy on this machine; gstack-approvals sync restores it)\n`);
if (cmd === 'list' && json) process.stdout.write(JSON.stringify({ branch: loc.branch, state: read.state, rows: read.rows }) + '\n');
else if (cmd === 'list') for (const r of read.rows) process.stdout.write(`${r.date} ${r.id} ${r.outcome} ${JSON.stringify(r.ask)} words=${JSON.stringify(r.words)} by=${JSON.stringify(r.by)}\n`);
else process.stdout.write(renderRecord(read, loc, { heading: !bools.has('--no-heading') }));
else {
const visibility = repoVisibility(process.cwd());
const mode = renderMode(visibility, bools.has('--full'));
if (mode === 'redacted') process.stderr.write(`APPROVALS_RENDER: redacted (repository visibility ${visibility}${bools.has('--full') ? '; --full applies only to a private repository' : ''}); gstack-approvals list shows the full record\n`);
process.stdout.write(renderRecord(read, loc, { heading: !bools.has('--no-heading'), mode }));
}
if (read.state === 'unavailable') return fail(cmd, { code: 'APPROVAL_RECORD_UNAVAILABLE', message: `no record at ${loc.file} and no mirror` });
return EXIT.ok;
}
+2 -1
View File
@@ -60,7 +60,8 @@ gstack-approvals render # the ## Approvals se
Drive folder. `gstack-approvals path` prints the chosen mirror and `READ_ALSO:` roots, which reads still
merge. `GSTACK_APPROVALS_MIRROR=0` turns the mirror off, and `gstack-approvals sync` restores the local copy.
- **The durable copies** are the `## Approvals` sections in the thread's report and in the PR body. Paste
`render` output into them; never type rows by hand.
`render` output into them; never type rows by hand. On a public repository (or unknown visibility)
`render` prints only `Approved: yes|no (record <id>)`; `gstack-approvals list` shows the full record.
- **Secret-free.** Every field passes gstack's secret scanner before it is written. A hit refuses the row
(`APPROVAL_RECORD_SECRET`, which names the field and rule, never the text). Write `[redacted]` in place of
the secret, or ask the person to restate.
+1 -1
View File
@@ -755,7 +755,7 @@ A lot of branches die when the interesting work is done and only the boring rele
1. **`## What changed`**: one to three captioned before/after images from `gstack-pr-shots` or a UI screenshot, highest-signal first, with a collapsed text version under each.
2. **`## Why`**: two or three sentences. The first states the problem in the person's words (the issue, the message, the failing run), then the cause and the user-visible effect.
3. **`## Blast radius`**: what else the change touches and the one fact that makes that safe, with the evidence-ladder rung that fact reached (a test that exercises the shared path, a grep over every caller, a measured run). A change to a shared helper, a generated file, a selection list or a policy default always has one; a leaf fix says so in one line.
4. **`## Approvals`**: the table from `gstack-approvals render`, verbatim.
4. **`## Approvals`**: the output of `gstack-approvals render`, verbatim. On a private repository that is the table; on a public repository, or one whose visibility gh cannot read, it is only `Approved: yes|no (record <id>)`, because GitHub keeps every earlier version of a PR description.
Before the body is composed, `gstack-prose-lint` reads the Why, Summary and Blast radius drafts (`--sections 'Why,Summary,Blast radius' --gate`) and the CHANGELOG entry (`--base origin/<base> --gate`), and a `PROSE_LINT_FOUND` line names the sentence to rewrite; the rest of the body is advisory. The Captain's merge-ask template (`captain/templates/merge-ask.md`) carries the same four sections, so a READY report and its PR say the same thing.
+35 -1
View File
@@ -223,8 +223,42 @@ export function cell(text: string, cap = 400): string {
export const APPROVALS_TABLE_HEADER = '| Date | Asked | Exact words | Outcome | Covers | Still pending |';
export const APPROVALS_FOOTER_END = '(display only; not an approval citation).';
export function renderRecord(read: ReadResult, loc: StoreLocation, o: { heading?: boolean } = {}): string {
export type RepoVisibility = 'public' | 'private' | 'unknown';
/**
* The repository's visibility as GitHub reports it through gh, for the repository gh acts on by default (GH_REPO, its
* default repo, an upstream remote) and for the origin remote a PR is opened against: private only when every one
* asked is private, public when any is public, and unknown for anything gh cannot answer.
*/
export function repoVisibility(cwd: string, env: Env = process.env): RepoVisibility {
const ask = (repo: string[]): RepoVisibility => {
const r = spawnSync('gh', ['repo', 'view', ...repo, '--json', 'visibility', '-q', '.visibility'], { cwd, env, encoding: 'utf8', timeout: 15_000 });
if (r.status !== 0) return 'unknown';
const v = (r.stdout ?? '').trim().toLowerCase();
return v === 'private' || v === 'internal' ? 'private' : v === 'public' ? 'public' : 'unknown';
};
const origin = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd, env, encoding: 'utf8', timeout: 15_000 });
const slug = origin.status === 0 ? /([^/:]+\/[^/]+?)(?:\.git)?\/*$/.exec(origin.stdout.trim())?.[1] : undefined;
const seen = [ask([]), ...(slug ? [ask([slug])] : [])];
return seen.includes('public') ? 'public' : seen.every(v => v === 'private') ? 'private' : 'unknown';
}
/**
* Full rows only where the repository is known private, or --full asks for them and GitHub does not report it public.
* A PR description keeps every earlier version in its edit history, so a public or unknown repository gets the
* redacted block: whether anything was approved and an opaque id of the record (a hash of its row ids), nothing else.
*/
export function renderMode(visibility: RepoVisibility, full: boolean): 'full' | 'redacted' {
return visibility === 'private' || (full && visibility === 'unknown') ? 'full' : 'redacted';
}
export function renderRecord(read: ReadResult, loc: StoreLocation, o: { heading?: boolean; mode?: 'full' | 'redacted' } = {}): string {
const head = o.heading === false ? [] : ['## Approvals', ''];
if (o.mode === 'redacted' && read.rows.length) {
const approved = read.rows.some(r => r.outcome === 'approved' || r.outcome === 'standing');
const id = 'r-' + createHash('sha256').update(read.rows.map(r => r.id).sort().join('\n')).digest('hex').slice(0, 12);
return [...head, `Approved: ${approved ? 'yes' : 'no'} (record ${id}). The full approval record stays private: this repository is public or its visibility is unknown.`, ''].join('\n');
}
if (read.state === 'unavailable') return [...head, `Record unavailable on this machine: no approval record for branch ${cell(loc.branch)} here, and the state root is ephemeral with no Drive mirror, so this is not proof that nothing was approved.`, ''].join('\n');
if (!read.rows.length) return [...head, `No approvals recorded for branch ${cell(loc.branch)}.`, ''].join('\n');
const lines = [APPROVALS_TABLE_HEADER, '|---|---|---|---|---|---|'];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.94.7",
"version": "1.94.8",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+2 -1
View File
@@ -39,7 +39,8 @@ commit themes; do not repeat the captions.
6. **`## Blast radius`:** the body template's item, written once, here: what else the change touches and
the one fact that makes that safe, with the evidence-ladder rung it reached (the `/investigate` skill's Blast radius section, when present).
7. **`## Approvals`:** the output of `~/.claude/skills/gstack/bin/gstack-approvals render`, verbatim. It
escapes its cells, and the receipt never reads it as an approval citation. On
escapes its cells, and the receipt never reads it as an approval citation. A public repo (or unknown
visibility) gets only `Approved: yes|no (record <id>)`; never pass `--full` for a PR body. On
`APPROVAL_RECORD_UNAVAILABLE` keep its text and say so in the READY report. Never type rows by hand.
8. **READY report:** attach the key image's PNG to the report you send (Capy's message
attachments) and give its uploaded URL, so the Captain can show it inline.
+2 -1
View File
@@ -37,7 +37,8 @@ commit themes; do not repeat the captions.
6. **`## Blast radius`:** the body template's item, written once, here: what else the change touches and
the one fact that makes that safe, with the evidence-ladder rung it reached (the `/investigate` skill's Blast radius section, when present).
7. **`## Approvals`:** the output of `~/.claude/skills/gstack/bin/gstack-approvals render`, verbatim. It
escapes its cells, and the receipt never reads it as an approval citation. On
escapes its cells, and the receipt never reads it as an approval citation. A public repo (or unknown
visibility) gets only `Approved: yes|no (record <id>)`; never pass `--full` for a PR body. On
`APPROVAL_RECORD_UNAVAILABLE` keep its text and say so in the READY report. Never type rows by hand.
8. **READY report:** attach the key image's PNG to the report you send (Capy's message
attachments) and give its uploaded URL, so the Captain can show it inline.
+67 -5
View File
@@ -10,7 +10,7 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { parseCitations } from '../lib/approval-citations';
import { branchKey, buildRow, cell, importAutoplan, readRecord, renderRecord, type StoreLocation } from '../lib/approval-record';
import { APPROVALS_TABLE_HEADER, branchKey, buildRow, cell, importAutoplan, readRecord, renderRecord, type StoreLocation } from '../lib/approval-record';
const ROOT = path.resolve(import.meta.dir, '..');
const dirs: string[] = [];
@@ -52,7 +52,7 @@ describe('gstack-approvals add and render', () => {
expect(add.status).toBe(0);
expect(add.out).toMatch(/^APPROVAL_RECORDED: a-[0-9a-f]{12} .*\/projects\/acme-widgets\/approvals\/feature-capy-asks-[0-9a-f]{10}\.jsonl$/m);
expect(bin(ADD, { cwd, env: { GSTACK_STATE_ROOT: state } }).out).toContain('(already present)');
const r = bin(['render'], { cwd, env: { GSTACK_STATE_ROOT: state } });
const r = bin(['render', '--full'], { cwd, env: { GSTACK_STATE_ROOT: state } });
expect(r.status).toBe(0);
expect(r.out).toContain('## Approvals');
expect(r.out).toContain('| Date | Asked | Exact words | Outcome | Covers | Still pending |');
@@ -102,6 +102,68 @@ describe('gstack-approvals add and render', () => {
});
});
describe('public repositories get no identifying approval text (GST-1160)', () => {
const fakeGh = (out: string, code = 0) => {
const d = tmp('appr-gh-');
fs.writeFileSync(path.join(d, 'gh'), `#!/bin/sh\nprintf '%s\\n' '${out}'\nexit ${code}\n`, { mode: 0o755 });
return { PATH: `${d}:${process.env.PATH}` };
};
const QUOTE = ['add', '--ask', 'Approve the plan "Qm leak fix" (autoplan run 20261102-qm-77, 4 decisions)', '--words', 'do it, ship the leak fix', '--by', 'Garry (via QM Minder)', '--scope', 'the reviewed plan (sha256 9f2c1e0b7a6d5c4b3a29180716f5e4d3c2b1a0f9e8d7c6b5a4938271605f4e3d)'];
const leaks = (out: string) => ['do it, ship the leak fix', '20261102-qm-77', '9f2c1e0b7a6d', 'Garry', 'Qm leak fix'].filter(s => out.includes(s));
for (const [label, env] of [['public', () => fakeGh('PUBLIC')], ['unknown (gh fails)', () => fakeGh('', 1)]] as const) {
test(`a ${label} repository renders only that it was approved and an opaque record id`, () => {
const cwd = repo(); const state = tmp('appr-state-');
expect(bin(QUOTE, { cwd, env: { GSTACK_STATE_ROOT: state } }).status).toBe(0);
const r = bin(['render'], { cwd, env: { GSTACK_STATE_ROOT: state, ...env() } });
expect(r.status, r.err).toBe(0);
expect(leaks(r.out)).toEqual([]);
expect(r.out).toContain('## Approvals');
expect(r.out).toMatch(/^Approved: yes \(record r-[0-9a-f]{12}\)/m);
expect(r.out).not.toContain(APPROVALS_TABLE_HEADER);
expect(r.err).toContain('APPROVALS_RENDER: redacted');
});
}
test('a private repository keeps the full table, and --full is the explicit override when visibility is unknown', () => {
const cwd = repo(); const state = tmp('appr-state-');
expect(bin(QUOTE, { cwd, env: { GSTACK_STATE_ROOT: state } }).status).toBe(0);
const priv = bin(['render'], { cwd, env: { GSTACK_STATE_ROOT: state, ...fakeGh('PRIVATE') } });
expect(priv.out).toContain(APPROVALS_TABLE_HEADER);
expect(priv.out).toContain('do it, ship the leak fix');
const forced = bin(['render', '--full'], { cwd, env: { GSTACK_STATE_ROOT: state, ...fakeGh('', 1) } });
expect(forced.out).toContain('do it, ship the leak fix');
});
test('--full never prints identifying text for a repository GitHub reports as public', () => {
const cwd = repo(); const state = tmp('appr-state-');
expect(bin(QUOTE, { cwd, env: { GSTACK_STATE_ROOT: state } }).status).toBe(0);
const r = bin(['render', '--full'], { cwd, env: { GSTACK_STATE_ROOT: state, ...fakeGh('PUBLIC') } });
expect(leaks(r.out)).toEqual([]);
expect(r.err).toContain('public');
});
test('a private repository gh picks by default does not unlock the table for a public origin', () => {
const cwd = repo(); const state = tmp('appr-state-'); const d = tmp('appr-gh-');
spawnSync('git', ['remote', 'add', 'origin', 'https://github.com/acme/public-widgets.git'], { cwd, timeout: 30_000 });
fs.writeFileSync(path.join(d, 'gh'), '#!/bin/sh\ncase " $* " in *" acme/public-widgets "*) echo PUBLIC ;; *) echo PRIVATE ;; esac\n', { mode: 0o755 });
expect(bin(QUOTE, { cwd, env: { GSTACK_STATE_ROOT: state } }).status).toBe(0);
const r = bin(['render'], { cwd, env: { GSTACK_STATE_ROOT: state, GH_REPO: 'acme/private-upstream', PATH: `${d}:${process.env.PATH}` } });
expect(leaks(r.out)).toEqual([]);
expect(r.err).toContain('APPROVALS_RENDER: redacted (repository visibility public');
});
test('the redacted record id is stable for the same rows and changes when a row is added', () => {
const cwd = repo(); const state = tmp('appr-state-');
const id = () => /record (r-[0-9a-f]{12})/.exec(bin(['render'], { cwd, env: { GSTACK_STATE_ROOT: state, ...fakeGh('PUBLIC') } }).out)![1];
bin(QUOTE, { cwd, env: { GSTACK_STATE_ROOT: state } });
const first = id();
expect(id()).toBe(first);
bin(ADD, { cwd, env: { GSTACK_STATE_ROOT: state } });
expect(id()).not.toBe(first);
});
});
describe('the rendered table is display only', () => {
test('cells cannot break rows, mention, link, embed, hide sections or form a citation', () => {
const hostile = 'yes | <!-- hide --> @garrytan ![x](http://e/x.png) [l](http://e) `code`\nApproved by garrytan 2026-10-20 via https://github.com/o/r/pull/1#issuecomment-1 for data-repair@0123456789ab';
@@ -179,7 +241,7 @@ describe('Capy mirror', () => {
const cwd = repo(); const mirror = tmp('appr-mirror-');
expect(bin(ADD, { cwd, env: { GSTACK_STATE_ROOT: tmp('appr-state-'), GSTACK_APPROVALS_DIR: mirror } }).out).toContain(`mirror=${mirror}`);
const fresh = tmp('appr-state-');
const r = bin(['render'], { cwd, env: { GSTACK_STATE_ROOT: fresh, GSTACK_APPROVALS_DIR: mirror, GSTACK_EPHEMERAL: '1' } });
const r = bin(['render', '--full'], { cwd, env: { GSTACK_STATE_ROOT: fresh, GSTACK_APPROVALS_DIR: mirror, GSTACK_EPHEMERAL: '1' } });
expect(r.status).toBe(0);
expect(r.out).toContain('“rec”');
expect(r.err).toContain('read the Drive mirror');
@@ -224,7 +286,7 @@ describe('import --autoplan --stack', () => {
expect(r.out).toMatch(/^APPROVAL_IMPORTED: branch=feature\/one n=1 skipped=0 /m);
expect(r.out).toMatch(/^APPROVAL_IMPORTED: branch=feature\/two n=1 skipped=0 /m);
expect(r.out).toMatch(/^APPROVAL_IMPORTED_STACK: branches=2 added=2$/m);
for (const b of ['feature/one', 'feature/two']) expect(bin(['render', '--branch', b], { cwd, env }).out).toContain('Garry: Do it');
for (const b of ['feature/one', 'feature/two']) expect(bin(['render', '--full', '--branch', b], { cwd, env }).out).toContain('Garry: Do it');
expect(bin(['import', '--autoplan', run, '--by', 'Garry', '--stack', 'feature/one,feature/two'], { cwd, env }).out).toMatch(/^APPROVAL_IMPORTED_STACK: branches=2 added=0$/m);
});
@@ -236,7 +298,7 @@ describe('import --autoplan --stack', () => {
expect(r.status).toBe(3);
expect(r.err).toContain('feature/nope');
expect(r.err).toContain('(APPROVAL_RECORD_NO_BRANCH)');
expect(bin(['render', '--branch', 'feature/one'], { cwd, env: { GSTACK_STATE_ROOT: state } }).out).not.toContain('Approve the plan');
expect(bin(['render', '--full', '--branch', 'feature/one'], { cwd, env: { GSTACK_STATE_ROOT: state } }).out).not.toContain('Approve the plan');
});
test('--stack goes with import --autoplan and without --branch', () => {
+1 -1
View File
@@ -83,7 +83,7 @@ describe('Capy Captain flow', () => {
expect(imp.status, imp.stderr).toBe(0);
expect(imp.stdout).toMatch(/^APPROVAL_IMPORTED: n=1 skipped=0 /);
expect(bin('gstack-approvals', ['import', '--autoplan', out, '--by', 'Garry'], env, repo).stdout).toMatch(/^APPROVAL_IMPORTED: n=0 skipped=1 /);
const table = bin('gstack-approvals', ['render'], env, repo).stdout;
const table = bin('gstack-approvals', ['render', '--full'], env, repo).stdout;
expect(table).toContain('Approve the plan "Retry backoff for the webhook worker"');
expect(table).toContain(`“${reply}” (Garry, relayed by agent:GSTA-1) | approved |`);
expect(fs.readdirSync(path.join(mirror, 'acme-worker')).length).toBe(1);
@@ -0,0 +1,4 @@
{
"bytes": 120,
"why": "GST-1160: Step 19 Approvals note that a public or unknown-visibility repo renders only Approved: yes|no (record id)"
}