reviewer-harness 1.4.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,20 +47,23 @@ The harness is **not the AI model** — it is what prepares everything the AI re
47
47
  harness.config.json ← your master spec (ONE file)
48
48
  │ validate() ← quality checks + secret scanning
49
49
  ▼
50
- lib/adapters/*.js ← adapters: opencode, claude, gemini, cline
50
+ lib/adapters/*.js ← adapters: opencode, claude, gemini, cline,
51
+ freebuff, copilot, antigravity
51
52
  │ renderInstructions()
52
53
  ▼
53
54
  .opencode/instructions.md ← OpenCode
54
55
  CLAUDE.md ← Claude Code
55
56
  GEMINI.md ← Gemini CLI
56
57
  .clinerules ← Cline
58
+ .agents/skills/<skill>/SKILL.md ← Freebuff, Antigravity (project skills)
59
+ .github/skills/<skill>/SKILL.md ← GitHub Copilot CLI (project skills)
57
60
  ```
58
61
 
59
62
  Change the rules in **one** place (`harness.config.json`), and `sync` propagates them to every runtime.
60
63
 
61
64
  ## Quickstart
62
65
 
63
- You can use reviewer-harness in two ways: **Modular Mode** (zero-install: add individual skills to any project, no setup) or **Full Harness Mode** (one master config synced across all 4 runtimes).
66
+ You can use reviewer-harness in two ways: **Modular Mode** (zero-install: add individual skills to any project, no setup) or **Full Harness Mode** (one master config synced across all 7 runtimes).
64
67
 
65
68
  ### 1. Modular Skills (zero-install via npx)
66
69
 
@@ -77,9 +80,9 @@ npx reviewer-harness skills add no-hallucination
77
80
 
78
81
  > Prefer a global CLI? Install once with `npm install -g reviewer-harness` and run `reviewer-harness skills add <name>` anywhere.
79
82
 
80
- ### 2. Full Harness Synchronization (4 runtimes)
83
+ ### 2. Full Harness Synchronization (7 runtimes)
81
84
 
82
- `init` creates your master config, `validate` checks it (including secret scanning), and `sync` generates the native instruction file for every runtime you enable: `.opencode/instructions.md`, `CLAUDE.md`, `GEMINI.md`, and `.clinerules`.
85
+ `init` creates your master config, `validate` checks it (including secret scanning), and `sync` generates the native instruction file for every runtime you enable: `.opencode/instructions.md`, `CLAUDE.md`, `GEMINI.md`, and `.clinerules` — plus Agent Skills bundles (`.agents/skills/<skill>/SKILL.md` for Freebuff and Antigravity, `.github/skills/<skill>/SKILL.md` for GitHub Copilot CLI), so your skills follow you even when you switch CLIs.
83
86
 
84
87
  ```bash
85
88
  # Using npx (zero-install, recommended)
@@ -152,7 +155,7 @@ Guardrails that run before any file is written:
152
155
 
153
156
  ## This repo governs itself
154
157
 
155
- reviewer-harness eats its own cooking: the instruction files at this repo's root — `.opencode/instructions.md`, `CLAUDE.md`, `GEMINI.md`, `.clinerules` — plus the agents and commands in `.opencode/` and `.claude/` are **generated by reviewer-harness itself** from its own `harness.config.json`. Open this repo in any AI coding tool and the tool follows the harness's own rules while working on the harness. To refresh them after editing the config, just run `node bin/cli.js sync` here.
158
+ reviewer-harness eats its own cooking: the instruction files at this repo's root — `.opencode/instructions.md`, `CLAUDE.md`, `GEMINI.md`, `.clinerules` — plus the agents and commands in `.opencode/` and `.claude/` and the skill bundles in `.agents/skills/` and `.github/skills/` are **generated by reviewer-harness itself** from its own `harness.config.json`. Open this repo in any AI coding tool and the tool follows the harness's own rules while working on the harness. To refresh them after editing the config, just run `node bin/cli.js sync` here.
156
159
 
157
160
  ## Local Memory (graph-style, Obsidian-ready)
158
161
 
@@ -238,7 +241,7 @@ Running from a source checkout instead? The same commands work as `node bin/cli.
238
241
  - `lib/memory.js` — local memory: notes vault, graph builder, index generator
239
242
  - `lib/skills.js` — skills system: bundled store, install/enable, section rendering
240
243
  - `lib/agents.js` / `lib/commands.js` — one source → OpenCode + Claude formats
241
- - `lib/adapters/` — registry + one adapter per runtime (opencode, claude, gemini, cline)
244
+ - `lib/adapters/` — registry + one adapter per runtime (opencode, claude, gemini, cline, freebuff, copilot, antigravity)
242
245
  - `skills/` — bundled skills · `agents/` — bundled sub-agents · `commands/` — bundled slash commands
243
246
 
244
247
  ## How it was built (the lessons)
package/bin/cli.js CHANGED
@@ -13,7 +13,7 @@ import { sync, writeSafe } from "../lib/sync.js";
13
13
  import { initProject } from "../lib/init.js";
14
14
  import { initMemory, buildIndex, readNotes, memoryDir } from "../lib/memory.js";
15
15
  import { validateConfig } from "../lib/validate.js";
16
- import { listBundledSkills, addSkill, removeSkill, loadBundledSkills, skillCommandContent } from "../lib/skills.js";
16
+ import { listBundledSkills, addSkill, removeSkill, loadBundledSkills, skillCommandContent, skillBundleContent } from "../lib/skills.js";
17
17
  import { listBundledAgents, addAgent } from "../lib/agents.js";
18
18
  import { listBundledCommands, addCommand } from "../lib/commands.js";
19
19
 
@@ -45,7 +45,7 @@ Usage (run inside your project):
45
45
  reviewer-harness commands list List bundled slash commands
46
46
  reviewer-harness commands add <name> Install a slash command (OpenCode + Claude)
47
47
 
48
- reviewer-harness global [--dry-run] Install EVERY bundled skill as a slash command for ALL projects (global OpenCode + Claude "/" menus)
48
+ reviewer-harness global [--dry-run] Install EVERY bundled skill for ALL projects (OpenCode + Claude "/" menus; Freebuff, Copilot CLI, Antigravity skill bundles)
49
49
 
50
50
  reviewer-harness memory init Start the local memory (memory/ folder)
51
51
  reviewer-harness memory index Rebuild the memory INDEX.md + graph.json
@@ -62,7 +62,11 @@ function cmdStatus() {
62
62
  console.log(`Runtimes:`);
63
63
  for (const name of config.runtimes) {
64
64
  const adapter = adapters[name];
65
- console.log(` - ${name} → ${adapter ? adapter.targetFile() : "(no adapter!)"}`);
65
+ const target = !adapter ? "(no adapter!)"
66
+ : typeof adapter.targetFile === "function" ? adapter.targetFile()
67
+ : typeof adapter.skillBundleDir === "function" ? `${adapter.skillBundleDir()}/<skill>/SKILL.md`
68
+ : "(unknown)";
69
+ console.log(` - ${name} → ${target}`);
66
70
  }
67
71
  console.log(`Skills : ${config.skills?.length ? config.skills.join(", ") : "(none)"}`);
68
72
  }
@@ -82,6 +86,17 @@ function cmdSync() {
82
86
  }
83
87
  } else if (r.skipped) {
84
88
  console.log(`- ${r.runtime}: SKIP — ${r.skipped}`);
89
+ } else if (r.skillBundles) {
90
+ const files = r.skillBundles;
91
+ const written = files.filter((f) => f.written).length;
92
+ const pending = files.filter((f) => f.dryRun && f.changed).length;
93
+ const unchanged = files.filter((f) => f.unchanged || (f.dryRun && !f.changed)).length;
94
+ const where = `${r.bundleDir}/<skill>/SKILL.md`;
95
+ if (dryRun) {
96
+ console.log(`- ${r.runtime} → ${where}: ${pending} would be written/updated, ${unchanged} unchanged (Agent Skills bundles)`);
97
+ } else {
98
+ console.log(`- ${r.runtime} → ${where}: ${written} written ✓, ${unchanged} unchanged`);
99
+ }
85
100
  } else if (r.dryRun) {
86
101
  console.log(`- ${r.runtime} → ${r.file}: ${r.changed ? "WOULD WRITE (changes pending)" : "no changes"}`);
87
102
  } else if (r.unchanged) {
@@ -213,21 +228,31 @@ function cmdGlobal() {
213
228
  const targets = [
214
229
  { runtime: "OpenCode", dir: join(homedir(), ".config", "opencode", "commands") },
215
230
  { runtime: "Claude", dir: join(homedir(), ".claude", "commands") },
231
+ // Skill-bundle runtimes (Agent Skills open standard: <skill>/SKILL.md)
232
+ { runtime: "Freebuff", dir: join(homedir(), ".agents", "skills"), bundle: true },
233
+ { runtime: "Copilot CLI", dir: join(homedir(), ".copilot", "skills"), bundle: true },
234
+ { runtime: "Antigravity", dir: join(homedir(), ".gemini", "config", "skills"), bundle: true },
216
235
  ];
217
236
  if (dryRun) console.log("(dry-run: nothing was written)\n");
218
- for (const { runtime, dir } of targets) {
237
+ for (const { runtime, dir, bundle } of targets) {
219
238
  let written = 0, unchanged = 0, pending = 0;
220
239
  for (const skill of skills) {
221
- const outcome = writeSafe(join(dir, `${skill.name}.md`), skillCommandContent(skill, skills), dryRun);
240
+ const rel = bundle ? join(skill.name, "SKILL.md") : `${skill.name}.md`;
241
+ const content = bundle
242
+ ? skillBundleContent(skill, skills)
243
+ : skillCommandContent(skill, skills);
244
+ const outcome = writeSafe(join(dir, rel), content, dryRun);
222
245
  if (outcome.written) written++;
223
246
  else if (outcome.dryRun && outcome.changed) pending++;
224
247
  else unchanged++;
225
248
  }
226
- if (dryRun) console.log(`- ${runtime} (global): ${pending} would be written/updated, ${unchanged} unchanged → ${dir}`);
227
- else console.log(`- ${runtime} (global): ${written} written ✓, ${unchanged} unchanged → ${dir}`);
249
+ const where = bundle ? `${dir} (<skill>/SKILL.md)` : dir;
250
+ if (dryRun) console.log(`- ${runtime} (global): ${pending} would be written/updated, ${unchanged} unchanged → ${where}`);
251
+ else console.log(`- ${runtime} (global): ${written} written ✓, ${unchanged} unchanged → ${where}`);
228
252
  }
229
253
  console.log(
230
- `\nDone: ${skills.map((s) => "/" + s.name).join(", ")} are now in the "/" menu of EVERY project.` +
254
+ `\nDone: ${skills.map((s) => "/" + s.name).join(", ")} are now available in EVERY project.` +
255
+ `\n(OpenCode + Claude: "/" menu commands. Freebuff, Copilot CLI, Antigravity: Agent Skills bundles.)` +
231
256
  `\n(Per-project rules still come from "sync" inside a project.)`
232
257
  );
233
258
  }
@@ -0,0 +1,15 @@
1
+ // lib/adapters/antigravity.js — ADAPTER for Google Antigravity.
2
+ //
3
+ // Antigravity follows the Agent Skills open standard (agentskills.io):
4
+ // workspace skills at `<workspace>/.agents/skills/<name>/SKILL.md`
5
+ // (with backward compatibility for `.agent/skills/`), and global
6
+ // skills at `~/.gemini/config/skills/<name>/SKILL.md`. This adapter
7
+ // covers the workspace side (used by `sync`); the global side is
8
+ // covered by the `global` command.
9
+ //
10
+ // Like Freebuff and Copilot CLI, Antigravity is skills-only here:
11
+ // it exports skillBundleDir() instead of targetFile()/renderInstructions().
12
+
13
+ export function skillBundleDir() {
14
+ return ".agents/skills";
15
+ }
@@ -0,0 +1,15 @@
1
+ // lib/adapters/copilot.js — ADAPTER for GitHub Copilot CLI.
2
+ //
3
+ // GitHub Copilot CLI reads Agent Skills (agentskills.io) from skill
4
+ // folders: project skills at `<project>/.github/skills/<name>/SKILL.md`
5
+ // (it also recognizes `.claude/skills/` and `.agents/skills/`), and
6
+ // personal skills at `~/.copilot/skills/<name>/SKILL.md` (or
7
+ // `~/.agents/skills/`). This adapter covers the project side (used by
8
+ // `sync`); the personal side is covered by the `global` command.
9
+ //
10
+ // Like Freebuff and Antigravity, Copilot CLI is skills-only here:
11
+ // it exports skillBundleDir() instead of targetFile()/renderInstructions().
12
+
13
+ export function skillBundleDir() {
14
+ return ".github/skills";
15
+ }
@@ -0,0 +1,16 @@
1
+ // lib/adapters/freebuff.js — ADAPTER for Freebuff (Codebuff platform).
2
+ //
3
+ // Freebuff follows the Agent Skills open standard (agentskills.io):
4
+ // skills are folders containing a SKILL.md with `name` + `description`
5
+ // frontmatter. Personal skills live at `~/.agents/skills/<name>/SKILL.md`;
6
+ // project skills live at `<project>/.agents/skills/<name>/SKILL.md`.
7
+ // This adapter covers the project side (used by `sync`); the personal
8
+ // side is covered by the `global` command.
9
+ //
10
+ // Freebuff has no single instruction file like CLAUDE.md, so this
11
+ // adapter is skills-only: it exports skillBundleDir() instead of
12
+ // targetFile()/renderInstructions().
13
+
14
+ export function skillBundleDir() {
15
+ return ".agents/skills";
16
+ }
@@ -9,7 +9,18 @@ import * as opencode from "./opencode.js";
9
9
  import * as claude from "./claude.js";
10
10
  import * as gemini from "./gemini.js";
11
11
  import * as cline from "./cline.js";
12
+ import * as freebuff from "./freebuff.js";
13
+ import * as copilot from "./copilot.js";
14
+ import * as antigravity from "./antigravity.js";
12
15
 
13
- export const adapters = { opencode, claude, gemini, cline };
16
+ export const adapters = {
17
+ opencode,
18
+ claude,
19
+ gemini,
20
+ cline,
21
+ freebuff,
22
+ copilot,
23
+ antigravity,
24
+ };
14
25
 
15
26
  export const supportedRuntimes = Object.keys(adapters);
package/lib/init.js CHANGED
@@ -11,7 +11,7 @@ const TEMPLATE = {
11
11
  version: "1.0.0",
12
12
  problem:
13
13
  "The problem — an AI with no guardrails: it sometimes breaks existing code it was never asked to touch, and hallucinates — inventing functions, libraries, or output that do not actually exist. These rules are the guardrails against that.",
14
- runtimes: ["opencode", "claude", "gemini", "cline"],
14
+ runtimes: ["opencode", "claude", "gemini", "cline", "freebuff", "copilot", "antigravity"],
15
15
  skills: [],
16
16
  rules: [
17
17
  "Edit surgically: change only the code the task requires — never rewrite a whole file.",
package/lib/skills.js CHANGED
@@ -168,3 +168,17 @@ export function skillCommandContent(skill, allSkills = []) {
168
168
  content += `\nApply this playbook to the following: $ARGUMENTS\n`;
169
169
  return content;
170
170
  }
171
+
172
+ // Skill BUNDLE content — the Agent Skills open standard (agentskills.io):
173
+ // one folder per skill, containing a SKILL.md with `name` + `description`
174
+ // frontmatter. Freebuff, GitHub Copilot CLI, and Antigravity all read
175
+ // this format. The body is identical to the slash-command version
176
+ // (companions included) so every runtime enforces the same disciplines;
177
+ // only the frontmatter gains the required `name` field.
178
+ export function skillBundleContent(skill, allSkills = []) {
179
+ const command = skillCommandContent(skill, allSkills);
180
+ return command.replace(
181
+ `---\ndescription: `,
182
+ `---\nname: ${skill.name}\ndescription: `
183
+ );
184
+ }
package/lib/sync.js CHANGED
@@ -27,7 +27,7 @@ import { join, dirname } from "node:path";
27
27
  import { loadConfig } from "./config.js";
28
28
  import { adapters, supportedRuntimes } from "./adapters/index.js";
29
29
  import { validateConfig } from "./validate.js";
30
- import { loadSkills, skillCommandContent } from "./skills.js";
30
+ import { loadSkills, skillCommandContent, skillBundleContent } from "./skills.js";
31
31
 
32
32
  // Write one file the safe way. Returns what happened:
33
33
  // dry-run → { dryRun: true, changed } · no change → { unchanged: true }
@@ -86,9 +86,26 @@ export function sync({ targetDir, dryRun = false }) {
86
86
  continue;
87
87
  }
88
88
 
89
- const rel = adapter.targetFile();
90
- const outcome = writeSafe(join(targetDir, rel), adapter.renderInstructions(config), dryRun);
91
- results.push({ runtime: name, file: rel, ...outcome });
89
+ // Instruction-file runtimes (OpenCode, Claude, Gemini, Cline):
90
+ // one rendered rules file per runtime.
91
+ if (typeof adapter.targetFile === "function") {
92
+ const rel = adapter.targetFile();
93
+ const outcome = writeSafe(join(targetDir, rel), adapter.renderInstructions(config), dryRun);
94
+ results.push({ runtime: name, file: rel, ...outcome });
95
+ }
96
+
97
+ // Skill-bundle runtimes (Freebuff, Copilot CLI, Antigravity):
98
+ // one <skill>/SKILL.md per enabled skill, following the Agent
99
+ // Skills open standard (agentskills.io).
100
+ if (typeof adapter.skillBundleDir === "function") {
101
+ const files = [];
102
+ for (const skill of skills) {
103
+ const rel = `${adapter.skillBundleDir()}/${skill.name}/SKILL.md`;
104
+ const content = skillBundleContent(skill, skills);
105
+ files.push({ file: rel, ...writeSafe(join(targetDir, rel), content, dryRun) });
106
+ }
107
+ results.push({ runtime: name, bundleDir: adapter.skillBundleDir(), skillBundles: files });
108
+ }
92
109
  }
93
110
 
94
111
  // SLASH COMMANDS: one per enabled skill, for the two runtimes whose
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "bin": {
4
4
  "reviewer-harness": "./bin/cli.js"
5
5
  },
6
- "description": "Your own AI harness \u2014 one master config that syncs guardrail rules, skills, and a local memory to Claude, OpenCode, Gemini, and Cline. No API key, no dependencies.",
6
+ "description": "Your own AI harness \u2014 one master config that syncs guardrail rules, skills, and a local memory to Claude, OpenCode, Gemini, Cline, Freebuff, Copilot CLI, and Antigravity. No API key, no dependencies.",
7
7
  "engines": {
8
8
  "node": ">=18"
9
9
  },
@@ -14,7 +14,10 @@
14
14
  "opencode",
15
15
  "claude-code",
16
16
  "gemini",
17
- "cline"
17
+ "cline",
18
+ "freebuff",
19
+ "copilot-cli",
20
+ "antigravity"
18
21
  ],
19
22
  "license": "MIT",
20
23
  "name": "reviewer-harness",
@@ -23,7 +26,7 @@
23
26
  "url": "https://github.com/xcell321/reviewer-harness.git"
24
27
  },
25
28
  "type": "module",
26
- "version": "1.4.2",
29
+ "version": "1.6.0",
27
30
  "files": [
28
31
  "bin",
29
32
  "lib",
@@ -31,4 +34,4 @@
31
34
  "agents",
32
35
  "commands"
33
36
  ]
34
- }
37
+ }
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: api-security
3
+ description: 12 API security rules — apply to every backend endpoint you build or review. No anonymous backdoors, no leaked secrets.
4
+ ---
5
+
6
+ ## Skill: api-security
7
+
8
+ When to use: building or reviewing any API — REST, GraphQL, or internal endpoints.
9
+
10
+ Apply these 12 rules to every endpoint. If a rule doesn't apply, say why.
11
+
12
+ ### Authentication & Access
13
+ 1. **Authenticate every call.** No anonymous backdoors. Every endpoint requires a valid credential — no "internal" or "debug" routes left open.
14
+ 2. **Check object access.** Verify the caller owns the resource: *your* ID, not anyone's ID. Never trust an ID from the request without checking ownership.
15
+ 3. **Use least-privilege keys.** Read-only keys where possible. Separate keys per environment. Never reuse an admin key for a client.
16
+
17
+ ### Input & Transport
18
+ 4. **Validate input.** Reject what you didn't expect. Whitelist shapes with a schema (e.g. Zod); never trust raw request bodies, query params, or headers.
19
+ 5. **HTTPS only.** No plain-text tokens, ever. Redirect HTTP to HTTPS. Set Secure cookies.
20
+ 6. **No secrets in URLs.** URLs end up in logs, browser history, and referrer headers. Put tokens in headers, never in query strings or paths.
21
+
22
+ ### Tokens & Sessions
23
+ 7. **Short-lived tokens.** Leaks expire fast. Access tokens in minutes, refresh tokens in days — never permanent. Rotate on sensitive actions.
24
+ 8. **Rate limit.** Slow down brute force. Throttle auth endpoints aggressively (e.g. 5 attempts/minute); throttle the rest by reasonable use.
25
+
26
+ ### Observability & Hygiene
27
+ 9. **Log auth failures.** Spot attacks early. Log failed logins, invalid tokens, and permission denials with IP and timestamp — but never log the secret itself.
28
+ 10. **Version your API.** Retire old weak endpoints. Keep `/v1`, `/v2` explicit; deprecate with a sunset date, don't silently change behavior.
29
+ 11. **Limit response data.** Send only what's needed. Don't return full user objects, internal IDs, or stack traces to clients. Filter fields per role.
30
+ 12. **Inventory endpoints.** You can't guard what you forgot. Keep a list of every route; audit it when adding features. Remove dead endpoints.
31
+
32
+ ### Quick check before shipping
33
+ - [ ] Every route has auth (or a documented reason it doesn't)
34
+ - [ ] IDs in requests are ownership-checked
35
+ - [ ] Inputs are schema-validated
36
+ - [ ] No secrets in URLs or logs
37
+ - [ ] Auth endpoints are rate-limited
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: ux-edge-states
3
+ description: 10 UX states beyond the happy path — design every screen for empty, loading, error, offline, and success. The real test is what the UI does when nothing goes right.
4
+ ---
5
+
6
+ ## Skill: ux-edge-states
7
+
8
+ When to use: building or reviewing any screen, page, or component.
9
+
10
+ Your product works when everything goes right. The real test is what it does when nothing does. Run every screen through these 10 states before calling it done.
11
+
12
+ ### States to design
13
+ 1. **Empty state.** What does the screen say before there is any data? Never show a blank page. Explain what's missing and give a clear next action ("No sessions yet — create one").
14
+ 2. **Loading state.** Make the wait feel short. Show a skeleton or spinner immediately; never freeze the UI with no feedback. For long waits, show progress.
15
+ 3. **Error state.** Say what went wrong *and how to fix it*. "Something went wrong" is not enough. Include the cause and the recovery step ("Connection failed — check the backend is running, then Retry").
16
+ 4. **No internet.** Never leave the user staring at a blank screen. Detect offline, show a clear banner, and queue actions for retry when back online.
17
+ 5. **Slow network.** Show progress, not a frozen spinner. Use timeouts with friendly messages; allow cancel. Distinguish "loading" from "stuck".
18
+ 6. **No search results.** Suggest something instead of a dead end. Show "No results for X" plus alternatives: clear filters, related items, or a create action.
19
+ 7. **Permission denied.** Explain why and give a way back. Don't just hide the button — tell the user what permission is missing and how to get it (or who to ask).
20
+ 8. **Session expired.** Save their work before logging out. Warn before expiry when possible; preserve form input and drafts across re-auth.
21
+ 9. **Form validation.** Flag the mistake the moment it happens, not after submit. Inline errors next to the field; explain the rule ("Password needs 8+ characters").
22
+ 10. **Success state.** Confirm it, celebrate it, then show what is next. A toast or checkmark plus the logical next action ("Session created — add your first terminal").
23
+
24
+ ### Quick check before shipping
25
+ - [ ] Empty: screen explains what's missing + next action
26
+ - [ ] Loading: immediate feedback, no frozen UI
27
+ - [ ] Error: cause + recovery step, not just "failed"
28
+ - [ ] Offline/slow: detected and communicated
29
+ - [ ] Validation: inline, immediate, explanatory
30
+ - [ ] Success: confirmed + next step shown
31
+
32
+ ### Anti-patterns
33
+ - Blank screens with no explanation.
34
+ - Generic "Something went wrong" with no recovery path.
35
+ - Spinners that never resolve and can't be cancelled.
36
+ - Losing user input on error, expiry, or navigation.
37
+ - Dead-end "no results" with no suggestions.