{
  "error": {
    "message": "{\"error\":{\"message\":\"{\\n  \\\"error\\\": {\\n    \\\"code\\\": 403,\\n    \\\"message\\\": \\\"Your API key was reported as leaked. Please use another API key.\\\",\\n    \\\"status\\\": \\\"PERMISSION_DENIED\\\"\\n  }\\n}\\n\",\"code\":403,\"status\":\"Forbidden\"}}",
    "stack": "ApiError: {\"error\":{\"message\":\"{\\n  \\\"error\\\": {\\n    \\\"code\\\": 403,\\n    \\\"message\\\": \\\"Your API key was reported as leaked. Please use another API key.\\\",\\n    \\\"status\\\": \\\"PERMISSION_DENIED\\\"\\n  }\\n}\\n\",\"code\":403,\"status\":\"Forbidden\"}}\n    at throwErrorIfNotOK (file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-67TFD6HC.js:36178:24)\n    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)\n    at async file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-67TFD6HC.js:35941:7\n    at async Models.generateContentStream (file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-67TFD6HC.js:37037:16)\n    at async file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-UQGLVPZQ.js:278827:19\n    at async file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-UQGLVPZQ.js:255732:23\n    at async retryWithBackoff (file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-UQGLVPZQ.js:275704:23)\n    at async GeminiChat.makeApiCallAndProcessStream (file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-UQGLVPZQ.js:311287:28)\n    at async GeminiChat.streamWithRetries (file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-UQGLVPZQ.js:311125:29)\n    at async Turn.run (file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-UQGLVPZQ.js:311618:24)"
  },
  "context": [
    {
      "role": "user",
      "parts": [
        {
          "text": "<session_context>\nThis is the Gemini CLI. We are setting up the context for our chat.\nToday's date is Wednesday, June 24, 2026 (formatted according to the user's locale).\nMy operating system is: linux\nThe project's temporary directory is: /home/claude/.gemini/tmp/pr-review-ckxs96\n- **Workspace Directories:**\n  - /tmp/pr-review-CKxs96\n- **Directory Structure:**\n\nShowing up to 200 items (files + folders).\n\n/tmp/pr-review-CKxs96/\n\n\n\n</session_context>"
        }
      ]
    },
    {
      "role": "user",
      "parts": [
        {
          "text": "You are a dedicated code reviewer for this repository. You are deeply familiar with its conventions and codebase.\n\n## Project Conventions (from CLAUDE.md)\n## gstack (REQUIRED — global install)\n\n**Before doing ANY work, verify gstack is installed:**\n\n```bash\ntest -d ~/.claude/skills/gstack/bin && echo \"GSTACK_OK\" || echo \"GSTACK_MISSING\"\n```\n\nIf GSTACK_MISSING: STOP. Do not proceed. Tell the user:\n\n> gstack is required for all AI-assisted work in this repo.\n> Install it:\n> ```bash\n> git clone --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack\n> cd ~/.claude/skills/gstack && ./setup --team\n> ```\n> Then restart your AI coding tool.\n\nDo not skip skills, ignore gstack errors, or work around missing gstack.\n\nUsing gstack skills: After install, skills like /qa, /ship, /review, /investigate,\nand /browse are available. Use /browse for all web browsing.\nUse ~/.claude/skills/gstack/... for gstack file paths (the global path).\n\n\n## Review Task\n\nPR #27: \"fix(auto-review): harden watch state loading\"\n\nDescription:\n## Summary\n- Validate auto-review watch-state JSON before loading or saving\n- Keep the watch loop alive by resetting corrupt state to an empty map\n- Replace local auto-review `any` usage with concrete config/PR types\n\n## Verification\n- `node ../../node_modules/vitest/dist/cli.js --run test/config.test.ts`\n- `node ../../node_modules/vitest/dist/cli.js --run`\n- `npm run check`\n\n\nReview the diff below. Provide your response as a JSON object with these keys:\n- \"summary\": a concise markdown summary. Tag issues with severity: [blocking] for must-fix, [suggestion] for improvements, [nitpick] for style.\n- \"comments\": array of { \"path\", \"line\", \"body\" } — inline review comments with severity tags\n- \"verdict\": \"approve\" (no blocking issues) or \"request_changes\" (blocking issues found)\n- \"workflow_suggestions\": array of strings — suggestions for improving the development workflow (e.g., \"Consider splitting this PR\", \"Add a pre-commit hook for X\")\n\nFocus on meaningful issues: bugs, security, performance, correctness. Skip trivial style nits.\nReturn ONLY valid JSON, no markdown fences.\n\nDiff:\n```diff\nFile: packages/gbharg-auto-review/src/cli.ts (modified, +14/-3)\n@@ -6,6 +6,7 @@ import { parsePlanContext } from \"./plan-context.ts\";\n import { collapseLatestReviews, evaluateMergePolicy } from \"./policy.ts\";\n import { dispatchCloudReviews, resolveDispatchMode } from \"./runner.ts\";\n import { evaluateReviewScope } from \"./scope.ts\";\n+import type { PullRequestMetadata, WatchState } from \"./types.ts\";\n \n type Command = \"check\" | \"dispatch\" | \"watch\";\n \n@@ -109,8 +110,8 @@ async function runDispatch(\n async function runWatch(repo: string, config: ReturnType<typeof loadConfig>, cwd: string): Promise<void> {\n \tconst intervalMs = config.pollIntervalMs ?? 30_000;\n \tfor (;;) {\n-\t\tconst state = loadWatchState();\n-\t\tlet prs: any;\n+\t\tconst state = loadWatchStateForWatchLoop();\n+\t\tlet prs: PullRequestMetadata[];\n \t\ttry {\n \t\t\tprs = await listOpenReviewablePullRequests(repo, cwd);\n \t\t} catch (error) {\n@@ -119,7 +120,7 @@ async function runWatch(repo: string, config: ReturnType<typeof loadConfig>, cwd\n \t\t\tawait new Promise((resolve) => setTimeout(resolve, intervalMs));\n \t\t\tcontinue;\n \t\t}\n-\t\tconst activePrNumbers = new Set(prs.map((pr: any) => String(pr.number)));\n+\t\tconst activePrNumbers = new Set(prs.map((pr) => String(pr.number)));\n \t\tconst nextSeenHeads = Object.fromEntries(\n \t\t\tObject.entries(state.pullRequests).filter(([prNumber]) => activePrNumbers.has(prNumber)),\n \t\t);\n@@ -146,6 +147,16 @@ async function runWatch(repo: string, config: ReturnType<typeof loadConfig>, cwd\n \t}\n }\n \n+function loadWatchStateForWatchLoop(): WatchState {\n+\ttry {\n+\t\treturn loadWatchState();\n+\t} catch (error) {\n+\t\tconst message = error instanceof Error ? error.message : String(error);\n+\t\tconsole.error(`watch: failed to load state; resetting to empty state: ${message}`);\n+\t\treturn { pullRequests: {} };\n+\t}\n+}\n+\n function parseCommandArgs(args: string[]): { command: Command; flagArgs: string[] } {\n \tconst first = args[0];\n \tif (!first || first.startsWith(\"--\")) {\n\nFile: packages/gbharg-auto-review/src/config.ts (modified, +18/-1)\n@@ -52,10 +52,19 @@ export function loadConfig(cwd: string, explicitPath?: string): ReviewCloudConfi\n \n export function loadWatchState(path = DEFAULT_STATE_PATH): WatchState {\n \tif (!existsSync(path)) return { pullRequests: {} };\n-\treturn JSON.parse(readFileSync(path, \"utf-8\")) as WatchState;\n+\tlet parsed: unknown;\n+\ttry {\n+\t\tparsed = JSON.parse(readFileSync(path, \"utf-8\")) as unknown;\n+\t} catch (error) {\n+\t\tconst message = error instanceof Error ? error.message : String(error);\n+\t\tthrow new Error(`Auto-review watch state at ${path} is not valid JSON: ${message}`);\n+\t}\n+\tassertWatchState(parsed, path);\n+\treturn parsed;\n }\n \n export function saveWatchState(state: WatchState, path = DEFAULT_STATE_PATH): void {\n+\tassertWatchState(state, path);\n \tmkdirSync(dirname(path), { recursive: true });\n \twriteFileSync(path, `${JSON.stringify(state, null, 2)}\\n`, \"utf-8\");\n }\n@@ -141,6 +150,14 @@ function assertCommandRecord(value: unknown, name: string): void {\n \t}\n }\n \n+function assertWatchState(value: unknown, path: string): asserts value is WatchState {\n+\tif (!isRecord(value) || !isStringRecord(value.pullRequests)) {\n+\t\tthrow new Error(\n+\t\t\t`Auto-review watch state at ${path} must be an object with a pullRequests property mapping PR numbers to string SHAs`,\n+\t\t);\n+\t}\n+}\n+\n function isRecord(value: unknown): value is Record<string, unknown> {\n \treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n }\n\nFile: packages/gbharg-auto-review/src/index.ts (modified, +5/-3)\n@@ -8,6 +8,8 @@ import { inferGitHubRepo } from \"./repo.ts\";\n import { dispatchCloudReviews } from \"./runner.ts\";\n import { evaluateReviewScope } from \"./scope.ts\";\n \n+type AutoReviewConfig = ReturnType<typeof loadConfig>;\n+\n const ArgsSchema = Type.Object({\n \tpr: Type.Optional(Type.Number({ description: \"Pull request number\" })),\n \trepo: Type.Optional(Type.String({ description: \"Repository in owner/repo format\" })),\n@@ -24,7 +26,7 @@ export default function registerAutoReviewExtension(pi: ExtensionAPI) {\n \t\t\t\treturn;\n \t\t\t}\n \t\t\tconst pr = parsed.pr ?? (await getCurrentPrNumber(ctx.cwd, repo));\n-\t\t\tlet config: any;\n+\t\t\tlet config: AutoReviewConfig;\n \t\t\ttry {\n \t\t\t\tconfig = loadConfig(ctx.cwd);\n \t\t\t} catch (error) {\n@@ -53,7 +55,7 @@ export default function registerAutoReviewExtension(pi: ExtensionAPI) {\n \t\t\t\treturn;\n \t\t\t}\n \t\t\tconst prNumber = parsed.pr ?? (await getCurrentPrNumber(ctx.cwd, repo));\n-\t\t\tlet config: any;\n+\t\t\tlet config: AutoReviewConfig;\n \t\t\ttry {\n \t\t\t\tconfig = loadConfig(ctx.cwd);\n \t\t\t} catch (error) {\n@@ -120,7 +122,7 @@ export default function registerAutoReviewExtension(pi: ExtensionAPI) {\n \n \t\tconst prMatch = event.input.command.match(/\\bgh\\s+pr\\s+merge\\s+(\\d+)\\b/);\n \t\tconst pr = prMatch ? Number(prMatch[1]) : await getCurrentPrNumber(ctx.cwd, repo);\n-\t\tlet config: any;\n+\t\tlet config: AutoReviewConfig;\n \t\ttry {\n \t\t\tconfig = loadConfig(ctx.cwd);\n \t\t} catch (error) {\n\nFile: packages/gbharg-auto-review/test/config.test.ts (added, +53/-0)\n@@ -0,0 +1,53 @@\n+import { mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\n+import { tmpdir } from \"node:os\";\n+import { join } from \"node:path\";\n+import { afterEach, describe, expect, it } from \"vitest\";\n+import { loadWatchState, saveWatchState } from \"../src/config.ts\";\n+\n+const tempDirs: string[] = [];\n+\n+function tempStatePath(): string {\n+\tconst dir = mkdtempSync(join(tmpdir(), \"pi-auto-review-config-test-\"));\n+\ttempDirs.push(dir);\n+\treturn join(dir, \"state.json\");\n+}\n+\n+afterEach(() => {\n+\twhile (tempDirs.length > 0) {\n+\t\tconst dir = tempDirs.pop();\n+\t\tif (dir) rmSync(dir, { recursive: true, force: true });\n+\t}\n+});\n+\n+describe(\"watch state\", () => {\n+\tit(\"returns an empty state when the file does not exist\", () => {\n+\t\texpect(loadWatchState(tempStatePath())).toEqual({ pullRequests: {} });\n+\t});\n+\n+\tit(\"round-trips a valid state file\", () => {\n+\t\tconst path = tempStatePath();\n+\t\tconst state = { pullRequests: { \"42\": \"abc123\" } };\n+\t\tsaveWatchState(state, path);\n+\t\texpect(loadWatchState(path)).toEqual(state);\n+\t});\n+\n+\tit(\"rejects malformed JSON\", () => {\n+\t\tconst path = tempStatePath();\n+\t\twriteFileSync(path, \"{not-json\", \"utf-8\");\n+\t\texpect(() => loadWatchState(path)).toThrow(/not valid JSON/);\n+\t});\n+\n+\tit(\"rejects invalid state shape\", () => {\n+\t\tconst path = tempStatePath();\n+\t\twriteFileSync(path, JSON.stringify({ pullRequests: { \"42\": 42 } }), \"utf-8\");\n+\t\texpect(() => loadWatchState(path)).toThrow(/pullRequests property mapping PR numbers to string SHAs/);\n+\t});\n+\n+\tit(\"rejects invalid state shape before saving\", () => {\n+\t\tconst path = tempStatePath();\n+\t\tconst invalidState = { pullRequests: { \"42\": 42 } };\n+\t\texpect(() => saveWatchState(invalidState as never, path)).toThrow(\n+\t\t\t/pullRequests property mapping PR numbers to string SHAs/,\n+\t\t);\n+\t});\n+});\n```\n\n-"
        }
      ]
    },
    {
      "role": "user",
      "parts": [
        {
          "text": "You are a dedicated code reviewer for this repository. You are deeply familiar with its conventions and codebase.\n\n## Project Conventions (from CLAUDE.md)\n## gstack (REQUIRED — global install)\n\n**Before doing ANY work, verify gstack is installed:**\n\n```bash\ntest -d ~/.claude/skills/gstack/bin && echo \"GSTACK_OK\" || echo \"GSTACK_MISSING\"\n```\n\nIf GSTACK_MISSING: STOP. Do not proceed. Tell the user:\n\n> gstack is required for all AI-assisted work in this repo.\n> Install it:\n> ```bash\n> git clone --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack\n> cd ~/.claude/skills/gstack && ./setup --team\n> ```\n> Then restart your AI coding tool.\n\nDo not skip skills, ignore gstack errors, or work around missing gstack.\n\nUsing gstack skills: After install, skills like /qa, /ship, /review, /investigate,\nand /browse are available. Use /browse for all web browsing.\nUse ~/.claude/skills/gstack/... for gstack file paths (the global path).\n\n\n## Review Task\n\nPR #27: \"fix(auto-review): harden watch state loading\"\n\nDescription:\n## Summary\n- Validate auto-review watch-state JSON before loading or saving\n- Keep the watch loop alive by resetting corrupt state to an empty map\n- Replace local auto-review `any` usage with concrete config/PR types\n\n## Verification\n- `node ../../node_modules/vitest/dist/cli.js --run test/config.test.ts`\n- `node ../../node_modules/vitest/dist/cli.js --run`\n- `npm run check`\n\n\nReview the diff below. Provide your response as a JSON object with these keys:\n- \"summary\": a concise markdown summary. Tag issues with severity: [blocking] for must-fix, [suggestion] for improvements, [nitpick] for style.\n- \"comments\": array of { \"path\", \"line\", \"body\" } — inline review comments with severity tags\n- \"verdict\": \"approve\" (no blocking issues) or \"request_changes\" (blocking issues found)\n- \"workflow_suggestions\": array of strings — suggestions for improving the development workflow (e.g., \"Consider splitting this PR\", \"Add a pre-commit hook for X\")\n\nFocus on meaningful issues: bugs, security, performance, correctness. Skip trivial style nits.\nReturn ONLY valid JSON, no markdown fences.\n\nDiff:\n```diff\nFile: packages/gbharg-auto-review/src/cli.ts (modified, +14/-3)\n@@ -6,6 +6,7 @@ import { parsePlanContext } from \"./plan-context.ts\";\n import { collapseLatestReviews, evaluateMergePolicy } from \"./policy.ts\";\n import { dispatchCloudReviews, resolveDispatchMode } from \"./runner.ts\";\n import { evaluateReviewScope } from \"./scope.ts\";\n+import type { PullRequestMetadata, WatchState } from \"./types.ts\";\n \n type Command = \"check\" | \"dispatch\" | \"watch\";\n \n@@ -109,8 +110,8 @@ async function runDispatch(\n async function runWatch(repo: string, config: ReturnType<typeof loadConfig>, cwd: string): Promise<void> {\n \tconst intervalMs = config.pollIntervalMs ?? 30_000;\n \tfor (;;) {\n-\t\tconst state = loadWatchState();\n-\t\tlet prs: any;\n+\t\tconst state = loadWatchStateForWatchLoop();\n+\t\tlet prs: PullRequestMetadata[];\n \t\ttry {\n \t\t\tprs = await listOpenReviewablePullRequests(repo, cwd);\n \t\t} catch (error) {\n@@ -119,7 +120,7 @@ async function runWatch(repo: string, config: ReturnType<typeof loadConfig>, cwd\n \t\t\tawait new Promise((resolve) => setTimeout(resolve, intervalMs));\n \t\t\tcontinue;\n \t\t}\n-\t\tconst activePrNumbers = new Set(prs.map((pr: any) => String(pr.number)));\n+\t\tconst activePrNumbers = new Set(prs.map((pr) => String(pr.number)));\n \t\tconst nextSeenHeads = Object.fromEntries(\n \t\t\tObject.entries(state.pullRequests).filter(([prNumber]) => activePrNumbers.has(prNumber)),\n \t\t);\n@@ -146,6 +147,16 @@ async function runWatch(repo: string, config: ReturnType<typeof loadConfig>, cwd\n \t}\n }\n \n+function loadWatchStateForWatchLoop(): WatchState {\n+\ttry {\n+\t\treturn loadWatchState();\n+\t} catch (error) {\n+\t\tconst message = error instanceof Error ? error.message : String(error);\n+\t\tconsole.error(`watch: failed to load state; resetting to empty state: ${message}`);\n+\t\treturn { pullRequests: {} };\n+\t}\n+}\n+\n function parseCommandArgs(args: string[]): { command: Command; flagArgs: string[] } {\n \tconst first = args[0];\n \tif (!first || first.startsWith(\"--\")) {\n\nFile: packages/gbharg-auto-review/src/config.ts (modified, +18/-1)\n@@ -52,10 +52,19 @@ export function loadConfig(cwd: string, explicitPath?: string): ReviewCloudConfi\n \n export function loadWatchState(path = DEFAULT_STATE_PATH): WatchState {\n \tif (!existsSync(path)) return { pullRequests: {} };\n-\treturn JSON.parse(readFileSync(path, \"utf-8\")) as WatchState;\n+\tlet parsed: unknown;\n+\ttry {\n+\t\tparsed = JSON.parse(readFileSync(path, \"utf-8\")) as unknown;\n+\t} catch (error) {\n+\t\tconst message = error instanceof Error ? error.message : String(error);\n+\t\tthrow new Error(`Auto-review watch state at ${path} is not valid JSON: ${message}`);\n+\t}\n+\tassertWatchState(parsed, path);\n+\treturn parsed;\n }\n \n export function saveWatchState(state: WatchState, path = DEFAULT_STATE_PATH): void {\n+\tassertWatchState(state, path);\n \tmkdirSync(dirname(path), { recursive: true });\n \twriteFileSync(path, `${JSON.stringify(state, null, 2)}\\n`, \"utf-8\");\n }\n@@ -141,6 +150,14 @@ function assertCommandRecord(value: unknown, name: string): void {\n \t}\n }\n \n+function assertWatchState(value: unknown, path: string): asserts value is WatchState {\n+\tif (!isRecord(value) || !isStringRecord(value.pullRequests)) {\n+\t\tthrow new Error(\n+\t\t\t`Auto-review watch state at ${path} must be an object with a pullRequests property mapping PR numbers to string SHAs`,\n+\t\t);\n+\t}\n+}\n+\n function isRecord(value: unknown): value is Record<string, unknown> {\n \treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n }\n\nFile: packages/gbharg-auto-review/src/index.ts (modified, +5/-3)\n@@ -8,6 +8,8 @@ import { inferGitHubRepo } from \"./repo.ts\";\n import { dispatchCloudReviews } from \"./runner.ts\";\n import { evaluateReviewScope } from \"./scope.ts\";\n \n+type AutoReviewConfig = ReturnType<typeof loadConfig>;\n+\n const ArgsSchema = Type.Object({\n \tpr: Type.Optional(Type.Number({ description: \"Pull request number\" })),\n \trepo: Type.Optional(Type.String({ description: \"Repository in owner/repo format\" })),\n@@ -24,7 +26,7 @@ export default function registerAutoReviewExtension(pi: ExtensionAPI) {\n \t\t\t\treturn;\n \t\t\t}\n \t\t\tconst pr = parsed.pr ?? (await getCurrentPrNumber(ctx.cwd, repo));\n-\t\t\tlet config: any;\n+\t\t\tlet config: AutoReviewConfig;\n \t\t\ttry {\n \t\t\t\tconfig = loadConfig(ctx.cwd);\n \t\t\t} catch (error) {\n@@ -53,7 +55,7 @@ export default function registerAutoReviewExtension(pi: ExtensionAPI) {\n \t\t\t\treturn;\n \t\t\t}\n \t\t\tconst prNumber = parsed.pr ?? (await getCurrentPrNumber(ctx.cwd, repo));\n-\t\t\tlet config: any;\n+\t\t\tlet config: AutoReviewConfig;\n \t\t\ttry {\n \t\t\t\tconfig = loadConfig(ctx.cwd);\n \t\t\t} catch (error) {\n@@ -120,7 +122,7 @@ export default function registerAutoReviewExtension(pi: ExtensionAPI) {\n \n \t\tconst prMatch = event.input.command.match(/\\bgh\\s+pr\\s+merge\\s+(\\d+)\\b/);\n \t\tconst pr = prMatch ? Number(prMatch[1]) : await getCurrentPrNumber(ctx.cwd, repo);\n-\t\tlet config: any;\n+\t\tlet config: AutoReviewConfig;\n \t\ttry {\n \t\t\tconfig = loadConfig(ctx.cwd);\n \t\t} catch (error) {\n\nFile: packages/gbharg-auto-review/test/config.test.ts (added, +53/-0)\n@@ -0,0 +1,53 @@\n+import { mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\n+import { tmpdir } from \"node:os\";\n+import { join } from \"node:path\";\n+import { afterEach, describe, expect, it } from \"vitest\";\n+import { loadWatchState, saveWatchState } from \"../src/config.ts\";\n+\n+const tempDirs: string[] = [];\n+\n+function tempStatePath(): string {\n+\tconst dir = mkdtempSync(join(tmpdir(), \"pi-auto-review-config-test-\"));\n+\ttempDirs.push(dir);\n+\treturn join(dir, \"state.json\");\n+}\n+\n+afterEach(() => {\n+\twhile (tempDirs.length > 0) {\n+\t\tconst dir = tempDirs.pop();\n+\t\tif (dir) rmSync(dir, { recursive: true, force: true });\n+\t}\n+});\n+\n+describe(\"watch state\", () => {\n+\tit(\"returns an empty state when the file does not exist\", () => {\n+\t\texpect(loadWatchState(tempStatePath())).toEqual({ pullRequests: {} });\n+\t});\n+\n+\tit(\"round-trips a valid state file\", () => {\n+\t\tconst path = tempStatePath();\n+\t\tconst state = { pullRequests: { \"42\": \"abc123\" } };\n+\t\tsaveWatchState(state, path);\n+\t\texpect(loadWatchState(path)).toEqual(state);\n+\t});\n+\n+\tit(\"rejects malformed JSON\", () => {\n+\t\tconst path = tempStatePath();\n+\t\twriteFileSync(path, \"{not-json\", \"utf-8\");\n+\t\texpect(() => loadWatchState(path)).toThrow(/not valid JSON/);\n+\t});\n+\n+\tit(\"rejects invalid state shape\", () => {\n+\t\tconst path = tempStatePath();\n+\t\twriteFileSync(path, JSON.stringify({ pullRequests: { \"42\": 42 } }), \"utf-8\");\n+\t\texpect(() => loadWatchState(path)).toThrow(/pullRequests property mapping PR numbers to string SHAs/);\n+\t});\n+\n+\tit(\"rejects invalid state shape before saving\", () => {\n+\t\tconst path = tempStatePath();\n+\t\tconst invalidState = { pullRequests: { \"42\": 42 } };\n+\t\texpect(() => saveWatchState(invalidState as never, path)).toThrow(\n+\t\t\t/pullRequests property mapping PR numbers to string SHAs/,\n+\t\t);\n+\t});\n+});\n```\n\n-"
        }
      ]
    }
  ]
}