-`grep -c "requestGuidanceFullscreen(" web/src/route/route.ts` returns exactly 2 (activateGuidance + resume — the CALL sites; the import line does not match because it carries no `(`)
-`grep -c "engageCloseProtection(" web/src/route/route.ts` returns exactly 2 (the CALL sites — activateGuidance + resume; the import line carries no `(` and so is not counted, which is why the paren form is used)
The final phase hardens everything phases 1–6 shipped and closes with a checklist-signed private launch: one consolidated privacy-audit tool (zero-telemetry v1, PRIV-02/GEO-03 re-verified end-to-end), PX-04 fullscreen + landscape QA across the full device matrix with a measured battery-drain record (<10%/hr), a consolidated budget record (CPU/subrequests/bundles/KV), all parked review debts paid (the 3 deferred INFOs; /cari re-arm attempt + honest documentation), the known-limitations doc shipped and linked from /help — and the private beta launched to the user's first KL drivers when the checklist signs.
No new product features. This phase hardens outcomes; its empty/error states ARE the polish surface.
</domain>
<decisions>
## Implementation Decisions
### Privacy audit (SC3)
- **D-01:** A **consolidated automated audit script** (`npm run privacy-audit`, repo tool): grep gates over ALL src trees (no storage-API writes outside the platform allowlist — the phase-3 gate's law extended repo-wide; no coordinate/payload logging — the category-only vocabulary verified by schema scan), a KV listing probe (only TTL-keyed namespaces), a bundle scan (**zero telemetry: no analytics, no beacons — the ONLY runtime fetches are OpenFreeMap tiles, the Telegram API, and the app's own Worker endpoints**), plus a **signed written audit record** citing the evidence (PRIV-02/GEO-03 re-verified end-to-end). Reusable by forks (GPL audit tool).
- **D-02:** **Zero telemetry in v1** — opt-in telemetry stays an IDEAS.md aspiration post-validation; nothing to opt into yet, nothing to audit but absence.
### Device matrix + battery (SC1, PX-04)
- **D-03:** **Fullscreen on guidance start** via `requestFullscreen()` behind the 8.0 capability guard (phase-6 guard precedent), with orientation-lock used where available; landscape usability per device in the QA pass; landscape layout fixes are CSS-only media queries (tokens binding; the cockpit/banner/card geometry adapts, the map stays Rams-constant).
- **D-04:** The **battery target is a RECORDED real measurement**: one Android guidance run ≥30 min screen-on, battery % read at start/end (method + caveats written into the gate file); the D-15 distance-throttle is the software half. The <10%/hr target is recorded as measured, never synthesized (no bench harness — over-engineering for one phone).
- **D-05:** The QA pass is **one consolidated walk**: Android + iOS + Desktop + browser × (boot, search→route, guidance, minimize/resume, trip-end, theme flip, landscape/fullscreen) — a single checklist in the phase gate file; user walks once; NOT-RUN legs recorded honestly (the phase 3/4 protocol; iOS legs if the device is available — Phase 4 verified iOS present).
### Budgets + deferred debt (SC4)
- **D-06:** A **consolidated budget record** (`npm run budget-audit` output + written record checked into verification): Worker CPU/subrequests (06-04's measured ladder re-run against the live graph), bundle sizes (BA9 et al.), KV write counts per request class (cron = 0 writes — the free-plan law), asset cache headers. Current numbers vs free-tier limits, in one document.
- **D-07:** **Debts paid here**: (1) 02-REVIEW's 3 deferred INFOs — IN-01 (dead `ParsedInitData` export + unreachable branch + read-helper intent comments), IN-02 (stale wrangler.jsonc comment), IN-03 (exact-900s boundary test — the security-critical comparator pin); (2) the /cari menu: re-run `register-commands.mjs` (scopes were re-armed in phase 4) + record the Telegram non-default-scope defect honestly in the limitations doc; (3) phase 3's stale-fix assumption notes (A1/A3) reviewed and closed or re-pointed. **NOT here**: ALT landmarks (c2) — committed to the v1.x milestone, not this phase.
### Known-limitations doc (SC4)
- **D-08:** `docs/KNOWN-LIMITATIONS.md` in-repo, written for **drivers AND forks**: OSM data gaps (no traffic confidence, no lane guidance, speed limits where mapped), cross-metro CPU degradation (the c3 class), the /cari menu defect + its workaround, beta quality statement, and the roadmap pointers (WASM-router track, ALT, engine B). Surfaced from the bot: `/help` gains one line pointing at the doc (strings-module copy).
### Launch definition
- **D-09:** **Private launch = checklist-signed private beta**: the user's chosen first drivers (friends/family/testers in KL) receive the bot link; word-of-mouth distribution. No app store, no marketing, no landing page (post-v1 idea). The launch checklist: known-limitations shipped · privacy audit signed · budget record green · full device matrix walked · `/help` + `/privacy` accurate · repo pushed public under GPL-3.0. **The milestone completes when the checklist is signed** — the launch act itself is the user's, not the workflow's.
The repo's gate lineage to consolidate: GEO3 (web memory-only) ← PG1/PG2 (Worker TTL writes) ← RP1/RP2 (Worker routing shape) ← BA1/BA5/BA8 (bundle). The audit tool extends the same laws repo-wide, it does not invent new ones.
**Comment-stripped source scan** — copy verbatim from `web/test/no-storage.test.mjs` lines 29–42 (identical twins exist in `bot/test/route-privacy.test.mjs` lines 42–59 and `web/test/homescreen.test.mjs` lines 240–244):
```javascript
function listFiles(dir, exts, acc = []) {
for (const entry of readdirSync(dir)) {
const full = path.join(dir, entry);
if (statSync(full).isDirectory()) listFiles(full, exts, acc);
else if (exts.some((e) => entry.endsWith(e))) acc.push(full);
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1"); // line comments (not URLs inside strings — benign over-stripping, gate is prefer-fail reading)
}
```
> Planner note: the `(^|[^:\\])` guard keeps `https://` URLs alive; the header comment says the over-stripping is deliberate "prefer-fail reading". Reuse exactly; do not write a smarter stripper (RESEARCH Pitfall 6).
**The KV write-TTL law the audit must extend repo-wide** — `bot/test/privacy.test.mjs` lines 32–49 (PG1): every `.put(` line carries `expirationTtl` ON THE SAME LINE, and it must reference a NAMED constant, never an inline literal:
assert.ok(ttl && /^[A-Z_]{2,}$/.test(ttl), `${file}:${i + 1} — expirationTtl must reference a named TTL constant, not an inline literal`);
}
});
```
**Positive host allowlist (prefer allow over ban for runtime fetches)** — `bot/test/route-privacy.test.mjs` lines 94–105 show the negative-host form; BA1 lines 64–70 show the CDN ban list. D-01 flips this to a POSITIVE allowlist (OpenFreeMap tiles, Telegram API, own Worker) — keep the per-file `assert` message style from RP1c:
// ... per file: assert.ok(hit === null, `${relative} references routing-host "${hit?.[0]}" — <the law and why>`);
```
**CLI + exit-code contract** — `theme/scripts/audit.mjs` lines 20–24 define the repo's audit vocabulary (reuse it — OpenCode's discretion area explicitly covers exit codes, and this precedent already exists):
```text
* Exit codes (no silent passes — T-1-07):
* 0 clean — every checked declaration derives from tokens
* 1 violations — each printed as file:line: declaration
* 2 drift/error — token sheet degraded (see message)
```
And its driver shape (lines 797–814): print every violation, then a single `AUDIT FAILED — N …` / `AUDIT CLEAN — …` summary line, then `process.exit(0|1)`. RESEARCH Pattern 4 adds exit **2 = missing credentials/environment** so a skipped live probe is never mistaken for a pass.
**Bundle-scan mechanics** — `web/test/build-artifact.test.mjs`: the DirEnt walk (lines 79–87), the PRODUCTION-only scope law (BA5 lines 133–147 — `web/dist-harness/` is deliberately NOT clean; a gate pointed at it inverts itself), and BA8's JS-assets-only rule for crypto scans (lines 175–187 — the official Telegram SDK script in `index.html` is EXTERNAL and has its own crypto; the HTML fetch-host allowlist must whitelist `telegram.org`'s script tag explicitly, RESEARCH Pitfall 5/A4).
**Analogs:** `routing/scripts/measure-route-cpu.mjs` (the measurement half — RE-RUN, not rewritten), `web/test/build-artifact.test.mjs` BA9 (the print-the-budget law)
**Header contract** — `routing/scripts/measure-route-cpu.mjs` lines 3–9 set the law the budget audit inherits:
```text
* A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src,
* web/src, or routing/src imports it. The free plan gives a Worker 10 ms of
* CPU per request (Error 1102 beyond it), so the A*-over-the-real-graph cost
* is MEASURED AND PRINTED, never hoped for ("budget gates print their
assert.ok(total <= 204800, `route payload is ${total} gz bytes — over the 204800-byte (200 KiB) D-11 budget by ${total - 204800}. …`);
```
**Gate-fail message carries the mitigation ladder** — `measure-route-cpu.mjs` lines 180–185:
```javascript
if (p95 > P95_BUDGET_MS) {
console.error(`GATE FAIL: p95 ${p95.toFixed(3)} ms exceeds the ${P95_BUDGET_MS} ms budget (A1) — apply the mitigation ladder: (a) … (b) … (c) …`);
process.exit(1);
}
console.log(`GATE PASS: p95 ${p95.toFixed(3)} ms <= ${P95_BUDGET_MS} ms budget — A1 retired for this graph`);
process.exit(0);
```
The budget audit re-runs `node routing/scripts/measure-route-cpu.mjs` (do not fork its logic — D-06/RESEARCH "Don't Hand-Roll") and adds: gzip'd bundle bytes (BA9 inputs: `routing/src`, `web/src/route`, `web/src/guidance`, `route-fixtures.json` — lines 204–209), KV write classes per request source (`session:`/`chat:` at `SESSION_TTL_SECONDS` — `bot/src/kv/sessions.ts` line 43; `loc:` at `PIN_TTL_SECONDS` — `location-pin.ts` line 25; `guidance:` at `GUIDANCE_SESSION_TTL_SECONDS` — `guidance-sessions.ts` line 27; cron = 0 writes per `webhook.ts` lines 49–57), and live asset cache headers once `_headers` ships. Reference ceilings table is in RESEARCH §Pattern 5.
test("HS1 a client without addToHomeScreen is a no-op — nothing thrown, no affordance", () => { /* method absent → no-op */ });
test("HS2 an old client (isVersionAtLeast 8.0 false) is a no-op", () => { /* version 7.9 → no-op, "the status was never even queried" */ });
```
**Throwing-platform-call-is-non-error** — HS7 (lines 214–219) is the exact shape for `requestFullscreen()` rejection:
```javascript
wa.addToHomeScreen = () => { throw new Error("platform refused"); };
assert.doesNotThrow(() => btn.dispatch("click"), "a rejected addToHomeScreen must not break the boot");
```
**The source-scan ban closer** — HS9 (lines 240–248): end the suite with a comment-stripped scan of the new module banning the anti-pattern APIs (here: `Element.requestFullscreen` as primary path, `screen.orientation.lock`).
Test IDs should follow the repo's prefix convention (`HS*`, `GS*`, `R*` → use e.g. `FS*`).
---
### `web/test/failure-state-matrix.test.mjs` (test, named vectors)
**Named-vector discipline** — `guidance-replay.test.mjs` header lines 15–33 enumerates cases R1–R12/T1–T3 with the law each pins; RESEARCH Pattern 3's `FAILURE_VECTORS` array is the data-driven form. The failure matrix test should import the REAL modules (`../src/route/route.ts`, `../src/guidance/guidance.ts`) and assert on the designed state tokens, e.g.:
```javascript
const g = await import("../src/guidance/guidance.ts");
For fullscreen the guard is BOTH halves (method presence AND 8.0) — `homescreen.ts` line 33 is the tightest form:
```typescript
if (typeof wa.addToHomeScreen !== "function" || !wa.isVersionAtLeast("8.0")) return;
```
plus the best-effort try/catch (homescreen.ts lines 51–58) and the event observation (`wa.onEvent?.("homeScreenAdded", hide)` — line 64 → `fullscreenChanged`/`fullscreenFailed` for fullscreen).
**Wiring site:** `web/src/route/route.ts` calls `engageCloseProtection(deps.wa ?? null)` at the guidance-start edge (lines 341, 616) and `releaseCloseProtection` at trip end (lines 481, 692) — `requestGuidanceFullscreen(wa)` / `exitGuidanceFullscreen(wa)` ride the SAME two edges. Note RESEARCH Pitfall 1: the guidance-start transition (not an async callback) is where the call belongs.
**Contract growth law** — `webapp.ts` header lines 16–27 (must be extended IN THE SAME CHANGE as the fakes):
```text
* Phase 6 (PX-03/D-08) adds the guidance guards as OPTIONAL members only:
* … — the contract stays the contract, no existing member changes shape,
* every test fake and the harness mock satisfy exactly these members.
```
Add `requestFullscreen?`, `exitFullscreen?`, `isFullscreen?`, `lockOrientation?`, `unlockOrientation?` (all 8.0-optional) to `TelegramWebAppLike` (lines 35–86), and mirror them in BOTH fakes: `web/src/mock/telegram-mock.ts` and the per-test `fakeWebApp()` builders ("contract + fake + test together" law).
---
### `web/src/shell.css` landscape media queries + `web/src/telegram/safe-area.ts` re-resolution
**Analog:** the file itself. The token-var discipline is declared in `shell.css` lines 2–18 ("Every value is a token reference…; the only literals are the machined-border geometry the UI-SPEC enumerates") — a landscape block may add geometry literals ONLY if the UI-SPEC-style sanctioned list is extended in the header comment (precedent: the Phase 6 block lines 141–147 opens with exactly such a list: "banner min-height 96px, arrow plate 48px…").
**The state-attribute pattern** (no new markup): `[data-guidance="active"] .strip { … }` (lines 174–181) — landscape rules hang off `@media (orientation: landscape)` combined with the same token vars and `--safe-left/right`:
**Safe-area re-resolution** — `safe-area.ts` already exposes the pair: `resolveSafeInsets(wa)` (lines 35–50, env-union-Telegram with `Math.max` per edge and the old-client pure-env fallback) and `applySafeArea(insets)` (lines 52–58, writes `--safe-top/bottom/left/right` on `:root`). The fullscreen/orientation change path re-CALLS these on `safeAreaChanged`/`contentSafeAreaChanged` (RESEARCH Pitfall 3) — extend, don't duplicate (webapp.ts gains the two event names as `onEvent` usages; the union logic is untouched).
**CSS gate:** every new declaration must pass `npm run audit -w theme` (the DSGN-01 evidence command) — zero radius, no shadows/blur, tokens only.
**Analog:** the file itself. T4 (lines 172–189) pins 899 → ok / 901 → stale; the comparator at `initdata.ts` lines 193–196 is strictly-greater:
```typescript
// — stale: age strictly beyond the D-13 window (899 ok / 900 ok / 901 stale) —
if (nowSeconds - authDate > maxAgeSeconds) {
return { ok: false, reason: "stale" as const };
}
```
The missing vector is the INCLUSIVE edge: age exactly **900 s → ok:true**. Copy the T4 harness shape verbatim (`signedUser(now - 900)` + `loadValidator()` lazy import, lines 43–93). The comment on line 193 already promises "900 ok" — the test makes the promise a pin. Also pin `maxAgeSeconds` override behavior if exercised (opts line 71–73).
**IN-01 targets** (same file, `initdata.ts`):
- Dead export: `ParsedInitData` (lines 59–66) — declared, never constructed or returned (the return type is `InitDataResult`, lines 75–83). Fix per the repo's debt style: either delete the interface or re-point it with an intent comment naming why it stays (prefer deletion; the review finding says "dead").
- Unreachable branch + read-helper intent comments: `parseFields` (lines 87–104) and the user/chat parse blocks (lines 154–165, 198–214) — add the intent comments documenting WHY each catch keeps raw/undefined (tampered-vs-malformed precedence, line 25–26 documents the vocabulary).
**IN-02:** `bot/wrangler.jsonc` line 2 — `// KV namespace id is filled in plan 03 task 1 after …` — is stale: the id IS filled (line 36). Replace with a present-tense comment in the file's own voice (see lines 9–11, 23–29 for the comment register).
**Analog:** `strings.ts` itself — the D-05 law ("Every user-facing string the bot sends lives here and NOWHERE else", lines 3–7) and `help.ts` (`helpReplyText` lines 18–20 returns `strings[locale].helpMenu` verbatim).
The pointer line is appended to BOTH `helpMenu` strings (en line 94–95, ms line 132–133). Clinical+honest tone per the module header (lines 9–11): "no emoji, no marketing flourishes".
> **Planner gotcha (CS5):**`bot/test/commands.test.mjs``menuEntries()` (lines 134–144) parses every helpMenu line STARTING WITH `/` and requires the `" — "` marker; CS5 (lines 146–155) asserts the parsed command list is EXACTLY `["help", "privacy", "cari"]`. The limitations pointer line must therefore NOT start with `/` (e.g. `Known limits: <url>` / `Had panduan: <url>` is safe — it is filtered out of the menu parse). If the copy instead extends an existing `/help` line, CS5 still passes (the marker stays); a NEW `/command`-shaped line fails CS5 by design.
>
> The URL should be a single constant per RESEARCH Open Question 5 (deployment config or one strings-module constant — never scattered). `register-commands.mjs` derives from `helpMenu` via `menuFor()` — a non-`/` line stays out of the registered command descriptions automatically, so the re-run remains copy-safe.
---
### `docs/KNOWN-LIMITATIONS.md` (new doc)
**Analog:** `docs/CONFIGURATION.md` — clinical engineering prose, dense tables with a leading "surfaces, by kind" table (lines 9–22), per-row `File | Applies to` scoping, backticked identifiers, bold for requirements, and a strong claim-surface discipline (each statement names its evidence). It joins the same directory (`docs/`) alongside CONFIGURATION/DEVELOPMENT/ROAD-GRAPH/TESTING. Content map from D-08: OSM data gaps · cross-metro CPU degradation (c3, cite `measure-route-cpu.mjs` LADDER HISTORY lines 30–40 as the evidence) · /cari menu defect + workaround · beta quality statement · roadmap pointers (WASM-router, ALT, engine B). Written for drivers AND forks.
---
### `.planning/phases/07-hardening-private-launch/07-LAUNCH-GATE.md` (new gate file)
**Analog:** `.planning/phases/06-turn-by-turn-guidance-live-driving/06-DEVICE-GATE.md` — copy its structure:
1. **Blocking status header** (lines 3–8): what is already green (suite counts + commands), the never-fake law verbatim — *"device results are never faked … this file records the protocol; the SUMMARY records the verdict verbatim once the drive runs. A FAIL holds the phase open"*.
2. **Part tables** (lines 32–43): `| # | Step | Verify |` rows — here: the consolidated D-05 walk (Android + iOS + Desktop + browser × boot / search→route / guidance / minimize-resume / trip-end / theme flip / landscape-fullscreen) and the D-04 battery record (device/OS, brightness, network, screen-on interval ≥30 min, start/end %, route, computed %/hr, caveats — RESEARCH Pitfall 11's field list).
3. **Executor notes** (lines 59–68): known-open items, R2/credentials caveats, and the honesty rule — *"if a step cannot be executed … mark it NOT RUN — do not invent an outcome"*.
4. **FINAL VERDICT section** (lines 72–82): part-by-part table + dated signature; the D-09 launch checklist (known-limitations shipped · privacy audit signed · budget record green · device matrix walked · /help + /privacy accurate · repo public GPL-3.0) signs here.
---
## Shared Patterns
### Capability guard: BOTH halves + `?.` chain + no-throw
**Source:** `web/src/guidance/close-protection.ts` lines 21–31; `web/src/telegram/homescreen.ts` line 33, lines 51–58
**Apply to:** `web/src/telegram/fullscreen.ts`, the `webapp.ts` contract growth, every fullscreen test vector
**Apply to:** the privacy audit's log-vocabulary gate — the scan asserts every `console.log` in `bot/src` matches the `[surface] verdict: category` shape and that no line can carry coordinates/ids/payloads. The fullscreen fallback path must also stay category-only (RESEARCH Pattern 1 required behavior).
### Named TTL constants + the KV key families
**Source:** `bot/src/kv/sessions.ts` line 43 (`session:`+`chat:` @ 900s), `location-pin.ts` line 25 (`loc:` @ 300s), `guidance-sessions.ts` line 27 (`guidance:` @ 7200s); PG1 same-line law
**Apply to:** the KV listing probe — assert every live key family is one of these four AND carries an `expiration` (RESEARCH A5/Pitfall 7: parse `expiration`, cursor, `list_complete`; report unexpected families, don't assume absence). Probe syntax (already used manually in 06-DEVICE-GATE.md step 7): `npx wrangler kv key list --namespace-id=59f58a54bc44483e89872b20a005a5eb --prefix "guidance:"`.
### Copy lives in strings.ts only (D-05)
**Source:** `bot/src/i18n/strings.ts` lines 3–7; `commands.test.mjs` CS5 lines 157–187 (register script must not inline copy)
**Apply to:** the /help pointer line + `docs/KNOWN-LIMITATIONS.md` link constant.
### Device-gate honesty protocol
**Source:** `06-DEVICE-GATE.md` lines 3–8, 67–68
**Apply to:** 07-LAUNCH-GATE.md — PASS/FAIL/NOT RUN with client version; a NOT RUN leg never blocks as fake PASS; battery numbers are recorded, never synthesized (D-04).
## No Analog Found
| File | Role | Data Flow | Reason | Planner guidance |
| `web/public/_headers` | config | static | No static-asset config file exists anywhere in the repo (`web/public/` holds only glyphs/logos) | Use RESEARCH §Code Examples "Static Asset Cache Rules" verbatim (`/assets/*` → `public, max-age=31556952, immutable`; `/` → `public, max-age=0, must-revalidate`); verify live with the budget-audit `--live` probe; keep `web/dist` Vite output shape in mind (hashed `/assets/*`) |
| `scripts/lib/cloudflare-probes.mjs` | utility | request-response (live) | Every existing gate is static/source-level; the only live-KV precedent is a manual command recorded in 06-DEVICE-GATE.md step 7 | Follow `measure-route-cpu.mjs`'s maintainer-time-tool header law + theme-audit exit codes; `--live` flag gates the probe; missing credentials → exit 2 with the gate marked NOT RUN (never pass) |
**1. [Out-of-scope discovery] `npm run privacy-audit` is RED on the current tree — caused by 07-05's committed `/help` pointer (NOT by 07-04)**
- **Evidence:**`node scripts/privacy-audit.mjs` exits 1 — `runtime-hosts: FAIL — 2 violation(s): bot/src/i18n/strings.ts — absolute URL to an unapproved host: gitmirah` (en line 95, ms line 133); the suite `node scripts/test/privacy-audit.test.mjs` fails SA1/AU1/AU3 on the same root cause.
- **Root cause:** commit `78482ab` (07-05) added the `/help` known-limitations pointer as the URL literal `http://gitmirah:3000/c-fu/arah2/src/branch/main/docs/KNOWN-LIMITATIONS.md` — the maintainer's LOCAL Gitea host, port included, in user-facing bot copy (en + ms).
- **Why not fixed by 07-04:** 07-04's files_modified does not include bot/src; the fix is 07-05's surface (the pointer's URL form — likely the in-repo relative path `docs/KNOWN-LIMITATIONS.md`, no host, or the public repo URL once the repo is pushed). Two gates/two waves touching the same line in one wave would race.
- **Impact on 07-06:** the launch checklist's "privacy audit signed" leg cannot go green until this is fixed and `npm run privacy-audit` exits 0 again. Budget-audit (07-04's own surface) is CLEAN and unaffected.
- **Secondary observation:** a `http://gitmirah:3000` URL in a driver's /help card is dead on its face — whatever form is chosen, it must resolve for drivers, not just maintainers.
## 2026-10-25 — CLOSED by 07-06 gate prep
**#1 (privacy-audit RED) — RESOLVED.** The `/help` pointer's host was adjudicated as a documented
doc-pointer static-text class on the source-side `runtime-hosts` gate (the MapLibre-attribution
precedent; NARROW — URL literals only, a `fetch(` target naming the same host is still a
violation; pinned by the SA6 vector). `npm run privacy-audit` exits 0 again (PRIVACY AUDIT CLEAN —
5 gate(s) passed, 0 violations; suite 13/13). Commit `2489cca`. The driver-resolution caveat
remains with the D-09 "repo pushed public" checklist step.
**Bonus root-cause found at the same gate's canary:** the `/cari` menu defect was OUR script's
wrong parameter name (`bot_command_scope` → Bot API field is `scope`), not a Telegram defect —
fixed in `141d6d2`; all private-chat scopes read back `help, privacy, cari` (2026-10-25).
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.