diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..de9c16a1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+dist/
+runtime/
+.env
+*.pem
+*.sqlite
+*.sqlite-shm
+*.sqlite-wal
diff --git a/README.md b/README.md
index b5324c1b..549e48dd 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,36 @@
-# evaos-code-review-bot
-Local ZCode-backed PR review bot pilot for evaOS/Electric Sheep repos
+# evaOS Code Review Bot
+
+Pilot local worker for ZCode/GLM-5.2 pull request reviews.
+
+## Safety Defaults
+
+- Reviews only the allowlisted pilot repos in `config.example.json`.
+- Skips draft PRs by default.
+- Posts at most one review per `{repo, pr, head_sha}`.
+- Never submits `APPROVE`.
+- Uses `REQUEST_CHANGES` only for validated P0/P1 findings.
+- Drops findings that cannot be placed on current RIGHT-side diff lines.
+- Drops secret-looking findings instead of redacting and posting them.
+- Redacts secret-looking material from local evidence logs before writing them.
+- Verifies the ZCode worktree is clean, including untracked files, after every review run.
+- Caps ZCode prompt patch bytes and kills long ZCode runs with `zcode.timeoutMs`.
+- Installs a temporary per-worktree ZCode policy that allows read-only file tools, disables Bash/mutation/subagents, then restores/removes it before the clean check.
+- Sets `ZCODE_MODEL_RETRY_MAX_RETRIES=0` by default so provider rate limits fail fast instead of multiplying requests.
+
+## Commands
+
+```bash
+npm run doctor
+npm run run-once -- --config /Volumes/LEXAR/Codex/evaos-code-review-bot/config/canary-dry-run.json --dry-run true --repo electricsheephq/WorldOS --pr 1161
+npm run daemon -- --config /Volumes/LEXAR/Codex/evaos-code-review-bot/config/canary-dry-run.json --dry-run true
+```
+
+Posting reviews requires a GitHub App installed on the pilot repos:
+
+```bash
+export EVAOS_REVIEW_BOT_APP_ID=...
+export EVAOS_REVIEW_BOT_PRIVATE_KEY_PATH=/path/to/private-key.pem
+npm run run-once -- --config /Volumes/LEXAR/Codex/evaos-code-review-bot/config/canary-live.json --dry-run false
+```
+
+The worker derives transient ZCode CLI model environment from the existing ZCode app config at `/Volumes/LEXAR/zcode/.zcode/v2/config.json`; it does not copy the Z.ai API key into this repository.
diff --git a/config.example.json b/config.example.json
new file mode 100644
index 00000000..eb68c9f7
--- /dev/null
+++ b/config.example.json
@@ -0,0 +1,22 @@
+{
+ "pilotRepos": ["electricsheephq/WorldOS", "100yenadmin/evaOS-GUI"],
+ "pollIntervalMs": 90000,
+ "skipDrafts": true,
+ "canaryPulls": ["electricsheephq/WorldOS#1161", "100yenadmin/evaOS-GUI#497"],
+ "workRoot": "/Volumes/LEXAR/repos/evaos-code-review-bot/runtime",
+ "statePath": "/Volumes/LEXAR/Codex/evaos-code-review-bot/state/reviews.sqlite",
+ "evidenceDir": "/Volumes/LEXAR/Codex/evaos-code-review-bot/evidence",
+ "zcode": {
+ "cliPath": "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs",
+ "appConfigPath": "/Volumes/LEXAR/zcode/.zcode/v2/config.json",
+ "model": "GLM-5.2",
+ "timeoutMs": 180000,
+ "maxPatchBytes": 80000,
+ "retryMaxRetries": 0
+ },
+ "github": {
+ "appId": "set via EVAOS_REVIEW_BOT_APP_ID",
+ "privateKeyPath": "set via EVAOS_REVIEW_BOT_PRIVATE_KEY_PATH",
+ "token": "optional fallback token for local development only"
+ }
+}
diff --git a/docs/github-app-setup.md b/docs/github-app-setup.md
new file mode 100644
index 00000000..9f708ca0
--- /dev/null
+++ b/docs/github-app-setup.md
@@ -0,0 +1,29 @@
+# GitHub App Setup
+
+Create a GitHub App named `evaOS Code Review Bot` with slug `evaos-code-review-bot`.
+
+GitHub does not provide a direct noninteractive `gh app create` command for this. Use the GitHub App manifest handshake or the GitHub web UI. The manifest flow requires a signed-in user to open the organization app-registration URL, approve the manifest, then exchange GitHub's temporary `code` for the app ID and PEM within one hour.
+
+Required repository permissions:
+
+- Contents: read
+- Pull requests: read/write
+- Checks: read
+- Actions: read
+- Metadata: read
+
+Install the app only on:
+
+- `electricsheephq/WorldOS`
+- `100yenadmin/evaOS-GUI`
+
+After installation, save the generated private key outside the repository and launch the worker with:
+
+```bash
+export EVAOS_REVIEW_BOT_APP_ID=...
+export EVAOS_REVIEW_BOT_PRIVATE_KEY_PATH=/absolute/path/to/evaos-code-review-bot.private-key.pem
+npm run doctor
+npm run run-once -- --dry-run true
+```
+
+Do not run with `--dry-run false` until dry-run evidence exists for both pilot repos and `npm test` plus `npm run build` pass.
diff --git a/docs/launchd.md b/docs/launchd.md
new file mode 100644
index 00000000..50b44914
--- /dev/null
+++ b/docs/launchd.md
@@ -0,0 +1,31 @@
+# Launchd Pilot
+
+Launchd should stay disabled until GitHub App installation is complete and a real ZCode dry-run succeeds without rate limiting.
+
+Recommended first live command:
+
+```bash
+cd /Volumes/LEXAR/repos/evaos-code-review-bot
+export EVAOS_REVIEW_BOT_APP_ID=4184532
+export EVAOS_REVIEW_BOT_PRIVATE_KEY_PATH=/Volumes/LEXAR/Codex/evaos-code-review-bot/secrets/evaos-code-review-bot.private-key.pem
+npm run run-once -- --config /Volumes/LEXAR/Codex/evaos-code-review-bot/config/canary-dry-run.json --dry-run true --repo electricsheephq/WorldOS --pr 1161
+```
+
+After the GitHub App is installed, use app credentials and keep `--dry-run true` for the first observation window:
+
+```bash
+export EVAOS_REVIEW_BOT_APP_ID=4184532
+export EVAOS_REVIEW_BOT_PRIVATE_KEY_PATH=/Volumes/LEXAR/Codex/evaos-code-review-bot/secrets/evaos-code-review-bot.private-key.pem
+npm run daemon -- --config /Volumes/LEXAR/Codex/evaos-code-review-bot/config/canary-dry-run.json --dry-run true
+```
+
+When installed as a LaunchAgent, write stdout/stderr to `~/Library/Logs/evaos-code-review-bot/`. On this Mac, launchd failed with `EX_CONFIG` when those paths pointed directly at the Lexar volume; copy the local launch logs into the Lexar evidence packet after each proof window.
+
+Only switch to `--dry-run false` after:
+
+- current-head duplicate reruns post nothing,
+- review-plan JSON contains only valid current-diff lines,
+- no secret-like text appears in comments or logs,
+- ZCode worktrees stay clean after runs,
+- GLM/Z.ai rate limits are not firing,
+- `npm run doctor` reports `readMode: "app_installation"` and successful read checks for every pilot repo.
diff --git a/launchd/evaos-code-review-bot.plist.example b/launchd/evaos-code-review-bot.plist.example
new file mode 100644
index 00000000..0e68b6ed
--- /dev/null
+++ b/launchd/evaos-code-review-bot.plist.example
@@ -0,0 +1,47 @@
+
+
+
+
+ Label
+ com.electricsheephq.evaos-code-review-bot
+ WorkingDirectory
+ /Volumes/LEXAR/repos/evaos-code-review-bot
+ ProgramArguments
+
+ /opt/homebrew/bin/node
+ /Volumes/LEXAR/repos/evaos-code-review-bot/node_modules/tsx/dist/cli.mjs
+ src/cli.ts
+ daemon
+ --config
+ /Volumes/LEXAR/Codex/evaos-code-review-bot/config/canary-dry-run.json
+ --dry-run
+ true
+
+ EnvironmentVariables
+
+ PATH
+ /opt/homebrew/bin:/opt/homebrew/sbin:/Users/lume/.bun/bin:/Users/lume/gbrain/bin:/Users/lume/.local/bin:/Users/lume/.openclaw/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
+ HOME
+ /Users/lume
+ USER
+ lume
+ LOGNAME
+ lume
+ SHELL
+ /bin/zsh
+ EVAOS_REVIEW_BOT_APP_ID
+ REPLACE_WITH_APP_ID
+ EVAOS_REVIEW_BOT_PRIVATE_KEY_PATH
+ /absolute/path/to/private-key.pem
+
+ RunAtLoad
+
+ KeepAlive
+
+ StandardOutPath
+ /Users/lume/Library/Logs/evaos-code-review-bot/launchd.out.log
+ StandardErrorPath
+ /Users/lume/Library/Logs/evaos-code-review-bot/launchd.err.log
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..361c34ae
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1680 @@
+{
+ "name": "evaos-code-review-bot",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "evaos-code-review-bot",
+ "version": "0.1.0",
+ "devDependencies": {
+ "@types/node": "^24.0.15",
+ "tsx": "^4.20.3",
+ "typescript": "^5.8.3",
+ "vitest": "^3.2.4"
+ },
+ "engines": {
+ "node": ">=26"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
+ "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
+ "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
+ "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
+ "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
+ "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
+ "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
+ "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
+ "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
+ "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
+ "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
+ "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
+ "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
+ "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
+ "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
+ "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
+ "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
+ "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
+ "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.13.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
+ "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz",
+ "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.6",
+ "@vitest/utils": "3.2.6",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz",
+ "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.2.6",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz",
+ "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz",
+ "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.6",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz",
+ "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.6",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz",
+ "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz",
+ "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.6",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.15",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.16",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
+ "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.62.2",
+ "@rollup/rollup-android-arm64": "4.62.2",
+ "@rollup/rollup-darwin-arm64": "4.62.2",
+ "@rollup/rollup-darwin-x64": "4.62.2",
+ "@rollup/rollup-freebsd-arm64": "4.62.2",
+ "@rollup/rollup-freebsd-x64": "4.62.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-musl": "4.62.2",
+ "@rollup/rollup-openbsd-x64": "4.62.2",
+ "@rollup/rollup-openharmony-arm64": "4.62.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-literal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tsx": {
+ "version": "4.22.4",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
+ "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
+ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz",
+ "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.6",
+ "@vitest/mocker": "3.2.6",
+ "@vitest/pretty-format": "^3.2.6",
+ "@vitest/runner": "3.2.6",
+ "@vitest/snapshot": "3.2.6",
+ "@vitest/spy": "3.2.6",
+ "@vitest/utils": "3.2.6",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.6",
+ "@vitest/ui": "3.2.6",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..2467526e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "evaos-code-review-bot",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "description": "Local ZCode-backed GitHub PR review bot pilot for evaOS repos.",
+ "engines": {
+ "node": ">=26"
+ },
+ "scripts": {
+ "build": "tsc -p tsconfig.json",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "doctor": "tsx src/cli.ts doctor",
+ "run-once": "tsx src/cli.ts run-once",
+ "daemon": "tsx src/cli.ts daemon"
+ },
+ "devDependencies": {
+ "@types/node": "^24.0.15",
+ "tsx": "^4.20.3",
+ "typescript": "^5.8.3",
+ "vitest": "^3.2.4"
+ }
+}
diff --git a/src/cli.ts b/src/cli.ts
new file mode 100644
index 00000000..3576e6e6
--- /dev/null
+++ b/src/cli.ts
@@ -0,0 +1,132 @@
+import { loadConfig } from "./config.js";
+import { formatDaemonLog } from "./daemon-log.js";
+import { GitHubApi } from "./github.js";
+import { runOnce } from "./worker.js";
+import { resolveZCodeProviderEnv } from "./zcode-env.js";
+
+async function main(): Promise {
+ const args = parseArgs(process.argv.slice(2));
+ const command = args._[0];
+
+ if (command === "doctor") {
+ const config = loadConfig(args.config);
+ const zcode = resolveZCodeProviderEnv({
+ appConfigPath: config.zcode.appConfigPath,
+ model: config.zcode.model,
+ providerId: config.zcode.providerId
+ });
+ const github = new GitHubApi(config.github);
+ const readChecks = [];
+ for (const repo of config.pilotRepos) {
+ try {
+ await github.listOpenPulls(repo);
+ readChecks.push({ repo, ok: true });
+ } catch (error) {
+ readChecks.push({
+ repo,
+ ok: false,
+ error: error instanceof Error ? error.message : String(error)
+ });
+ }
+ }
+ console.log(JSON.stringify({
+ ok: readChecks.every((check) => check.ok),
+ pilotRepos: config.pilotRepos,
+ canaryPulls: config.canaryPulls ?? [],
+ statePath: config.statePath,
+ workRoot: config.workRoot,
+ zcode: zcode.redacted,
+ github: {
+ canPostAsApp: github.canPostAsApp(),
+ readMode: github.canPostAsApp() ? "app_installation" : "fallback_token",
+ hasFallbackReadToken: Boolean(config.github.token),
+ readChecks
+ }
+ }, null, 2));
+ if (readChecks.some((check) => !check.ok)) process.exitCode = 1;
+ return;
+ }
+
+ if (command === "run-once") {
+ await runOnce({
+ configPath: args.config,
+ dryRun: args["dry-run"] !== "false",
+ repo: args.repo,
+ pullNumber: args.pr ? Number(args.pr) : undefined,
+ useZCode: args.zcode !== "false"
+ });
+ return;
+ }
+
+ if (command === "daemon") {
+ const config = loadConfig(args.config);
+ let cycle = 0;
+ for (;;) {
+ cycle += 1;
+ const dryRun = args["dry-run"] !== "false";
+ console.log(formatDaemonLog({
+ event: "daemon_cycle_start",
+ cycle,
+ dryRun,
+ pilotRepos: config.pilotRepos,
+ canaryPulls: config.canaryPulls ?? []
+ }));
+ try {
+ const result = await runOnce({ configPath: args.config, dryRun });
+ console.log(formatDaemonLog({
+ event: "daemon_cycle_complete",
+ cycle,
+ dryRun,
+ result
+ }));
+ } catch (error) {
+ console.error(formatDaemonLog({
+ event: "daemon_cycle_failed",
+ level: "error",
+ cycle,
+ dryRun,
+ error: error instanceof Error ? error.message : String(error)
+ }));
+ throw error;
+ }
+ await new Promise((resolve) => setTimeout(resolve, config.pollIntervalMs));
+ }
+ }
+
+ throw new Error(`Unknown command: ${command ?? "(missing)"}`);
+}
+
+function parseArgs(argv: string[]): ParsedArgs {
+ const parsed: ParsedArgs = { _: [] };
+ for (let index = 0; index < argv.length; index += 1) {
+ const arg = argv[index]!;
+ if (!arg.startsWith("--")) {
+ parsed._.push(arg);
+ continue;
+ }
+ const key = arg.slice(2);
+ const next = argv[index + 1];
+ if (next && !next.startsWith("--")) {
+ parsed[key] = next;
+ index += 1;
+ } else {
+ parsed[key] = "true";
+ }
+ }
+ return parsed;
+}
+
+interface ParsedArgs {
+ _: string[];
+ config?: string;
+ repo?: string;
+ pr?: string;
+ "dry-run"?: string;
+ zcode?: string;
+ [key: string]: string | string[] | undefined;
+}
+
+main().catch((error: unknown) => {
+ console.error(error instanceof Error ? error.message : String(error));
+ process.exitCode = 1;
+});
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 00000000..c81b187c
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,68 @@
+import { existsSync, readFileSync } from "node:fs";
+
+export interface BotConfig {
+ pilotRepos: string[];
+ pollIntervalMs: number;
+ skipDrafts: boolean;
+ workRoot: string;
+ statePath: string;
+ evidenceDir: string;
+ canaryPulls?: string[];
+ zcode: {
+ cliPath: string;
+ appConfigPath: string;
+ model: string;
+ providerId?: string;
+ timeoutMs: number;
+ maxPatchBytes: number;
+ retryMaxRetries: number;
+ };
+ github: {
+ appId?: string;
+ privateKeyPath?: string;
+ token?: string;
+ };
+}
+
+const DEFAULT_CONFIG: BotConfig = {
+ pilotRepos: ["electricsheephq/WorldOS", "100yenadmin/evaOS-GUI"],
+ pollIntervalMs: 90_000,
+ skipDrafts: true,
+ workRoot: "/Volumes/LEXAR/repos/evaos-code-review-bot/runtime",
+ statePath: "/Volumes/LEXAR/Codex/evaos-code-review-bot/state/reviews.sqlite",
+ evidenceDir: "/Volumes/LEXAR/Codex/evaos-code-review-bot/evidence",
+ canaryPulls: undefined,
+ zcode: {
+ cliPath: "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs",
+ appConfigPath: "/Volumes/LEXAR/zcode/.zcode/v2/config.json",
+ model: "GLM-5.2",
+ timeoutMs: 180_000,
+ maxPatchBytes: 80_000,
+ retryMaxRetries: 0
+ },
+ github: {}
+};
+
+export function loadConfig(configPath?: string): BotConfig {
+ const fromFile = configPath && existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {};
+ const merged = deepMerge(DEFAULT_CONFIG, fromFile) as BotConfig;
+
+ merged.github.appId = process.env.EVAOS_REVIEW_BOT_APP_ID ?? merged.github.appId;
+ merged.github.privateKeyPath = process.env.EVAOS_REVIEW_BOT_PRIVATE_KEY_PATH ?? merged.github.privateKeyPath;
+ merged.github.token = process.env.GITHUB_TOKEN ?? merged.github.token;
+
+ return merged;
+}
+
+function deepMerge(base: unknown, overlay: unknown): unknown {
+ if (!isRecord(base) || !isRecord(overlay)) return overlay ?? base;
+ const output: Record = { ...base };
+ for (const [key, value] of Object.entries(overlay)) {
+ output[key] = key in output ? deepMerge(output[key], value) : value;
+ }
+ return output;
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
diff --git a/src/daemon-log.ts b/src/daemon-log.ts
new file mode 100644
index 00000000..f946802e
--- /dev/null
+++ b/src/daemon-log.ts
@@ -0,0 +1,29 @@
+import { redactSecrets } from "./secrets.js";
+
+export interface DaemonLogEvent {
+ event: string;
+ level?: "info" | "error";
+ [key: string]: unknown;
+}
+
+export function formatDaemonLog(input: DaemonLogEvent, now = new Date()): string {
+ const { level = "info", ...rest } = input;
+ return JSON.stringify({
+ ts: now.toISOString(),
+ level,
+ ...redactRecord(rest)
+ });
+}
+
+function redactRecord(value: Record): Record {
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, redactObject(entry)]));
+}
+
+function redactObject(value: unknown): unknown {
+ if (typeof value === "string") return redactSecrets(value);
+ if (Array.isArray(value)) return value.map((entry) => redactObject(entry));
+ if (value && typeof value === "object") {
+ return redactRecord(value as Record);
+ }
+ return value;
+}
diff --git a/src/diff.ts b/src/diff.ts
new file mode 100644
index 00000000..29406da2
--- /dev/null
+++ b/src/diff.ts
@@ -0,0 +1,61 @@
+import type { DroppedFinding, Finding, PullFilePatch } from "./types.js";
+
+const HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
+
+export function collectRightSideLines(files: PullFilePatch[]): Map> {
+ const byPath = new Map>();
+
+ for (const file of files) {
+ const lines = new Set();
+ let newLine: number | null = null;
+
+ for (const rawLine of (file.patch ?? "").split("\n")) {
+ const hunk = rawLine.match(HUNK_HEADER);
+ if (hunk) {
+ newLine = Number(hunk[1]);
+ continue;
+ }
+
+ if (newLine === null || rawLine.startsWith("\\ No newline")) continue;
+
+ if (rawLine.startsWith("+") && !rawLine.startsWith("+++")) {
+ lines.add(newLine);
+ newLine += 1;
+ continue;
+ }
+
+ if (rawLine.startsWith("-") && !rawLine.startsWith("---")) continue;
+
+ lines.add(newLine);
+ newLine += 1;
+ }
+
+ if (lines.size > 0) byPath.set(file.filename, lines);
+ }
+
+ return byPath;
+}
+
+export function validateFindingLocations(findings: Finding[], files: PullFilePatch[]): {
+ valid: Finding[];
+ dropped: DroppedFinding[];
+} {
+ const rightLines = collectRightSideLines(files);
+ const valid: Finding[] = [];
+ const dropped: DroppedFinding[] = [];
+
+ for (const finding of findings) {
+ const fileLines = rightLines.get(finding.path);
+ if (!fileLines) {
+ dropped.push({ ...finding, reason: "file_not_in_diff" });
+ continue;
+ }
+ if (!fileLines.has(finding.line)) {
+ dropped.push({ ...finding, reason: "line_not_in_current_diff" });
+ continue;
+ }
+ valid.push(finding);
+ }
+
+ return { valid, dropped };
+}
diff --git a/src/findings.ts b/src/findings.ts
new file mode 100644
index 00000000..d2097e32
--- /dev/null
+++ b/src/findings.ts
@@ -0,0 +1,133 @@
+import { containsSecretLikeText, redactSecrets } from "./secrets.js";
+import type { DroppedFinding, Finding, ReviewComment, ReviewEvent, Severity } from "./types.js";
+
+const SEVERITY_RANK: Record = {
+ P0: 0,
+ P1: 1,
+ P2: 2,
+ P3: 3
+};
+
+const SEVERITIES = new Set(["P0", "P1", "P2", "P3"]);
+
+export function parseFindings(value: unknown): { findings: Finding[]; dropped: DroppedFinding[] } {
+ const rawFindings = Array.isArray(value)
+ ? value
+ : isRecord(value) && Array.isArray(value.findings)
+ ? value.findings
+ : [];
+ const findings: Finding[] = [];
+ const dropped: DroppedFinding[] = [];
+
+ for (const raw of rawFindings) {
+ if (!isRecord(raw)) {
+ dropped.push({ reason: "invalid_schema" });
+ continue;
+ }
+
+ const severity = raw.severity;
+ const path = raw.path;
+ const line = raw.line;
+ const title = raw.title;
+ const body = raw.body;
+ const confidence = raw.confidence;
+
+ if (
+ !SEVERITIES.has(severity as Severity) ||
+ typeof path !== "string" ||
+ typeof line !== "number" ||
+ !Number.isInteger(line) ||
+ line <= 0 ||
+ typeof title !== "string" ||
+ title.trim().length === 0 ||
+ typeof body !== "string" ||
+ body.trim().length === 0 ||
+ typeof confidence !== "number" ||
+ confidence < 0 ||
+ confidence > 1
+ ) {
+ dropped.push({ reason: "invalid_schema" });
+ continue;
+ }
+
+ findings.push({
+ severity: severity as Severity,
+ path,
+ line,
+ title: title.trim(),
+ body: body.trim(),
+ confidence,
+ ...(typeof raw.why_this_matters === "string" && raw.why_this_matters.trim()
+ ? { why_this_matters: raw.why_this_matters.trim() }
+ : {})
+ });
+ }
+
+ return { findings, dropped };
+}
+
+export function normalizeFindingsForReview(
+ findings: Finding[],
+ options: { maxInlineComments?: number } = {}
+): { comments: ReviewComment[]; dropped: DroppedFinding[] } {
+ const maxInlineComments = options.maxInlineComments ?? 25;
+ const accepted: Finding[] = [];
+ const dropped: DroppedFinding[] = [];
+
+ for (const finding of findings) {
+ if (containsSecretLikeText(`${finding.title}\n${finding.body}\n${finding.why_this_matters ?? ""}`)) {
+ dropped.push({ ...redactFinding(finding), reason: "secret_detected" });
+ continue;
+ }
+ accepted.push(finding);
+ }
+
+ accepted.sort((a, b) => {
+ const severity = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
+ if (severity !== 0) return severity;
+ const path = a.path.localeCompare(b.path);
+ if (path !== 0) return path;
+ return a.line - b.line || a.title.localeCompare(b.title);
+ });
+
+ const kept = accepted.slice(0, maxInlineComments);
+ for (const finding of accepted.slice(maxInlineComments)) {
+ dropped.push({ ...finding, reason: "comment_cap_exceeded" });
+ }
+
+ return {
+ comments: kept.map((finding) => ({
+ path: finding.path,
+ line: finding.line,
+ side: "RIGHT",
+ severity: finding.severity,
+ title: finding.title,
+ body: formatReviewComment(finding)
+ })),
+ dropped
+ };
+}
+
+function redactFinding(finding: T): T {
+ return {
+ ...finding,
+ title: redactSecrets(finding.title),
+ body: redactSecrets(finding.body),
+ ...(finding.why_this_matters ? { why_this_matters: redactSecrets(finding.why_this_matters) } : {})
+ };
+}
+
+export function decideReviewEvent(findings: Pick[]): ReviewEvent {
+ return findings.some((finding) => finding.severity === "P0" || finding.severity === "P1")
+ ? "REQUEST_CHANGES"
+ : "COMMENT";
+}
+
+export function formatReviewComment(finding: Finding): string {
+ const why = finding.why_this_matters ? `\n\nWhy this matters: ${finding.why_this_matters}` : "";
+ return `**${finding.severity}: ${finding.title}**\n\n${finding.body}${why}`;
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
diff --git a/src/git.ts b/src/git.ts
new file mode 100644
index 00000000..4423accb
--- /dev/null
+++ b/src/git.ts
@@ -0,0 +1,83 @@
+import { mkdirSync, rmSync } from "node:fs";
+import { join } from "node:path";
+import { spawnSync } from "node:child_process";
+
+export interface PreparedWorktree {
+ path: string;
+ headSha: string;
+}
+
+export function preparePullWorktree(input: {
+ repo: string;
+ pullNumber: number;
+ expectedHeadSha: string;
+ workRoot: string;
+}): PreparedWorktree {
+ const safeRepo = input.repo.replace(/[^A-Za-z0-9_.-]+/g, "__");
+ const mirrorPath = join(input.workRoot, "mirrors", `${safeRepo}.git`);
+ const worktreePath = join(input.workRoot, "worktrees", `${safeRepo}__pr-${input.pullNumber}__${input.expectedHeadSha.slice(0, 12)}`);
+ const repoUrl = `https://github.com/${input.repo}.git`;
+
+ mkdirSync(join(input.workRoot, "mirrors"), { recursive: true });
+ mkdirSync(join(input.workRoot, "worktrees"), { recursive: true });
+
+ if (!existsAsGitMirror(mirrorPath)) {
+ run("git", ["clone", "--mirror", repoUrl, mirrorPath]);
+ } else {
+ run("git", ["--git-dir", mirrorPath, "remote", "set-url", "origin", repoUrl]);
+ }
+
+ run("git", [
+ "--git-dir",
+ mirrorPath,
+ "fetch",
+ "--prune",
+ "origin",
+ `+refs/pull/${input.pullNumber}/head:refs/pull/${input.pullNumber}/head`,
+ "+refs/heads/*:refs/heads/*"
+ ]);
+
+ rmSync(worktreePath, { recursive: true, force: true });
+ run("git", ["--git-dir", mirrorPath, "worktree", "prune"]);
+ run("git", [
+ "--git-dir",
+ mirrorPath,
+ "worktree",
+ "add",
+ "--detach",
+ worktreePath,
+ `refs/pull/${input.pullNumber}/head`
+ ]);
+
+ const actualHeadSha = run("git", ["-C", worktreePath, "rev-parse", "HEAD"]).stdout.trim();
+ if (actualHeadSha !== input.expectedHeadSha) {
+ throw new Error(`Worktree head mismatch for ${input.repo}#${input.pullNumber}: ${actualHeadSha} !== ${input.expectedHeadSha}`);
+ }
+
+ return { path: worktreePath, headSha: actualHeadSha };
+}
+
+export function assertGitClean(worktreePath: string): void {
+ run("git", ["-C", worktreePath, "diff", "--exit-code"]);
+ run("git", ["-C", worktreePath, "diff", "--cached", "--exit-code"]);
+ const status = run("git", ["-C", worktreePath, "status", "--porcelain=v1", "--untracked-files=all"]).stdout.trim();
+ if (status) {
+ throw new Error(`Worktree has untracked or modified files after review:\n${status}`);
+ }
+}
+
+function existsAsGitMirror(path: string): boolean {
+ const result = spawnSync("git", ["--git-dir", path, "rev-parse", "--is-bare-repository"], { encoding: "utf8" });
+ return result.status === 0 && result.stdout.trim() === "true";
+}
+
+function run(command: string, args: string[]): { stdout: string; stderr: string } {
+ const result = spawnSync(command, args, {
+ encoding: "utf8",
+ maxBuffer: 10 * 1024 * 1024
+ });
+ if (result.status !== 0) {
+ throw new Error(`${command} ${args.join(" ")} failed: ${result.stderr || result.stdout}`);
+ }
+ return { stdout: result.stdout, stderr: result.stderr };
+}
diff --git a/src/github.ts b/src/github.ts
new file mode 100644
index 00000000..6ed3502b
--- /dev/null
+++ b/src/github.ts
@@ -0,0 +1,156 @@
+import { createSign } from "node:crypto";
+import { readFileSync } from "node:fs";
+import type { PullFilePatch, PullRequestSummary, ReviewComment, ReviewEvent } from "./types.js";
+
+export interface GitHubApiOptions {
+ appId?: string;
+ privateKeyPath?: string;
+ token?: string;
+ apiBaseUrl?: string;
+}
+
+export class GitHubApi {
+ private readonly appId?: string;
+ private readonly privateKey?: string;
+ private readonly token?: string;
+ private readonly apiBaseUrl: string;
+ private installationTokens = new Map();
+
+ constructor(options: GitHubApiOptions) {
+ this.appId = options.appId;
+ this.privateKey = options.privateKeyPath ? readFileSync(options.privateKeyPath, "utf8") : undefined;
+ this.token = options.token;
+ this.apiBaseUrl = options.apiBaseUrl ?? "https://api.github.com";
+ }
+
+ canPostAsApp(): boolean {
+ return Boolean(this.appId && this.privateKey);
+ }
+
+ async listOpenPulls(repo: string): Promise {
+ return this.request(`/repos/${repo}/pulls?state=open&per_page=100`, {
+ token: await this.getReadToken(repo)
+ });
+ }
+
+ async getPull(repo: string, pullNumber: number): Promise {
+ return this.request(`/repos/${repo}/pulls/${pullNumber}`, {
+ token: await this.getReadToken(repo)
+ });
+ }
+
+ async listPullFiles(repo: string, pullNumber: number): Promise {
+ const files: PullFilePatch[] = [];
+ for (let page = 1; ; page += 1) {
+ const chunk = await this.request(
+ `/repos/${repo}/pulls/${pullNumber}/files?per_page=100&page=${page}`,
+ { token: await this.getReadToken(repo) }
+ );
+ files.push(...chunk);
+ if (chunk.length < 100) return files;
+ }
+ }
+
+ async createReview(input: {
+ repo: string;
+ pullNumber: number;
+ event: ReviewEvent;
+ body: string;
+ comments: ReviewComment[];
+ }): Promise<{ html_url?: string; id: number }> {
+ if (!this.canPostAsApp()) {
+ throw new Error("GitHub App credentials are required before posting reviews.");
+ }
+ const token = await this.getInstallationToken(input.repo);
+ return this.request<{ html_url?: string; id: number }>(`/repos/${input.repo}/pulls/${input.pullNumber}/reviews`, {
+ method: "POST",
+ token,
+ body: {
+ event: input.event,
+ body: input.body,
+ comments: input.comments.map((comment) => ({
+ path: comment.path,
+ line: comment.line,
+ side: comment.side,
+ body: comment.body
+ }))
+ }
+ });
+ }
+
+ private async getInstallationToken(repo: string): Promise {
+ const cached = this.installationTokens.get(repo);
+ if (cached && cached.expiresAt > Date.now() + 60_000) return cached.token;
+ if (!this.appId || !this.privateKey) throw new Error("Missing GitHub App credentials.");
+
+ const jwt = createAppJwt(this.appId, this.privateKey);
+ const installation = await this.request<{ id: number }>(`/repos/${repo}/installation`, { token: jwt });
+ const token = await this.request<{ token: string; expires_at: string }>(
+ `/app/installations/${installation.id}/access_tokens`,
+ { method: "POST", token: jwt, body: { repositories: [repo.split("/")[1]] } }
+ );
+
+ this.installationTokens.set(repo, {
+ token: token.token,
+ expiresAt: new Date(token.expires_at).getTime()
+ });
+ return token.token;
+ }
+
+ private async getReadToken(repo: string): Promise {
+ if (this.canPostAsApp()) return this.getInstallationToken(repo);
+ return this.token;
+ }
+
+ private async request(
+ path: string,
+ options: { method?: string; token?: string; body?: unknown } = {}
+ ): Promise {
+ const token = options.token ?? this.token;
+ let response: Response;
+ try {
+ response = await fetch(`${this.apiBaseUrl}${path}`, {
+ method: options.method ?? "GET",
+ headers: {
+ Accept: "application/vnd.github+json",
+ "X-GitHub-Api-Version": "2022-11-28",
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ ...(options.body === undefined ? {} : { "Content-Type": "application/json" })
+ },
+ body: options.body === undefined ? undefined : JSON.stringify(options.body)
+ });
+ } catch (error) {
+ throw new Error(`GitHub API fetch failed for ${path}: ${describeFetchError(error)}`);
+ }
+
+ if (!response.ok) {
+ const text = await response.text();
+ throw new Error(`GitHub API ${response.status} ${response.statusText} for ${path}: ${text.slice(0, 400)}`);
+ }
+
+ return (await response.json()) as T;
+ }
+}
+
+function describeFetchError(error: unknown): string {
+ if (!(error instanceof Error)) return String(error);
+ const cause = "cause" in error ? (error as Error & { cause?: unknown }).cause : undefined;
+ const causeMessage = cause instanceof Error ? `${cause.name}: ${cause.message}` : cause ? String(cause) : "";
+ return causeMessage ? `${error.message}; cause=${causeMessage}` : error.message;
+}
+
+export function createAppJwt(appId: string, privateKey: string, nowSeconds = Math.floor(Date.now() / 1000)): string {
+ const header = base64UrlJson({ alg: "RS256", typ: "JWT" });
+ const payload = base64UrlJson({
+ iat: nowSeconds - 60,
+ exp: nowSeconds + 540,
+ iss: appId
+ });
+ const unsigned = `${header}.${payload}`;
+ const signature = createSign("RSA-SHA256").update(unsigned).sign(privateKey, "base64url");
+ return `${unsigned}.${signature}`;
+}
+
+function base64UrlJson(value: unknown): string {
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
+}
diff --git a/src/secrets.ts b/src/secrets.ts
new file mode 100644
index 00000000..613d2aed
--- /dev/null
+++ b/src/secrets.ts
@@ -0,0 +1,20 @@
+const SECRET_PATTERNS: RegExp[] = [
+ /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g,
+ /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g,
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}\b/gi,
+ /\b(?:api[_-]?key|token|secret|password|cookie|session)\b\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{16,}/gi,
+ /\b[A-Za-z0-9]{3,}[-_](?:secret|token|password|cookie)[-_][A-Za-z0-9_-]{3,}\b/gi,
+ /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/g
+];
+
+export function containsSecretLikeText(input: string): boolean {
+ return SECRET_PATTERNS.some((pattern) => {
+ pattern.lastIndex = 0;
+ return pattern.test(input);
+ });
+}
+
+export function redactSecrets(input: string): string {
+ return SECRET_PATTERNS.reduce((text, pattern) => text.replace(pattern, "[redacted-secret]"), input);
+}
diff --git a/src/state.ts b/src/state.ts
new file mode 100644
index 00000000..0e2e3cbd
--- /dev/null
+++ b/src/state.ts
@@ -0,0 +1,67 @@
+import { mkdirSync } from "node:fs";
+import { dirname } from "node:path";
+import { DatabaseSync } from "node:sqlite";
+import type { ReviewEvent } from "./types.js";
+
+export type ProcessedStatus = "dry_run" | "posted" | "skipped" | "failed";
+
+export interface ProcessedReviewRecord {
+ repo: string;
+ pullNumber: number;
+ headSha: string;
+ status: ProcessedStatus;
+ event?: ReviewEvent;
+ reviewUrl?: string;
+ error?: string;
+}
+
+export class ReviewStateStore {
+ private readonly db: DatabaseSync;
+
+ constructor(dbPath: string) {
+ mkdirSync(dirname(dbPath), { recursive: true });
+ this.db = new DatabaseSync(dbPath);
+ this.db.exec(`
+ create table if not exists processed_reviews (
+ repo text not null,
+ pull_number integer not null,
+ head_sha text not null,
+ status text not null,
+ event text,
+ review_url text,
+ error text,
+ created_at text not null default (datetime('now')),
+ primary key (repo, pull_number, head_sha)
+ );
+ `);
+ }
+
+ hasProcessed(repo: string, pullNumber: number, headSha: string): boolean {
+ const row = this.db
+ .prepare("select 1 from processed_reviews where repo = ? and pull_number = ? and head_sha = ? limit 1")
+ .get(repo, pullNumber, headSha);
+ return Boolean(row);
+ }
+
+ recordProcessed(record: ProcessedReviewRecord): void {
+ this.db
+ .prepare(
+ `insert or replace into processed_reviews
+ (repo, pull_number, head_sha, status, event, review_url, error, created_at)
+ values (?, ?, ?, ?, ?, ?, ?, datetime('now'))`
+ )
+ .run(
+ record.repo,
+ record.pullNumber,
+ record.headSha,
+ record.status,
+ record.event ?? null,
+ record.reviewUrl ?? null,
+ record.error ?? null
+ );
+ }
+
+ close(): void {
+ this.db.close();
+ }
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 00000000..73dd3bb8
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,61 @@
+export type Severity = "P0" | "P1" | "P2" | "P3";
+
+export type ReviewEvent = "COMMENT" | "REQUEST_CHANGES";
+
+export interface Finding {
+ severity: Severity;
+ path: string;
+ line: number;
+ title: string;
+ body: string;
+ confidence: number;
+ why_this_matters?: string;
+}
+
+export interface DroppedFinding extends Partial {
+ reason: string;
+}
+
+export interface PullFilePatch {
+ filename: string;
+ patch?: string | null;
+}
+
+export interface PullRequestSummary {
+ number: number;
+ title: string;
+ draft: boolean;
+ head: {
+ sha: string;
+ ref: string;
+ repo?: {
+ full_name: string;
+ clone_url?: string;
+ } | null;
+ };
+ base: {
+ sha: string;
+ ref: string;
+ repo: {
+ full_name: string;
+ clone_url?: string;
+ };
+ };
+ html_url: string;
+}
+
+export interface ReviewComment {
+ path: string;
+ line: number;
+ side: "RIGHT";
+ body: string;
+ severity: Severity;
+ title: string;
+}
+
+export interface ReviewPlan {
+ event: ReviewEvent;
+ comments: ReviewComment[];
+ dropped: DroppedFinding[];
+ summary: string;
+}
diff --git a/src/worker.ts b/src/worker.ts
new file mode 100644
index 00000000..461d80d7
--- /dev/null
+++ b/src/worker.ts
@@ -0,0 +1,192 @@
+import { mkdirSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+import { loadConfig, type BotConfig } from "./config.js";
+import { validateFindingLocations } from "./diff.js";
+import { decideReviewEvent, normalizeFindingsForReview } from "./findings.js";
+import { assertGitClean, preparePullWorktree } from "./git.js";
+import { GitHubApi } from "./github.js";
+import { redactSecrets } from "./secrets.js";
+import { ReviewStateStore } from "./state.js";
+import { buildReviewPrompt, runZCodeReview } from "./zcode.js";
+import type { PullRequestSummary, ReviewPlan } from "./types.js";
+
+export interface RunOnceOptions {
+ configPath?: string;
+ dryRun: boolean;
+ repo?: string;
+ pullNumber?: number;
+ useZCode?: boolean;
+}
+
+export interface RunOnceResult {
+ reposScanned: number;
+ pullsSeen: number;
+ reviewed: number;
+ skippedDraft: number;
+ skippedCanary: number;
+ skippedProcessed: number;
+}
+
+type ReviewPullResult = "reviewed" | "skipped_draft" | "skipped_canary" | "skipped_processed";
+
+export async function runOnce(options: RunOnceOptions): Promise {
+ const config = loadConfig(options.configPath);
+ const github = new GitHubApi(config.github);
+ const state = new ReviewStateStore(config.statePath);
+ const result: RunOnceResult = {
+ reposScanned: 0,
+ pullsSeen: 0,
+ reviewed: 0,
+ skippedDraft: 0,
+ skippedCanary: 0,
+ skippedProcessed: 0
+ };
+ try {
+ const repos = options.repo ? [options.repo] : config.pilotRepos;
+ for (const repo of repos) {
+ result.reposScanned += 1;
+ const pulls = options.pullNumber
+ ? [await github.getPull(repo, options.pullNumber)]
+ : await github.listOpenPulls(repo);
+ result.pullsSeen += pulls.length;
+ for (const pull of pulls) {
+ const status = await reviewPull({
+ config,
+ github,
+ state,
+ repo,
+ pull,
+ dryRun: options.dryRun,
+ useZCode: options.useZCode ?? true
+ });
+ if (status === "reviewed") result.reviewed += 1;
+ if (status === "skipped_draft") result.skippedDraft += 1;
+ if (status === "skipped_canary") result.skippedCanary += 1;
+ if (status === "skipped_processed") result.skippedProcessed += 1;
+ }
+ }
+ return result;
+ } finally {
+ state.close();
+ }
+}
+
+export async function reviewPull(input: {
+ config: BotConfig;
+ github: GitHubApi;
+ state: ReviewStateStore;
+ repo: string;
+ pull: PullRequestSummary;
+ dryRun: boolean;
+ useZCode: boolean;
+}): Promise {
+ const { config, github, state, repo, pull } = input;
+ if (config.skipDrafts && pull.draft) return "skipped_draft";
+ if (!isCanaryAllowed(config, repo, pull.number)) return "skipped_canary";
+ if (state.hasProcessed(repo, pull.number, pull.head.sha)) return "skipped_processed";
+
+ const evidenceDir = join(config.evidenceDir, localDateFolder(), repo.replace("/", "__"), `pr-${pull.number}`, pull.head.sha);
+ mkdirSync(evidenceDir, { recursive: true });
+
+ const files = await github.listPullFiles(repo, pull.number);
+ const worktree = preparePullWorktree({
+ repo,
+ pullNumber: pull.number,
+ expectedHeadSha: pull.head.sha,
+ workRoot: config.workRoot
+ });
+
+ const prompt = buildReviewPrompt({ repo, pull, files, maxPatchBytes: config.zcode.maxPatchBytes });
+ writeFileSync(join(evidenceDir, "review-prompt.txt"), redactSecrets(prompt));
+
+ const zcodeResult = input.useZCode
+ ? runZCodeReview({
+ cwd: worktree.path,
+ prompt,
+ cliPath: config.zcode.cliPath,
+ appConfigPath: config.zcode.appConfigPath,
+ model: config.zcode.model,
+ providerId: config.zcode.providerId,
+ evidenceDir,
+ timeoutMs: config.zcode.timeoutMs,
+ retryMaxRetries: config.zcode.retryMaxRetries
+ })
+ : { findings: [], droppedFromSchema: [], rawResponse: "{\"findings\":[]}" };
+
+ assertGitClean(worktree.path);
+
+ const located = validateFindingLocations(zcodeResult.findings, files);
+ const normalized = normalizeFindingsForReview(located.valid, { maxInlineComments: 25 });
+ const comments = normalized.comments;
+ const dropped = sanitizeDroppedFindings([...zcodeResult.droppedFromSchema, ...located.dropped, ...normalized.dropped]);
+ const event = decideReviewEvent(comments);
+ const plan: ReviewPlan = {
+ event,
+ comments,
+ dropped,
+ summary: buildSummary({ repo, pull, comments, dropped, dryRun: input.dryRun })
+ };
+
+ writeFileSync(join(evidenceDir, "review-plan.json"), `${JSON.stringify(plan, null, 2)}\n`);
+
+ if (input.dryRun) {
+ state.recordProcessed({ repo, pullNumber: pull.number, headSha: pull.head.sha, status: "dry_run", event });
+ return "reviewed";
+ }
+
+ const reviewGithub = new GitHubApi(config.github);
+ const review = await reviewGithub.createReview({
+ repo,
+ pullNumber: pull.number,
+ event,
+ body: plan.summary,
+ comments
+ });
+ state.recordProcessed({
+ repo,
+ pullNumber: pull.number,
+ headSha: pull.head.sha,
+ status: "posted",
+ event,
+ reviewUrl: review.html_url
+ });
+ return "reviewed";
+}
+
+export function isCanaryAllowed(config: Pick, repo: string, pullNumber: number): boolean {
+ if (!config.canaryPulls || config.canaryPulls.length === 0) return true;
+ return new Set(config.canaryPulls).has(`${repo}#${pullNumber}`);
+}
+
+export function localDateFolder(now = new Date()): string {
+ const year = String(now.getFullYear()).padStart(4, "0");
+ const month = String(now.getMonth() + 1).padStart(2, "0");
+ const day = String(now.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+}
+
+function sanitizeDroppedFindings(dropped: ReviewPlan["dropped"]): ReviewPlan["dropped"] {
+ return dropped.map((finding) => ({
+ ...finding,
+ ...(typeof finding.title === "string" ? { title: redactSecrets(finding.title) } : {}),
+ ...(typeof finding.body === "string" ? { body: redactSecrets(finding.body) } : {}),
+ ...(typeof finding.why_this_matters === "string"
+ ? { why_this_matters: redactSecrets(finding.why_this_matters) }
+ : {})
+ }));
+}
+
+function buildSummary(input: {
+ repo: string;
+ pull: PullRequestSummary;
+ comments: { severity: string }[];
+ dropped: { reason: string }[];
+ dryRun: boolean;
+}): string {
+ const p0p1 = input.comments.filter((comment) => comment.severity === "P0" || comment.severity === "P1").length;
+ return [
+ `evaOS ZCode review ${input.dryRun ? "dry run" : "result"} for ${input.repo}#${input.pull.number} at ${input.pull.head.sha}.`,
+ `Inline comments: ${input.comments.length}. High-severity comments: ${p0p1}. Dropped findings: ${input.dropped.length}.`,
+ "Pilot policy: this bot never approves PRs; it requests changes only for validated P0/P1 findings."
+ ].join("\n\n");
+}
diff --git a/src/zcode-env.ts b/src/zcode-env.ts
new file mode 100644
index 00000000..85d9ddc5
--- /dev/null
+++ b/src/zcode-env.ts
@@ -0,0 +1,81 @@
+import { readFileSync } from "node:fs";
+
+interface ResolveZCodeProviderEnvOptions {
+ appConfigPath: string;
+ model: string;
+ providerId?: string;
+}
+
+export interface ResolvedZCodeProviderEnv {
+ ZCODE_MODEL: string;
+ ZCODE_BASE_URL: string;
+ ZCODE_API_KEY: string;
+ redacted: {
+ providerId: string;
+ model: string;
+ baseURL: string;
+ apiKey: string;
+ };
+}
+
+export function resolveZCodeProviderEnv(options: ResolveZCodeProviderEnvOptions): ResolvedZCodeProviderEnv {
+ const config = JSON.parse(readFileSync(options.appConfigPath, "utf8")) as {
+ provider?: Record;
+ };
+ const providers = Object.entries(config.provider ?? {});
+ const match = providers.find(([providerId, provider]) => {
+ if (options.providerId && providerId !== options.providerId) return false;
+ return Boolean(
+ provider.enabled &&
+ provider.options?.apiKey &&
+ provider.options.baseURL &&
+ provider.models &&
+ Object.prototype.hasOwnProperty.call(provider.models, options.model)
+ );
+ });
+
+ if (!match) {
+ throw new Error(`No enabled ZCode provider found for ${options.providerId ?? "any provider"}/${options.model}`);
+ }
+
+ const [providerId, provider] = match;
+ const providerOptions = provider.options;
+ if (!providerOptions?.apiKey || !providerOptions.baseURL) {
+ throw new Error(`Selected ZCode provider ${providerId} is missing apiKey or baseURL.`);
+ }
+
+ return {
+ ZCODE_MODEL: `${providerId}/${options.model}`,
+ ZCODE_BASE_URL: providerOptions.baseURL,
+ ZCODE_API_KEY: providerOptions.apiKey,
+ redacted: {
+ providerId,
+ model: options.model,
+ baseURL: providerOptions.baseURL,
+ apiKey: `[redacted len=${providerOptions.apiKey.length}]`
+ }
+ };
+}
+
+export function buildZCodeRuntimeEnv(options: {
+ baseEnv: Record;
+ providerEnv: ResolvedZCodeProviderEnv;
+ retryMaxRetries: number;
+}): Record {
+ return {
+ ...options.baseEnv,
+ ZCODE_MODEL: options.providerEnv.ZCODE_MODEL,
+ ZCODE_BASE_URL: options.providerEnv.ZCODE_BASE_URL,
+ ZCODE_API_KEY: options.providerEnv.ZCODE_API_KEY,
+ ZCODE_MODEL_RETRY_MAX_RETRIES: String(Math.max(0, Math.floor(options.retryMaxRetries)))
+ };
+}
+
+interface ZCodeProviderConfig {
+ enabled?: boolean;
+ options?: {
+ apiKey?: string;
+ baseURL?: string;
+ };
+ models?: Record;
+}
diff --git a/src/zcode.ts b/src/zcode.ts
new file mode 100644
index 00000000..c7d3c284
--- /dev/null
+++ b/src/zcode.ts
@@ -0,0 +1,277 @@
+import { spawnSync } from "node:child_process";
+import { existsSync, mkdirSync, readFileSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+import { parseFindings } from "./findings.js";
+import { redactSecrets } from "./secrets.js";
+import { buildZCodeRuntimeEnv, resolveZCodeProviderEnv } from "./zcode-env.js";
+import type { Finding, PullFilePatch, PullRequestSummary } from "./types.js";
+
+export interface ZCodeReviewResult {
+ findings: Finding[];
+ droppedFromSchema: ReturnType["dropped"];
+ rawResponse: string;
+}
+
+export function buildReviewPrompt(input: {
+ repo: string;
+ pull: PullRequestSummary;
+ files: PullFilePatch[];
+ maxPatchBytes?: number;
+}): string {
+ const fileList = input.files.map((file) => `- ${file.filename}`).join("\n");
+ let remainingPatchBytes = input.maxPatchBytes ?? 80_000;
+ const patches = input.files
+ .map((file) => {
+ const rawPatch = file.patch ?? "[binary or too large for GitHub patch]";
+ const patch = truncateToBudget(rawPatch, remainingPatchBytes);
+ remainingPatchBytes = Math.max(0, remainingPatchBytes - Buffer.byteLength(patch));
+ return `### ${file.filename}\n\n\`\`\`diff\n${patch}\n\`\`\``;
+ })
+ .join("\n\n");
+
+ return [
+ "You are evaOS Code Review Bot. Review this pull request aggressively for correctness, security, data loss, CI-breaking behavior, Unity/game regression risk, and missing high-signal tests.",
+ "Do not modify files. Do not run project tests, package scripts, builds, app commands, or arbitrary PR code.",
+ "Do not call Bash or shell commands. If more context is needed, use read-only file inspection only. If that is impossible, return no findings rather than executing code.",
+ "Only inspect the checkout and the diff provided below.",
+ "Return JSON only, with shape: {\"findings\":[{\"severity\":\"P0|P1|P2|P3\",\"path\":\"relative/file\",\"line\":123,\"title\":\"short title\",\"body\":\"specific actionable explanation\",\"confidence\":0.0,\"why_this_matters\":\"optional\"}],\"summary\":\"short review summary\"}.",
+ "Use P0/P1 only for validated correctness, security, data-loss, CI-breaking, or release-regression issues. Prefer no finding over speculative noise.",
+ "Every finding must point at a RIGHT-side line in the current diff.",
+ "",
+ `Repository: ${input.repo}`,
+ `Pull request: #${input.pull.number} ${input.pull.title}`,
+ `Head SHA: ${input.pull.head.sha}`,
+ "",
+ "Files:",
+ fileList,
+ "",
+ "Diff:",
+ patches
+ ].join("\n");
+}
+
+export function runZCodeReview(input: {
+ cwd: string;
+ prompt: string;
+ cliPath: string;
+ appConfigPath: string;
+ model: string;
+ providerId?: string;
+ evidenceDir?: string;
+ timeoutMs?: number;
+ retryMaxRetries?: number;
+}): ZCodeReviewResult {
+ const zcodeEnv = resolveZCodeProviderEnv({
+ appConfigPath: input.appConfigPath,
+ model: input.model,
+ providerId: input.providerId
+ });
+
+ const prompts = [
+ input.prompt,
+ buildStrictJsonRetryPrompt(input.prompt)
+ ];
+ let lastParseError: unknown;
+
+ for (let attempt = 1; attempt <= prompts.length; attempt += 1) {
+ const result = withTemporaryZCodeReviewPolicy(input.cwd, input.evidenceDir, () =>
+ spawnSync(process.execPath, [
+ input.cliPath,
+ "--cwd",
+ input.cwd,
+ "--mode",
+ "plan",
+ "--json",
+ "--no-browser",
+ "--prompt",
+ prompts[attempt - 1]!
+ ], {
+ env: buildZCodeRuntimeEnv({
+ baseEnv: process.env,
+ providerEnv: zcodeEnv,
+ retryMaxRetries: input.retryMaxRetries ?? 0
+ }),
+ encoding: "utf8",
+ maxBuffer: 20 * 1024 * 1024,
+ timeout: input.timeoutMs ?? 180_000
+ })
+ );
+
+ const stdout = redactSecrets(result.stdout.replaceAll(zcodeEnv.ZCODE_API_KEY, "[redacted-secret]"));
+ const stderr = redactSecrets(result.stderr.replaceAll(zcodeEnv.ZCODE_API_KEY, "[redacted-secret]"));
+ if (input.evidenceDir) {
+ mkdirSync(input.evidenceDir, { recursive: true });
+ writeFileSync(join(input.evidenceDir, `zcode-attempt-${attempt}-stdout.jsonl`), stdout);
+ writeFileSync(join(input.evidenceDir, `zcode-attempt-${attempt}-stderr.txt`), stderr);
+ writeFileSync(join(input.evidenceDir, "zcode-last-stdout.jsonl"), stdout);
+ writeFileSync(join(input.evidenceDir, "zcode-last-stderr.txt"), stderr);
+ }
+
+ if (result.status !== 0) {
+ if (result.error) {
+ throw new Error(`ZCode failed before completion: ${result.error.message}`);
+ }
+ throw new Error(`ZCode failed with status ${result.status}: ${stderr || stdout.slice(0, 1000)}`);
+ }
+
+ try {
+ const rawResponse = extractZCodeResponse(result.stdout);
+ const parsed = JSON.parse(extractJsonObject(rawResponse));
+ const { findings, dropped } = parseFindings(parsed);
+ return { findings, droppedFromSchema: dropped, rawResponse };
+ } catch (error) {
+ lastParseError = error;
+ }
+ }
+
+ throw new Error(
+ `ZCode response did not contain a parseable JSON review after ${prompts.length} attempts: ${
+ lastParseError instanceof Error ? lastParseError.message : String(lastParseError)
+ }`
+ );
+}
+
+function buildStrictJsonRetryPrompt(originalPrompt: string): string {
+ return [
+ "Your previous review output was rejected because it was not valid JSON.",
+ "Repeat the review and return ONLY the required JSON object. Do not include markdown, prose, analysis, confidence narration, or code fences.",
+ "The response must parse with JSON.parse and must have this exact top-level shape:",
+ "{\"findings\":[{\"severity\":\"P0|P1|P2|P3\",\"path\":\"relative/file\",\"line\":123,\"title\":\"short title\",\"body\":\"specific actionable explanation\",\"confidence\":0.0,\"why_this_matters\":\"optional\"}],\"summary\":\"short review summary\"}",
+ "If you cannot produce a finding with a current RIGHT-side diff line, return {\"findings\":[],\"summary\":\"No validated current-diff findings.\"}.",
+ "",
+ originalPrompt
+ ].join("\n");
+}
+
+export function withTemporaryZCodeReviewPolicy(cwd: string, evidenceDir: string | undefined, run: () => T): T {
+ const configDir = join(cwd, ".zcode");
+ const configPath = join(configDir, "config.json");
+ const hadConfigDir = existsSync(configDir);
+ const originalConfig = existsSync(configPath)
+ ? { contents: readFileSync(configPath, "utf8"), mode: statSync(configPath).mode }
+ : null;
+ const policy = buildZCodeReviewPolicy();
+
+ mkdirSync(configDir, { recursive: true });
+ writeFileSync(configPath, `${JSON.stringify(policy, null, 2)}\n`, { mode: 0o600 });
+ if (evidenceDir) {
+ mkdirSync(evidenceDir, { recursive: true });
+ writeFileSync(join(evidenceDir, "zcode-review-policy.json"), `${JSON.stringify(policy, null, 2)}\n`);
+ }
+
+ try {
+ return run();
+ } finally {
+ if (originalConfig) {
+ writeFileSync(configPath, originalConfig.contents, { mode: originalConfig.mode });
+ } else {
+ rmSync(configPath, { force: true });
+ if (!hadConfigDir) {
+ try {
+ rmdirSync(configDir);
+ } catch {
+ // Leave a non-empty directory in place; the clean-worktree guard will catch it.
+ }
+ }
+ }
+ }
+}
+
+function buildZCodeReviewPolicy(): unknown {
+ return {
+ permission: {
+ mode: "build",
+ allowedTools: ["Read", "Grep", "Glob", "LS"],
+ disallowedTools: [
+ "Bash",
+ "Shell",
+ "Edit",
+ "Write",
+ "MultiEdit",
+ "NotebookEdit",
+ "WebFetch",
+ "WebSearch",
+ "Task",
+ "Agent",
+ "Workflow",
+ "SendMessage"
+ ],
+ autoApproveHighRisk: false,
+ allowMediumRiskInAuto: false
+ },
+ features: {
+ subagent: false,
+ mcp: false,
+ memory: false,
+ skill: false
+ },
+ memory: {
+ use: false,
+ write: false,
+ autoConsolidate: false
+ },
+ toolConcurrency: {
+ maxConcurrency: 1
+ }
+ };
+}
+
+function truncateToBudget(text: string, maxBytes: number): string {
+ if (maxBytes <= 0) return "[patch omitted: prompt budget exhausted]";
+ const bytes = Buffer.byteLength(text);
+ if (bytes <= maxBytes) return text;
+ return `${text.slice(0, maxBytes)}\n[patch truncated to fit prompt budget]`;
+}
+
+export function extractZCodeResponse(stdout: string): string {
+ try {
+ const parsed = JSON.parse(stdout) as { response?: unknown };
+ if (typeof parsed.response === "string") return parsed.response;
+ } catch {
+ // Fall through to JSONL parsing for older ZCode CLI builds.
+ }
+
+ const candidates = stdout
+ .split(/\n+/)
+ .map((line) => line.trim())
+ .filter(Boolean)
+ .map((line) => {
+ try {
+ return JSON.parse(line) as { response?: unknown };
+ } catch {
+ return null;
+ }
+ })
+ .filter((value): value is { response?: unknown } => Boolean(value));
+
+ const response = [...candidates].reverse().find((value) => typeof value.response === "string")?.response;
+ if (typeof response !== "string") throw new Error("ZCode JSON output did not include a string response.");
+ return response;
+}
+
+export function extractJsonObject(text: string): string {
+ const fencedMatches = [...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)];
+ for (const fenced of fencedMatches) {
+ const candidate = fenced[1]!.trim();
+ if (isReviewJsonObject(candidate)) return candidate;
+ }
+
+ const starts = [...text.matchAll(/\{/g)].map((match) => match.index).filter((index): index is number => index !== undefined);
+ const ends = [...text.matchAll(/\}/g)].map((match) => match.index).filter((index): index is number => index !== undefined);
+ for (const start of starts.reverse()) {
+ for (const end of ends.filter((index) => index > start).reverse()) {
+ const candidate = text.slice(start, end + 1).trim();
+ if (isReviewJsonObject(candidate)) return candidate;
+ }
+ }
+ throw new Error("ZCode response did not contain a parseable JSON review object.");
+}
+
+function isReviewJsonObject(candidate: string): boolean {
+ try {
+ const parsed = JSON.parse(candidate) as { findings?: unknown };
+ return typeof parsed === "object" && parsed !== null && Array.isArray(parsed.findings);
+ } catch {
+ return false;
+ }
+}
diff --git a/tests/canary.test.ts b/tests/canary.test.ts
new file mode 100644
index 00000000..eaa811b4
--- /dev/null
+++ b/tests/canary.test.ts
@@ -0,0 +1,26 @@
+import { describe, expect, it } from "vitest";
+import { isCanaryAllowed, localDateFolder } from "../src/worker.js";
+
+describe("canary PR allowlist", () => {
+ it("allows all PRs when no canary list is configured", () => {
+ expect(isCanaryAllowed({}, "electricsheephq/WorldOS", 1161)).toBe(true);
+ expect(isCanaryAllowed({ canaryPulls: [] }, "100yenadmin/evaOS-GUI", 497)).toBe(true);
+ });
+
+ it("allows only exact repo and pull-number matches when configured", () => {
+ const config = {
+ canaryPulls: ["electricsheephq/WorldOS#1161", "100yenadmin/evaOS-GUI#497"]
+ };
+
+ expect(isCanaryAllowed(config, "electricsheephq/WorldOS", 1161)).toBe(true);
+ expect(isCanaryAllowed(config, "100yenadmin/evaOS-GUI", 497)).toBe(true);
+ expect(isCanaryAllowed(config, "electricsheephq/WorldOS", 1185)).toBe(false);
+ expect(isCanaryAllowed(config, "100yenadmin/evaOS-GUI", 410)).toBe(false);
+ });
+});
+
+describe("local evidence date folders", () => {
+ it("uses the process local date instead of UTC ISO date folders", () => {
+ expect(localDateFolder(new Date(2026, 6, 1, 0, 5, 0))).toBe("2026-07-01");
+ });
+});
diff --git a/tests/daemon-log.test.ts b/tests/daemon-log.test.ts
new file mode 100644
index 00000000..8a3b59a0
--- /dev/null
+++ b/tests/daemon-log.test.ts
@@ -0,0 +1,44 @@
+import { describe, expect, it } from "vitest";
+import { formatDaemonLog } from "../src/daemon-log.js";
+
+describe("daemon heartbeat logs", () => {
+ it("emits structured JSON with cycle and result counters", () => {
+ const log = JSON.parse(formatDaemonLog({
+ event: "daemon_cycle_complete",
+ cycle: 2,
+ dryRun: true,
+ result: {
+ reposScanned: 2,
+ pullsSeen: 4,
+ reviewed: 0,
+ skippedDraft: 1,
+ skippedCanary: 2,
+ skippedProcessed: 1
+ }
+ }, new Date("2026-07-01T00:00:00.000Z")));
+
+ expect(log).toMatchObject({
+ ts: "2026-07-01T00:00:00.000Z",
+ level: "info",
+ event: "daemon_cycle_complete",
+ cycle: 2,
+ dryRun: true,
+ result: {
+ reposScanned: 2,
+ reviewed: 0,
+ skippedProcessed: 1
+ }
+ });
+ });
+
+ it("redacts secret-looking strings before they reach launchd logs", () => {
+ const log = formatDaemonLog({
+ event: "daemon_cycle_failed",
+ level: "error",
+ error: "request failed with ghp_1234567890abcdefghijklmnopqrstuvwx"
+ }, new Date("2026-07-01T00:00:00.000Z"));
+
+ expect(log).toContain("[redacted-secret]");
+ expect(log).not.toContain("ghp_1234567890abcdefghijklmnopqrstuvwx");
+ });
+});
diff --git a/tests/diff.test.ts b/tests/diff.test.ts
new file mode 100644
index 00000000..21e14cad
--- /dev/null
+++ b/tests/diff.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, it } from "vitest";
+import { collectRightSideLines, validateFindingLocations } from "../src/diff.js";
+import type { Finding } from "../src/types.js";
+
+describe("diff coordinate validation", () => {
+ const files = [
+ {
+ filename: "src/game/combat.ts",
+ patch: [
+ "@@ -10,5 +10,7 @@ export function resolveTurn() {",
+ " const before = true;",
+ "-const removed = legacy();",
+ "+const added = nextFrame();",
+ "+const alsoAdded = true;",
+ " return before;",
+ "@@ -42,2 +44,3 @@ export function render() {",
+ " draw();",
+ "+flush();"
+ ].join("\n")
+ }
+ ];
+
+ it("treats only RIGHT-side diff lines as inline-commentable", () => {
+ const lines = collectRightSideLines(files);
+
+ expect(lines.get("src/game/combat.ts")).toEqual(new Set([10, 11, 12, 13, 44, 45]));
+ });
+
+ it("drops deleted, missing, and out-of-diff findings while preserving valid findings", () => {
+ const findings: Finding[] = [
+ { severity: "P1", path: "src/game/combat.ts", line: 11, title: "Valid", body: "This is valid.", confidence: 0.9 },
+ { severity: "P1", path: "src/game/combat.ts", line: 12, title: "Also valid", body: "This is also valid.", confidence: 0.9 },
+ { severity: "P1", path: "src/game/combat.ts", line: 9, title: "Deleted", body: "Cannot comment here.", confidence: 0.9 },
+ { severity: "P1", path: "src/other.ts", line: 1, title: "Missing", body: "Wrong file.", confidence: 0.9 }
+ ];
+
+ const result = validateFindingLocations(findings, files);
+
+ expect(result.valid.map((finding) => finding.title)).toEqual(["Valid", "Also valid"]);
+ expect(result.dropped).toEqual([
+ expect.objectContaining({ title: "Deleted", reason: "line_not_in_current_diff" }),
+ expect.objectContaining({ title: "Missing", reason: "file_not_in_diff" })
+ ]);
+ });
+});
diff --git a/tests/findings.test.ts b/tests/findings.test.ts
new file mode 100644
index 00000000..7e1c6406
--- /dev/null
+++ b/tests/findings.test.ts
@@ -0,0 +1,64 @@
+import { describe, expect, it } from "vitest";
+import { decideReviewEvent, normalizeFindingsForReview } from "../src/findings.js";
+import type { Finding } from "../src/types.js";
+
+describe("finding normalization and review policy", () => {
+ it("keeps validated findings, caps aggressive inline comments, and sorts by severity", () => {
+ const findings: Finding[] = Array.from({ length: 30 }, (_, index) => ({
+ severity: index === 29 ? "P0" : index % 2 === 0 ? "P2" : "P3",
+ path: "a.ts",
+ line: 1,
+ title: `Finding ${index}`,
+ body: "A concrete review comment.",
+ confidence: 0.8
+ }));
+
+ const result = normalizeFindingsForReview(findings, { maxInlineComments: 25 });
+
+ expect(result.comments).toHaveLength(25);
+ expect(result.comments[0]?.severity).toBe("P0");
+ expect(result.dropped.filter((drop) => drop.reason === "comment_cap_exceeded")).toHaveLength(5);
+ });
+
+ it("drops any finding whose title or body contains secret-looking material", () => {
+ const token = ["ghp", "1234567890abcdefghijklmnopqrstuvwx"].join("_");
+ const result = normalizeFindingsForReview([
+ {
+ severity: "P1",
+ path: "a.ts",
+ line: 1,
+ title: "Leaked token",
+ body: `The raw token ${token} should never be posted.`,
+ confidence: 0.99
+ }
+ ]);
+
+ expect(result.comments).toEqual([]);
+ expect(result.dropped).toEqual([expect.objectContaining({ reason: "secret_detected" })]);
+ expect(JSON.stringify(result.dropped)).not.toContain(token);
+ });
+
+ it("drops findings that repeat hyphenated fixture tokens", () => {
+ const fixtureToken = ["super", "secret", "token"].join("-");
+ const result = normalizeFindingsForReview([
+ {
+ severity: "P1",
+ path: "scripts/check-public-sensitive-content.js",
+ line: 64,
+ title: "Scanner self-trip",
+ body: `The scanner repeats ${fixtureToken} in its own source.`,
+ confidence: 0.9
+ }
+ ]);
+
+ expect(result.comments).toEqual([]);
+ expect(result.dropped).toEqual([expect.objectContaining({ reason: "secret_detected" })]);
+ expect(JSON.stringify(result.dropped)).not.toContain(fixtureToken);
+ });
+
+ it("requests changes only for P0 or P1 findings", () => {
+ expect(decideReviewEvent([{ severity: "P2" }, { severity: "P3" }])).toBe("COMMENT");
+ expect(decideReviewEvent([{ severity: "P1" }])).toBe("REQUEST_CHANGES");
+ expect(decideReviewEvent([{ severity: "P0" }])).toBe("REQUEST_CHANGES");
+ });
+});
diff --git a/tests/git-clean.test.ts b/tests/git-clean.test.ts
new file mode 100644
index 00000000..d67af242
--- /dev/null
+++ b/tests/git-clean.test.ts
@@ -0,0 +1,24 @@
+import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { spawnSync } from "node:child_process";
+import { afterEach, describe, expect, it } from "vitest";
+import { assertGitClean } from "../src/git.js";
+
+describe("assertGitClean", () => {
+ const roots: string[] = [];
+
+ afterEach(() => {
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
+ });
+
+ it("fails on untracked files left by a review run", () => {
+ const root = mkdtempSync(join(tmpdir(), "git-clean-"));
+ roots.push(root);
+ const init = spawnSync("git", ["init"], { cwd: root, encoding: "utf8" });
+ expect(init.status).toBe(0);
+ writeFileSync(join(root, "left-behind.txt"), "mutation\n");
+
+ expect(() => assertGitClean(root)).toThrow(/untracked or modified files/);
+ });
+});
diff --git a/tests/github-app-read.test.ts b/tests/github-app-read.test.ts
new file mode 100644
index 00000000..e38e6850
--- /dev/null
+++ b/tests/github-app-read.test.ts
@@ -0,0 +1,55 @@
+import { generateKeyPairSync } from "node:crypto";
+import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { GitHubApi } from "../src/github.js";
+
+describe("GitHub App read authentication", () => {
+ const roots: string[] = [];
+ const originalFetch = globalThis.fetch;
+
+ afterEach(() => {
+ globalThis.fetch = originalFetch;
+ vi.restoreAllMocks();
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
+ });
+
+ it("uses installation tokens for PR read calls when App credentials are configured", async () => {
+ const root = mkdtempSync(join(tmpdir(), "github-app-read-"));
+ roots.push(root);
+ const privateKeyPath = join(root, "app.pem");
+ const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
+ writeFileSync(privateKeyPath, privateKey.export({ type: "pkcs1", format: "pem" }));
+
+ const calls: Array<{ url: string; authorization?: string }> = [];
+ globalThis.fetch = vi.fn(async (url, init) => {
+ const authorization = new Headers(init?.headers).get("authorization") ?? undefined;
+ calls.push({ url: String(url), authorization });
+ if (String(url).endsWith("/repos/owner/repo/installation")) {
+ return jsonResponse({ id: 123 });
+ }
+ if (String(url).endsWith("/app/installations/123/access_tokens")) {
+ return jsonResponse({ token: "installation-token", expires_at: "2999-01-01T00:00:00Z" });
+ }
+ if (String(url).endsWith("/repos/owner/repo/pulls?state=open&per_page=100")) {
+ return jsonResponse([]);
+ }
+ return jsonResponse({ message: "unexpected" }, 404);
+ }) as typeof fetch;
+
+ const github = new GitHubApi({ appId: "4184532", privateKeyPath, token: "fallback-token" });
+ await github.listOpenPulls("owner/repo");
+
+ const readCall = calls.find((call) => call.url.endsWith("/repos/owner/repo/pulls?state=open&per_page=100"));
+ expect(readCall?.authorization).toBe("Bearer installation-token");
+ expect(readCall?.authorization).not.toBe("Bearer fallback-token");
+ });
+});
+
+function jsonResponse(body: unknown, status = 200): Response {
+ return new Response(JSON.stringify(body), {
+ status,
+ headers: { "Content-Type": "application/json" }
+ });
+}
diff --git a/tests/secrets.test.ts b/tests/secrets.test.ts
new file mode 100644
index 00000000..a79a1b0e
--- /dev/null
+++ b/tests/secrets.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it } from "vitest";
+import { containsSecretLikeText, redactSecrets } from "../src/secrets.js";
+
+describe("secret redaction", () => {
+ it("detects and redacts hyphenated fixture tokens", () => {
+ const fixtureToken = ["super", "secret", "token"].join("-");
+ const text = `fixture contains ${fixtureToken} in source`;
+
+ expect(containsSecretLikeText(text)).toBe(true);
+ expect(redactSecrets(text)).toBe("fixture contains [redacted-secret] in source");
+ });
+
+ it("redacts raw email addresses from evidence and comments", () => {
+ const text = "Use person@example.com only via env.";
+
+ expect(containsSecretLikeText(text)).toBe(true);
+ expect(redactSecrets(text)).toBe("Use [redacted-secret] only via env.");
+ });
+});
diff --git a/tests/state.test.ts b/tests/state.test.ts
new file mode 100644
index 00000000..fe929e29
--- /dev/null
+++ b/tests/state.test.ts
@@ -0,0 +1,32 @@
+import { mkdtempSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, describe, expect, it } from "vitest";
+import { ReviewStateStore } from "../src/state.js";
+
+describe("review state store", () => {
+ const roots: string[] = [];
+
+ afterEach(() => {
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
+ });
+
+ it("deduplicates one review per repo, PR, and head SHA", () => {
+ const root = mkdtempSync(join(tmpdir(), "evaos-review-state-"));
+ roots.push(root);
+ const store = new ReviewStateStore(join(root, "state.sqlite"));
+
+ expect(store.hasProcessed("electricsheephq/WorldOS", 1205, "abc123")).toBe(false);
+ store.recordProcessed({
+ repo: "electricsheephq/WorldOS",
+ pullNumber: 1205,
+ headSha: "abc123",
+ status: "dry_run",
+ event: "COMMENT"
+ });
+
+ expect(store.hasProcessed("electricsheephq/WorldOS", 1205, "abc123")).toBe(true);
+ expect(store.hasProcessed("electricsheephq/WorldOS", 1205, "def456")).toBe(false);
+ store.close();
+ });
+});
diff --git a/tests/synthetic-review.test.ts b/tests/synthetic-review.test.ts
new file mode 100644
index 00000000..4ea6a3ae
--- /dev/null
+++ b/tests/synthetic-review.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, it } from "vitest";
+import { validateFindingLocations } from "../src/diff.js";
+import { decideReviewEvent, normalizeFindingsForReview } from "../src/findings.js";
+import type { Finding } from "../src/types.js";
+
+describe("synthetic high-severity review fixture", () => {
+ it("turns a validated P1 regression finding into REQUEST_CHANGES", () => {
+ const files = [
+ {
+ filename: "Assets/Scripts/CombatTurn.cs",
+ patch: [
+ "@@ -25,4 +25,5 @@ public void ResolveTurn() {",
+ " ApplyStartOfTurnEffects();",
+ "+player.Health = 1;",
+ " ResolveQueuedActions();",
+ " }"
+ ].join("\n")
+ }
+ ];
+ const findings: Finding[] = [
+ {
+ severity: "P1",
+ path: "Assets/Scripts/CombatTurn.cs",
+ line: 26,
+ title: "Combat health reset breaks active fights",
+ body: "The new assignment forces every player to one health during turn resolution, which can incorrectly kill or near-kill healthy players.",
+ confidence: 0.96,
+ why_this_matters: "This is a deterministic gameplay regression in the core combat loop."
+ }
+ ];
+
+ const located = validateFindingLocations(findings, files);
+ const normalized = normalizeFindingsForReview(located.valid);
+
+ expect(located.dropped).toEqual([]);
+ expect(normalized.comments).toHaveLength(1);
+ expect(decideReviewEvent(normalized.comments)).toBe("REQUEST_CHANGES");
+ });
+});
diff --git a/tests/zcode-env.test.ts b/tests/zcode-env.test.ts
new file mode 100644
index 00000000..599efc7e
--- /dev/null
+++ b/tests/zcode-env.test.ts
@@ -0,0 +1,70 @@
+import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, describe, expect, it } from "vitest";
+import { buildZCodeRuntimeEnv, resolveZCodeProviderEnv } from "../src/zcode-env.js";
+
+describe("ZCode provider env", () => {
+ const roots: string[] = [];
+
+ afterEach(() => {
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
+ });
+
+ it("derives transient CLI env from the enabled GLM provider without logging the key", () => {
+ const root = mkdtempSync(join(tmpdir(), "zcode-env-"));
+ roots.push(root);
+ const configPath = join(root, "config.json");
+ writeFileSync(
+ configPath,
+ JSON.stringify({
+ provider: {
+ "builtin:zai-coding-plan": {
+ enabled: true,
+ kind: "anthropic",
+ options: {
+ apiKey: "zai-secret-key",
+ baseURL: "https://api.z.ai/api/anthropic"
+ },
+ models: {
+ "GLM-5.2": {}
+ }
+ }
+ }
+ })
+ );
+
+ const env = resolveZCodeProviderEnv({ appConfigPath: configPath, model: "GLM-5.2" });
+
+ expect(env.ZCODE_MODEL).toBe("builtin:zai-coding-plan/GLM-5.2");
+ expect(env.ZCODE_BASE_URL).toBe("https://api.z.ai/api/anthropic");
+ expect(env.ZCODE_API_KEY).toBe("zai-secret-key");
+ expect(JSON.stringify(env.redacted)).not.toContain("zai-secret-key");
+ });
+
+ it("sets retry env to prevent provider rate-limit retry storms", () => {
+ const env = buildZCodeRuntimeEnv({
+ baseEnv: { KEEP_ME: "yes" },
+ providerEnv: {
+ ZCODE_MODEL: "builtin:zai-coding-plan/GLM-5.2",
+ ZCODE_BASE_URL: "https://api.z.ai/api/anthropic",
+ ZCODE_API_KEY: "secret",
+ redacted: {
+ providerId: "builtin:zai-coding-plan",
+ model: "GLM-5.2",
+ baseURL: "https://api.z.ai/api/anthropic",
+ apiKey: "[redacted len=6]"
+ }
+ },
+ retryMaxRetries: 0
+ });
+
+ expect(env).toMatchObject({
+ KEEP_ME: "yes",
+ ZCODE_MODEL: "builtin:zai-coding-plan/GLM-5.2",
+ ZCODE_BASE_URL: "https://api.z.ai/api/anthropic",
+ ZCODE_API_KEY: "secret",
+ ZCODE_MODEL_RETRY_MAX_RETRIES: "0"
+ });
+ });
+});
diff --git a/tests/zcode-output.test.ts b/tests/zcode-output.test.ts
new file mode 100644
index 00000000..fc3a6582
--- /dev/null
+++ b/tests/zcode-output.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, it } from "vitest";
+import { extractJsonObject, extractZCodeResponse } from "../src/zcode.js";
+
+describe("ZCode output parsing", () => {
+ it("accepts pretty JSON emitted by current ZCode CLI", () => {
+ const stdout = JSON.stringify(
+ {
+ sessionId: "sess_123",
+ response: "```json\n{\"findings\":[]}\n```"
+ },
+ null,
+ 2
+ );
+
+ expect(extractZCodeResponse(stdout)).toContain("\"findings\":[]");
+ });
+
+ it("keeps JSONL compatibility for older ZCode CLI output", () => {
+ const stdout = [
+ JSON.stringify({ event: "started" }),
+ JSON.stringify({ response: "{\"findings\":[]}" })
+ ].join("\n");
+
+ expect(extractZCodeResponse(stdout)).toBe("{\"findings\":[]}");
+ });
+
+ it("extracts the final review JSON when ZCode adds prose with earlier braces", () => {
+ const response = [
+ "I checked a callback like `confirmDrop(ctxMenu.item, () => postInvMove(...))` before finalizing.",
+ "Here is the result:",
+ "{\"findings\":[],\"summary\":\"No validated current-diff findings.\"}"
+ ].join("\n\n");
+
+ expect(JSON.parse(extractJsonObject(response))).toEqual({
+ findings: [],
+ summary: "No validated current-diff findings."
+ });
+ });
+});
diff --git a/tests/zcode-policy.test.ts b/tests/zcode-policy.test.ts
new file mode 100644
index 00000000..18e788a7
--- /dev/null
+++ b/tests/zcode-policy.test.ts
@@ -0,0 +1,51 @@
+import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, describe, expect, it } from "vitest";
+import { withTemporaryZCodeReviewPolicy } from "../src/zcode.js";
+
+describe("temporary ZCode review policy", () => {
+ const roots: string[] = [];
+
+ afterEach(() => {
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
+ });
+
+ it("installs a restrictive project policy only for the ZCode run", () => {
+ const root = mkdtempSync(join(tmpdir(), "zcode-policy-"));
+ const evidence = mkdtempSync(join(tmpdir(), "zcode-policy-evidence-"));
+ roots.push(root, evidence);
+
+ const result = withTemporaryZCodeReviewPolicy(root, evidence, () => {
+ const policyPath = join(root, ".zcode", "config.json");
+ const policy = JSON.parse(readFileSync(policyPath, "utf8")) as {
+ permission: { allowedTools: string[]; disallowedTools: string[] };
+ features: { subagent: boolean };
+ };
+
+ expect(policy.permission.allowedTools).toEqual(["Read", "Grep", "Glob", "LS"]);
+ expect(policy.permission.disallowedTools).toContain("Bash");
+ expect(policy.features.subagent).toBe(false);
+ return "reviewed";
+ });
+
+ expect(result).toBe("reviewed");
+ expect(existsSync(join(root, ".zcode", "config.json"))).toBe(false);
+ expect(readFileSync(join(evidence, "zcode-review-policy.json"), "utf8")).toContain("\"Bash\"");
+ });
+
+ it("restores an existing repo ZCode config after the run", () => {
+ const root = mkdtempSync(join(tmpdir(), "zcode-policy-existing-"));
+ roots.push(root);
+ const configDir = join(root, ".zcode");
+ const configPath = join(configDir, "config.json");
+ mkdirSync(configDir);
+ writeFileSync(configPath, "{\"features\":{\"subagent\":true}}\n");
+
+ withTemporaryZCodeReviewPolicy(root, undefined, () => {
+ expect(readFileSync(configPath, "utf8")).toContain("\"Bash\"");
+ });
+
+ expect(readFileSync(configPath, "utf8")).toBe("{\"features\":{\"subagent\":true}}\n");
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..fa8d1da7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2023",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "skipLibCheck": true,
+ "outDir": "dist",
+ "rootDir": ".",
+ "types": ["node", "vitest/globals"]
+ },
+ "include": ["src/**/*.ts", "tests/**/*.ts"]
+}
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 00000000..9d7acaee
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ include: ["tests/**/*.test.ts"],
+ pool: "threads",
+ poolOptions: {
+ threads: {
+ singleThread: true
+ }
+ }
+ }
+});